Reading profile fixes and feedback (#3853)

This commit is contained in:
Fesaa 2025-06-14 19:06:05 +02:00 committed by GitHub
parent fc968f0044
commit c6d157c863
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 3973 additions and 58 deletions

View file

@ -64,6 +64,9 @@ public sealed record UserReadingProfileDto
/// <inheritdoc cref="AppUserReadingProfile.WidthOverride"/>
public int? WidthOverride { get; set; }
/// <inheritdoc cref="AppUserReadingProfile.DisableWidthOverride"/>
public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never;
#endregion
#region EpubReader

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class AppUserReadingProfileDisableWidthOverrideBreakPoint : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DisableWidthOverride",
table: "AppUserReadingProfiles",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DisableWidthOverride",
table: "AppUserReadingProfiles");
}
}
}

View file

@ -665,6 +665,9 @@ namespace API.Data.Migrations
.HasColumnType("TEXT")
.HasDefaultValue("Dark");
b.Property<int>("DisableWidthOverride")
.HasColumnType("INTEGER");
b.Property<bool>("EmulateBook")
.HasColumnType("INTEGER");
@ -704,7 +707,7 @@ namespace API.Data.Migrations
b.Property<int>("ScalingOption")
.HasColumnType("INTEGER");
b.PrimitiveCollection<string>("SeriesIds")
b.Property<string>("SeriesIds")
.HasColumnType("TEXT");
b.Property<bool>("ShowScreenHints")

View file

@ -1,9 +1,22 @@
using System.Collections.Generic;
using System.ComponentModel;
using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
namespace API.Entities;
public enum BreakPoint
{
[Description("Never")]
Never = 0,
[Description("Mobile")]
Mobile = 1,
[Description("Tablet")]
Tablet = 2,
[Description("Desktop")]
Desktop = 3,
}
public class AppUserReadingProfile
{
public int Id { get; set; }
@ -72,6 +85,10 @@ public class AppUserReadingProfile
/// Manga Reader Option: Optional fixed width override
/// </summary>
public int? WidthOverride { get; set; } = null;
/// <summary>
/// Manga Reader Option: Disable the width override if the screen is past the breakpoint
/// </summary>
public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never;
#endregion

View file

@ -432,6 +432,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
existingProfile.SwipeToPaginate = dto.SwipeToPaginate;
existingProfile.AllowAutomaticWebtoonReaderDetection = dto.AllowAutomaticWebtoonReaderDetection;
existingProfile.WidthOverride = dto.WidthOverride;
existingProfile.DisableWidthOverride = dto.DisableWidthOverride;
// Book Reader
existingProfile.BookReaderMargin = dto.BookReaderMargin;

View file

@ -17,7 +17,7 @@ public static class Configuration
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
public static readonly string KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development
? "https://plus.kavitareader.com" : "https://plus.kavitareader.com";
? "http://localhost:5020" : "https://plus.kavitareader.com";
public static readonly string StatsApiUrl = "https://stats.kavitareader.com";
public static int Port

View file

@ -12,6 +12,7 @@ import {PdfLayoutMode} from "./pdf-layout-mode";
import {PdfSpreadMode} from "./pdf-spread-mode";
import {Series} from "../series";
import {Library} from "../library/library";
import {UserBreakpoint} from "../../shared/_services/utility.service";
export enum ReadingProfileKind {
Default = 0,
@ -39,6 +40,7 @@ export interface ReadingProfile {
swipeToPaginate: boolean;
allowAutomaticWebtoonReaderDetection: boolean;
widthOverride?: number;
disableWidthOverride: UserBreakpoint;
// Book Reader
bookReaderMargin: number;
@ -75,3 +77,4 @@ export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multi
export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}];
export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}];
export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}];
export const breakPoints = [UserBreakpoint.Never, UserBreakpoint.Mobile, UserBreakpoint.Tablet, UserBreakpoint.Desktop]

View file

@ -20,5 +20,6 @@ export enum WikiLink {
UpdateNative = 'https://wiki.kavitareader.com/guides/updating/updating-native',
UpdateDocker = 'https://wiki.kavitareader.com/guides/updating/updating-docker',
OpdsClients = 'https://wiki.kavitareader.com/guides/features/opds/#opds-capable-clients',
Guides = 'https://wiki.kavitareader.com/guides'
Guides = 'https://wiki.kavitareader.com/guides',
ReadingProfiles = "https://wiki.kavitareader.com/guides/user-settings/reading-profiles/",
}

