Add UserBreakpoint API, disable width override after configured breakpoint

This commit is contained in:
Amelia 2025-06-11 01:16:01 +02:00
parent 43c4969d5c
commit b6e46e2f2d
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
19 changed files with 3942 additions and 47 deletions

View file

@ -64,6 +64,9 @@ public sealed record UserReadingProfileDto
/// <inheritdoc cref="AppUserReadingProfile.WidthOverride"/> /// <inheritdoc cref="AppUserReadingProfile.WidthOverride"/>
public int? WidthOverride { get; set; } public int? WidthOverride { get; set; }
/// <inheritdoc cref="AppUserReadingProfile.DisableWidthOverride"/>
public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never;
#endregion #endregion
#region EpubReader #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") .HasColumnType("TEXT")
.HasDefaultValue("Dark"); .HasDefaultValue("Dark");
b.Property<int>("DisableWidthOverride")
.HasColumnType("INTEGER");
b.Property<bool>("EmulateBook") b.Property<bool>("EmulateBook")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -704,7 +707,7 @@ namespace API.Data.Migrations
b.Property<int>("ScalingOption") b.Property<int>("ScalingOption")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.PrimitiveCollection<string>("SeriesIds") b.Property<string>("SeriesIds")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("ShowScreenHints") b.Property<bool>("ShowScreenHints")

View file

