Kavita/UI/Web/src/app/book-reader/reader-settings/reader-settings.component.ts
Joseph Milazzo 4e49aa47ce
Change Detection: On Push aka UI Smoothness (#1369)
* Updated Series Info Cards to use OnPush and hooked in progress events when we do a mark as read/unread on entities. These events update progress bars but also will now trigger a re-calculation on Read Time Left.

* Removed Library Card Component

* Refactored manga reader title and subtitle calculation to the backend.

* Coverted card actionables to onPush

* Series Card on push cleanup

* Updated edit collection tags for on push

* Update cover image chooser for on push

* Cleaned up carsouel reel

* Updated cover image to allow for uploading gif and webp files

* Bulk add to collection on push

* Updated bulk operation to use on push. Updated bulk operation to have mark as unread and read buttons explicitly. Updated so add to collection is visible and delete.

Fixed a bug where manage library component wasn't invoking the trackBy function

* Updating entity title for on push

* Removed file info component

* Updated Mange Library for on push

* Entity info cards on push

* List item on push

* Updated icon and title for on push and fixed some missing change detection on series detail

* Restricted the typeahead interface to simplify the design

* Edit Series Relation now shows a value in the dropdown for Parent relationships and disables the field.

* Updated edit series relation to focus on new typeahead when adding a new relationship

* Added some documentation and when Scanning a library, don't allow the user to enqueue the same job multiple times.

* Applied the No-enqueue if already enqueued logic to other tasks

* Library detail on push

* Updated events widget to onpush

* Card detail drawer on push. Card detail cover chooser now will show all chapter's covers for selection in cover chooser.

* Chapter metadata detail on push

* Removed Card Detail modal

* All collections on push

* Removed some comments

* Updated bulk selection to use an observable rather than function calls so new on push strategy works

* collection detail now uses on push and scroller is placed on correct element

* Updated library recommended to on push. Ensure that when mark as read occurs, the appropriate streams are refreshed.

* Updated library detail to on push

* Update metadata fiter to onpush. Bugs found and reported to Project

* person badge on push

* Read more on push

* Updated tag badge to on push

* User login on push

* When initing side nav, don't call an authenticated api until we are sure a user is logged in

* Updated splash container to on push

* Dashboard on push

* Side nav slight refactor around some api calls

* Cleaned up series card on push to use same cdRef naming convention

* Updated Static Files to use caching

* Added width and height to logo image

* shortcuts modal on push

* reading lists on push

* Reading list detail on push

* draggable ordered list on push

* Refactored reading-list-detail to use a new item which drastically reduces renders on operations

* series format on push

* circular loader on push

* Badge Expander on push

* update notification modal on push

* drawer on push

* Edit Series Modal on push

* reset password on push

* review series modal on push

* series metadata detail on push

* theme manager on push

* confirm reset password on push

* register on push

* confirm migration email on push

* confirm email on push

* add email to account migration on push

* user preferences on push. Made global settings default open

* edit series relation on push

* Fixed an edge case bug for next chapter where if the current volume had a single chapter of 1 and the next volume had a chapter number of 0, it would say there are no more chapters.

* Updated infinite scroller with on push support

* Moved some animations over to typeahead, not integrated yet.

* Manga reader is now on push

* Reader settings on push

* refactored how we close the book

* Updated table of contents for on push

* Updated book reader for on push. Fixed a bug where table of contents wasn't showing current page anchor due to a scroll calulation bug

* Small code tweak

* Icon and title on push

* nav header on push

* grouped typeahead on push

* typeahead on push and added a new trackby identity function to allow even faster rendering of big lists

* pdf reader on push

* code cleanup
2022-07-11 08:57:07 -07:00

298 lines
11 KiB
TypeScript

import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Subject, take, takeUntil } from 'rxjs';
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
import { BookTheme } from 'src/app/_models/preferences/book-theme';
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
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 '../book.service';
import { BookBlackTheme } from '../_models/book-black-theme';
import { BookDarkTheme } from '../_models/book-dark-theme';
import { BookWhiteTheme } from '../_models/book-white-theme';
/**
* Used for book reader. Do not use for other components
*/
export interface PageStyle {
'font-family': string;
'font-size': string;
'line-height': string;
'margin-left': string;
'margin-right': string;
}
export const bookColorThemes = [
{
name: 'Dark',
colorHash: '#292929',
isDarkTheme: true,
isDefault: true,
provider: ThemeProvider.System,
selector: 'brtheme-dark',
content: BookDarkTheme
},
{
name: 'Black',
colorHash: '#000000',
isDarkTheme: true,
isDefault: false,
provider: ThemeProvider.System,
selector: 'brtheme-black',
content: BookBlackTheme
},
{
name: 'White',
colorHash: '#FFFFFF',
isDarkTheme: false,
isDefault: false,
provider: ThemeProvider.System,
selector: 'brtheme-white',
content: BookWhiteTheme
},
];
const mobileBreakpointMarginOverride = 700;
@Component({
selector: 'app-reader-settings',
templateUrl: './reader-settings.component.html',
styleUrls: ['./reader-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReaderSettingsComponent implements OnInit, OnDestroy {
/**
* Outputs when clickToPaginate is changed
*/
@Output() clickToPaginateChanged: EventEmitter<boolean> = new EventEmitter();
/**
* Outputs when a style is updated and the reader needs to render it
*/
@Output() styleUpdate: EventEmitter<PageStyle> = new EventEmitter();
/**
* Outputs when a theme/dark mode is updated
*/
@Output() colorThemeUpdate: EventEmitter<BookTheme> = new EventEmitter();
/**
* Outputs when a layout mode is updated
*/
@Output() layoutModeUpdate: EventEmitter<BookPageLayoutMode> = new EventEmitter();
/**
* Outputs when fullscreen is toggled
*/
@Output() fullscreen: EventEmitter<void> = new EventEmitter();
/**
* Outputs when reading direction is changed
*/
@Output() readingDirection: EventEmitter<ReadingDirection> = new EventEmitter();
/**
* Outputs when immersive mode is changed
*/
@Output() immersiveMode: EventEmitter<boolean> = new EventEmitter();
user!: User;
/**
* List of all font families user can select from
*/
fontOptions: Array<string> = [];
fontFamilies: Array<FontFamily> = [];
/**
* Internal property used to capture all the different css properties to render on all elements
*/
pageStyles!: PageStyle;
readingDirectionModel: ReadingDirection = ReadingDirection.LeftToRight;
activeTheme: BookTheme | undefined;
isFullscreen: boolean = false;
settingsForm: FormGroup = new FormGroup({});
/**
* System provided themes
*/
themes: Array<BookTheme> = bookColorThemes;
private onDestroy: Subject<void> = new Subject();
get BookPageLayoutMode(): typeof BookPageLayoutMode {
return BookPageLayoutMode;
}
get ReadingDirection() {
return ReadingDirection;
}
constructor(private bookService: BookService, private accountService: AccountService,
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService,
private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.fontFamilies = this.bookService.getFontFamilies();
this.fontOptions = this.fontFamilies.map(f => f.title);
this.cdRef.markForCheck();
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;
}
this.readingDirectionModel = this.user.preferences.bookReaderReadingDirection;
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntil(this.onDestroy)).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(takeUntil(this.onDestroy)).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(takeUntil(this.onDestroy)).subscribe(value => {
this.clickToPaginateChanged.emit(value);
});
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntil(this.onDestroy)).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(takeUntil(this.onDestroy)).subscribe(value => {
this.pageStyles['margin-left'] = value + '%';
this.pageStyles['margin-right'] = value + '%';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((layoutMode: BookPageLayoutMode) => {
this.layoutModeUpdate.emit(layoutMode);
});
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user.preferences.bookReaderImmersiveMode, []));
this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).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.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();
}
});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
resetSettings() {
if (this.user) {
this.setPageStyles(this.user.preferences.bookReaderFontFamily, this.user.preferences.bookReaderFontSize + '%', this.user.preferences.bookReaderMargin + '%', this.user.preferences.bookReaderLineSpacing + '%');
} else {
this.setPageStyles();
}
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
this.cdRef.markForCheck();
this.styleUpdate.emit(this.pageStyles);
}
/**
* Internal method to be used by resetSettings. Pass items in with quantifiers
*/
setPageStyles(fontFamily?: string, fontSize?: string, margin?: string, lineHeight?: string, colorTheme?: string) {
const windowWidth = window.innerWidth
|| this.document.documentElement.clientWidth
|| this.document.body.clientWidth;
let defaultMargin = '15%';
if (windowWidth <= mobileBreakpointMarginOverride) {
defaultMargin = '5%';
}
this.pageStyles = {
'font-family': fontFamily || this.pageStyles['font-family'] || 'default',
'font-size': fontSize || this.pageStyles['font-size'] || '100%',
'margin-left': margin || this.pageStyles['margin-left'] || defaultMargin,
'margin-right': margin || this.pageStyles['margin-right'] || defaultMargin,
'line-height': lineHeight || this.pageStyles['line-height'] || '100%'
};
}
setTheme(themeName: string) {
const theme = this.themes.find(t => t.name === themeName);
this.activeTheme = theme;
this.cdRef.markForCheck();
this.colorThemeUpdate.emit(theme);
}
toggleReadingDirection() {
if (this.readingDirectionModel === ReadingDirection.LeftToRight) {
this.readingDirectionModel = ReadingDirection.RightToLeft;
} else {
this.readingDirectionModel = ReadingDirection.LeftToRight;
}
this.cdRef.markForCheck();
this.readingDirection.emit(this.readingDirectionModel);
}
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
this.cdRef.markForCheck();
this.fullscreen.emit();
}
}