View file

@ -0,0 +1,25 @@
import {Pipe, PipeTransform} from '@angular/core';
import {translate} from "@jsverse/transloco";
import {UserBreakpoint} from "../shared/_services/utility.service";
@Pipe({
name: 'breakpoint'
})
export class BreakpointPipe implements PipeTransform {
transform(value: UserBreakpoint): string {
const v = parseInt(value + '', 10) as UserBreakpoint;
switch (v) {
case UserBreakpoint.Never:
return translate('breakpoint-pipe.never');
case UserBreakpoint.Mobile:
return translate('breakpoint-pipe.mobile');
case UserBreakpoint.Tablet:
return translate('breakpoint-pipe.tablet');
case UserBreakpoint.Desktop:
return translate('breakpoint-pipe.desktop');
}
throw new Error("unknown breakpoint value: " + value);
}
}

View file

@ -107,6 +107,7 @@ export class AppComponent implements OnInit {
const vh = window.innerHeight * 0.01;
this.document.documentElement.style.setProperty('--vh', `${vh}px`);
this.utilityService.activeBreakpointSource.next(this.utilityService.getActiveBreakpoint());
this.utilityService.updateUserBreakpoint();
}
ngOnInit(): void {

View file

@ -33,8 +33,9 @@
<div infinite-scroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="50">
@for(item of webtoonImages | async; let index = $index; track item.src) {
<img src="{{item.src}}" style="display: block;" [ngStyle]="{'width': widthOverride$ | async}"
<img src="{{item.src}}" style="display: block;"
[style.filter]="(darkness$ | async) ?? '' | safeStyle"
[style.width]="widthOverride()"
class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}"
rel="nofollow"
alt="image"

View file

@ -1,20 +1,20 @@
import {AsyncPipe, DOCUMENT, NgStyle} from '@angular/common';
import {AsyncPipe, DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
Component, computed,
DestroyRef, effect,
ElementRef,
EventEmitter,
inject,
Inject,
Inject, Injector,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
Renderer2, Signal,
SimpleChanges,
ViewChild
} from '@angular/core';
@ -25,11 +25,13 @@ import {ReaderService} from '../../../_services/reader.service';
import {PAGING_DIRECTION} from '../../_models/reader-enums';
import {WebtoonImage} from '../../_models/webtoon-image';
import {MangaReaderService} from '../../_service/manga-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
import {TranslocoDirective} from "@jsverse/transloco";
import {InfiniteScrollModule} from "ngx-infinite-scroll";
import {ReaderSetting} from "../../_models/reader-setting";
import {SafeStylePipe} from "../../../_pipes/safe-style.pipe";
import {UtilityService} from "../../../shared/_services/utility.service";
import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
/**
* How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load
@ -63,7 +65,7 @@ const enum DEBUG_MODES {
templateUrl: './infinite-scroller.component.html',
styleUrls: ['./infinite-scroller.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [AsyncPipe, TranslocoDirective, InfiniteScrollModule, SafeStylePipe, NgStyle]
imports: [AsyncPipe, TranslocoDirective, InfiniteScrollModule, SafeStylePipe]
})
export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {
@ -71,6 +73,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
private readonly readerService = inject(ReaderService);
private readonly renderer = inject(Renderer2);
private readonly scrollService = inject(ScrollService);
private readonly utilityService = inject(UtilityService);
private readonly injector = inject(Injector);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
@ -91,6 +95,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
*/
@Input({required: true}) urlProvider!: (page: number) => string;
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) readingProfile!: ReadingProfile;
@Output() pageNumberChange: EventEmitter<number> = new EventEmitter<number>();
@Output() loadNextChapter: EventEmitter<void> = new EventEmitter<void>();
@Output() loadPrevChapter: EventEmitter<void> = new EventEmitter<void>();
@ -174,13 +179,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
*/
debugLogFilter: Array<string> = ['[PREFETCH]', '[Intersection]', '[Visibility]', '[Image Load]'];
/**
* Width override for manual width control
* 2 observables needed to avoid flickering, probably due to data races, when changing the width
* this allows to precisely define execution order
*/
widthOverride$ : Observable<string> = new Observable<string>();
widthSliderValue$ : Observable<string> = new Observable<string>();
readerSettings!: Signal<ReaderSetting>;
widthOverride!: Signal<string>;
get minPageLoaded() {
return Math.min(...Object.values(this.imagesLoaded));
@ -240,30 +240,37 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
takeUntilDestroyed(this.destroyRef)
);
// We need the injector as toSignal is only allowed in injection context
// https://angular.dev/guide/signals#injection-context
this.readerSettings = toSignal(this.readerSettings$, {injector: this.injector, requireSync: true});
this.widthSliderValue$ = this.readerSettings$.pipe(
map(values => (parseInt(values.widthSlider) <= 0) ? '' : values.widthSlider + '%'),
takeUntilDestroyed(this.destroyRef)
);
// Automatically updates when the breakpoint changes, or when reader settings changes
this.widthOverride = computed(() => {
const breakpoint = this.utilityService.activeUserBreakpoint();
const value = this.readerSettings().widthSlider;
this.widthOverride$ = this.widthSliderValue$;
if (breakpoint <= this.readingProfile.disableWidthOverride) {
return '';
}
return (parseInt(value) <= 0) ? '' : value + '%';
});
//perform jump so the page stays in view
this.widthSliderValue$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
effect(() => {
const width = this.widthOverride(); // needs to be at the top for effect to work
this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum);
if(!this.currentPageElem)
return;
let images = Array.from(document.querySelectorAll('img[id^="page-"]')) as HTMLImageElement[];
images.forEach((img) => {
this.renderer.setStyle(img, "width", val);
this.renderer.setStyle(img, "width", width);
});
this.widthOverride$ = this.widthSliderValue$;
this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
this.currentPageElem.scrollIntoView();
this.cdRef.markForCheck();
});
}, {injector: this.injector});
if (this.goToPage) {
this.goToPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {

View file

@ -93,7 +93,8 @@
[readerSettings$]="readerSettings$"
[bookmark$]="showBookmarkEffect$"
[pageNum$]="pageNum$"
[showClickOverlay$]="showClickOverlay$">
[showClickOverlay$]="showClickOverlay$"
[readingProfile]="readingProfile">
</app-single-renderer>
<app-double-renderer [image$]="currentImage$"
@ -133,7 +134,8 @@
(loadPrevChapter)="loadPrevChapter()"
[bookmarkPage]="showBookmarkEffectEvent"
[fullscreenToggled]="fullscreenEvent"
[readerSettings$]="readerSettings$">
[readerSettings$]="readerSettings$"
[readingProfile]="readingProfile">
</app-infinite-scroller>
</div>
}

View file

@ -1283,8 +1283,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
/**
* Calls the correct prev- or nextPage method based on the direction, readingDirection, and readerMode
*
* readingDirection is ignored when readerMode is Webtoon or UpDown
*
* KeyDirection.Right: right or bottom click
* KeyDirection.Left: left or top click
* @param event
* @param direction
*/
handlePageChange(event: any, direction: KeyDirection) {
if (this.readerMode === ReaderMode.Webtoon) {
// Webtoons and UpDown reading mode should not take ReadingDirection into account
if (this.readerMode === ReaderMode.Webtoon || this.readerMode === ReaderMode.UpDown) {
if (direction === KeyDirection.Right) {
this.nextPage(event);
} else {
@ -1292,6 +1303,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
return;
}
if (direction === KeyDirection.Right) {
this.readingDirection === ReadingDirection.LeftToRight ? this.nextPage(event) : this.prevPage(event);
} else if (direction === KeyDirection.Left) {

View file

@ -3,7 +3,7 @@
[style.filter]="(darkness$ | async) ?? '' | safeStyle" [style.height]="(imageContainerHeight$ | async) ?? '' | safeStyle">
@if(currentImage) {
<img alt=" "
style="width: {{widthOverride$ | async}}"
[style.width]="widthOverride()"
#image
[src]="currentImage.src"
id="image-1"

View file

@ -2,15 +2,15 @@ import { DOCUMENT, NgIf, AsyncPipe } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
Component, computed, DestroyRef, effect,
EventEmitter,
inject,
Inject,
Inject, Injector,
Input,
OnInit,
Output
Output, signal, Signal, WritableSignal
} from '@angular/core';
import {combineLatest, filter, map, Observable, of, shareReplay, switchMap, tap} from 'rxjs';
import {combineLatest, combineLatestWith, filter, map, Observable, of, shareReplay, switchMap, tap} from 'rxjs';
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
import { ReaderMode } from 'src/app/_models/preferences/reader-mode';
import { LayoutMode } from '../../_models/layout-mode';
@ -18,8 +18,10 @@ import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting';
import { ImageRenderer } from '../../_models/renderer';
import { MangaReaderService } from '../../_service/manga-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {takeUntilDestroyed, toObservable, toSignal} from "@angular/core/rxjs-interop";
import { SafeStylePipe } from '../../../_pipes/safe-style.pipe';
import {UtilityService} from "../../../shared/_services/utility.service";
import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
@Component({
selector: 'app-single-renderer',
@ -30,7 +32,11 @@ import { SafeStylePipe } from '../../../_pipes/safe-style.pipe';
})
export class SingleRendererComponent implements OnInit, ImageRenderer {
private readonly utilityService = inject(UtilityService);
private readonly injector = inject(Injector);
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) readingProfile!: ReadingProfile;
@Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input({required: true}) bookmark$!: Observable<number>;
@Input({required: true}) showClickOverlay$!: Observable<boolean>;
@ -52,16 +58,14 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
pageNum: number = 0;
maxPages: number = 1;
/**
* Width override for maunal width control
*/
widthOverride$ : Observable<string> = new Observable<string>();
readerSettings!: Signal<ReaderSetting>;
widthOverride!: Signal<string>;
get ReaderMode() {return ReaderMode;}
get LayoutMode() {return LayoutMode;}
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: MangaReaderService,
@Inject(DOCUMENT) private document: Document) { }
@Inject(DOCUMENT) private document: Document) {}
ngOnInit(): void {
this.readerModeClass$ = this.readerSettings$.pipe(
@ -71,12 +75,16 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
takeUntilDestroyed(this.destroyRef)
);
//handle manual width
this.widthOverride$ = this.readerSettings$.pipe(
map(values => (parseInt(values.widthSlider) <= 0) ? '' : values.widthSlider + '%'),
takeUntilDestroyed(this.destroyRef)
);
this.readerSettings = toSignal(this.readerSettings$, {injector: this.injector, requireSync: true});
this.widthOverride = computed(() => {
const breakpoint = this.utilityService.activeUserBreakpoint();
const value = this.readerSettings().widthSlider;
if (breakpoint <= this.readingProfile.disableWidthOverride) {
return '';
}
return (parseInt(value) <= 0) ? '' : value + '%';
});
this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook),

View file

@ -1,5 +1,5 @@
import {HttpParams} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Inject, Injectable, signal, Signal} from '@angular/core';
import {Chapter} from 'src/app/_models/chapter';
import {LibraryType} from 'src/app/_models/library/library';
import {MangaFormat} from 'src/app/_models/manga-format';
@ -8,6 +8,8 @@ import {Series} from 'src/app/_models/series';
import {Volume} from 'src/app/_models/volume';
import {translate} from "@jsverse/transloco";
import {debounceTime, ReplaySubject, shareReplay} from "rxjs";
import {DOCUMENT} from "@angular/common";
import getComputedStyle from "@popperjs/core/lib/dom-utils/getComputedStyle";
export enum KEY_CODES {
RIGHT_ARROW = 'ArrowRight',
@ -27,12 +29,37 @@ export enum KEY_CODES {
SHIFT = 'Shift'
}
/**
* Use {@link UserBreakpoint} and {@link UtilityService.activeUserBreakpoint} for breakpoint that should depend on user settings
*/
export enum Breakpoint {
Mobile = 768,
Tablet = 1280,
Desktop = 1440
}
/*
Breakpoints, but they're derived from css vars in the theme
*/
export enum UserBreakpoint {
/**
* This is to be used in the UI/as value to disable the functionality with breakpoint, will not actually be set as a breakpoint
*/
Never = 0,
/**
* --mobile-breakpoint
*/
Mobile = 1,
/**
* --tablet-breakpoint
*/
Tablet = 2,
/**
* --desktop-breakpoint, does not actually matter as everything that's not mobile or tablet will be desktop
*/
Desktop = 3,
}
@Injectable({
providedIn: 'root'
@ -42,11 +69,19 @@ export class UtilityService {
public readonly activeBreakpointSource = new ReplaySubject<Breakpoint>(1);
public readonly activeBreakpoint$ = this.activeBreakpointSource.asObservable().pipe(debounceTime(60), shareReplay({bufferSize: 1, refCount: true}));
/**
* The currently active breakpoint, is {@link UserBreakpoint.Never} until the app has loaded
*/
public readonly activeUserBreakpoint = signal<UserBreakpoint>(UserBreakpoint.Never);
// TODO: I need an isPhone/Tablet so that I can easily trigger different views
mangaFormatKeys: string[] = [];
constructor(@Inject(DOCUMENT) private document: Document) {
}
sortChapters = (a: Chapter, b: Chapter) => {
return a.minNumber - b.minNumber;
@ -132,6 +167,34 @@ export class UtilityService {
return Breakpoint.Desktop;
}
updateUserBreakpoint(): void {
this.activeUserBreakpoint.set(this.getActiveUserBreakpoint());
}
private getActiveUserBreakpoint(): UserBreakpoint {
const style = getComputedStyle(this.document.body)
const mobileBreakPoint = this.parseOrDefault<number>(style.getPropertyValue('--setting-mobile-breakpoint'), Breakpoint.Mobile);
const tabletBreakPoint = this.parseOrDefault<number>(style.getPropertyValue('--setting-tablet-breakpoint'), Breakpoint.Tablet);
//const desktopBreakPoint = this.parseOrDefault<number>(style.getPropertyValue('--setting-desktop-breakpoint'), Breakpoint.Desktop);
if (window.innerWidth <= mobileBreakPoint) {
return UserBreakpoint.Mobile;
} else if (window.innerWidth <= tabletBreakPoint) {
return UserBreakpoint.Tablet;
}
// Fallback to desktop
return UserBreakpoint.Desktop;
}
private parseOrDefault<T>(s: string, def: T): T {
const ret = parseInt(s, 10);
if (isNaN(ret)) {
return def;
}
return ret as T;
}
isInViewport(element: Element, additionalTopOffset: number = 0) {
const rect = element.getBoundingClientRect();
return (

View file

@ -10,13 +10,13 @@
</div>
<p class="ps-2">{{t('description')}}</p>
<p class="ps-2 text-muted">{{t('extra-tip')}}</p>
<p class="ps-2 text-muted">{{t('extra-tip')}} <a target="_blank" rel="noopener noreferrer" [href]="WikiLink.ReadingProfiles">{{t('wiki-title')}}</a></p>
<div class="row g-0 ">
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller">
<div class="pe-2">
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller mb-2 mb-sm-0">
<div class="pe-sm-2">
@if (readingProfiles.length < virtualScrollerBreakPoint) {
@for (readingProfile of readingProfiles; track readingProfile.id) {
@ -32,7 +32,7 @@
</div>
</div>
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-3">
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-0 ps-sm-3">
<div class="card p-3">
@if (selectedProfile === null) {
<p class="ps-2">{{t('no-selected')}}</p>
@ -46,7 +46,9 @@
<div class="mb-2 d-flex justify-content-between align-items-center">
<app-setting-item [title]="''" [showEdit]="false" [canEdit]="selectedProfile.kind !== ReadingProfileKind.Default">
<ng-template #view>
<span [class.clickable]="selectedProfile.kind !== ReadingProfileKind.Default">
{{readingProfileForm.get('name')!.value}}
</span>
</ng-template>
<ng-template #edit>
<input class="form-control" type="text" formControlName="name" [disabled]="selectedProfile.kind === ReadingProfileKind.Default">
@ -250,6 +252,22 @@
</div>
}
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('disable-width-override-label')" [subtitle]="t('disable-width-override-tooltip')">
<ng-template #view>
{{readingProfileForm.get('disableWidthOverride')!.value | breakpoint}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="disableWidthOverride">
@for (opt of breakPoints; track opt) {
<option [value]="opt">{{opt | breakpoint}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
</div>
}
</ng-template>

View file

@ -1,7 +1,5 @@
@use '../../../series-detail-common';
.group-item {
background-color: transparent;

View file

@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnIni
import {ReadingProfileService} from "../../_services/reading-profile.service";
import {
bookLayoutModes,
bookWritingStyles,
bookWritingStyles, breakPoints,
layoutModes,
pageSplitOptions,
pdfScrollModes,
@ -47,6 +47,8 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {ToastrService} from "ngx-toastr";
import {ConfirmService} from "../../shared/confirm.service";
import {WikiLink} from "../../_models/wiki";
import {BreakpointPipe} from "../../_pipes/breakpoint.pipe";
enum TabId {
ImageReader = "image-reader",
@ -85,6 +87,7 @@ enum TabId {
NgbNavOutlet,
LoadingComponent,
NgbTooltip,
BreakpointPipe,
],
templateUrl: './manage-reading-profiles.component.html',
styleUrl: './manage-reading-profiles.component.scss',
@ -193,6 +196,7 @@ export class ManageReadingProfilesComponent implements OnInit {
this.readingProfileForm.addControl('backgroundColor', new FormControl(this.selectedProfile.backgroundColor, []));
this.readingProfileForm.addControl('allowAutomaticWebtoonReaderDetection', new FormControl(this.selectedProfile.allowAutomaticWebtoonReaderDetection, []));
this.readingProfileForm.addControl('widthOverride', new FormControl(this.selectedProfile.widthOverride, [Validators.min(0), Validators.max(100)]));
this.readingProfileForm.addControl('disableWidthOverride', new FormControl(this.selectedProfile.disableWidthOverride, []))
// Epub reader
this.readingProfileForm.addControl('bookReaderFontFamily', new FormControl(this.selectedProfile.bookReaderFontFamily, []));
@ -237,10 +241,10 @@ export class ManageReadingProfilesComponent implements OnInit {
} else {
const profile = this.packData();
this.readingProfileService.updateProfile(profile).subscribe({
next: _ => {
next: newProfile => {
this.readingProfiles = this.readingProfiles.map(p => {
if (p.id !== profile.id) return p;
return profile;
return newProfile;
});
this.cdRef.markForCheck();
},
@ -260,6 +264,7 @@ export class ManageReadingProfilesComponent implements OnInit {
data.pageSplitOption = parseInt(data.pageSplitOption as unknown as string);
data.readerMode = parseInt(data.readerMode as unknown as string);
data.layoutMode = parseInt(data.layoutMode as unknown as string);
data.disableWidthOverride = parseInt(data.disableWidthOverride as unknown as string);
data.bookReaderReadingDirection = parseInt(data.bookReaderReadingDirection as unknown as string);
data.bookReaderWritingStyle = parseInt(data.bookReaderWritingStyle as unknown as string);
@ -316,4 +321,6 @@ export class ManageReadingProfilesComponent implements OnInit {
protected readonly pdfScrollModes = pdfScrollModes;
protected readonly TabId = TabId;
protected readonly ReadingProfileKind = ReadingProfileKind;
protected readonly WikiLink = WikiLink;
protected readonly breakPoints = breakPoints;
}

View file

@ -2882,10 +2882,17 @@
"pdf-light": "Light",
"pdf-dark": "Dark"
},
"breakpoint-pipe": {
"never": "Never",
"mobile": "Mobile",
"tablet": "Tablet",
"desktop": "Desktop"
},
"manage-reading-profiles": {
"description": "Not all your series may be read in the same way, set up distinct reading profiles per library or series to make getting back in your series as seamless as possible.",
"extra-tip": "Assign reading profiles via the action menu on series and libraries, or in bulk. When changing settings in a reader, a hidden profile is created that remembers your choices for that series (not for pdfs). This profile is removed when you assign or update one of your own reading profiles to the series.",
"extra-tip": "Assign reading profiles via the action menu on series and libraries, or in bulk. When changing settings in a reader, a hidden profile is created that remembers your choices for that series (not for pdfs). This profile is removed when you assign one of your own reading profiles to the series. More information can be found on the",
"wiki-title": "wiki",
"profiles-title": "Your reading profiles",
"default-profile": "Default",
"add": "{{common.add}}",
@ -2920,6 +2927,8 @@
"allow-auto-webtoon-reader-tooltip": "Switch into Webtoon Reader mode if pages look like a webtoon. Some false positives may occur.",
"width-override-label": "{{manga-reader.width-override-label}}",
"width-override-tooltip": "Override width of images in the reader",
"disable-width-override-label": "Disable width override",
"disable-width-override-tooltip": "Prevent the width override from taking effect when your screen is at least the configured breakpoint or smaller",
"reset": "{{common.reset}}",
"book-reader-settings-title": "Book Reader",

View file

@ -442,4 +442,10 @@
/** Search **/
--input-hint-border-color: #aeaeae;
--input-hint-text-color: lightgrey;
/** Breakpoint **/
--setting-mobile-breakpoint: 768;
--setting-tablet-breakpoint: 1280;
--setting-desktop-breakpoint: 1440;
}