Reading Profiles (#3845)

Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Fesaa 2025-06-08 16:16:44 +02:00 committed by GitHub
parent ea28d64302
commit 1856b01a46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 8118 additions and 1159 deletions

View file

@ -1,7 +1,7 @@
import { ThemeProvider } from "./site-theme";
import {ThemeProvider} from "./site-theme";
/**
* Theme for the the book reader contents
* Theme for the book reader contents
*/
export interface BookTheme {
name: string;

View file

@ -1,47 +1,7 @@
import {LayoutMode} from 'src/app/manga-reader/_models/layout-mode';
import {BookPageLayoutMode} from '../readers/book-page-layout-mode';
import {PageLayoutMode} from '../page-layout-mode';
import {PageSplitOption} from './page-split-option';
import {ReaderMode} from './reader-mode';
import {ReadingDirection} from './reading-direction';
import {ScalingOption} from './scaling-option';
import {SiteTheme} from './site-theme';
import {WritingStyle} from "./writing-style";
import {PdfTheme} from "./pdf-theme";
import {PdfScrollMode} from "./pdf-scroll-mode";
import {PdfLayoutMode} from "./pdf-layout-mode";
import {PdfSpreadMode} from "./pdf-spread-mode";
export interface Preferences {
// Manga Reader
readingDirection: ReadingDirection;
scalingOption: ScalingOption;
pageSplitOption: PageSplitOption;
readerMode: ReaderMode;
autoCloseMenu: boolean;
layoutMode: LayoutMode;
backgroundColor: string;
showScreenHints: boolean;
emulateBook: boolean;
swipeToPaginate: boolean;
allowAutomaticWebtoonReaderDetection: boolean;
// Book Reader
bookReaderMargin: number;
bookReaderLineSpacing: number;
bookReaderFontSize: number;
bookReaderFontFamily: string;
bookReaderTapToPaginate: boolean;
bookReaderReadingDirection: ReadingDirection;
bookReaderWritingStyle: WritingStyle;
bookReaderThemeName: string;
bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean;
// PDF Reader
pdfTheme: PdfTheme;
pdfScrollMode: PdfScrollMode;
pdfSpreadMode: PdfSpreadMode;
// Global
theme: SiteTheme;
@ -58,15 +18,3 @@ export interface Preferences {
wantToReadSync: boolean;
}
export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}];
export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}];
export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}];
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}];
export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}];
export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}];
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}];

View file

@ -0,0 +1,77 @@
import {LayoutMode} from 'src/app/manga-reader/_models/layout-mode';
import {BookPageLayoutMode} from '../readers/book-page-layout-mode';
import {PageLayoutMode} from '../page-layout-mode';
import {PageSplitOption} from './page-split-option';
import {ReaderMode} from './reader-mode';
import {ReadingDirection} from './reading-direction';
import {ScalingOption} from './scaling-option';
import {WritingStyle} from "./writing-style";
import {PdfTheme} from "./pdf-theme";
import {PdfScrollMode} from "./pdf-scroll-mode";
import {PdfLayoutMode} from "./pdf-layout-mode";
import {PdfSpreadMode} from "./pdf-spread-mode";
import {Series} from "../series";
import {Library} from "../library/library";
export enum ReadingProfileKind {
Default = 0,
User = 1,
Implicit = 2,
}
export interface ReadingProfile {
id: number;
name: string;
normalizedName: string;
kind: ReadingProfileKind;
// Manga Reader
readingDirection: ReadingDirection;
scalingOption: ScalingOption;
pageSplitOption: PageSplitOption;
readerMode: ReaderMode;
autoCloseMenu: boolean;
layoutMode: LayoutMode;
backgroundColor: string;
showScreenHints: boolean;
emulateBook: boolean;
swipeToPaginate: boolean;
allowAutomaticWebtoonReaderDetection: boolean;
widthOverride?: number;
// Book Reader
bookReaderMargin: number;
bookReaderLineSpacing: number;
bookReaderFontSize: number;
bookReaderFontFamily: string;
bookReaderTapToPaginate: boolean;
bookReaderReadingDirection: ReadingDirection;
bookReaderWritingStyle: WritingStyle;
bookReaderThemeName: string;
bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean;
// PDF Reader
pdfTheme: PdfTheme;
pdfScrollMode: PdfScrollMode;
pdfSpreadMode: PdfSpreadMode;
// relations
seriesIds: number[];
libraryIds: number[];
}
export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}];
export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}];
export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}];
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}];
export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}];
export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}];
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}];

View file

@ -0,0 +1,18 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {ReadingProfileService} from "../_services/reading-profile.service";
@Injectable({
providedIn: 'root'
})
export class ReadingProfileResolver implements Resolve<any> {
constructor(private readingProfileService: ReadingProfileService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
// Extract seriesId from route params or parent route
const seriesId = route.params['seriesId'] || route.parent?.params['seriesId'];
return this.readingProfileService.getForSeries(seriesId);
}
}

View file

@ -1,10 +1,14 @@
import { Routes } from '@angular/router';
import { BookReaderComponent } from '../book-reader/_components/book-reader/book-reader.component';
import {Routes} from '@angular/router';
import {BookReaderComponent} from '../book-reader/_components/book-reader/book-reader.component';
import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver";
export const routes: Routes = [
{
path: ':chapterId',
component: BookReaderComponent,
resolve: {
readingProfile: ReadingProfileResolver
}
}
];

View file

@ -1,15 +1,22 @@
import { Routes } from '@angular/router';
import { MangaReaderComponent } from '../manga-reader/_components/manga-reader/manga-reader.component';
import {Routes} from '@angular/router';
import {MangaReaderComponent} from '../manga-reader/_components/manga-reader/manga-reader.component';
import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver";
export const routes: Routes = [
{
path: ':chapterId',
component: MangaReaderComponent
component: MangaReaderComponent,
resolve: {
readingProfile: ReadingProfileResolver
}
},
{
// This will allow the MangaReader to have a list to use for next/prev chapters rather than natural sort order
path: ':chapterId/list/:listId',
component: MangaReaderComponent
component: MangaReaderComponent,
resolve: {
readingProfile: ReadingProfileResolver
}
}
];

View file

@ -1,9 +1,13 @@
import { Routes } from '@angular/router';
import { PdfReaderComponent } from '../pdf-reader/_components/pdf-reader/pdf-reader.component';
import {Routes} from '@angular/router';
import {PdfReaderComponent} from '../pdf-reader/_components/pdf-reader/pdf-reader.component';
import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver";
export const routes: Routes = [
{
path: ':chapterId',
component: PdfReaderComponent,
resolve: {
readingProfile: ReadingProfileResolver
}
}
];

View file

@ -122,6 +122,14 @@ export enum Action {
* Merge two (or more?) entities
*/
Merge = 29,
/**
* Add to a reading profile
*/
SetReadingProfile = 30,
/**
* Remove the reading profile from the entity
*/
ClearReadingProfile = 31,
}
/**
@ -342,6 +350,37 @@ export class ActionFactoryService {
requiredRoles: [Role.Admin],
children: [],
},
{
action: Action.Submenu,
title: 'reading-profiles',
description: '',
callback: this.dummyCallback,
shouldRender: this.dummyShouldRender,
requiresAdmin: false,
requiredRoles: [],
children: [
{
action: Action.SetReadingProfile,
title: 'set-reading-profile',
description: 'set-reading-profile-tooltip',
callback: this.dummyCallback,
shouldRender: this.dummyShouldRender,
requiresAdmin: false,
requiredRoles: [],
children: [],
},
{
action: Action.ClearReadingProfile,
title: 'clear-reading-profile',
description: 'clear-reading-profile-tooltip',
callback: this.dummyCallback,
shouldRender: this.dummyShouldRender,
requiresAdmin: false,
requiredRoles: [],
children: [],
},
],
},
{
action: Action.Submenu,
title: 'others',
@ -528,7 +567,7 @@ export class ActionFactoryService {
requiresAdmin: false,
requiredRoles: [],
children: [],
},
}
],
},
{
@ -555,6 +594,37 @@ export class ActionFactoryService {
}
],
},
{
action: Action.Submenu,
title: 'reading-profiles',
description: '',
callback: this.dummyCallback,
shouldRender: this.dummyShouldRender,
requiresAdmin: false,
requiredRoles: [],
children: [
{
action: Action.SetReadingProfile,
title: 'set-reading-profile',
description: 'set-reading-profile-tooltip',
callback: this.dummyCallback,
shouldRender: this.dummyShouldRender,
requiresAdmin: false,
requiredRoles: [],
children: [],
},
{
action: Action.ClearReadingProfile,
title: 'clear-reading-profile',
description: 'clear-reading-profile-tooltip',
callback: this.dummyCallback,
shouldRender: this.dummyShouldRender,
requiresAdmin: false,
requiredRoles: [],
children: [],
},
],
},
{
action: Action.Submenu,
title: 'others',
@ -1047,7 +1117,10 @@ export class ActionFactoryService {
if (action.children === null || action.children?.length === 0) return;
action.children?.forEach((childAction) => {
// Ensure action children are a copy of the parent (since parent does a shallow mapping)
action.children = action.children.map(d => { return {...d}; });
action.children.forEach((childAction) => {
this.applyCallback(childAction, callback, shouldRenderFunc);
});
}
@ -1055,10 +1128,13 @@ export class ActionFactoryService {
public applyCallbackToList(list: Array<ActionItem<any>>,
callback: ActionCallback<any>,
shouldRenderFunc: ActionShouldRenderFunc<any> = this.dummyShouldRender): Array<ActionItem<any>> {
// Create a clone of the list to ensure we aren't affecting the default state
const actions = list.map((a) => {
return { ...a };
});
actions.forEach((action) => this.applyCallback(action, callback, shouldRenderFunc));
return actions;
}

View file

@ -31,6 +31,9 @@ import {ChapterService} from "./chapter.service";
import {VolumeService} from "./volume.service";
import {DefaultModalOptions} from "../_models/default-modal-options";
import {MatchSeriesModalComponent} from "../_single-module/match-series-modal/match-series-modal.component";
import {
BulkSetReadingProfileModalComponent
} from "../cards/_modals/bulk-set-reading-profile-modal/bulk-set-reading-profile-modal.component";
export type LibraryActionCallback = (library: Partial<Library>) => void;
@ -813,4 +816,56 @@ export class ActionService {
});
}
/**
* Sets the reading profile for multiple series
* @param series
* @param callback
*/
setReadingProfileForMultiple(series: Array<Series>, callback?: BooleanActionCallback) {
if (this.readingListModalRef != null) { return; }
this.readingListModalRef = this.modalService.open(BulkSetReadingProfileModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' });
this.readingListModalRef.componentInstance.seriesIds = series.map(s => s.id)
this.readingListModalRef.componentInstance.title = ""
this.readingListModalRef.closed.pipe(take(1)).subscribe(() => {
this.readingListModalRef = null;
if (callback) {
callback(true);
}
});
this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => {
this.readingListModalRef = null;
if (callback) {
callback(false);
}
});
}
/**
* Sets the reading profile for multiple series
* @param library
* @param callback
*/
setReadingProfileForLibrary(library: Library, callback?: BooleanActionCallback) {
if (this.readingListModalRef != null) { return; }
this.readingListModalRef = this.modalService.open(BulkSetReadingProfileModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' });
this.readingListModalRef.componentInstance.libraryId = library.id;
this.readingListModalRef.componentInstance.title = ""
this.readingListModalRef.closed.pipe(take(1)).subscribe(() => {
this.readingListModalRef = null;
if (callback) {
callback(true);
}
});
this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => {
this.readingListModalRef = null;
if (callback) {
callback(false);
}
});
}
}

View file

@ -0,0 +1,70 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {ReadingProfile} from "../_models/preferences/reading-profiles";
@Injectable({
providedIn: 'root'
})
export class ReadingProfileService {
private readonly httpClient = inject(HttpClient);
baseUrl = environment.apiUrl;
getForSeries(seriesId: number, skipImplicit: boolean = false) {
return this.httpClient.get<ReadingProfile>(this.baseUrl + `reading-profile/${seriesId}?skipImplicit=${skipImplicit}`);
}
getForLibrary(libraryId: number) {
return this.httpClient.get<ReadingProfile | null>(this.baseUrl + `reading-profile/library?libraryId=${libraryId}`);
}
updateProfile(profile: ReadingProfile) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + 'reading-profile', profile);
}
updateParentProfile(seriesId: number, profile: ReadingProfile) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + `reading-profile/update-parent?seriesId=${seriesId}`, profile);
}
createProfile(profile: ReadingProfile) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + 'reading-profile/create', profile);
}
promoteProfile(profileId: number) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + "reading-profile/promote?profileId=" + profileId, {});
}
updateImplicit(profile: ReadingProfile, seriesId: number) {
return this.httpClient.post<ReadingProfile>(this.baseUrl + "reading-profile/series?seriesId="+seriesId, profile);
}
getAllProfiles() {
return this.httpClient.get<ReadingProfile[]>(this.baseUrl + 'reading-profile/all');
}
delete(id: number) {
return this.httpClient.delete(this.baseUrl + `reading-profile?profileId=${id}`);
}
addToSeries(id: number, seriesId: number) {
return this.httpClient.post(this.baseUrl + `reading-profile/series/${seriesId}?profileId=${id}`, {});
}
clearSeriesProfiles(seriesId: number) {
return this.httpClient.delete(this.baseUrl + `reading-profile/series/${seriesId}`, {});
}
addToLibrary(id: number, libraryId: number) {
return this.httpClient.post(this.baseUrl + `reading-profile/library/${libraryId}?profileId=${id}`, {});
}
clearLibraryProfiles(libraryId: number) {
return this.httpClient.delete(this.baseUrl + `reading-profile/library/${libraryId}`, {});
}
bulkAddToSeries(id: number, seriesIds: number[]) {
return this.httpClient.post(this.baseUrl + `reading-profile/bulk?profileId=${id}`, seriesIds);
}
}

View file