@ -4,6 +4,14 @@ using API.Entities.Enums.UserPreferences;
namespace API.Entities; namespace API.Entities;
public enum BreakPoint
{
Never = 0,
Mobile = 1,
Tablet = 2,
Desktop = 3,
}
public class AppUserReadingProfile public class AppUserReadingProfile
{ {
public int Id { get; set; } public int Id { get; set; }
@ -72,6 +80,10 @@ public class AppUserReadingProfile
/// Manga Reader Option: Optional fixed width override /// Manga Reader Option: Optional fixed width override
/// </summary> /// </summary>
public int? WidthOverride { get; set; } = null; 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 #endregion

View file

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

View file

@ -12,6 +12,7 @@ import {PdfLayoutMode} from "./pdf-layout-mode";
import {PdfSpreadMode} from "./pdf-spread-mode"; import {PdfSpreadMode} from "./pdf-spread-mode";
import {Series} from "../series"; import {Series} from "../series";
import {Library} from "../library/library"; import {Library} from "../library/library";
import {UserBreakpoint} from "../../shared/_services/utility.service";
export enum ReadingProfileKind { export enum ReadingProfileKind {
Default = 0, Default = 0,
@ -39,6 +40,7 @@ export interface ReadingProfile {
swipeToPaginate: boolean; swipeToPaginate: boolean;
allowAutomaticWebtoonReaderDetection: boolean; allowAutomaticWebtoonReaderDetection: boolean;
widthOverride?: number; widthOverride?: number;
disableWidthOverride: UserBreakpoint;
// Book Reader // Book Reader
bookReaderMargin: number; 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 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 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 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

@ -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('preferences.breakpoints.never');
case UserBreakpoint.Mobile:
return translate('preferences.breakpoints.mobile');
case UserBreakpoint.Tablet:
return translate('preferences.breakpoints.tablet');
case UserBreakpoint.Desktop:
return translate('preferences.breakpoints.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; const vh = window.innerHeight * 0.01;
this.document.documentElement.style.setProperty('--vh', `${vh}px`); this.document.documentElement.style.setProperty('--vh', `${vh}px`);
this.utilityService.activeBreakpointSource.next(this.utilityService.getActiveBreakpoint()); this.utilityService.activeBreakpointSource.next(this.utilityService.getActiveBreakpoint());
this.utilityService.updateUserBreakpoint();
} }
ngOnInit(): void { ngOnInit(): void {

View file

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

View file

@ -1,20 +1,20 @@
import {AsyncPipe, DOCUMENT, NgStyle} from '@angular/common'; import {AsyncPipe, DOCUMENT} from '@angular/common';
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component, computed,
DestroyRef, DestroyRef, effect,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
inject, inject,
Inject, Inject, Injector,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
Renderer2, Renderer2, Signal,
SimpleChanges, SimpleChanges,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
@ -25,11 +25,13 @@ import {ReaderService} from '../../../_services/reader.service';
import {PAGING_DIRECTION} from '../../_models/reader-enums'; import {PAGING_DIRECTION} from '../../_models/reader-enums';
import {WebtoonImage} from '../../_models/webtoon-image'; import {WebtoonImage} from '../../_models/webtoon-image';
import {MangaReaderService} from '../../_service/manga-reader.service'; 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 {TranslocoDirective} from "@jsverse/transloco";
import {InfiniteScrollModule} from "ngx-infinite-scroll"; import {InfiniteScrollModule} from "ngx-infinite-scroll";
import {ReaderSetting} from "../../_models/reader-setting"; import {ReaderSetting} from "../../_models/reader-setting";
import {SafeStylePipe} from "../../../_pipes/safe-style.pipe"; 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 * 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', templateUrl: './infinite-scroller.component.html',
styleUrls: ['./infinite-scroller.component.scss'], styleUrls: ['./infinite-scroller.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [AsyncPipe, TranslocoDirective, InfiniteScrollModule, SafeStylePipe, NgStyle] imports: [AsyncPipe, TranslocoDirective, InfiniteScrollModule, SafeStylePipe]
}) })
export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit { 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 readerService = inject(ReaderService);
private readonly renderer = inject(Renderer2); private readonly renderer = inject(Renderer2);
private readonly scrollService = inject(ScrollService); private readonly scrollService = inject(ScrollService);
private readonly utilityService = inject(UtilityService);
private readonly injector = inject(Injector);
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef); 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}) urlProvider!: (page: number) => string;
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>; @Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) readingProfile!: ReadingProfile;
@Output() pageNumberChange: EventEmitter<number> = new EventEmitter<number>(); @Output() pageNumberChange: EventEmitter<number> = new EventEmitter<number>();
@Output() loadNextChapter: EventEmitter<void> = new EventEmitter<void>(); @Output() loadNextChapter: EventEmitter<void> = new EventEmitter<void>();
@Output() loadPrevChapter: 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]']; debugLogFilter: Array<string> = ['[PREFETCH]', '[Intersection]', '[Visibility]', '[Image Load]'];
/** readerSettings!: Signal<ReaderSetting>;
* Width override for manual width control widthOverride!: Signal<string>;
* 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>();
get minPageLoaded() { get minPageLoaded() {
return Math.min(...Object.values(this.imagesLoaded)); return Math.min(...Object.values(this.imagesLoaded));
@ -240,30 +240,39 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
takeUntilDestroyed(this.destroyRef) 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}) as Signal<ReaderSetting>;
this.widthSliderValue$ = this.readerSettings$.pipe( // Automatically updates when the breakpoint changes, or when reader settings changes
map(values => (parseInt(values.widthSlider) <= 0) ? '' : values.widthSlider + '%'), this.widthOverride = computed(() => {
takeUntilDestroyed(this.destroyRef) //console.log("updating widthOverride")
); 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 //perform jump so the page stays in view (NOT WORKING)
this.widthSliderValue$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { effect(() => {
//console.log("width changing!")
this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum); this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum);
if(!this.currentPageElem) if(!this.currentPageElem)
return; return;
const width = this.widthOverride();
let images = Array.from(document.querySelectorAll('img[id^="page-"]')) as HTMLImageElement[]; let images = Array.from(document.querySelectorAll('img[id^="page-"]')) as HTMLImageElement[];
images.forEach((img) => { 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.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
this.currentPageElem.scrollIntoView(); this.currentPageElem.scrollIntoView();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); }, {injector: this.injector});
if (this.goToPage) { if (this.goToPage) {
this.goToPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => { this.goToPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import {HttpParams} from '@angular/common/http'; 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 {Chapter} from 'src/app/_models/chapter';
import {LibraryType} from 'src/app/_models/library/library'; import {LibraryType} from 'src/app/_models/library/library';
import {MangaFormat} from 'src/app/_models/manga-format'; 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 {Volume} from 'src/app/_models/volume';
import {translate} from "@jsverse/transloco"; import {translate} from "@jsverse/transloco";
import {debounceTime, ReplaySubject, shareReplay} from "rxjs"; import {debounceTime, ReplaySubject, shareReplay} from "rxjs";
import {DOCUMENT} from "@angular/common";
import getComputedStyle from "@popperjs/core/lib/dom-utils/getComputedStyle";
export enum KEY_CODES { export enum KEY_CODES {
RIGHT_ARROW = 'ArrowRight', RIGHT_ARROW = 'ArrowRight',
@ -27,12 +29,37 @@ export enum KEY_CODES {
SHIFT = 'Shift' SHIFT = 'Shift'
} }
/**
* @deprecated Use {@link UserBreakpoint} and {@link UtilityService.activeUserBreakpoint}
*/
export enum Breakpoint { export enum Breakpoint {
Mobile = 768, Mobile = 768,
Tablet = 1280, Tablet = 1280,
Desktop = 1440 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -42,11 +69,19 @@ export class UtilityService {
public readonly activeBreakpointSource = new ReplaySubject<Breakpoint>(1); public readonly activeBreakpointSource = new ReplaySubject<Breakpoint>(1);
public readonly activeBreakpoint$ = this.activeBreakpointSource.asObservable().pipe(debounceTime(60), shareReplay({bufferSize: 1, refCount: true})); 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 // TODO: I need an isPhone/Tablet so that I can easily trigger different views
mangaFormatKeys: string[] = []; mangaFormatKeys: string[] = [];
constructor(@Inject(DOCUMENT) private document: Document) {
}
sortChapters = (a: Chapter, b: Chapter) => { sortChapters = (a: Chapter, b: Chapter) => {
return a.minNumber - b.minNumber; return a.minNumber - b.minNumber;
@ -132,6 +167,33 @@ export class UtilityService {
return Breakpoint.Desktop; 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('--mobile-breakpoint'), Breakpoint.Mobile);
const tabletBreakPoint = this.parseOrDefault<number>(style.getPropertyValue('--tablet-breakpoint'), Breakpoint.Tablet);
const desktopBreakPoint = this.parseOrDefault<number>(style.getPropertyValue('--desktop-breakpoint'), Breakpoint.Desktop);
if (window.innerWidth <= mobileBreakPoint) {
return UserBreakpoint.Mobile;
} else if (window.innerWidth >= mobileBreakPoint && window.innerWidth <= tabletBreakPoint) {
return UserBreakpoint.Tablet;
}
return UserBreakpoint.Desktop;
}
private parseOrDefault<T>(s: string, def: T): T {
try {
return parseInt(s, 10) as T;
} catch (e) {
return def;
}
}
isInViewport(element: Element, additionalTopOffset: number = 0) { isInViewport(element: Element, additionalTopOffset: number = 0) {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
return ( return (

View file

@ -252,6 +252,22 @@
</div> </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> </div>
} }
</ng-template> </ng-template>

View file

@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnIni
import {ReadingProfileService} from "../../_services/reading-profile.service"; import {ReadingProfileService} from "../../_services/reading-profile.service";
import { import {
bookLayoutModes, bookLayoutModes,
bookWritingStyles, bookWritingStyles, breakPoints,
layoutModes, layoutModes,
pageSplitOptions, pageSplitOptions,
pdfScrollModes, pdfScrollModes,
@ -48,6 +48,7 @@ import {LoadingComponent} from "../../shared/loading/loading.component";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import {ConfirmService} from "../../shared/confirm.service"; import {ConfirmService} from "../../shared/confirm.service";
import {WikiLink} from "../../_models/wiki"; import {WikiLink} from "../../_models/wiki";
import {BreakpointPipe} from "../../_pipes/breakpoint.pipe";
enum TabId { enum TabId {
ImageReader = "image-reader", ImageReader = "image-reader",
@ -86,6 +87,7 @@ enum TabId {
NgbNavOutlet, NgbNavOutlet,
LoadingComponent, LoadingComponent,
NgbTooltip, NgbTooltip,
BreakpointPipe,
], ],
templateUrl: './manage-reading-profiles.component.html', templateUrl: './manage-reading-profiles.component.html',
styleUrl: './manage-reading-profiles.component.scss', styleUrl: './manage-reading-profiles.component.scss',
@ -194,6 +196,7 @@ export class ManageReadingProfilesComponent implements OnInit {
this.readingProfileForm.addControl('backgroundColor', new FormControl(this.selectedProfile.backgroundColor, [])); this.readingProfileForm.addControl('backgroundColor', new FormControl(this.selectedProfile.backgroundColor, []));
this.readingProfileForm.addControl('allowAutomaticWebtoonReaderDetection', new FormControl(this.selectedProfile.allowAutomaticWebtoonReaderDetection, [])); 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('widthOverride', new FormControl(this.selectedProfile.widthOverride, [Validators.min(0), Validators.max(100)]));
this.readingProfileForm.addControl('disableWidthOverride', new FormControl(this.selectedProfile.disableWidthOverride, []))
// Epub reader // Epub reader
this.readingProfileForm.addControl('bookReaderFontFamily', new FormControl(this.selectedProfile.bookReaderFontFamily, [])); this.readingProfileForm.addControl('bookReaderFontFamily', new FormControl(this.selectedProfile.bookReaderFontFamily, []));
@ -261,6 +264,7 @@ export class ManageReadingProfilesComponent implements OnInit {
data.pageSplitOption = parseInt(data.pageSplitOption as unknown as string); data.pageSplitOption = parseInt(data.pageSplitOption as unknown as string);
data.readerMode = parseInt(data.readerMode as unknown as string); data.readerMode = parseInt(data.readerMode as unknown as string);
data.layoutMode = parseInt(data.layoutMode 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.bookReaderReadingDirection = parseInt(data.bookReaderReadingDirection as unknown as string);
data.bookReaderWritingStyle = parseInt(data.bookReaderWritingStyle as unknown as string); data.bookReaderWritingStyle = parseInt(data.bookReaderWritingStyle as unknown as string);
@ -318,4 +322,5 @@ export class ManageReadingProfilesComponent implements OnInit {
protected readonly TabId = TabId; protected readonly TabId = TabId;
protected readonly ReadingProfileKind = ReadingProfileKind; protected readonly ReadingProfileKind = ReadingProfileKind;
protected readonly WikiLink = WikiLink; protected readonly WikiLink = WikiLink;
protected readonly breakPoints = breakPoints;
} }

View file

@ -2820,7 +2820,13 @@
"pdf-odd": "Odd", "pdf-odd": "Odd",
"pdf-even": "Even", "pdf-even": "Even",
"pdf-light": "Light", "pdf-light": "Light",
"pdf-dark": "Dark" "pdf-dark": "Dark",
"breakpoints": {
"never": "Never",
"mobile": "Mobile",
"tablet": "Tablet",
"desktop": "Desktop"
}
}, },
"manage-reading-profiles": { "manage-reading-profiles": {
@ -2861,6 +2867,8 @@
"allow-auto-webtoon-reader-tooltip": "Switch into Webtoon Reader mode if pages look like a webtoon. Some false positives may occur.", "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-label": "{{manga-reader.width-override-label}}",
"width-override-tooltip": "Override width of images in the reader", "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 smaller than the configured breakpoint",
"reset": "{{common.reset}}", "reset": "{{common.reset}}",
"book-reader-settings-title": "Book Reader", "book-reader-settings-title": "Book Reader",

View file

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