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"/>
public int? WidthOverride { get; set; }
/// <inheritdoc cref="AppUserReadingProfile.DisableWidthOverride"/>
public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never;
#endregion
#region EpubReader

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
this.document.documentElement.style.setProperty('--vh', `${vh}px`);
this.utilityService.activeBreakpointSource.next(this.utilityService.getActiveBreakpoint());
this.utilityService.updateUserBreakpoint();
}
ngOnInit(): void {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import {HttpParams} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Inject, Injectable, signal, Signal} from '@angular/core';
import {Chapter} from 'src/app/_models/chapter';
import {LibraryType} from 'src/app/_models/library/library';
import {MangaFormat} from 'src/app/_models/manga-format';
@ -8,6 +8,8 @@ import {Series} from 'src/app/_models/series';
import {Volume} from 'src/app/_models/volume';
import {translate} from "@jsverse/transloco";
import {debounceTime, ReplaySubject, shareReplay} from "rxjs";
import {DOCUMENT} from "@angular/common";
import getComputedStyle from "@popperjs/core/lib/dom-utils/getComputedStyle";
export enum KEY_CODES {
RIGHT_ARROW = 'ArrowRight',
@ -27,12 +29,37 @@ export enum KEY_CODES {
SHIFT = 'Shift'
}
/**
* @deprecated Use {@link UserBreakpoint} and {@link UtilityService.activeUserBreakpoint}
*/
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,33 @@ 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('--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) {
const rect = element.getBoundingClientRect();
return (

View file

@ -252,6 +252,22 @@
</div>
}
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('disable-width-override-label')" [subtitle]="t('disable-width-override-tooltip')">
<ng-template #view>
{{readingProfileForm.get('disableWidthOverride')!.value | breakpoint}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="disableWidthOverride">
@for (opt of breakPoints; track opt) {
<option [value]="opt">{{opt | breakpoint}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
</div>
}
</ng-template>

View file

@ -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,
@ -48,6 +48,7 @@ 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",
@ -86,6 +87,7 @@ enum TabId {
NgbNavOutlet,
LoadingComponent,
NgbTooltip,
BreakpointPipe,
],
templateUrl: './manage-reading-profiles.component.html',
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('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, []));
@ -261,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);
@ -318,4 +322,5 @@ export class ManageReadingProfilesComponent implements OnInit {
protected readonly TabId = TabId;
protected readonly ReadingProfileKind = ReadingProfileKind;
protected readonly WikiLink = WikiLink;
protected readonly breakPoints = breakPoints;
}

View file

@ -2820,7 +2820,13 @@
"pdf-odd": "Odd",
"pdf-even": "Even",
"pdf-light": "Light",
"pdf-dark": "Dark"
"pdf-dark": "Dark",
"breakpoints": {
"never": "Never",
"mobile": "Mobile",
"tablet": "Tablet",
"desktop": "Desktop"
}
},
"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.",
"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 smaller than the configured breakpoint",
"reset": "{{common.reset}}",
"book-reader-settings-title": "Book Reader",

View file

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