@ -9,12 +9,12 @@ import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {allEncodeFormats} from '../_models/encode-format';
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {allCoverImageSizes, CoverImageSize} from '../_models/cover-image-size';
import {pageLayoutModes} from "../../_models/preferences/preferences";
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
import {EncodeFormatPipe} from "../../_pipes/encode-format.pipe";
import {CoverImageSizePipe} from "../../_pipes/cover-image-size.pipe";
import {ConfirmService} from "../../shared/confirm.service";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {pageLayoutModes} from "../../_models/preferences/reading-profiles";
@Component({
selector: 'app-manage-media-settings',

View file

@ -61,6 +61,8 @@
<a ngbNavLink>{{t('settings-header')}}</a>
<ng-template ngbNavContent>
<app-reader-settings
[seriesId]="seriesId"
[readingProfile]="readingProfile"
(colorThemeUpdate)="updateColorTheme($event)"
(styleUpdate)="updateReaderStyles($event)"
(clickToPaginateChanged)="showPaginationOverlay($event)"

View file

@ -21,7 +21,6 @@ import {ToastrService} from 'ngx-toastr';
import {forkJoin, fromEvent, merge, of} from 'rxjs';
import {catchError, debounceTime, distinctUntilChanged, take, tap} from 'rxjs/operators';
import {Chapter} from 'src/app/_models/chapter';
import {AccountService} from 'src/app/_services/account.service';
import {NavService} from 'src/app/_services/nav.service';
import {CHAPTER_ID_DOESNT_EXIST, CHAPTER_ID_NOT_FETCHED, ReaderService} from 'src/app/_services/reader.service';
import {SeriesService} from 'src/app/_services/series.service';
@ -40,7 +39,6 @@ import {LibraryType} from 'src/app/_models/library/library';
import {BookTheme} from 'src/app/_models/preferences/book-theme';
import {BookPageLayoutMode} from 'src/app/_models/readers/book-page-layout-mode';
import {PageStyle, ReaderSettingsComponent} from '../reader-settings/reader-settings.component';
import {User} from 'src/app/_models/user';
import {ThemeService} from 'src/app/_services/theme.service';
import {ScrollService} from 'src/app/_services/scroll.service';
import {PAGING_DIRECTION} from 'src/app/manga-reader/_models/reader-enums';
@ -63,6 +61,7 @@ import {
PersonalToCEvent
} from "../personal-table-of-contents/personal-table-of-contents.component";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
import {ConfirmService} from "../../../shared/confirm.service";
@ -121,7 +120,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly accountService = inject(AccountService);
private readonly seriesService = inject(SeriesService);
private readonly readerService = inject(ReaderService);
private readonly renderer = inject(Renderer2);
@ -148,7 +146,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
volumeId!: number;
chapterId!: number;
chapter!: Chapter;
user!: User;
readingProfile!: ReadingProfile;
/**
* Reading List id. Defaults to -1.
@ -610,7 +608,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.libraryId = parseInt(libraryId, 10);
this.seriesId = parseInt(seriesId, 10);
this.chapterId = parseInt(chapterId, 10);
@ -623,19 +620,23 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.cdRef.markForCheck();
this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => {
this.readingProfile = data['readingProfile'];
this.cdRef.markForCheck();
this.memberService.hasReadingProgress(this.libraryId).pipe(take(1)).subscribe(hasProgress => {
if (!hasProgress) {
this.toggleDrawer();
this.toastr.info(translate('toasts.book-settings-info'));
if (this.readingProfile == null) {
this.router.navigateByUrl('/home');
return;
}
});
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.user = user;
this.init();
}
this.memberService.hasReadingProgress(this.libraryId).pipe(take(1)).subscribe(hasProgress => {
if (!hasProgress) {
this.toggleDrawer();
this.toastr.info(translate('toasts.book-settings-info'));
}
});
this.init();
});
}
@ -670,7 +671,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.chapters = results.chapters;
this.pageNum = results.progress.pageNum;
this.cdRef.markForCheck();
if (results.progress.bookScrollId) this.lastSeenScrollPartPath = results.progress.bookScrollId;
if (results.progress.bookScrollId) {
this.lastSeenScrollPartPath = results.progress.bookScrollId;
}
this.continuousChaptersStack.push(this.chapterId);
@ -771,6 +775,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readerService.closeReader(this.readingListMode, this.readingListId);
}
sortElements(a: Element, b: Element) {
const aTop = a.getBoundingClientRect().top;
const bTop = b.getBoundingClientRect().top;
@ -1049,7 +1054,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Virtual Paging stuff
this.updateWidthAndHeightCalcs();
this.updateLayoutMode(this.layoutMode || BookPageLayoutMode.Default);
this.updateLayoutMode(this.layoutMode);
this.addEmptyPageIfRequired();
// Find all the part ids and their top offset

View file

@ -1,172 +1,190 @@
<ng-container *transloco="let t; read: 'reader-settings'">
<!-- IDEA: Move the whole reader drawer into this component and have it self contained -->
<form [formGroup]="settingsForm">
<div ngbAccordion [closeOthers]="false" #acc="ngbAccordion">
<div ngbAccordionItem id="general-panel" title="General Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button ngbAccordionButton class="accordion-button" type="button" [attr.aria-expanded]="acc.isExpanded('general-panel')" aria-controls="collapseOne">
{{t('general-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="control-container" >
<div class="controls">
<div class="mb-3">
<label for="library-type" class="form-label">{{t('font-family-label')}}</label>
<select class="form-select" id="library-type" formControlName="bookReaderFontFamily">
<option [value]="opt" *ngFor="let opt of fontOptions; let i = index">{{opt | titlecase}}</option>
</select>
@if (readingProfile !== null) {
<ng-container *transloco="let t; read: 'reader-settings'">
<!-- IDEA: Move the whole reader drawer into this component and have it self contained -->
<form [formGroup]="settingsForm">
<div ngbAccordion [closeOthers]="false" #acc="ngbAccordion">
<div ngbAccordionItem id="general-panel" title="General Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button ngbAccordionButton class="accordion-button" type="button" [attr.aria-expanded]="acc.isExpanded('general-panel')" aria-controls="collapseOne">
{{t('general-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="control-container" >
<div class="controls">
<div class="mb-3">
<label for="library-type" class="form-label">{{t('font-family-label')}}</label>
<select class="form-select" id="library-type" formControlName="bookReaderFontFamily">
<option [value]="opt" *ngFor="let opt of fontOptions; let i = index">{{opt | titlecase}}</option>
</select>
</div>
</div>
</div>
<div class="row g-0 controls">
<label for="fontsize" class="form-label col-6">{{t('font-size-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
<div class="row g-0 controls">
<label for="fontsize" class="form-label col-6">{{t('font-size-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
<i class="fa-solid fa-font" style="font-size: 12px;"></i>
<input type="range" class="form-range ms-2 me-2" id="fontsize" min="50" max="300" step="10" formControlName="bookReaderFontSize" [ngbTooltip]="settingsForm.get('bookReaderFontSize')?.value + '%'">
<i class="fa-solid fa-font" style="font-size: 24px;"></i>
</span>
</div>
</div>
<div class="row g-0 controls">
<label for="linespacing" class="form-label col-6">{{t('line-spacing-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
1x
<input type="range" class="form-range ms-2 me-2" id="linespacing" min="100" max="200" step="10" formControlName="bookReaderLineSpacing" [ngbTooltip]="settingsForm.get('bookReaderLineSpacing')?.value + '%'">
2.5x
<div class="row g-0 controls">
<label for="linespacing" class="form-label col-6">{{t('line-spacing-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
{{t('line-spacing-min-label')}}
<input type="range" class="form-range ms-2 me-2" id="linespacing" min="100" max="200" step="10" formControlName="bookReaderLineSpacing" [ngbTooltip]="settingsForm.get('bookReaderLineSpacing')?.value + '%'">
{{t('line-spacing-max-label')}}
</span>
</div>
</div>
<div class="row g-0 controls">
<label for="margin" class="form-label col-6">{{t('margin-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
<div class="row g-0 controls">
<label for="margin" class="form-label col-6">{{t('margin-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
<i class="fa-solid fa-outdent"></i>
<input type="range" class="form-range ms-2 me-2" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" [ngbTooltip]="settingsForm.get('bookReaderMargin')?.value + '%'">
<i class="fa-solid fa-indent"></i>
</span>
</div>
</div>
<div class="row g-0 justify-content-between mt-2">
<button (click)="resetSettings()" class="btn btn-primary col">{{t('reset-to-defaults')}}</button>
<div class="row g-0 justify-content-between mt-2">
<button (click)="resetSettings()" class="btn btn-primary col">{{t('reset-to-defaults')}}</button>
</div>
</div>
</div>
</ng-template>
</ng-template>
</div>
</div>
</div>
</div>
<div ngbAccordionItem id="reader-panel" title="Reader Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('reader-panel')" aria-controls="collapseOne">
{{t('reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="readingdirection" class="form-label">{{t('reading-direction-label')}}</label>
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirectionModel === ReadingDirection.LeftToRight ? t('left-to-right') : t('right-to-left')}}">
<i class="fa {{readingDirectionModel === ReadingDirection.LeftToRight ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i>
<span class="phone-hidden">&nbsp;{{readingDirectionModel === ReadingDirection.LeftToRight ? t('left-to-right') : t('right-to-left')}}</span>
</button>
</div>
<div class="controls" style="display: flex; justify-content: space-between; align-items: center; ">
<label for="writing-style" class="form-label">{{t('writing-style-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="writingStyleTooltip" role="button" tabindex="0" aria-describedby="writingStyle-help"></i></label>
<ng-template #writingStyleTooltip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="writingStyle-help"><ng-container [ngTemplateOutlet]="writingStyleTooltip"></ng-container></span>
<button (click)="toggleWritingStyle()" id="writing-style" class="btn btn-icon" aria-labelledby="writingStyle-help" title="{{writingStyleModel === WritingStyle.Horizontal ? t('horizontal') : t('vertical')}}">
<i class="fa {{writingStyleModel === WritingStyle.Horizontal ? 'fa-arrows-left-right' : 'fa-arrows-up-down' }}" aria-hidden="true"></i>
<span class="phone-hidden"> {{writingStyleModel === WritingStyle.Horizontal ? t('horizontal') : t('vertical') }}</span>
</button>
<div ngbAccordionItem id="reader-panel" title="Reader Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('reader-panel')" aria-controls="collapseOne">
{{t('reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="readingdirection" class="form-label">{{t('reading-direction-label')}}</label>
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirectionModel === ReadingDirection.LeftToRight ? t('left-to-right') : t('right-to-left')}}">
<i class="fa {{readingDirectionModel === ReadingDirection.LeftToRight ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i>
<span class="phone-hidden">&nbsp;{{readingDirectionModel === ReadingDirection.LeftToRight ? t('left-to-right') : t('right-to-left')}}</span>
</button>
</div>
<div class="controls" style="display: flex; justify-content: space-between; align-items: center; ">
<label for="writing-style" class="form-label">{{t('writing-style-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="writingStyleTooltip" role="button" tabindex="0" aria-describedby="writingStyle-help"></i></label>
<ng-template #writingStyleTooltip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="writingStyle-help"><ng-container [ngTemplateOutlet]="writingStyleTooltip"></ng-container></span>
<button (click)="toggleWritingStyle()" id="writing-style" class="btn btn-icon" aria-labelledby="writingStyle-help" title="{{writingStyleModel === WritingStyle.Horizontal ? t('horizontal') : t('vertical')}}">
<i class="fa {{writingStyleModel === WritingStyle.Horizontal ? 'fa-arrows-left-right' : 'fa-arrows-up-down' }}" aria-hidden="true"></i>
<span class="phone-hidden"> {{writingStyleModel === WritingStyle.Horizontal ? t('horizontal') : t('vertical') }}</span>
</button>
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="tap-pagination" class="form-label">{{t('tap-to-paginate-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tapPagination-help"></i></label>
<ng-template #tapPaginationTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="tapPagination-help">
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="tap-pagination" class="form-label">{{t('tap-to-paginate-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tapPagination-help"></i></label>
<ng-template #tapPaginationTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="tapPagination-help">
<ng-container [ngTemplateOutlet]="tapPaginationTooltip"></ng-container>
</span>
<div class="form-check form-switch">
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tapPagination-help">
<label>{{settingsForm.get('bookReaderTapToPaginate')?.value ? t('on') : t('off')}} </label>
<div class="form-check form-switch">
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tapPagination-help">
<label>{{settingsForm.get('bookReaderTapToPaginate')?.value ? t('on') : t('off')}} </label>
</div>
</div>
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="immersive-mode" class="form-label">{{t('immersive-mode-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="immersiveModeTooltip" role="button" tabindex="0" aria-describedby="immersiveMode-help"></i></label>
<ng-template #immersiveModeTooltip>{{t('immersive-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="immersiveMode-help">
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="immersive-mode" class="form-label">{{t('immersive-mode-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="immersiveModeTooltip" role="button" tabindex="0" aria-describedby="immersiveMode-help"></i></label>
<ng-template #immersiveModeTooltip>{{t('immersive-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="immersiveMode-help">
<ng-container [ngTemplateOutlet]="immersiveModeTooltip"></ng-container>
</span>
<div class="form-check form-switch">
<input type="checkbox" id="immersive-mode" formControlName="bookReaderImmersiveMode" class="form-check-input" aria-labelledby="immersiveMode-help">
<label>{{settingsForm.get('bookReaderImmersiveMode')?.value ? t('on') : t('off')}} </label>
<div class="form-check form-switch">
<input type="checkbox" id="immersive-mode" formControlName="bookReaderImmersiveMode" class="form-check-input" aria-labelledby="immersiveMode-help">
<label>{{settingsForm.get('bookReaderImmersiveMode')?.value ? t('on') : t('off')}} </label>
</div>
</div>
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="fullscreen" class="form-label">{{t('fullscreen-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top"
[ngbTooltip]="fullscreenTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>{{t('fullscreen-tooltip')}}</ng-template>
<span class="visually-hidden" id="fullscreen-help">
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="fullscreen" class="form-label">{{t('fullscreen-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top"
[ngbTooltip]="fullscreenTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>{{t('fullscreen-tooltip')}}</ng-template>
<span class="visually-hidden" id="fullscreen-help">
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
</span>
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
<i class="fa {{isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}} {{isFullscreen ? 'icon-primary-color' : ''}}" aria-hidden="true"></i>
<span *ngIf="activeTheme?.isDarkTheme">&nbsp;{{isFullscreen ? t('exit') : t('enter')}}</span>
</button>
</div>
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
<i class="fa {{isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}} {{isFullscreen ? 'icon-primary-color' : ''}}" aria-hidden="true"></i>
<span *ngIf="activeTheme?.isDarkTheme">&nbsp;{{isFullscreen ? t('exit') : t('enter')}}</span>
</button>
</div>
<div class="controls">
<label id="layout-mode" class="form-label" style="margin-bottom:0.5rem">{{t('layout-mode-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="layoutTooltip" role="button" tabindex="1" aria-describedby="layout-help"></i></label>
<ng-template #layoutTooltip><span [innerHTML]="t('layout-mode-tooltip')"></span></ng-template>
<span class="visually-hidden" id="layout-help">
<div class="controls">
<label id="layout-mode" class="form-label" style="margin-bottom:0.5rem">{{t('layout-mode-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="layoutTooltip" role="button" tabindex="1" aria-describedby="layout-help"></i></label>
<ng-template #layoutTooltip><span [innerHTML]="t('layout-mode-tooltip')"></span></ng-template>
<span class="visually-hidden" id="layout-help">
<ng-container [ngTemplateOutlet]="layoutTooltip"></ng-container>
</span>
<br>
<div class="btn-group d-flex justify-content-center" role="group" [attr.aria-label]="t('layout-mode-label')">
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Default" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">{{t('layout-mode-option-scroll')}}</label>
<br>
<div class="btn-group d-flex justify-content-center" role="group" [attr.aria-label]="t('layout-mode-label')">
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Default" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">{{t('layout-mode-option-scroll')}}</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column1" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">{{t('layout-mode-option-1col')}}</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column1" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">{{t('layout-mode-option-1col')}}</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column2" class="btn-check" id="layout-mode-col2" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col2">{{t('layout-mode-option-2col')}}</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column2" class="btn-check" id="layout-mode-col2" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col2">{{t('layout-mode-option-2col')}}</label>
</div>
</div>
</div>
</ng-template>
</ng-template>
</div>
</div>
</div>
</div>
<div ngbAccordionItem id="color-panel" [title]="t('color-theme-title')" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('color-panel')" aria-controls="collapseOne">
{{t('color-theme-title')}}
<div ngbAccordionItem id="color-panel" [title]="t('color-theme-title')" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('color-panel')" aria-controls="collapseOne">
{{t('color-theme-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls">
<ng-container *ngFor="let theme of themes">
<button class="btn btn-icon color" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
<div class="dot" [ngStyle]="{'background-color': theme.colorHash}"></div>
{{t(theme.translationKey)}}
</button>
</ng-container>
</div>
</ng-template>
</div>
</div>
</div>
<div class="row g-0 mt-2">
<button class="btn btn-primary col-12 mb-2"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit || !parentReadingProfile"
(click)="updateParentPref()">
{{ t('update-parent', {name: parentReadingProfile?.name || t('loading')}) }}
</button>
<button class="btn btn-primary col-12 mb-2"
[ngbTooltip]="t('create-new-tooltip')"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit"
(click)="createNewProfileFromImplicit()">
{{ t('create-new') }}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls">
<ng-container *ngFor="let theme of themes">
<button class="btn btn-icon color" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
<div class="dot" [ngStyle]="{'background-color': theme.colorHash}"></div>
{{t(theme.translationKey)}}
</button>
</ng-container>
</div>
</ng-template>
</div>
</div>
</div>
</div>
</form>
</ng-container>
</div>
</form>
</ng-container>
}

View file

@ -1,32 +1,46 @@
import { DOCUMENT, NgFor, NgTemplateOutlet, NgIf, NgClass, NgStyle, TitleCasePipe } from '@angular/common';
import {DOCUMENT, NgClass, NgFor, NgIf, NgStyle, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
Component,
DestroyRef,
EventEmitter,
inject,
Inject,
Input,
OnInit,
Output
} from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { take } from 'rxjs';
import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode';
import { BookTheme } from 'src/app/_models/preferences/book-theme';
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
import { WritingStyle } from 'src/app/_models/preferences/writing-style';
import { ThemeProvider } from 'src/app/_models/preferences/site-theme';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import { ThemeService } from 'src/app/_services/theme.service';
import { FontFamily, BookService } from '../../_services/book.service';
import { BookBlackTheme } from '../../_models/book-black-theme';
import { BookDarkTheme } from '../../_models/book-dark-theme';
import { BookWhiteTheme } from '../../_models/book-white-theme';
import { BookPaperTheme } from '../../_models/book-paper-theme';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {skip, take} from 'rxjs';
import {BookPageLayoutMode} from 'src/app/_models/readers/book-page-layout-mode';
import {BookTheme} from 'src/app/_models/preferences/book-theme';
import {ReadingDirection} from 'src/app/_models/preferences/reading-direction';
import {WritingStyle} from 'src/app/_models/preferences/writing-style';
import {ThemeProvider} from 'src/app/_models/preferences/site-theme';
import {User} from 'src/app/_models/user';
import {AccountService} from 'src/app/_services/account.service';
import {ThemeService} from 'src/app/_services/theme.service';
import {BookService, FontFamily} from '../../_services/book.service';
import {BookBlackTheme} from '../../_models/book-black-theme';
import {BookDarkTheme} from '../../_models/book-dark-theme';
import {BookWhiteTheme} from '../../_models/book-white-theme';
import {BookPaperTheme} from '../../_models/book-paper-theme';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import {TranslocoDirective} from "@jsverse/transloco";
import {
NgbAccordionBody,
NgbAccordionButton,
NgbAccordionCollapse,
NgbAccordionDirective,
NgbAccordionHeader,
NgbAccordionItem,
NgbTooltip
} from '@ng-bootstrap/ng-bootstrap';
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
import {ReadingProfile, ReadingProfileKind} from "../../../_models/preferences/reading-profiles";
import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators";
import {ToastrService} from "ngx-toastr";
/**
* Used for book reader. Do not use for other components
@ -89,9 +103,13 @@ const mobileBreakpointMarginOverride = 700;
templateUrl: './reader-settings.component.html',
styleUrls: ['./reader-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgFor, NgbTooltip, NgTemplateOutlet, NgIf, NgClass, NgStyle, TitleCasePipe, TranslocoDirective]
imports: [ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionButton,
NgbAccordionCollapse, NgbAccordionBody, NgFor, NgbTooltip, NgTemplateOutlet, NgIf, NgClass, NgStyle,
TitleCasePipe, TranslocoDirective]
})
export class ReaderSettingsComponent implements OnInit {
@Input({required:true}) seriesId!: number;
@Input({required:true}) readingProfile!: ReadingProfile;
/**
* Outputs when clickToPaginate is changed
*/
@ -147,6 +165,11 @@ export class ReaderSettingsComponent implements OnInit {
settingsForm: FormGroup = new FormGroup({});
/**
* The reading profile itself, unless readingProfile is implicit
*/
parentReadingProfile: ReadingProfile | null = null;
/**
* System provided themes
*/
@ -166,136 +189,169 @@ export class ReaderSettingsComponent implements OnInit {
return WritingStyle;
}
constructor(private bookService: BookService, private accountService: AccountService,
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService,
private readonly cdRef: ChangeDetectorRef) {}
private readonly cdRef: ChangeDetectorRef, private readingProfileService: ReadingProfileService,
private toastr: ToastrService) {}
ngOnInit(): void {
if (this.readingProfile.kind === ReadingProfileKind.Implicit) {
this.readingProfileService.getForSeries(this.seriesId, true).subscribe(parent => {
this.parentReadingProfile = parent;
this.cdRef.markForCheck();
})
} else {
this.parentReadingProfile = this.readingProfile;
this.cdRef.markForCheck();
}
this.fontFamilies = this.bookService.getFontFamilies();
this.fontOptions = this.fontFamilies.map(f => f.title);
this.cdRef.markForCheck();
this.setupSettings();
this.setTheme(this.readingProfile.bookReaderThemeName || this.themeService.defaultBookTheme, false);
this.cdRef.markForCheck();
// Emit first time so book reader gets the setting
this.readingDirection.emit(this.readingDirectionModel);
this.bookReaderWritingStyle.emit(this.writingStyleModel);
this.clickToPaginateChanged.emit(this.readingProfile.bookReaderTapToPaginate);
this.layoutModeUpdate.emit(this.readingProfile.bookReaderLayoutMode);
this.immersiveMode.emit(this.readingProfile.bookReaderImmersiveMode);
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.user = user;
if (this.user.preferences.bookReaderFontFamily === undefined) {
this.user.preferences.bookReaderFontFamily = 'default';
}
if (this.user.preferences.bookReaderFontSize === undefined || this.user.preferences.bookReaderFontSize < 50) {
this.user.preferences.bookReaderFontSize = 100;
}
if (this.user.preferences.bookReaderLineSpacing === undefined || this.user.preferences.bookReaderLineSpacing < 100) {
this.user.preferences.bookReaderLineSpacing = 100;
}
if (this.user.preferences.bookReaderMargin === undefined) {
this.user.preferences.bookReaderMargin = 0;
}
if (this.user.preferences.bookReaderReadingDirection === undefined) {
this.user.preferences.bookReaderReadingDirection = ReadingDirection.LeftToRight;
}
if (this.user.preferences.bookReaderWritingStyle === undefined) {
this.user.preferences.bookReaderWritingStyle = WritingStyle.Horizontal;
}
this.readingDirectionModel = this.user.preferences.bookReaderReadingDirection;
this.writingStyleModel = this.user.preferences.bookReaderWritingStyle;
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(fontName => {
const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family;
if (familyName === 'default') {
this.pageStyles['font-family'] = 'inherit';
} else {
this.pageStyles['font-family'] = "'" + familyName + "'";
}
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
this.settingsForm.get('bookReaderFontSize')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['font-size'] = value + '%';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.clickToPaginateChanged.emit(value);
});
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['line-height'] = value + '%';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['margin-left'] = value + 'vw';
this.pageStyles['margin-right'] = value + 'vw';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((layoutMode: BookPageLayoutMode) => {
this.layoutModeUpdate.emit(layoutMode);
});
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user.preferences.bookReaderImmersiveMode, []));
this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((immersiveMode: boolean) => {
if (immersiveMode) {
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true);
}
this.immersiveMode.emit(immersiveMode);
});
this.setTheme(this.user.preferences.bookReaderThemeName || this.themeService.defaultBookTheme);
this.cdRef.markForCheck();
// Emit first time so book reader gets the setting
this.readingDirection.emit(this.readingDirectionModel);
this.bookReaderWritingStyle.emit(this.writingStyleModel);
this.clickToPaginateChanged.emit(this.user.preferences.bookReaderTapToPaginate);
this.layoutModeUpdate.emit(this.user.preferences.bookReaderLayoutMode);
this.immersiveMode.emit(this.user.preferences.bookReaderImmersiveMode);
this.resetSettings();
} else {
this.resetSettings();
}
// User needs to be loaded before we call this
this.resetSettings();
});
}
setupSettings() {
if (!this.readingProfile) return;
if (this.readingProfile.bookReaderFontFamily === undefined) {
this.readingProfile.bookReaderFontFamily = 'default';
}
if (this.readingProfile.bookReaderFontSize === undefined || this.readingProfile.bookReaderFontSize < 50) {
this.readingProfile.bookReaderFontSize = 100;
}
if (this.readingProfile.bookReaderLineSpacing === undefined || this.readingProfile.bookReaderLineSpacing < 100) {
this.readingProfile.bookReaderLineSpacing = 100;
}
if (this.readingProfile.bookReaderMargin === undefined) {
this.readingProfile.bookReaderMargin = 0;
}
if (this.readingProfile.bookReaderReadingDirection === undefined) {
this.readingProfile.bookReaderReadingDirection = ReadingDirection.LeftToRight;
}
if (this.readingProfile.bookReaderWritingStyle === undefined) {
this.readingProfile.bookReaderWritingStyle = WritingStyle.Horizontal;
}
this.readingDirectionModel = this.readingProfile.bookReaderReadingDirection;
this.writingStyleModel = this.readingProfile.bookReaderWritingStyle;
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.readingProfile.bookReaderFontFamily, []));
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(fontName => {
const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family;
if (familyName === 'default') {
this.pageStyles['font-family'] = 'inherit';
} else {
this.pageStyles['font-family'] = "'" + familyName + "'";
}
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.readingProfile.bookReaderFontSize, []));
this.settingsForm.get('bookReaderFontSize')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['font-size'] = value + '%';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.readingProfile.bookReaderTapToPaginate, []));
this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.clickToPaginateChanged.emit(value);
});
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.readingProfile.bookReaderLineSpacing, []));
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['line-height'] = value + '%';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.readingProfile.bookReaderMargin, []));
this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['margin-left'] = value + 'vw';
this.pageStyles['margin-right'] = value + 'vw';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('layoutMode', new FormControl(this.readingProfile.bookReaderLayoutMode, []));
this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((layoutMode: BookPageLayoutMode) => {
this.layoutModeUpdate.emit(layoutMode);
});
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.readingProfile.bookReaderImmersiveMode, []));
this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((immersiveMode: boolean) => {
if (immersiveMode) {
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true);
}
this.immersiveMode.emit(immersiveMode);
});
// Update implicit reading profile while changing settings
this.settingsForm.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
skip(1), // Skip the initial creation of the form, we do not want an implicit profile of this snapshot
takeUntilDestroyed(this.destroyRef),
tap(_ => this.updateImplicit())
).subscribe();
}
resetSettings() {
if (!this.readingProfile) return;
if (this.user) {
this.setPageStyles(this.user.preferences.bookReaderFontFamily, this.user.preferences.bookReaderFontSize + '%', this.user.preferences.bookReaderMargin + 'vw', this.user.preferences.bookReaderLineSpacing + '%');
this.setPageStyles(this.readingProfile.bookReaderFontFamily, this.readingProfile.bookReaderFontSize + '%', this.readingProfile.bookReaderMargin + 'vw', this.readingProfile.bookReaderLineSpacing + '%');
} else {
this.setPageStyles();
}
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
this.settingsForm.get('bookReaderFontSize')?.setValue(this.user.preferences.bookReaderFontSize);
this.settingsForm.get('bookReaderLineSpacing')?.setValue(this.user.preferences.bookReaderLineSpacing);
this.settingsForm.get('bookReaderMargin')?.setValue(this.user.preferences.bookReaderMargin);
this.settingsForm.get('bookReaderReadingDirection')?.setValue(this.user.preferences.bookReaderReadingDirection);
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(this.user.preferences.bookReaderTapToPaginate);
this.settingsForm.get('bookReaderLayoutMode')?.setValue(this.user.preferences.bookReaderLayoutMode);
this.settingsForm.get('bookReaderImmersiveMode')?.setValue(this.user.preferences.bookReaderImmersiveMode);
this.settingsForm.get('bookReaderWritingStyle')?.setValue(this.user.preferences.bookReaderWritingStyle);
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.readingProfile.bookReaderFontFamily);
this.settingsForm.get('bookReaderFontSize')?.setValue(this.readingProfile.bookReaderFontSize);
this.settingsForm.get('bookReaderLineSpacing')?.setValue(this.readingProfile.bookReaderLineSpacing);
this.settingsForm.get('bookReaderMargin')?.setValue(this.readingProfile.bookReaderMargin);
this.settingsForm.get('bookReaderReadingDirection')?.setValue(this.readingProfile.bookReaderReadingDirection);
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(this.readingProfile.bookReaderTapToPaginate);
this.settingsForm.get('bookReaderLayoutMode')?.setValue(this.readingProfile.bookReaderLayoutMode);
this.settingsForm.get('bookReaderImmersiveMode')?.setValue(this.readingProfile.bookReaderImmersiveMode);
this.settingsForm.get('bookReaderWritingStyle')?.setValue(this.readingProfile.bookReaderWritingStyle);
this.cdRef.detectChanges();
this.styleUpdate.emit(this.pageStyles);
}
updateImplicit() {
this.readingProfileService.updateImplicit(this.packReadingProfile(), this.seriesId).subscribe({
next: newProfile => {
this.readingProfile = newProfile;
this.cdRef.markForCheck();
},
error: err => {
console.error(err);
}
})
}
/**
* Internal method to be used by resetSettings. Pass items in with quantifiers
*/
@ -318,11 +374,15 @@ export class ReaderSettingsComponent implements OnInit {
};
}
setTheme(themeName: string) {
setTheme(themeName: string, update: boolean = true) {
const theme = this.themes.find(t => t.name === themeName);
this.activeTheme = theme;
this.cdRef.markForCheck();
this.colorThemeUpdate.emit(theme);
if (update) {
this.updateImplicit();
}
}
toggleReadingDirection() {
@ -334,6 +394,7 @@ export class ReaderSettingsComponent implements OnInit {
this.cdRef.markForCheck();
this.readingDirection.emit(this.readingDirectionModel);
this.updateImplicit();
}
toggleWritingStyle() {
@ -345,6 +406,7 @@ export class ReaderSettingsComponent implements OnInit {
this.cdRef.markForCheck();
this.bookReaderWritingStyle.emit(this.writingStyleModel);
this.updateImplicit();
}
toggleFullscreen() {
@ -352,4 +414,53 @@ export class ReaderSettingsComponent implements OnInit {
this.cdRef.markForCheck();
this.fullscreen.emit();
}
// menu only code
updateParentPref() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.readingProfileService.updateParentProfile(this.seriesId, this.packReadingProfile()).subscribe(newProfile => {
this.readingProfile = newProfile;
this.toastr.success(translate('manga-reader.reading-profile-updated'));
this.cdRef.markForCheck();
});
}
createNewProfileFromImplicit() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.readingProfileService.promoteProfile(this.readingProfile.id).subscribe(newProfile => {
this.readingProfile = newProfile;
this.parentReadingProfile = newProfile; // profile is no longer implicit
this.cdRef.markForCheck();
this.toastr.success(translate("manga-reader.reading-profile-promoted"));
});
}
private packReadingProfile(): ReadingProfile {
const modelSettings = this.settingsForm.getRawValue();
const data = {...this.readingProfile!};
data.bookReaderFontFamily = modelSettings.bookReaderFontFamily;
data.bookReaderFontSize = modelSettings.bookReaderFontSize
data.bookReaderLineSpacing = modelSettings.bookReaderLineSpacing;
data.bookReaderMargin = modelSettings.bookReaderMargin;
data.bookReaderTapToPaginate = modelSettings.bookReaderTapToPaginate;
data.bookReaderLayoutMode = modelSettings.layoutMode;
data.bookReaderImmersiveMode = modelSettings.bookReaderImmersiveMode;
data.bookReaderReadingDirection = this.readingDirectionModel;
data.bookReaderWritingStyle = this.writingStyleModel;
if (this.activeTheme) {
data.bookReaderThemeName = this.activeTheme.name;
}
return data;
}
protected readonly ReadingProfileKind = ReadingProfileKind;
}

View file

@ -48,6 +48,7 @@ export const BookPaperTheme = `
--btn-disabled-bg-color: #343a40;
--btn-disabled-text-color: #efefef;
--btn-disabled-border-color: #6c757d;
--btn-outline-primary-text-color: black;
/* Inputs */
--input-bg-color: white;
@ -89,6 +90,8 @@ export const BookPaperTheme = `
/* Custom variables */
--theme-bg-color: #fff3c9;
--bs-secondary-bg: darkgrey;
}
.reader-container {

View file

@ -51,6 +51,7 @@ export const BookWhiteTheme = `
--btn-disabled-bg-color: #343a40;
--btn-disabled-text-color: #efefef;
--btn-disabled-border-color: #6c757d;
--btn-outline-primary-text-color: black;
/* Inputs */
--input-bg-color: white;

View file

@ -0,0 +1,56 @@
<ng-container *transloco="let t; prefix: 'bulk-set-reading-profile-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
</div>
<form [formGroup]="profileForm">
<div class="modal-body">
@if (profiles.length >= MaxItems) {
<div class="mb-3">
<label for="filter" class="form-label">{{t('filter-label')}}</label>
<div class="input-group">
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="clear()">Clear</button>
</div>
</div>
}
<ul class="list-group">
@for(profile of profiles | filter: filterList; let i = $index; track profile.name) {
<li class="list-group-item clickable" tabindex="0" role="option" (click)="addToProfile(profile)">
<div class="p-2 group-item d-flex justify-content-between align-items-center">
<div class="fw-bold">{{profile.name | sentenceCase}}</div>
@if (currentProfile && currentProfile.name === profile.name) {
<span class="pill p-1 ms-1">{{t('bound')}}</span>
}
</div>
</li>
}
@if (profiles.length === 0 && !isLoading) {
<li class="list-group-item">{{t('no-data')}}</li>
}
@if (isLoading) {
<li class="list-group-item">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{t('loading')}}</span>
</div>
</li>
}
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
</div>
</form>
</ng-container>

View file

@ -0,0 +1,14 @@
.clickable:hover, .clickable:focus {
background-color: var(--list-group-hover-bg-color, --primary-color);
}
.pill {
font-size: .8rem;
background-color: var(--card-bg-color);
border-radius: 0.375rem;
color: var(--badge-text-color);
&.active {
background-color : var(--primary-color);
}
}

View file

@ -0,0 +1,120 @@
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, inject, Input, OnInit, ViewChild} from '@angular/core';
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {ToastrService} from "ngx-toastr";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
import {ReadingProfile, ReadingProfileKind} from "../../../_models/preferences/reading-profiles";
import {FilterPipe} from "../../../_pipes/filter.pipe";
import {SentenceCasePipe} from "../../../_pipes/sentence-case.pipe";
@Component({
selector: 'app-bulk-set-reading-profile-modal',
imports: [
ReactiveFormsModule,
FilterPipe,
TranslocoDirective,
SentenceCasePipe
],
templateUrl: './bulk-set-reading-profile-modal.component.html',
styleUrl: './bulk-set-reading-profile-modal.component.scss'
})
export class BulkSetReadingProfileModalComponent implements OnInit, AfterViewInit {
private readonly modal = inject(NgbActiveModal);
private readonly readingProfileService = inject(ReadingProfileService);
private readonly toastr = inject(ToastrService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly MaxItems = 8;
/**
* Modal Header - since this code is used for multiple flows
*/
@Input({required: true}) title!: string;
/**
* Series Ids to add to Reading Profile
*/
@Input() seriesIds: Array<number> = [];
@Input() libraryId: number | undefined;
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
currentProfile: ReadingProfile | null = null;
profiles: Array<ReadingProfile> = [];
isLoading: boolean = false;
profileForm: FormGroup = new FormGroup({
filterQuery: new FormControl('', []), // Used for inline filtering when too many RPs
});
ngOnInit(): void {
this.profileForm.addControl('title', new FormControl(this.title, []));
this.isLoading = true;
this.cdRef.markForCheck();
if (this.libraryId !== undefined) {
this.readingProfileService.getForLibrary(this.libraryId).subscribe(profile => {
this.currentProfile = profile;
this.cdRef.markForCheck();
});
} else if (this.seriesIds.length === 1) {
this.readingProfileService.getForSeries(this.seriesIds[0], true).subscribe(profile => {
this.currentProfile = profile;
this.cdRef.markForCheck();
});
}
this.readingProfileService.getAllProfiles().subscribe(profiles => {
this.profiles = profiles;
this.isLoading = false;
this.cdRef.markForCheck();
});
}
ngAfterViewInit() {
// Shift focus to input
if (this.inputElem) {
this.inputElem.nativeElement.select();
this.cdRef.markForCheck();
}
}
close() {
this.modal.close();
}
addToProfile(profile: ReadingProfile) {
if (this.seriesIds.length == 1) {
this.readingProfileService.addToSeries(profile.id, this.seriesIds[0]).subscribe(() => {
this.toastr.success(translate('toasts.series-bound-to-reading-profile', {name: profile.name}));
this.modal.close();
});
return;
}
if (this.seriesIds.length > 1) {
this.readingProfileService.bulkAddToSeries(profile.id, this.seriesIds).subscribe(() => {
this.toastr.success(translate('toasts.series-bound-to-reading-profile', {name: profile.name}));
this.modal.close();
});
return;
}
if (this.libraryId) {
this.readingProfileService.addToLibrary(profile.id, this.libraryId).subscribe(() => {
this.toastr.success(translate('toasts.library-bound-to-reading-profile', {name: profile.name}));
this.modal.close();
});
}
}
filterList = (listItem: ReadingProfile) => {
return listItem.name.toLowerCase().indexOf((this.profileForm.value.filterQuery || '').toLowerCase()) >= 0;
}
clear() {
this.profileForm.get('filterQuery')?.setValue('');
}
protected readonly ReadingProfileKind = ReadingProfileKind;
}

View file

@ -144,7 +144,7 @@ export class BulkSelectionService {
*/
getActions(callback: (action: ActionItem<any>, data: any) => void) {
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection,
Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList];
Action.Delete, Action.AddToWantToReadList, Action.RemoveFromWantToReadList, Action.SetReadingProfile];
if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) {
return this.applyFilterToList(this.actionFactory.getSeriesActions(callback), allowedActions);

View file

@ -24,7 +24,7 @@ import {RelationKind} from 'src/app/_models/series-detail/relation-kind';
import {DecimalPipe} from "@angular/common";
import {RelationshipPipe} from "../../_pipes/relationship.pipe";
import {Device} from "../../_models/device/device";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-drawer/series-preview-drawer.component";
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
@ -41,6 +41,7 @@ import {ScrollService} from "../../_services/scroll.service";
import {ReaderService} from "../../_services/reader.service";
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
import {DefaultModalOptions} from "../../_models/default-modal-options";
import {ReadingProfileService} from "../../_services/reading-profile.service";
function deepClone(obj: any): any {
if (obj === null || typeof obj !== 'object') {
@ -92,6 +93,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
private readonly downloadService = inject(DownloadService);
private readonly scrollService = inject(ScrollService);
private readonly readerService = inject(ReaderService);
private readonly readingProfilesService = inject(ReadingProfileService);
private readonly translocoService = inject(TranslocoService);
@Input({required: true}) series!: Series;
@Input() libraryId = 0;
@ -276,6 +279,14 @@ export class SeriesCardComponent implements OnInit, OnChanges {
case Action.Download:
this.downloadService.download('series', this.series);
break;
case Action.SetReadingProfile:
this.actionService.setReadingProfileForMultiple([series]);
break;
case Action.ClearReadingProfile:
this.readingProfilesService.clearSeriesProfiles(series.id).subscribe(() => {
this.toastr.success(this.translocoService.translate('actionable.cleared-profile'));
});
break;
default:
break;
}

View file

@ -149,6 +149,14 @@ export class LibraryDetailComponent implements OnInit {
this.loadPage();
});
break;
case Action.SetReadingProfile:
this.actionService.setReadingProfileForMultiple(selectedSeries, (success) => {
this.bulkLoader = false;
this.cdRef.markForCheck();
if (!success) return;
this.bulkSelectionService.deselectAll();
this.loadPage();
})
}
}

View file

@ -53,7 +53,6 @@
<div class="reading-area"
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : '100dvh'}" #readingArea>
@if (readerMode !== ReaderMode.Webtoon) {
<div appDblClick (dblclick)="bookmarkPage($event)" (singleClick)="toggleMenu()">
<app-canvas-renderer
@ -311,7 +310,17 @@
<div class="col-md-6 col-sm-12">
<button class="btn btn-primary" (click)="savePref()">{{t('save-globally')}}</button>
<button class="btn btn-primary"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit || !parentReadingProfile"
(click)="updateParentPref()">
{{ t('update-parent', {name: parentReadingProfile?.name || t('loading')}) }}
</button>
<button class="btn btn-primary ms-sm-2"
[ngbTooltip]="t('create-new-tooltip')"
[disabled]="readingProfile.kind !== ReadingProfileKind.Implicit"
(click)="createNewProfileFromImplicit()">
{{ t('create-new') }}
</button>
</div>
</div>
</form>

View file

@ -32,7 +32,7 @@ import {
import {ChangeContext, LabelType, NgxSliderModule, Options} from '@angular-slider/ngx-slider';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component';
import {Stack} from 'src/app/shared/data-structures/stack';
@ -40,7 +40,6 @@ import {Breakpoint, KEY_CODES, UtilityService} from 'src/app/shared/_services/ut
import {LibraryType} from 'src/app/_models/library/library';
import {MangaFormat} from 'src/app/_models/manga-format';
import {PageSplitOption} from 'src/app/_models/preferences/page-split-option';
import {layoutModes, pageSplitOptions} from 'src/app/_models/preferences/preferences';
import {ReaderMode} from 'src/app/_models/preferences/reader-mode';
import {ReadingDirection} from 'src/app/_models/preferences/reading-direction';
import {ScalingOption} from 'src/app/_models/preferences/scaling-option';
@ -70,6 +69,13 @@ import {LoadingComponent} from '../../../shared/loading/loading.component';
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {shareReplay} from "rxjs/operators";
import {DblClickDirective} from "../../../_directives/dbl-click.directive";
import {
layoutModes,
pageSplitOptions,
ReadingProfile,
ReadingProfileKind
} from "../../../_models/preferences/reading-profiles";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
import {ConfirmService} from "../../../shared/confirm.service";
@ -123,10 +129,10 @@ enum KeyDirection {
])
])
],
imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective]
imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective, NgbTooltip]
})
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -151,6 +157,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly modalService = inject(NgbModal);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly toastr = inject(ToastrService);
private readonly readingProfileService = inject(ReadingProfileService);
private readonly confirmService = inject(ConfirmService);
protected readonly readerService = inject(ReaderService);
protected readonly utilityService = inject(UtilityService);
@ -197,6 +204,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
totalSeriesPages = 0;
totalSeriesPagesRead = 0;
user!: User;
readingProfile!: ReadingProfile;
/**
* The reading profile itself, unless readingProfile is implicit
*/
parentReadingProfile: ReadingProfile | null = null;
generalSettingsForm!: FormGroup;
readingDirection = ReadingDirection.LeftToRight;
@ -491,6 +503,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true';
this.bookmarkMode = this.route.snapshot.queryParamMap.get('bookmarkMode') === 'true';
this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => {
this.readingProfile = data['readingProfile'];
if (this.readingProfile == null) {
this.router.navigateByUrl('/home');
return;
}
// Requires seriesId to be set
this.setupReaderSettings();
this.cdRef.markForCheck();
});
const readingListId = this.route.snapshot.queryParamMap.get('readingListId');
if (readingListId != null) {
this.readingListMode = true;
@ -505,101 +528,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.user = user;
this.hasBookmarkRights = this.accountService.hasBookmarkRole(user) || this.accountService.hasAdminRole(user);
this.readingDirection = this.user.preferences.readingDirection;
this.scalingOption = this.user.preferences.scalingOption;
this.pageSplitOption = this.user.preferences.pageSplitOption;
this.autoCloseMenu = this.user.preferences.autoCloseMenu;
this.readerMode = this.user.preferences.readerMode;
this.layoutMode = this.user.preferences.layoutMode || LayoutMode.Single;
this.backgroundColor = this.user.preferences.backgroundColor || '#000000';
this.readerService.setOverrideStyles(this.backgroundColor);
this.generalSettingsForm = this.formBuilder.nonNullable.group({
autoCloseMenu: new FormControl(this.autoCloseMenu),
pageSplitOption: new FormControl(this.pageSplitOption),
fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)),
widthSlider: new FormControl('none'),
layoutMode: new FormControl(this.layoutMode),
darkness: new FormControl(100),
emulateBook: new FormControl(this.user.preferences.emulateBook),
swipeToPaginate: new FormControl(this.user.preferences.swipeToPaginate)
});
this.readerModeSubject.next(this.readerMode);
this.pagingDirectionSubject.next(this.pagingDirection);
// We need a mergeMap when page changes
this.readerSettings$ = merge(this.generalSettingsForm.valueChanges, this.pagingDirection$, this.readerMode$).pipe(
map(_ => this.createReaderSettingsUpdate()),
takeUntilDestroyed(this.destroyRef),
);
this.updateForm();
this.pagingDirection$.pipe(
distinctUntilChanged(),
tap(dir => {
this.pagingDirection = dir;
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
this.readerMode$.pipe(
distinctUntilChanged(),
tap(mode => {
this.readerMode = mode;
this.disableDoubleRendererIfScreenTooSmall();
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
this.setupWidthOverrideTriggers();
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
const changeOccurred = parseInt(val, 10) !== this.layoutMode;
this.layoutMode = parseInt(val, 10);
if (this.layoutMode === LayoutMode.Single) {
this.generalSettingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption);
this.generalSettingsForm.get('pageSplitOption')?.enable();
this.generalSettingsForm.get('widthSlider')?.enable();
this.generalSettingsForm.get('fittingOption')?.enable();
this.generalSettingsForm.get('emulateBook')?.enable();
} else {
this.generalSettingsForm.get('pageSplitOption')?.setValue(PageSplitOption.NoSplit);
this.generalSettingsForm.get('pageSplitOption')?.disable();
this.generalSettingsForm.get('widthSlider')?.disable();
this.generalSettingsForm.get('fittingOption')?.setValue(this.mangaReaderService.translateScalingOption(ScalingOption.FitToHeight));
this.generalSettingsForm.get('fittingOption')?.disable();
this.generalSettingsForm.get('emulateBook')?.enable();
}
this.cdRef.markForCheck();
// Re-render the current page when we switch layouts
if (changeOccurred) {
this.setPageNum(this.adjustPagesForDoubleRenderer(this.pageNum));
this.loadPage();
}
});
this.generalSettingsForm.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
this.pageSplitOption = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10);
const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src));
// If we need to split on a menu change, then we need to re-render.
if (needsSplitting) {
// If we need to re-render, to ensure things layout properly, let's update paging direction & reset render
this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD);
this.canvasRenderer.reset();
this.loadPage();
}
});
this.memberService.hasReadingProgress(this.libraryId).pipe(take(1)).subscribe(progress => {
if (!progress) {
@ -607,9 +538,29 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.toastr.info(translate('manga-reader.first-time-reading-manga'));
}
});
});
this.init();
this.init();
// Update implicit reading profile while changing settings
this.generalSettingsForm.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef),
map(_ => this.packReadingProfile()),
distinctUntilChanged(),
tap(newProfile => {
this.readingProfileService.updateImplicit(newProfile, this.seriesId).subscribe({
next: updatedProfile => {
this.readingProfile = updatedProfile;
this.cdRef.markForCheck();
},
error: err => {
console.error(err);
}
})
})
).subscribe();
});
}
ngAfterViewInit() {
@ -697,6 +648,114 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
setupReaderSettings() {
if (this.readingProfile.kind === ReadingProfileKind.Implicit) {
this.readingProfileService.getForSeries(this.seriesId, true).subscribe(parent => {
this.parentReadingProfile = parent;
this.cdRef.markForCheck();
})
} else {
this.parentReadingProfile = this.readingProfile;
}
this.readingDirection = this.readingProfile.readingDirection;
this.scalingOption = this.readingProfile.scalingOption;
this.pageSplitOption = this.readingProfile.pageSplitOption;
this.autoCloseMenu = this.readingProfile.autoCloseMenu;
this.readerMode = this.readingProfile.readerMode;
this.layoutMode = this.readingProfile.layoutMode || LayoutMode.Single;
this.backgroundColor = this.readingProfile.backgroundColor || '#000000';
this.readerService.setOverrideStyles(this.backgroundColor);
this.generalSettingsForm = this.formBuilder.nonNullable.group({
autoCloseMenu: new FormControl(this.autoCloseMenu),
pageSplitOption: new FormControl(this.pageSplitOption),
fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)),
widthSlider: new FormControl(this.readingProfile.widthOverride ?? 'none'),
layoutMode: new FormControl(this.layoutMode),
darkness: new FormControl(100),
emulateBook: new FormControl(this.readingProfile.emulateBook),
swipeToPaginate: new FormControl(this.readingProfile.swipeToPaginate)
});
this.readerModeSubject.next(this.readerMode);
this.pagingDirectionSubject.next(this.pagingDirection);
// We need a mergeMap when page changes
this.readerSettings$ = merge(this.generalSettingsForm.valueChanges, this.pagingDirection$, this.readerMode$).pipe(
map(_ => this.createReaderSettingsUpdate()),
takeUntilDestroyed(this.destroyRef),
);
this.updateForm();
this.pagingDirection$.pipe(
distinctUntilChanged(),
tap(dir => {
this.pagingDirection = dir;
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
this.readerMode$.pipe(
distinctUntilChanged(),
tap(mode => {
this.readerMode = mode;
this.disableDoubleRendererIfScreenTooSmall();
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
this.setupWidthOverrideTriggers();
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
const changeOccurred = parseInt(val, 10) !== this.layoutMode;
this.layoutMode = parseInt(val, 10);
if (this.layoutMode === LayoutMode.Single) {
this.generalSettingsForm.get('pageSplitOption')?.setValue(this.readingProfile!.pageSplitOption);
this.generalSettingsForm.get('pageSplitOption')?.enable();
this.generalSettingsForm.get('widthSlider')?.enable();
this.generalSettingsForm.get('fittingOption')?.enable();
this.generalSettingsForm.get('emulateBook')?.enable();
} else {
this.generalSettingsForm.get('pageSplitOption')?.setValue(PageSplitOption.NoSplit);
this.generalSettingsForm.get('pageSplitOption')?.disable();
this.generalSettingsForm.get('widthSlider')?.disable();
this.generalSettingsForm.get('fittingOption')?.setValue(this.mangaReaderService.translateScalingOption(ScalingOption.FitToHeight));
this.generalSettingsForm.get('fittingOption')?.disable();
this.generalSettingsForm.get('emulateBook')?.enable();
}
this.cdRef.markForCheck();
// Re-render the current page when we switch layouts
if (changeOccurred) {
this.setPageNum(this.adjustPagesForDoubleRenderer(this.pageNum));
this.loadPage();
}
});
this.generalSettingsForm.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
this.pageSplitOption = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10);
const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src));
// If we need to split on a menu change, then we need to re-render.
if (needsSplitting) {
// If we need to re-render, to ensure things layout properly, let's update paging direction & reset render
this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD);
this.canvasRenderer.reset();
this.loadPage();
}
});
this.cdRef.markForCheck();
}
/**
* Width override is only valid under the following conditions:
* Image Scaling is Width
@ -750,7 +809,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
).subscribe();
// Set the default override to 0
widthOverrideControl.setValue(0);
//widthOverrideControl.setValue(0);
//send the current width override value to the label
this.widthOverrideLabel$ = this.readerSettings$?.pipe(
@ -783,7 +842,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
switchToWebtoonReaderIfPagesLikelyWebtoon() {
if (this.readerMode === ReaderMode.Webtoon) return;
if (!this.user.preferences.allowAutomaticWebtoonReaderDetection) return;
if (!this.readingProfile!.allowAutomaticWebtoonReaderDetection) return;
if (this.mangaReaderService.shouldBeWebtoonMode()) {
this.readerMode = ReaderMode.Webtoon;
@ -795,7 +854,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
disableDoubleRendererIfScreenTooSmall() {
if (window.innerWidth > window.innerHeight) {
this.generalSettingsForm.get('layoutMode')?.enable();
if (this.generalSettingsForm.get('layoutMode')?.disabled) {
this.generalSettingsForm.get('layoutMode')?.enable();
}
this.cdRef.markForCheck();
return;
}
@ -1463,7 +1524,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readingDirection = ReadingDirection.LeftToRight;
}
if (this.menuOpen && this.user.preferences.showScreenHints) {
if (this.menuOpen && this.readingProfile!.showScreenHints) {
this.showClickOverlay = true;
this.showClickOverlaySubject.next(true);
setTimeout(() => {
@ -1740,28 +1801,28 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
// menu only code
savePref() {
const modelSettings = this.generalSettingsForm.getRawValue();
// Get latest preferences from user, overwrite with what we manage in this UI, then save
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (!user) return;
const data = {...user.preferences};
data.layoutMode = parseInt(modelSettings.layoutMode, 10);
data.readerMode = this.readerMode;
data.autoCloseMenu = this.autoCloseMenu;
data.readingDirection = this.readingDirection;
data.emulateBook = modelSettings.emulateBook;
data.swipeToPaginate = modelSettings.swipeToPaginate;
data.pageSplitOption = parseInt(modelSettings.pageSplitOption, 10);
data.locale = data.locale || 'en';
updateParentPref() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.accountService.updatePreferences(data).subscribe(updatedPrefs => {
this.toastr.success(translate('manga-reader.user-preferences-updated'));
if (this.user) {
this.user.preferences = updatedPrefs;
this.cdRef.markForCheck();
}
})
this.readingProfileService.updateParentProfile(this.seriesId, this.packReadingProfile()).subscribe(newProfile => {
this.readingProfile = newProfile;
this.toastr.success(translate('manga-reader.reading-profile-updated'));
this.cdRef.markForCheck();
});
}
createNewProfileFromImplicit() {
if (this.readingProfile.kind !== ReadingProfileKind.Implicit) {
return;
}
this.readingProfileService.promoteProfile(this.readingProfile.id).subscribe(newProfile => {
this.readingProfile = newProfile;
this.parentReadingProfile = newProfile; // Profile is no longer implicit
this.toastr.success(translate("manga-reader.reading-profile-promoted"));
this.cdRef.markForCheck();
});
}
@ -1771,4 +1832,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return d;
}
private packReadingProfile(): ReadingProfile {
const modelSettings = this.generalSettingsForm.getRawValue();
const data = {...this.readingProfile!};
data.layoutMode = parseInt(modelSettings.layoutMode, 10);
data.readerMode = this.readerMode;
data.autoCloseMenu = this.autoCloseMenu;
data.readingDirection = this.readingDirection;
data.emulateBook = modelSettings.emulateBook;
data.swipeToPaginate = modelSettings.swipeToPaginate;
data.pageSplitOption = parseInt(modelSettings.pageSplitOption, 10);
data.widthOverride = modelSettings.widthSlider === 'none' ? null : modelSettings.widthSlider;
return data;
}
protected readonly ReadingProfileKind = ReadingProfileKind;
}

View file

@ -34,6 +34,9 @@ import {PdfSpreadMode} from "../../../_models/preferences/pdf-spread-mode";
import {SpreadType} from "ngx-extended-pdf-viewer/lib/options/spread-type";
import {PdfScrollModeTypePipe} from "../../_pipe/pdf-scroll-mode.pipe";
import {PdfSpreadTypePipe} from "../../_pipe/pdf-spread-mode.pipe";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-pdf-reader',
@ -54,6 +57,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
private readonly themeService = inject(ThemeService);
private readonly cdRef = inject(ChangeDetectorRef);
public readonly accountService = inject(AccountService);
private readonly readingProfileService = inject(ReadingProfileService);
public readonly readerService = inject(ReaderService);
public readonly utilityService = inject(UtilityService);
public readonly destroyRef = inject(DestroyRef);
@ -69,6 +73,7 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
chapterId!: number;
chapter!: Chapter;
user!: User;
readingProfile!: ReadingProfile;
/**
* Reading List id. Defaults to -1.
@ -162,6 +167,16 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
this.chapterId = parseInt(chapterId, 10);
this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true';
this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => {
this.readingProfile = data['readingProfile'];
if (this.readingProfile == null) {
this.router.navigateByUrl('/home');
return;
}
this.setupReaderSettings();
this.cdRef.markForCheck();
});
const readingListId = this.route.snapshot.queryParamMap.get('readingListId');
if (readingListId != null) {
@ -234,12 +249,14 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
}
}
init() {
setupReaderSettings() {
this.pageLayoutMode = this.convertPdfLayoutMode(PdfLayoutMode.Multiple);
this.scrollMode = this.convertPdfScrollMode(this.user.preferences.pdfScrollMode || PdfScrollMode.Vertical);
this.spreadMode = this.convertPdfSpreadMode(this.user.preferences.pdfSpreadMode || PdfSpreadMode.None);
this.theme = this.convertPdfTheme(this.user.preferences.pdfTheme || PdfTheme.Dark);
this.scrollMode = this.convertPdfScrollMode(this.readingProfile.pdfScrollMode || PdfScrollMode.Vertical);
this.spreadMode = this.convertPdfSpreadMode(this.readingProfile.pdfSpreadMode || PdfSpreadMode.None);
this.theme = this.convertPdfTheme(this.readingProfile.pdfTheme || PdfTheme.Dark);
}
init() {
this.backgroundColor = this.themeMap[this.theme].background;
this.fontColor = this.themeMap[this.theme].font; // TODO: Move this to an observable or something

View file

@ -110,6 +110,7 @@ import {LicenseService} from "../../../_services/license.service";
import {PageBookmark} from "../../../_models/readers/page-bookmark";
import {VolumeRemovedEvent} from "../../../_models/events/volume-removed-event";
import {ReviewsComponent} from "../../../_single-module/reviews/reviews.component";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
enum TabID {
@ -175,6 +176,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly scrollService = inject(ScrollService);
private readonly translocoService = inject(TranslocoService);
private readonly readingProfileService = inject(ReadingProfileService);
protected readonly bulkSelectionService = inject(BulkSelectionService);
protected readonly utilityService = inject(UtilityService);
protected readonly imageService = inject(ImageService);
@ -609,6 +611,14 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.actionService.sendToDevice(chapterIds, device);
break;
}
case Action.SetReadingProfile:
this.actionService.setReadingProfileForMultiple([this.series]);
break;
case Action.ClearReadingProfile:
this.readingProfileService.clearSeriesProfiles(this.seriesId).subscribe(() => {
this.toastr.success(this.translocoService.translate('actionable.cleared-profile'));
});
break;
default:
break;
}

View file

@ -217,6 +217,15 @@
</div>
}
}
@defer (when fragment === SettingsTabId.ReadingProfiles; prefetch on idle) {
@if (fragment === SettingsTabId.ReadingProfiles) {
<div class="scale col-md-12">
<app-manage-reading-profiles></app-manage-reading-profiles>
</div>
}
}
}
</div>
</ng-container>

View file

@ -52,43 +52,47 @@ import {ScrobblingHoldsComponent} from "../../../user-settings/user-holds/scrobb
import {
ManageMetadataSettingsComponent
} from "../../../admin/manage-metadata-settings/manage-metadata-settings.component";
import {
ManageReadingProfilesComponent
} from "../../../user-settings/manage-reading-profiles/manage-reading-profiles.component";
@Component({
selector: 'app-settings',
imports: [
ChangeAgeRestrictionComponent,
ChangeEmailComponent,
ChangePasswordComponent,
ManageDevicesComponent,
ManageOpdsComponent,
ManageScrobblingProvidersComponent,
ManageUserPreferencesComponent,
SideNavCompanionBarComponent,
ThemeManagerComponent,
TranslocoDirective,
UserStatsComponent,
AsyncPipe,
LicenseComponent,
ManageEmailSettingsComponent,
ManageLibraryComponent,
ManageMediaSettingsComponent,
ManageSettingsComponent,
ManageSystemComponent,
ManageTasksSettingsComponent,
ManageUsersComponent,
ServerStatsComponent,
SettingFragmentPipe,
ManageScrobblingComponent,
ManageMediaIssuesComponent,
ManageCustomizationComponent,
ImportMalCollectionComponent,
ImportCblComponent,
ManageMatchedMetadataComponent,
ManageUserTokensComponent,
EmailHistoryComponent,
ScrobblingHoldsComponent,
ManageMetadataSettingsComponent
],
imports: [
ChangeAgeRestrictionComponent,
ChangeEmailComponent,
ChangePasswordComponent,
ManageDevicesComponent,
ManageOpdsComponent,
ManageScrobblingProvidersComponent,
ManageUserPreferencesComponent,
SideNavCompanionBarComponent,
ThemeManagerComponent,
TranslocoDirective,
UserStatsComponent,
AsyncPipe,
LicenseComponent,
ManageEmailSettingsComponent,
ManageLibraryComponent,
ManageMediaSettingsComponent,
ManageSettingsComponent,
ManageSystemComponent,
ManageTasksSettingsComponent,
ManageUsersComponent,
ServerStatsComponent,
SettingFragmentPipe,
ManageScrobblingComponent,
ManageMediaIssuesComponent,
ManageCustomizationComponent,
ImportMalCollectionComponent,
ImportCblComponent,
ManageMatchedMetadataComponent,
ManageUserTokensComponent,
EmailHistoryComponent,
ScrobblingHoldsComponent,
ManageMetadataSettingsComponent,
ManageReadingProfilesComponent
],
templateUrl: './settings.component.html',
styleUrl: './settings.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -16,7 +16,7 @@ import {AsyncPipe, NgClass} from "@angular/common";
import {SideNavItemComponent} from "../side-nav-item/side-nav-item.component";
import {FilterPipe} from "../../../_pipes/filter.pipe";
import {FormsModule} from "@angular/forms";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {SideNavStream} from "../../../_models/sidenav/sidenav-stream";
import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum";
@ -25,6 +25,7 @@ import {SettingsTabId} from "../../preference-nav/preference-nav.component";
import {LicenseService} from "../../../_services/license.service";
import {CdkDrag, CdkDragDrop, CdkDropList} from "@angular/cdk/drag-drop";
import {ToastrService} from "ngx-toastr";
import {ReadingProfileService} from "../../../_services/reading-profile.service";
@Component({
selector: 'app-side-nav',
@ -53,7 +54,9 @@ export class SideNavComponent implements OnInit {
protected readonly licenseService = inject(LicenseService);
private readonly destroyRef = inject(DestroyRef);
private readonly actionFactoryService = inject(ActionFactoryService);
private readonly toastr = inject(ToastrService)
private readonly toastr = inject(ToastrService);
private readonly readingProfilesService = inject(ReadingProfileService);
private readonly translocoService = inject(TranslocoService);
cachedData: SideNavStream[] | null = null;
@ -175,6 +178,14 @@ export class SideNavComponent implements OnInit {
case (Action.Edit):
this.actionService.editLibrary(lib, () => window.scrollTo(0, 0));
break;
case (Action.SetReadingProfile):
this.actionService.setReadingProfileForLibrary(lib);
break;
case (Action.ClearReadingProfile):
this.readingProfilesService.clearLibraryProfiles(lib.id).subscribe(() => {
this.toastr.success(this.translocoService.translate('actionable.cleared-profile'));
});
break;
default:
break;
}

View file

@ -41,6 +41,7 @@ export enum SettingsTabId {
// Non-Admin
Account = 'account',
Preferences = 'preferences',
ReadingProfiles = 'reading-profiles',
Clients = 'clients',
Theme = 'theme',
Devices = 'devices',
@ -111,6 +112,7 @@ export class PreferenceNavComponent implements AfterViewInit {
children: [
new SideNavItem(SettingsTabId.Account, []),
new SideNavItem(SettingsTabId.Preferences),
new SideNavItem(SettingsTabId.ReadingProfiles),
new SideNavItem(SettingsTabId.Customize, [], undefined, [Role.ReadOnly]),
new SideNavItem(SettingsTabId.Clients),
new SideNavItem(SettingsTabId.Theme),

View file

@ -0,0 +1,509 @@
<ng-container *transloco="let t;prefix:'manage-reading-profiles'">
<app-loading [loading]="loading"></app-loading>
@if (!loading) {
<div class="position-relative">
<button class="btn btn-outline-primary position-absolute custom-position" [ngbTooltip]="t('add-tooltip')" (click)="addNew()" [title]="t('add')">
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden ms-1">{{t('add')}}</span>
</button>
</div>
<p class="ps-2">{{t('description')}}</p>
<p class="ps-2 text-muted">{{t('extra-tip')}}</p>
<div class="row g-0 ">
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller">
<div class="pe-2">
@if (readingProfiles.length < virtualScrollerBreakPoint) {
@for (readingProfile of readingProfiles; track readingProfile.id) {
<ng-container [ngTemplateOutlet]="readingProfileOption" [ngTemplateOutletContext]="{$implicit: readingProfile}"></ng-container>
}
} @else {
<virtual-scroller #scroll [items]="readingProfiles">
@for (readingProfile of scroll.viewPortItems; track readingProfile.id) {
<ng-container [ngTemplateOutlet]="readingProfileOption" [ngTemplateOutletContext]="{$implicit: readingProfile}"></ng-container>
}
</virtual-scroller>
}
</div>
</div>
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-3">
<div class="card p-3">
@if (selectedProfile === null) {
<p class="ps-2">{{t('no-selected')}}</p>
<p class="ps-2 text-muted">{{t('selection-tip')}}</p>
}
@if (readingProfileForm !== null && selectedProfile !== null) {
<form [formGroup]="readingProfileForm">
<div class="mb-2 d-flex justify-content-between align-items-center">
<app-setting-item [title]="''" [showEdit]="false" [canEdit]="selectedProfile.kind !== ReadingProfileKind.Default">
<ng-template #view>
{{readingProfileForm.get('name')!.value}}
</ng-template>
<ng-template #edit>
<input class="form-control" type="text" formControlName="name" [disabled]="selectedProfile.kind === ReadingProfileKind.Default">
</ng-template>
</app-setting-item>
@if (selectedProfile.id !== 0) {
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-danger" (click)="delete(selectedProfile!)" [disabled]="selectedProfile.kind === ReadingProfileKind.Default">
<i class="fa fa-trash" aria-hidden="true"></i>
<span class="visually-hidden">{{t('delete')}}</span>
</button>
</div>
}
</div>
<div class="carousel-tabs-container mb-2">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs">
<li [ngbNavItem]="TabId.ImageReader">
<a ngbNavLink (click)="activeTabId = TabId.ImageReader">{{t('image-reader-settings-title')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabId.ImageReader; prefetch on idle) {
<div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{readingProfileForm.get('readingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('scaling-option-label')" [subtitle]="t('scaling-option-tooltip')">
<ng-template #view>
{{readingProfileForm.get('scalingOption')!.value | scalingOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="scalingOption">
@for (opt of scalingOptions; track opt) {
<option [value]="opt.value">{{opt.value | scalingOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('page-splitting-label')" [subtitle]="t('page-splitting-tooltip')">
<ng-template #view>
{{readingProfileForm.get('pageSplitOption')!.value | pageSplitOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="pageSplitOption">
@for (opt of pageSplitOptions; track opt) {
<option [value]="opt.value">{{opt.value | pageSplitOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-mode-label')" [subtitle]="t('reading-mode-tooltip')">
<ng-template #view>
{{readingProfileForm.get('readerMode')!.value | readerMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readerMode">
@for (opt of readerModes; track opt) {
<option [value]="opt.value">{{opt.value | readerMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-label')" [subtitle]="t('layout-mode-tooltip')">
<ng-template #view>
{{readingProfileForm.get('layoutMode')!.value | layoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="layoutMode">
@for (opt of layoutModes; track opt) {
<option [value]="opt.value">{{opt.value | layoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('background-color-label')" [subtitle]="t('background-color-tooltip')">
<ng-template #view>
<div class="color-box-container">
<div class="color-box" [ngStyle]="{'background-color': selectedProfile!.backgroundColor}"></div>
<span class="hex-code">{{ selectedProfile!.backgroundColor.toUpperCase() }}</span>
</div>
</ng-template>
<ng-template #edit>
<input [value]="selectedProfile!.backgroundColor" class="form-control"
(colorPickerChange)="handleBackgroundColorChange($event)"
[style.background]="selectedProfile!.backgroundColor" [cpAlphaChannel]="'disabled'"
[(colorPicker)]="selectedProfile!.backgroundColor" />
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('auto-close-menu-label')" [subtitle]="t('auto-close-menu-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="autoCloseMenu" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('show-screen-hints-label')" [subtitle]="t('show-screen-hints-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="showScreenHints" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('emulate-comic-book-label')" [subtitle]="t('emulate-comic-book-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="emulate-comic-book"
formControlName="emulateBook" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('swipe-to-paginate-label')" [subtitle]="t('swipe-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="swipe-to-paginate"
formControlName="swipeToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('allow-auto-webtoon-reader-label')" [subtitle]="t('allow-auto-webtoon-reader-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="allow-auto-webtoon-reader"
formControlName="allowAutomaticWebtoonReaderDetection" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
@if (readingProfileForm.get("widthOverride"); as widthOverride) {
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('width-override-label')" [subtitle]="t('width-override-tooltip')">
<ng-template #view>
<label for="width-override-slider" class="form-label">
{{ widthOverrideLabel | sentenceCase }}
</label>
</ng-template>
<ng-template #edit>
<div class="d-flex justify-content-between align-items-center">
<span>{{ widthOverrideLabel | sentenceCase }}</span>
<button class="btn btn-secondary" (click)="widthOverride.setValue(null)">
{{t('reset')}}
</button>
</div>
<input id="width-override-slider" type="range" min="0" max="100" class="form-range" formControlName="widthOverride">
</ng-template>
</app-setting-item>
</div>
}
</div>
}
</ng-template>
</li>
<li [ngbNavItem]="TabId.BookReader">
<a ngbNavLink (click)="activeTabId = TabId.BookReader">{{t('book-reader-settings-title')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabId.BookReader; prefetch on idle) {
@if (selectedProfile !== null && readingProfileForm !== null) {
<div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('tap-to-paginate-label')" [subtitle]="t('tap-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="tap-to-paginate"
formControlName="bookReaderTapToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('immersive-mode-label')" [subtitle]="t('immersive-mode-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="bookReaderImmersiveMode" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderReadingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderReadingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-family-label')" [subtitle]="t('font-family-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderFontFamily')!.value | titlecase}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderFontFamily">
@for (opt of fontFamilies; track opt) {
<option [value]="opt">{{opt | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('writing-style-label')" [subtitle]="t('writing-style-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderWritingStyle')!.value | writingStyle}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderWritingStyle">
@for (opt of bookWritingStyles; track opt) {
<option [value]="opt.value">{{opt.value | writingStyle}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-book-label')" [subtitle]="t('layout-mode-book-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderLayoutMode')!.value | bookPageLayoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderLayoutMode">
@for (opt of bookLayoutModes; track opt) {
<option [value]="opt.value">{{opt.value | bookPageLayoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('color-theme-book-label')" [subtitle]="t('color-theme-book-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderThemeName')!.value}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderThemeName">
@for (opt of bookColorThemesTranslated; track opt) {
<option [value]="opt.name">{{opt.name | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-size-book-label')" [subtitle]="t('font-size-book-tooltip')">
<ng-template #view>
<span class="range-text">{{readingProfileForm.get('bookReaderFontSize')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="fontsize" min="50" max="300" step="10"
formControlName="bookReaderFontSize">
</div>
<span class="ps-2 col-2 align-middle">{{readingProfileForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('line-height-book-label')" [subtitle]="t('line-height-book-tooltip')">
<ng-template #view>
<span class="range-text">{{readingProfileForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing">
</div>
<span class="ps-2 col-2 align-middle">{{readingProfileForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('margin-book-label')" [subtitle]="t('margin-book-tooltip')">
<ng-template #view>
<span class="range-text">{{readingProfileForm.get('bookReaderMargin')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="margin" min="0" max="30" step="5"
formControlName="bookReaderMargin">
</div>
<span class="ps-2 col-2 align-middle">{{readingProfileForm!.get('bookReaderMargin')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
</div>
}
}
</ng-template>
</li>
<li [ngbNavItem]="TabId.PdfReader">
<a ngbNavLink (click)="activeTabId = TabId.PdfReader">{{t('pdf-reader-settings-title')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabId.PdfReader; prefetch on idle) {
<div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-spread-mode-label')" [subtitle]="t('pdf-spread-mode-tooltip')">
<ng-template #view>
{{readingProfileForm!.get('pdfSpreadMode')!.value | pdfSpreadMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfSpreadMode">
@for (opt of pdfSpreadModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfSpreadMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-theme-label')" [subtitle]="t('pdf-theme-tooltip')">
<ng-template #view>
{{readingProfileForm!.get('pdfTheme')!.value | pdfTheme}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfTheme">
@for (opt of pdfThemes; track opt) {
<option [value]="opt.value">{{opt.value | pdfTheme}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-scroll-mode-label')" [subtitle]="t('pdf-scroll-mode-tooltip')">
<ng-template #view>
{{readingProfileForm!.get('pdfScrollMode')!.value | pdfScrollMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfScrollMode">
@for (opt of pdfScrollModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfScrollMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
</div>
}
</ng-template>
</li>
</ul>
</div>
</form>
<div [ngbNavOutlet]="nav"></div>
}
</div>
</div>
</div>
<ng-template #readingProfileOption let-profile>
<div class="p-2 group-item d-flex justify-content-between align-items-start {{selectedProfile && profile.id === selectedProfile!.id ? 'active' : ''}}"
(click)="selectProfile(profile)">
<div class="fw-bold">{{profile.name | sentenceCase}}</div>
@if (profile.kind === ReadingProfileKind.Default) {
<span class="pill p-1 ms-1">{{t('default-profile')}}</span>
}
</div>
</ng-template>
}
</ng-container>

View file

@ -0,0 +1,38 @@
@use '../../../series-detail-common';
.group-item {
background-color: transparent;
&:hover {
background-color: var(--card-bg-color);
border-radius: 5px;
cursor: pointer;
}
&:active, &.active {
background-color: var(--card-bg-color);
border-radius: 5px;
}
}
.pill {
font-size: .8rem;
background-color: var(--card-bg-color);
border-radius: 0.375rem;
color: var(--badge-text-color);
&.active {
background-color : var(--primary-color);
}
}
.custom-position {
right: 15px;
top: -42px;
}
a:hover {
cursor: pointer;
}

View file

@ -0,0 +1,319 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnInit} from '@angular/core';
import {ReadingProfileService} from "../../_services/reading-profile.service";
import {
bookLayoutModes,
bookWritingStyles,
layoutModes,
pageSplitOptions,
pdfScrollModes,
pdfSpreadModes,
pdfThemes,
readingDirections,
readingModes,
ReadingProfile,
ReadingProfileKind,
scalingOptions
} from "../../_models/preferences/reading-profiles";
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {VirtualScrollerModule} from "@iharbeck/ngx-virtual-scroller";
import {User} from "../../_models/user";
import {AccountService} from "../../_services/account.service";
import {debounceTime, distinctUntilChanged, take, tap} from "rxjs/operators";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
import {BookService} from "../../book-reader/_services/book.service";
import {BookPageLayoutMode} from "../../_models/readers/book-page-layout-mode";
import {PdfTheme} from "../../_models/preferences/pdf-theme";
import {PdfScrollMode} from "../../_models/preferences/pdf-scroll-mode";
import {PdfSpreadMode} from "../../_models/preferences/pdf-spread-mode";
import {bookColorThemes} from "../../book-reader/_components/reader-settings/reader-settings.component";
import {BookPageLayoutModePipe} from "../../_pipes/book-page-layout-mode.pipe";
import {LayoutModePipe} from "../../_pipes/layout-mode.pipe";
import {PageSplitOptionPipe} from "../../_pipes/page-split-option.pipe";
import {PdfScrollModePipe} from "../../_pipes/pdf-scroll-mode.pipe";
import {PdfSpreadModePipe} from "../../_pipes/pdf-spread-mode.pipe";
import {PdfThemePipe} from "../../_pipes/pdf-theme.pipe";
import {ReaderModePipe} from "../../_pipes/reading-mode.pipe";
import {ReadingDirectionPipe} from "../../_pipes/reading-direction.pipe";
import {ScalingOptionPipe} from "../../_pipes/scaling-option.pipe";
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component";
import {WritingStylePipe} from "../../_pipes/writing-style.pipe";
import {ColorPickerDirective} from "ngx-color-picker";
import {NgbNav, NgbNavContent, NgbNavItem, NgbNavLinkBase, NgbNavOutlet, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {filter} from "rxjs";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {ToastrService} from "ngx-toastr";
import {ConfirmService} from "../../shared/confirm.service";
enum TabId {
ImageReader = "image-reader",
BookReader = "book-reader",
PdfReader = "pdf-reader",
}
@Component({
selector: 'app-manage-reading-profiles',
imports: [
TranslocoDirective,
NgTemplateOutlet,
VirtualScrollerModule,
SentenceCasePipe,
BookPageLayoutModePipe,
FormsModule,
LayoutModePipe,
PageSplitOptionPipe,
PdfScrollModePipe,
PdfSpreadModePipe,
PdfThemePipe,
ReactiveFormsModule,
ReaderModePipe,
ReadingDirectionPipe,
ScalingOptionPipe,
SettingItemComponent,
SettingSwitchComponent,
TitleCasePipe,
WritingStylePipe,
NgStyle,
ColorPickerDirective,
NgbNav,
NgbNavItem,
NgbNavLinkBase,
NgbNavContent,
NgbNavOutlet,
LoadingComponent,
NgbTooltip,
],
templateUrl: './manage-reading-profiles.component.html',
styleUrl: './manage-reading-profiles.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ManageReadingProfilesComponent implements OnInit {
virtualScrollerBreakPoint = 20;
fontFamilies: Array<string> = [];
readingProfiles: ReadingProfile[] = [];
user!: User;
activeTabId = TabId.ImageReader;
loading = true;
selectedProfile: ReadingProfile | null = null;
readingProfileForm: FormGroup | null = null;
bookColorThemesTranslated = bookColorThemes.map(o => {
const d = {...o};
d.name = translate('theme.' + d.translationKey);
return d;
});
constructor(
private readingProfileService: ReadingProfileService,
private cdRef: ChangeDetectorRef,
private accountService: AccountService,
private bookService: BookService,
private destroyRef: DestroyRef,
private toastr: ToastrService,
private confirmService: ConfirmService,
private transLoco: TranslocoService,
) {
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
this.cdRef.markForCheck();
}
ngOnInit(): void {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.user = user;
}
});
this.readingProfileService.getAllProfiles().subscribe(profiles => {
this.readingProfiles = profiles;
this.loading = false;
this.setupForm();
const defaultProfile = this.readingProfiles.find(rp => rp.kind === ReadingProfileKind.Default);
this.selectProfile(defaultProfile);
this.cdRef.markForCheck();
});
}
async delete(readingProfile: ReadingProfile) {
if (!await this.confirmService.confirm(this.transLoco.translate("manage-reading-profiles.confirm", {name: readingProfile.name}))) {
return;
}
this.readingProfileService.delete(readingProfile.id).subscribe(() => {
this.selectProfile(undefined);
this.readingProfiles = this.readingProfiles.filter(o => o.id !== readingProfile.id);
this.cdRef.markForCheck();
});
}
get widthOverrideLabel() {
const rawVal = this.readingProfileForm?.get('widthOverride')!.value;
if (!rawVal) {
return translate('reader-settings.off');
}
const val = parseInt(rawVal);
return (val <= 0) ? '' : val + '%'
}
setupForm() {
if (this.selectedProfile == null) {
return;
}
this.readingProfileForm = new FormGroup({})
if (this.fontFamilies.indexOf(this.selectedProfile.bookReaderFontFamily) < 0) {
this.selectedProfile.bookReaderFontFamily = 'default';
}
this.readingProfileForm.addControl('name', new FormControl(this.selectedProfile.name, Validators.required));
// Image reader
this.readingProfileForm.addControl('readingDirection', new FormControl(this.selectedProfile.readingDirection, []));
this.readingProfileForm.addControl('scalingOption', new FormControl(this.selectedProfile.scalingOption, []));
this.readingProfileForm.addControl('pageSplitOption', new FormControl(this.selectedProfile.pageSplitOption, []));
this.readingProfileForm.addControl('autoCloseMenu', new FormControl(this.selectedProfile.autoCloseMenu, []));
this.readingProfileForm.addControl('showScreenHints', new FormControl(this.selectedProfile.showScreenHints, []));
this.readingProfileForm.addControl('readerMode', new FormControl(this.selectedProfile.readerMode, []));
this.readingProfileForm.addControl('layoutMode', new FormControl(this.selectedProfile.layoutMode, []));
this.readingProfileForm.addControl('emulateBook', new FormControl(this.selectedProfile.emulateBook, []));
this.readingProfileForm.addControl('swipeToPaginate', new FormControl(this.selectedProfile.swipeToPaginate, []));
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)]));
// Epub reader
this.readingProfileForm.addControl('bookReaderFontFamily', new FormControl(this.selectedProfile.bookReaderFontFamily, []));
this.readingProfileForm.addControl('bookReaderFontSize', new FormControl(this.selectedProfile.bookReaderFontSize, []));
this.readingProfileForm.addControl('bookReaderLineSpacing', new FormControl(this.selectedProfile.bookReaderLineSpacing, []));
this.readingProfileForm.addControl('bookReaderMargin', new FormControl(this.selectedProfile.bookReaderMargin, []));
this.readingProfileForm.addControl('bookReaderReadingDirection', new FormControl(this.selectedProfile.bookReaderReadingDirection, []));
this.readingProfileForm.addControl('bookReaderWritingStyle', new FormControl(this.selectedProfile.bookReaderWritingStyle, []))
this.readingProfileForm.addControl('bookReaderTapToPaginate', new FormControl(this.selectedProfile.bookReaderTapToPaginate, []));
this.readingProfileForm.addControl('bookReaderLayoutMode', new FormControl(this.selectedProfile.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.readingProfileForm.addControl('bookReaderThemeName', new FormControl(this.selectedProfile.bookReaderThemeName || bookColorThemes[0].name, []));
this.readingProfileForm.addControl('bookReaderImmersiveMode', new FormControl(this.selectedProfile.bookReaderImmersiveMode, []));
// Pdf reader
this.readingProfileForm.addControl('pdfTheme', new FormControl(this.selectedProfile.pdfTheme || PdfTheme.Dark, []));
this.readingProfileForm.addControl('pdfScrollMode', new FormControl(this.selectedProfile.pdfScrollMode || PdfScrollMode.Vertical, []));
this.readingProfileForm.addControl('pdfSpreadMode', new FormControl(this.selectedProfile.pdfSpreadMode || PdfSpreadMode.None, []));
// Auto save
this.readingProfileForm.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged(),
filter(_ => this.readingProfileForm!.valid),
takeUntilDestroyed(this.destroyRef),
tap(_ => this.autoSave()),
).subscribe();
}
private autoSave() {
if (this.selectedProfile!.id == 0) {
this.readingProfileService.createProfile(this.packData()).subscribe({
next: createdProfile => {
this.selectedProfile = createdProfile;
this.readingProfiles.push(createdProfile);
this.cdRef.markForCheck();
},
error: err => {
console.log(err);
this.toastr.error(err.message);
}
})
} else {
const profile = this.packData();
this.readingProfileService.updateProfile(profile).subscribe({
next: _ => {
this.readingProfiles = this.readingProfiles.map(p => {
if (p.id !== profile.id) return p;
return profile;
});
this.cdRef.markForCheck();
},
error: err => {
console.log(err);
this.toastr.error(err.message);
}
})
}
}
private packData(): ReadingProfile {
const data: ReadingProfile = this.readingProfileForm!.getRawValue();
data.id = this.selectedProfile!.id;
data.readingDirection = parseInt(data.readingDirection as unknown as string);
data.scalingOption = parseInt(data.scalingOption as unknown as string);
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.bookReaderReadingDirection = parseInt(data.bookReaderReadingDirection as unknown as string);
data.bookReaderWritingStyle = parseInt(data.bookReaderWritingStyle as unknown as string);
data.bookReaderLayoutMode = parseInt(data.bookReaderLayoutMode as unknown as string);
data.pdfTheme = parseInt(data.pdfTheme as unknown as string);
data.pdfScrollMode = parseInt(data.pdfScrollMode as unknown as string);
data.pdfSpreadMode = parseInt(data.pdfSpreadMode as unknown as string);
return data;
}
handleBackgroundColorChange(color: string) {
if (!this.readingProfileForm || !this.selectedProfile) return;
this.readingProfileForm.markAsDirty();
this.readingProfileForm.markAsTouched();
this.selectedProfile.backgroundColor = color;
this.readingProfileForm.get('backgroundColor')?.setValue(color);
this.cdRef.markForCheck();
}
selectProfile(profile: ReadingProfile | undefined | null) {
if (profile === undefined) {
this.selectedProfile = null;
this.cdRef.markForCheck();
return;
}
this.selectedProfile = profile;
this.setupForm();
this.cdRef.markForCheck();
}
addNew() {
const defaultProfile = this.readingProfiles.find(f => f.kind === ReadingProfileKind.Default);
this.selectedProfile = {...defaultProfile!};
this.selectedProfile.kind = ReadingProfileKind.User;
this.selectedProfile.id = 0;
this.selectedProfile.name = "New Profile #" + (this.readingProfiles.length + 1);
this.setupForm();
this.cdRef.markForCheck();
}
protected readonly readingDirections = readingDirections;
protected readonly pdfSpreadModes = pdfSpreadModes;
protected readonly pageSplitOptions = pageSplitOptions;
protected readonly bookLayoutModes = bookLayoutModes;
protected readonly pdfThemes = pdfThemes;
protected readonly scalingOptions = scalingOptions;
protected readonly layoutModes = layoutModes;
protected readonly readerModes = readingModes;
protected readonly bookWritingStyles = bookWritingStyles;
protected readonly pdfScrollModes = pdfScrollModes;
protected readonly TabId = TabId;
protected readonly ReadingProfileKind = ReadingProfileKind;
}

View file

@ -115,383 +115,7 @@
</app-setting-switch>
}
</div>
<div class="setting-section-break"></div>
}
<h4 id="image-reader-heading" class="mt-3">{{t('image-reader-settings-title')}}</h4>
<ng-container>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{settingsForm.get('readingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('scaling-option-label')" [subtitle]="t('scaling-option-tooltip')">
<ng-template #view>
{{settingsForm.get('scalingOption')!.value | scalingOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="scalingOption">
@for (opt of scalingOptions; track opt) {
<option [value]="opt.value">{{opt.value | scalingOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('page-splitting-label')" [subtitle]="t('page-splitting-tooltip')">
<ng-template #view>
{{settingsForm.get('pageSplitOption')!.value | pageSplitOption}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="pageSplitOption">
@for (opt of pageSplitOptions; track opt) {
<option [value]="opt.value">{{opt.value | pageSplitOption}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-mode-label')" [subtitle]="t('reading-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('readerMode')!.value | readerMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="readerMode">
@for (opt of readerModes; track opt) {
<option [value]="opt.value">{{opt.value | readerMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-label')" [subtitle]="t('layout-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('layoutMode')!.value | layoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="image-reader-heading"
formControlName="layoutMode">
@for (opt of layoutModes; track opt) {
<option [value]="opt.value">{{opt.value | layoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('background-color-label')" [subtitle]="t('background-color-tooltip')">
<ng-template #view>
<div class="color-box-container">
<div class="color-box" [ngStyle]="{'background-color': user.preferences!.backgroundColor}"></div>
<span class="hex-code">{{ user.preferences!.backgroundColor.toUpperCase() }}</span>
</div>
</ng-template>
<ng-template #edit>
<input [value]="user!.preferences!.backgroundColor" class="form-control"
(colorPickerChange)="handleBackgroundColorChange($event)"
[style.background]="user!.preferences!.backgroundColor" [cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor" />
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('auto-close-menu-label')" [subtitle]="t('auto-close-menu-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="autoCloseMenu" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('show-screen-hints-label')" [subtitle]="t('show-screen-hints-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="showScreenHints" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('emulate-comic-book-label')" [subtitle]="t('emulate-comic-book-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="emulate-comic-book"
formControlName="emulateBook" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('swipe-to-paginate-label')" [subtitle]="t('swipe-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="swipe-to-paginate"
formControlName="swipeToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('allow-auto-webtoon-reader-label')" [subtitle]="t('allow-auto-webtoon-reader-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="allow-auto-webtoon-reader"
formControlName="allowAutomaticWebtoonReaderDetection" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
</ng-container>
<div class="setting-section-break"></div>
<h4 id="book-reader-heading" class="mt-3">{{t('book-reader-settings-title')}}</h4>
<ng-container>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('tap-to-paginate-label')" [subtitle]="t('tap-to-paginate-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch" id="tap-to-paginate"
formControlName="bookReaderTapToPaginate" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('immersive-mode-label')" [subtitle]="t('immersive-mode-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" role="switch"
formControlName="bookReaderImmersiveMode" class="form-check-input"
aria-labelledby="auto-close-label">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderReadingDirection')!.value | readingDirection}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderReadingDirection">
@for (opt of readingDirections; track opt) {
<option [value]="opt.value">{{opt.value | readingDirection}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-family-label')" [subtitle]="t('font-family-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderFontFamily')!.value | titlecase}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderFontFamily">
@for (opt of fontFamilies; track opt) {
<option [value]="opt">{{opt | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('writing-style-label')" [subtitle]="t('writing-style-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderWritingStyle')!.value | writingStyle}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderWritingStyle">
@for (opt of bookWritingStyles; track opt) {
<option [value]="opt.value">{{opt.value | writingStyle}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('layout-mode-book-label')" [subtitle]="t('layout-mode-book-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderLayoutMode')!.value | bookPageLayoutMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderLayoutMode">
@for (opt of bookLayoutModes; track opt) {
<option [value]="opt.value">{{opt.value | bookPageLayoutMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('color-theme-book-label')" [subtitle]="t('color-theme-book-tooltip')">
<ng-template #view>
{{settingsForm.get('bookReaderThemeName')!.value}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderThemeName">
@for (opt of bookColorThemesTranslated; track opt) {
<option [value]="opt.name">{{opt.name | titlecase}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('font-size-book-label')" [subtitle]="t('font-size-book-tooltip')">
<ng-template #view>
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="fontsize" min="50" max="300" step="10"
formControlName="bookReaderFontSize">
</div>
<span class="ps-2 col-2 align-middle">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('line-height-book-label')" [subtitle]="t('line-height-book-tooltip')">
<ng-template #view>
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing">
</div>
<span class="ps-2 col-2 align-middle">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('margin-book-label')" [subtitle]="t('margin-book-tooltip')">
<ng-template #view>
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</ng-template>
<ng-template #edit>
<div class="row g-0">
<div class="col-10">
<input type="range" class="form-range" id="margin" min="0" max="30" step="5"
formControlName="bookReaderMargin">
</div>
<span class="ps-2 col-2 align-middle">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</div>
</ng-template>
</app-setting-item>
</div>
</ng-container>
<div class="setting-section-break"></div>
<h4 id="pdf-reader-heading" class="mt-3">{{t('pdf-reader-settings-title')}}</h4>
<ng-container>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-spread-mode-label')" [subtitle]="t('pdf-spread-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('pdfSpreadMode')!.value | pdfSpreadMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfSpreadMode">
@for (opt of pdfSpreadModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfSpreadMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-theme-label')" [subtitle]="t('pdf-theme-tooltip')">
<ng-template #view>
{{settingsForm.get('pdfTheme')!.value | pdfTheme}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfTheme">
@for (opt of pdfThemes; track opt) {
<option [value]="opt.value">{{opt.value | pdfTheme}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('pdf-scroll-mode-label')" [subtitle]="t('pdf-scroll-mode-tooltip')">
<ng-template #view>
{{settingsForm.get('pdfScrollMode')!.value | pdfScrollMode}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="pdf-reader-heading"
formControlName="pdfScrollMode">
@for (opt of pdfScrollModes; track opt) {
<option [value]="opt.value">{{opt.value | pdfScrollMode}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
</ng-container>
</form>
}

View file

@ -1,17 +1,7 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {
bookLayoutModes,
bookWritingStyles,
layoutModes,
pageSplitOptions,
pdfScrollModes,
pdfSpreadModes,
pdfThemes,
Preferences,
readingDirections,
readingModes,
scalingOptions
Preferences
} from "../../_models/preferences/preferences";
import {AccountService} from "../../_services/account.service";
import {BookService} from "../../book-reader/_services/book.service";
@ -44,6 +34,13 @@ import {PdfThemePipe} from "../../_pipes/pdf-theme.pipe";
import {PdfScrollModePipe} from "../../_pipes/pdf-scroll-mode.pipe";
import {LicenseService} from "../../_services/license.service";
import {ColorPickerDirective} from "ngx-color-picker";
import {
bookLayoutModes, bookWritingStyles,
layoutModes, pageSplitOptions,
pdfScrollModes,
pdfSpreadModes,
pdfThemes, readingDirections, readingModes, scalingOptions
} from "../../_models/preferences/reading-profiles";
@Component({
selector: 'app-manga-user-preferences',
@ -83,23 +80,6 @@ export class ManageUserPreferencesComponent implements OnInit {
private readonly localizationService = inject(LocalizationService);
protected readonly licenseService = inject(LicenseService);
protected readonly readingDirections = readingDirections;
protected readonly scalingOptions = scalingOptions;
protected readonly pageSplitOptions = pageSplitOptions;
protected readonly readerModes = readingModes;
protected readonly layoutModes = layoutModes;
protected readonly bookWritingStyles = bookWritingStyles;
protected readonly bookLayoutModes = bookLayoutModes;
protected readonly pdfSpreadModes = pdfSpreadModes;
protected readonly pdfThemes = pdfThemes;
protected readonly pdfScrollModes = pdfScrollModes;
bookColorThemesTranslated = bookColorThemes.map(o => {
const d = {...o};
d.name = translate('theme.' + d.translationKey);
return d;
});
fontFamilies: Array<string> = [];
locales: Array<KavitaLocale> = [];
@ -145,37 +125,6 @@ export class ManageUserPreferencesComponent implements OnInit {
this.user = results.user;
this.user.preferences = results.pref;
if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) {
this.user.preferences.bookReaderFontFamily = 'default';
}
this.settingsForm.addControl('readingDirection', new FormControl(this.user.preferences.readingDirection, []));
this.settingsForm.addControl('scalingOption', new FormControl(this.user.preferences.scalingOption, []));
this.settingsForm.addControl('pageSplitOption', new FormControl(this.user.preferences.pageSplitOption, []));
this.settingsForm.addControl('autoCloseMenu', new FormControl(this.user.preferences.autoCloseMenu, []));
this.settingsForm.addControl('showScreenHints', new FormControl(this.user.preferences.showScreenHints, []));
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
this.settingsForm.addControl('emulateBook', new FormControl(this.user.preferences.emulateBook, []));
this.settingsForm.addControl('swipeToPaginate', new FormControl(this.user.preferences.swipeToPaginate, []));
this.settingsForm.addControl('backgroundColor', new FormControl(this.user.preferences.backgroundColor, []));
this.settingsForm.addControl('allowAutomaticWebtoonReaderDetection', new FormControl(this.user.preferences.allowAutomaticWebtoonReaderDetection, []));
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(this.user.preferences.bookReaderReadingDirection, []));
this.settingsForm.addControl('bookReaderWritingStyle', new FormControl(this.user.preferences.bookReaderWritingStyle, []))
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.addControl('bookReaderLayoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.settingsForm.addControl('bookReaderThemeName', new FormControl(this.user?.preferences.bookReaderThemeName || bookColorThemes[0].name, []));
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, []));
this.settingsForm.addControl('pdfTheme', new FormControl(this.user?.preferences.pdfTheme || PdfTheme.Dark, []));
this.settingsForm.addControl('pdfScrollMode', new FormControl(this.user?.preferences.pdfScrollMode || PdfScrollMode.Vertical, []));
this.settingsForm.addControl('pdfSpreadMode', new FormControl(this.user?.preferences.pdfSpreadMode || PdfSpreadMode.None, []));
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
this.settingsForm.addControl('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, []));
this.settingsForm.addControl('blurUnreadSummaries', new FormControl(this.user.preferences.blurUnreadSummaries, []));
@ -222,7 +171,7 @@ export class ManageUserPreferencesComponent implements OnInit {
reset() {
if (!this.user) return;
this.settingsForm.get('readingDirection')?.setValue(this.user.preferences.readingDirection, {onlySelf: true, emitEvent: false});
/*this.settingsForm.get('readingDirection')?.setValue(this.user.preferences.readingDirection, {onlySelf: true, emitEvent: false});
this.settingsForm.get('scalingOption')?.setValue(this.user.preferences.scalingOption, {onlySelf: true, emitEvent: false});
this.settingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption, {onlySelf: true, emitEvent: false});
this.settingsForm.get('autoCloseMenu')?.setValue(this.user.preferences.autoCloseMenu, {onlySelf: true, emitEvent: false});
@ -247,7 +196,7 @@ export class ManageUserPreferencesComponent implements OnInit {
this.settingsForm.get('pdfTheme')?.setValue(this.user?.preferences.pdfTheme || PdfTheme.Dark, {onlySelf: true, emitEvent: false});
this.settingsForm.get('pdfScrollMode')?.setValue(this.user?.preferences.pdfScrollMode || PdfScrollMode.Vertical, {onlySelf: true, emitEvent: false});
this.settingsForm.get('pdfSpreadMode')?.setValue(this.user?.preferences.pdfSpreadMode || PdfSpreadMode.None, {onlySelf: true, emitEvent: false});
this.settingsForm.get('pdfSpreadMode')?.setValue(this.user?.preferences.pdfSpreadMode || PdfSpreadMode.None, {onlySelf: true, emitEvent: false});*/
this.settingsForm.get('theme')?.setValue(this.user.preferences.theme, {onlySelf: true, emitEvent: false});
this.settingsForm.get('globalPageLayoutMode')?.setValue(this.user.preferences.globalPageLayoutMode, {onlySelf: true, emitEvent: false});
@ -265,7 +214,7 @@ export class ManageUserPreferencesComponent implements OnInit {
packSettings(): Preferences {
const modelSettings = this.settingsForm.value;
return {
readingDirection: parseInt(modelSettings.readingDirection, 10),
/*readingDirection: parseInt(modelSettings.readingDirection, 10),
scalingOption: parseInt(modelSettings.scalingOption, 10),
pageSplitOption: parseInt(modelSettings.pageSplitOption, 10),
autoCloseMenu: modelSettings.autoCloseMenu,
@ -282,34 +231,23 @@ export class ManageUserPreferencesComponent implements OnInit {
bookReaderReadingDirection: parseInt(modelSettings.bookReaderReadingDirection, 10),
bookReaderWritingStyle: parseInt(modelSettings.bookReaderWritingStyle, 10),
bookReaderLayoutMode: parseInt(modelSettings.bookReaderLayoutMode, 10),
bookReaderThemeName: modelSettings.bookReaderThemeName,
bookReaderThemeName: modelSettings.bookReaderThemeName,*/
theme: modelSettings.theme,
bookReaderImmersiveMode: modelSettings.bookReaderImmersiveMode,
//bookReaderImmersiveMode: modelSettings.bookReaderImmersiveMode,
globalPageLayoutMode: parseInt(modelSettings.globalPageLayoutMode, 10),
blurUnreadSummaries: modelSettings.blurUnreadSummaries,
promptForDownloadSize: modelSettings.promptForDownloadSize,
noTransitions: modelSettings.noTransitions,
emulateBook: modelSettings.emulateBook,
swipeToPaginate: modelSettings.swipeToPaginate,
//emulateBook: modelSettings.emulateBook,
//swipeToPaginate: modelSettings.swipeToPaginate,
collapseSeriesRelationships: modelSettings.collapseSeriesRelationships,
shareReviews: modelSettings.shareReviews,
locale: modelSettings.locale || 'en',
pdfTheme: parseInt(modelSettings.pdfTheme, 10),
pdfScrollMode: parseInt(modelSettings.pdfScrollMode, 10),
pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10),
//pdfTheme: parseInt(modelSettings.pdfTheme, 10),
//pdfScrollMode: parseInt(modelSettings.pdfScrollMode, 10),
//pdfSpreadMode: parseInt(modelSettings.pdfSpreadMode, 10),
aniListScrobblingEnabled: modelSettings.aniListScrobblingEnabled,
wantToReadSync: modelSettings.wantToReadSync
wantToReadSync: modelSettings.wantToReadSync,
};
}
handleBackgroundColorChange(color: string) {
this.settingsForm.markAsDirty();
this.settingsForm.markAsTouched();
if (this.user?.preferences) {
this.user.preferences.backgroundColor = color;
}
this.settingsForm.get('backgroundColor')?.setValue(color);
this.cdRef.markForCheck();
}
}

View file

@ -106,7 +106,7 @@
"user-preferences": {
"title": "User Dashboard",
"pref-description": "These are global settings that are bound to your account.",
"pref-description": "These are global settings that are bound to your account. Reader settings are located in Reading Profiles.",
"account-tab": "{{tabs.account-tab}}",
"preferences-tab": "{{tabs.preferences-tab}}",
@ -140,60 +140,6 @@
"want-to-read-sync-label": "Want To Read Sync",
"want-to-read-sync-tooltip": "Allow Kavita to add items to your Want to Read list based on AniList and MAL series in Pending readlist",
"image-reader-settings-title": "Image Reader",
"reading-direction-label": "Reading Direction",
"reading-direction-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"scaling-option-label": "Scaling Options",
"scaling-option-tooltip": "How to scale the image to your screen.",
"page-splitting-label": "Page Splitting",
"page-splitting-tooltip": "How to split a full width image (ie both left and right images are combined)",
"reading-mode-label": "Reading Mode",
"reading-mode-tooltip": "Change reader to paginate vertically, horizontally, or have an infinite scroll",
"layout-mode-label": "Layout Mode",
"layout-mode-tooltip": "Render a single image to the screen or two side-by-side images",
"background-color-label": "Background Color",
"background-color-tooltip": "Background Color of Image Reader",
"auto-close-menu-label": "Auto Close Menu",
"auto-close-menu-tooltip": "Should menu auto close",
"show-screen-hints-label": "Show Screen Hints",
"show-screen-hints-tooltip": "Show an overlay to help understand pagination area and direction",
"emulate-comic-book-label": "Emulate comic book",
"emulate-comic-book-tooltip": "Applies a shadow effect to emulate reading from a book",
"swipe-to-paginate-label": "Swipe to Paginate",
"swipe-to-paginate-tooltip": "Should swiping on the screen cause the next or previous page to be triggered",
"allow-auto-webtoon-reader-label": "Automatic Webtoon Reader Mode",
"allow-auto-webtoon-reader-tooltip": "Switch into Webtoon Reader mode if pages look like a webtoon. Some false positives may occur.",
"book-reader-settings-title": "Book Reader",
"tap-to-paginate-label": "Tap to Paginate",
"tap-to-paginate-tooltip": "Should the sides of the book reader screen allow tapping on it to move to prev/next page",
"immersive-mode-label": "Immersive Mode",
"immersive-mode-tooltip": "This will hide the menu behind a click on the reader document and turn tap to paginate on",
"reading-direction-book-label": "Reading Direction",
"reading-direction-book-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"font-family-label": "Font Family",
"font-family-tooltip": "Font family to load up. Default will load the book's default font",
"writing-style-label": "Writing Style",
"writing-style-tooltip": "Changes the direction of the text. Horizontal is left to right, vertical is top to bottom.",
"layout-mode-book-label": "Layout Mode",
"layout-mode-book-tooltip": "How content should be laid out. Scroll is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page",
"color-theme-book-label": "Color Theme",
"color-theme-book-tooltip": "What color theme to apply to the book reader content and menu",
"font-size-book-label": "Font Size",
"font-size-book-tooltip": "Percent of scaling to apply to font in the book",
"line-height-book-label": "Line Spacing",
"line-height-book-tooltip": "How much spacing between the lines of the book",
"margin-book-label": "Margin",
"margin-book-tooltip": "How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.",
"pdf-reader-settings-title": "PDF Reader",
"pdf-scroll-mode-label": "Scroll Mode",
"pdf-scroll-mode-tooltip": "How you scroll through pages. Vertical/Horizontal and Tap to Paginate (no scroll)",
"pdf-spread-mode-label": "Spread Mode",
"pdf-spread-mode-tooltip": "How pages should be laid out. Single or double (odd/even)",
"pdf-theme-label": "Theme",
"pdf-theme-tooltip": "Color theme of the reader",
"clients-opds-alert": "OPDS is not enabled on this server. This will not affect Tachiyomi users.",
"clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.",
"clients-api-key-tooltip": "The API key is like a password. Resetting it will invalidate any existing clients.",
@ -941,7 +887,7 @@
"series-detail": {
"page-settings-title": "Page Settings",
"close": "{{common.close}}",
"layout-mode-label": "{{user-preferences.layout-mode-book-label}}",
"layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}",
"layout-mode-option-card": "Card",
"layout-mode-option-list": "List",
"continue-from": "Continue {{title}}",
@ -1181,36 +1127,45 @@
"reader-settings": {
"general-settings-title": "General Settings",
"font-family-label": "{{user-preferences.font-family-label}}",
"font-size-label": "{{user-preferences.font-size-book-label}}",
"line-spacing-label": "{{user-preferences.line-height-book-label}}",
"margin-label": "{{user-preferences.margin-book-label}}",
"font-family-label": "{{manage-reading-profiles.font-family-label}}",
"font-size-label": "{{manage-reading-profiles.font-size-book-label}}",
"line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}",
"margin-label": "{{manage-reading-profiles.margin-book-label}}",
"reset-to-defaults": "Reset to Defaults",
"update-parent": "Save to {{name}}",
"loading": "loading",
"create-new": "New profile from implicit",
"create-new-tooltip": "Create a new manageable profile from your current implicit one",
"reading-profile-updated": "Reading profile updated",
"reading-profile-promoted": "Reading profile promoted",
"reader-settings-title": "Reader Settings",
"reading-direction-label": "{{user-preferences.reading-direction-book-label}}",
"reading-direction-label": "{{manage-reading-profiles.reading-direction-book-label}}",
"right-to-left": "Right to Left",
"left-to-right": "Left to Right",
"horizontal": "Horizontal",
"vertical": "Vertical",
"writing-style-label": "{{user-preferences.writing-style-label}}",
"writing-style-label": "{{manage-reading-profiles.writing-style-label}}",
"writing-style-tooltip": "Changes the direction of the text. Horizontal is left to right, vertical is top to bottom.",
"tap-to-paginate-label": "Tap Pagination",
"tap-to-paginate-tooltip": "Click the edges of the screen to paginate",
"on": "On",
"off": "Off",
"immersive-mode-label": "{{user-preferences.immersive-mode-label}}",
"immersive-mode-label": "{{manage-reading-profiles.immersive-mode-label}}",
"immersive-mode-tooltip": "This will hide the menu behind a click on the reader document and turn tap to paginate on",
"fullscreen-label": "Fullscreen",
"fullscreen-tooltip": "Put reader in fullscreen mode",
"exit": "Exit",
"enter": "Enter",
"layout-mode-label": "{{user-preferences.layout-mode-book-label}}",
"layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}",
"layout-mode-tooltip": "Scroll: Mirrors epub file (usually one long scrolling page per chapter).<br/>1 Column: Creates a single virtual page at a time.<br/>2 Column: Creates two virtual pages at a time laid out side-by-side.",
"layout-mode-option-scroll": "Scroll",
"layout-mode-option-1col": "1 Column",
"layout-mode-option-2col": "2 Column",
"color-theme-title": "Color Theme",
"line-spacing-min-label": "1x",
"line-spacing-max-label": "2.5x",
"theme-dark": "Dark",
"theme-black": "Black",
"theme-white": "White",
@ -1324,6 +1279,17 @@
"create": "{{common.create}}"
},
"bulk-set-reading-profile-modal": {
"title": "Set Reading profile",
"close": "{{common.close}}",
"filter-label": "{{common.filter}}",
"clear": "{{common.clear}}",
"no-data": "No collections created yet",
"loading": "{{common.loading}}",
"create": "{{common.create}}",
"bound": "Bound"
},
"entity-title": {
"special": "Special",
"issue-num": "{{common.issue-hash-num}}",
@ -1720,6 +1686,7 @@
"scrobble-holds": "Scrobble Holds",
"account": "Account",
"preferences": "Preferences",
"reading-profiles": "Reading Profiles",
"clients": "API Key / OPDS",
"devices": "Devices",
"user-stats": "Stats",
@ -1990,7 +1957,10 @@
"manga-reader": {
"back": "Back",
"save-globally": "Save Globally",
"update-parent": "{{reader-settings.update-parent}}",
"loading": "{{reader-settings.loading}}",
"create-new": "{{reader-settings.create-new}}",
"create-new-tooltip": "{{reader-settings.create-new-tooltip}}",
"incognito-alt": "Incognito mode is on. Toggle to turn off.",
"incognito-title": "Incognito Mode:",
"shortcuts-menu-alt": "Keyboard Shortcuts Modal",
@ -2012,9 +1982,9 @@
"height": "Height",
"width": "Width",
"width-override-label": "Width Override",
"off": "Off",
"off": "{{reader-settings.off}}",
"original": "Original",
"auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}",
"auto-close-menu-label": "{{manage-reading-profiles.auto-close-menu-label}}",
"swipe-enabled-label": "Swipe Enabled",
"enable-comic-book-label": "Emulate comic book",
"brightness-label": "Brightness",
@ -2026,8 +1996,9 @@
"layout-mode-switched": "Layout mode switched to Single due to insufficient space to render double layout",
"no-next-chapter": "No Next Chapter",
"no-prev-chapter": "No Previous Chapter",
"user-preferences-updated": "User preferences updated",
"emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}",
"reading-profile-updated": "Reading profile updated",
"reading-profile-promoted": "Reading profile promoted",
"emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}",
"series-progress": "Series Progress: {{percentage}}"
},
@ -2708,7 +2679,9 @@
"bulk-delete-libraries": "Are you sure you want to delete {{count}} libraries?",
"match-success": "Series matched correctly",
"webtoon-override": "Switching to Webtoon mode due to images representing a webtoon.",
"scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services."
"scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services.",
"series-bound-to-reading-profile": "Series bound to Reading Profile {{name}}",
"library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}"
},
"read-time-pipe": {
@ -2762,6 +2735,13 @@
"remove-from-on-deck": "Remove From On Deck",
"remove-from-on-deck-tooltip": "Remove series from showing from On Deck",
"reading-profiles": "Reading Profiles",
"set-reading-profile": "Set Reading Profile",
"set-reading-profile-tooltip": "Bind a Reading Profile to this Library",
"clear-reading-profile": "Clear Reading Profile",
"clear-reading-profile-tooltip": "Clear Reading Profile for this Library",
"cleared-profile": "Cleared Reading Profile",
"others": "Others",
"add-to-reading-list": "Add to Reading List",
"add-to-reading-list-tooltip": "Add to a Reading List",
@ -2843,6 +2823,80 @@
"pdf-dark": "Dark"
},
"manage-reading-profiles": {
"description": "Not all your series may be read in the same way, set up distinct reading profiles per library or series to make getting back in your series as seamless as possible.",
"extra-tip": "Assign reading profiles via the action menu on series and libraries, or in bulk. When changing settings in a reader, a hidden profile is created that remembers your choices for that series (not for pdfs). This profile is removed when you assign or update one of your own reading profiles to the series.",
"profiles-title": "Your reading profiles",
"default-profile": "Default",
"add": "{{common.add}}",
"add-tooltip": "Your new profile will be saved after making a change to it",
"make-default": "Set as default",
"no-selected": "No profile selected",
"confirm": "Are you sure you want to delete the reading profile {{name}}?",
"selection-tip": "Select a profile from the list, or create a new one at the top right",
"image-reader-settings-title": "Image Reader",
"reading-direction-label": "Reading Direction",
"reading-direction-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"scaling-option-label": "Scaling Options",
"scaling-option-tooltip": "How to scale the image to your screen.",
"page-splitting-label": "Page Splitting",
"page-splitting-tooltip": "How to split a full width image (ie both left and right images are combined)",
"reading-mode-label": "Reading Mode",
"reading-mode-tooltip": "Change reader to paginate vertically, horizontally, or have an infinite scroll",
"layout-mode-label": "Layout Mode",
"layout-mode-tooltip": "Render a single image to the screen or two side-by-side images",
"background-color-label": "Background Color",
"background-color-tooltip": "Background Color of Image Reader",
"auto-close-menu-label": "Auto Close Menu",
"auto-close-menu-tooltip": "Should menu auto close",
"show-screen-hints-label": "Show Screen Hints",
"show-screen-hints-tooltip": "Show an overlay to help understand pagination area and direction",
"emulate-comic-book-label": "Emulate comic book",
"emulate-comic-book-tooltip": "Applies a shadow effect to emulate reading from a book",
"swipe-to-paginate-label": "Swipe to Paginate",
"swipe-to-paginate-tooltip": "Should swiping on the screen cause the next or previous page to be triggered",
"allow-auto-webtoon-reader-label": "Automatic Webtoon Reader Mode",
"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",
"reset": "{{common.reset}}",
"book-reader-settings-title": "Book Reader",
"tap-to-paginate-label": "Tap to Paginate",
"tap-to-paginate-tooltip": "Should the sides of the book reader screen allow tapping on it to move to prev/next page",
"immersive-mode-label": "Immersive Mode",
"immersive-mode-tooltip": "This will hide the menu behind a click on the reader document and turn tap to paginate on",
"reading-direction-book-label": "Reading Direction",
"reading-direction-book-tooltip": "Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.",
"font-family-label": "Font Family",
"font-family-tooltip": "Font family to load up. Default will load the book's default font",
"writing-style-label": "Writing Style",
"writing-style-tooltip": "Changes the direction of the text. Horizontal is left to right, vertical is top to bottom.",
"layout-mode-book-label": "Layout Mode",
"layout-mode-book-tooltip": "How content should be laid out. Scroll is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page",
"color-theme-book-label": "Color Theme",
"color-theme-book-tooltip": "What color theme to apply to the book reader content and menu",
"font-size-book-label": "Font Size",
"font-size-book-tooltip": "Percent of scaling to apply to font in the book",
"line-height-book-label": "Line Spacing",
"line-height-book-tooltip": "How much spacing between the lines of the book",
"margin-book-label": "Margin",
"margin-book-tooltip": "How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.",
"pdf-reader-settings-title": "PDF Reader",
"pdf-scroll-mode-label": "Scroll Mode",
"pdf-scroll-mode-tooltip": "How you scroll through pages. Vertical/Horizontal and Tap to Paginate (no scroll)",
"pdf-spread-mode-label": "Spread Mode",
"pdf-spread-mode-tooltip": "How pages should be laid out. Single or double (odd/even)",
"pdf-theme-label": "Theme",
"pdf-theme-tooltip": "Color theme of the reader",
"reading-profile-series-settings-title": "Series",
"reading-profile-library-settings-title": "Library",
"delete": "{{common.delete}}"
},
"validation": {
"required-field": "This field is required",