Add UserBreakpoint API, disable width override after configured breakpoint
This commit is contained in:
parent
43c4969d5c
commit
b6e46e2f2d
19 changed files with 3942 additions and 47 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
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")
|
.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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
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('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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,10 +58,8 @@ 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;}
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue