Reading profile fixes and feedback (#3853)
This commit is contained in:
parent
fc968f0044
commit
c6d157c863
23 changed files with 3973 additions and 58 deletions
|
|
@ -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
|
||||
|
|
|
|||
3701
API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs
generated
Normal file
3701
API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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/",
|
||||
}
|
||||
|
|
|
|||
25
UI/Web/src/app/_pipes/breakpoint.pipe.ts
Normal file
25
UI/Web/src/app/_pipes/breakpoint.pipe.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
@use '../../../series-detail-common';
|
||||
|
||||
|
||||
|
||||
.group-item {
|
||||
background-color: transparent;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue