Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.
This commit is contained in:
Joe Milazzo 2023-05-21 12:30:32 -05:00 committed by GitHub
parent 9bc8361381
commit 9c06cccd35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 3964 additions and 20426 deletions

21753
UI/Web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,32 +7,29 @@
"build": "ng build", "build": "ng build",
"prod": "ng build --configuration production --aot --output-hashing=all", "prod": "ng build --configuration production --aot --output-hashing=all",
"explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json", "explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e" "e2e": "ng e2e"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^15.2.7", "@angular/animations": "^16.0.2",
"@angular/cdk": "^15.2.7", "@angular/cdk": "^16.0.1",
"@angular/common": "^15.2.7", "@angular/common": "^16.0.2",
"@angular/compiler": "^15.2.7", "@angular/compiler": "^16.0.2",
"@angular/core": "^15.2.7", "@angular/core": "^16.0.2",
"@angular/forms": "^15.2.7", "@angular/forms": "^16.0.2",
"@angular/localize": "^15.2.7", "@angular/localize": "^16.0.2",
"@angular/platform-browser": "^15.2.7", "@angular/platform-browser": "^16.0.2",
"@angular/platform-browser-dynamic": "^15.2.7", "@angular/platform-browser-dynamic": "^16.0.2",
"@angular/router": "^15.2.7", "@angular/router": "^16.0.2",
"@fortawesome/fontawesome-free": "^6.2.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@iharbeck/ngx-virtual-scroller": "^15.2.0", "@iharbeck/ngx-virtual-scroller": "^16.0.0",
"@iplab/ngx-file-upload": "^15.0.0", "@iplab/ngx-file-upload": "^16.0.1",
"@microsoft/signalr": "^7.0.5", "@microsoft/signalr": "^7.0.5",
"@ng-bootstrap/ng-bootstrap": "^14.0.1", "@ng-bootstrap/ng-bootstrap": "^14.1.1",
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.1.2", "@swimlane/ngx-charts": "^20.1.2",
"@tweenjs/tween.js": "^18.6.4", "@tweenjs/tween.js": "^20.0.3",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"eventsource": "^2.0.2", "eventsource": "^2.0.2",
@ -43,36 +40,30 @@
"ngx-extended-pdf-viewer": "^16.2.16", "ngx-extended-pdf-viewer": "^16.2.16",
"ngx-file-drop": "^15.0.0", "ngx-file-drop": "^15.0.0",
"ngx-slider-v2": "^15.0.4", "ngx-slider-v2": "^15.0.4",
"ngx-toastr": "^16.1.1", "ngx-toastr": "^17.0.2",
"rxjs": "^7.8.0", "rxjs": "^7.8.0",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"swiper": "^8.4.6", "swiper": "^8.4.6",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.12.0" "zone.js": "^0.13.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^15.2.6", "@angular-devkit/build-angular": "^16.0.2",
"@angular-eslint/builder": "15.2.1", "@angular-eslint/builder": "^16.0.2",
"@angular-eslint/eslint-plugin": "15.2.1", "@angular-eslint/eslint-plugin": "^16.0.2",
"@angular-eslint/eslint-plugin-template": "15.2.1", "@angular-eslint/eslint-plugin-template": "^16.0.2",
"@angular-eslint/schematics": "15.2.1", "@angular-eslint/schematics": "^16.0.2",
"@angular-eslint/template-parser": "15.2.1", "@angular-eslint/template-parser": "^16.0.2",
"@angular/cli": "^15.2.6", "@angular/cli": "^16.0.2",
"@angular/compiler-cli": "^15.2.7", "@angular/compiler-cli": "^16.0.2",
"@playwright/test": "^1.30.0",
"@types/d3": "^7.4.0", "@types/d3": "^7.4.0",
"@types/jest": "^27.5.2", "@types/node": "^20.2.1",
"@types/node": "^17.0.45",
"@typescript-eslint/eslint-plugin": "5.48.1", "@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.1", "@typescript-eslint/parser": "5.59.6",
"ajv": "^7.2.4", "eslint": "^8.41.0",
"eslint": "^8.31.0",
"jest": "^27.5.1",
"jest-preset-angular": "^11.1.2",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"playwright": "^1.30.0", "ts-node": "~10.9.1",
"ts-node": "~10.5.0", "typescript": "~5.0.4",
"typescript": "~4.9.4",
"webpack-bundle-analyzer": "^4.8.0" "webpack-bundle-analyzer": "^4.8.0"
} }
} }

View File

@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable, OnDestroy } from '@angular/core'; import {DestroyRef, inject, Injectable, OnDestroy} from '@angular/core';
import { of, ReplaySubject, Subject } from 'rxjs'; import { of, ReplaySubject, Subject } from 'rxjs';
import { filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
@ -14,20 +14,22 @@ import { UpdateEmailResponse } from '../_models/auth/update-email-response';
import { AgeRating } from '../_models/metadata/age-rating'; import { AgeRating } from '../_models/metadata/age-rating';
import { AgeRestriction } from '../_models/metadata/age-restriction'; import { AgeRestriction } from '../_models/metadata/age-restriction';
import { TextResonse } from '../_types/text-response'; import { TextResonse } from '../_types/text-response';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
export enum Role { export enum Role {
Admin = 'Admin', Admin = 'Admin',
ChangePassword = 'Change Password', ChangePassword = 'Change Password',
Bookmark = 'Bookmark', Bookmark = 'Bookmark',
Download = 'Download', Download = 'Download',
ChangeRestriction = 'Change Restriction' ChangeRestriction = 'Change Restriction'
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AccountService implements OnDestroy { export class AccountService {
private readonly destroyRef = inject(DestroyRef);
baseUrl = environment.apiUrl; baseUrl = environment.apiUrl;
userKey = 'kavita-user'; userKey = 'kavita-user';
public lastLoginKey = 'kavita-lastlogin'; public lastLoginKey = 'kavita-lastlogin';
@ -42,21 +44,14 @@ export class AccountService implements OnDestroy {
*/ */
private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined; private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined;
private readonly onDestroy = new Subject<void>(); constructor(private httpClient: HttpClient, private router: Router,
constructor(private httpClient: HttpClient, private router: Router,
private messageHub: MessageHubService, private themeService: ThemeService) { private messageHub: MessageHubService, private themeService: ThemeService) {
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate), messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
map(evt => evt.payload as UserUpdateEvent), map(evt => evt.payload as UserUpdateEvent),
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username), filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
switchMap(() => this.refreshToken())) switchMap(() => this.refreshToken()))
.subscribe(() => {}); .subscribe(() => {});
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
hasAdminRole(user: User) { hasAdminRole(user: User) {
return user && user.roles.includes(Role.Admin); return user && user.roles.includes(Role.Admin);
@ -91,7 +86,7 @@ export class AccountService implements OnDestroy {
this.messageHub.createHubConnection(user, this.hasAdminRole(user)); this.messageHub.createHubConnection(user, this.hasAdminRole(user));
} }
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
@ -114,7 +109,7 @@ export class AccountService implements OnDestroy {
this.currentUser = user; this.currentUser = user;
this.currentUserSource.next(user); this.currentUserSource.next(user);
this.stopRefreshTokenTimer(); this.stopRefreshTokenTimer();
if (this.currentUser !== undefined) { if (this.currentUser !== undefined) {
@ -135,15 +130,15 @@ export class AccountService implements OnDestroy {
/** /**
* Registers the first admin on the account. Only used for that. All other registrations must occur through invite * Registers the first admin on the account. Only used for that. All other registrations must occur through invite
* @param model * @param model
* @returns * @returns
*/ */
register(model: {username: string, password: string, email: string}) { register(model: {username: string, password: string, email: string}) {
return this.httpClient.post<User>(this.baseUrl + 'account/register', model).pipe( return this.httpClient.post<User>(this.baseUrl + 'account/register', model).pipe(
map((user: User) => { map((user: User) => {
return user; return user;
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
@ -177,8 +172,8 @@ export class AccountService implements OnDestroy {
/** /**
* Given a user id, returns a full url for setting up the user account * Given a user id, returns a full url for setting up the user account
* @param userId * @param userId
* @returns * @returns
*/ */
getInviteUrl(userId: number, withBaseUrl: boolean = true) { getInviteUrl(userId: number, withBaseUrl: boolean = true) {
return this.httpClient.get<string>(this.baseUrl + 'account/invite-url?userId=' + userId + '&withBaseUrl=' + withBaseUrl, TextResonse); return this.httpClient.get<string>(this.baseUrl + 'account/invite-url?userId=' + userId + '&withBaseUrl=' + withBaseUrl, TextResonse);
@ -214,7 +209,7 @@ export class AccountService implements OnDestroy {
/** /**
* This will get latest preferences for a user and cache them into user store * This will get latest preferences for a user and cache them into user store
* @returns * @returns
*/ */
getPreferences() { getPreferences() {
return this.httpClient.get<Preferences>(this.baseUrl + 'users/get-preferences').pipe(map(pref => { return this.httpClient.get<Preferences>(this.baseUrl + 'users/get-preferences').pipe(map(pref => {
@ -223,7 +218,7 @@ export class AccountService implements OnDestroy {
this.setCurrentUser(this.currentUser); this.setCurrentUser(this.currentUser);
} }
return pref; return pref;
}), takeUntil(this.onDestroy)); }), takeUntilDestroyed(this.destroyRef));
} }
updatePreferences(userPreferences: Preferences) { updatePreferences(userPreferences: Preferences) {
@ -233,13 +228,13 @@ export class AccountService implements OnDestroy {
this.setCurrentUser(this.currentUser); this.setCurrentUser(this.currentUser);
} }
return settings; return settings;
}), takeUntil(this.onDestroy)); }), takeUntilDestroyed(this.destroyRef));
} }
getUserFromLocalStorage(): User | undefined { getUserFromLocalStorage(): User | undefined {
const userString = localStorage.getItem(this.userKey); const userString = localStorage.getItem(this.userKey);
if (userString) { if (userString) {
return JSON.parse(userString) return JSON.parse(userString)
}; };
@ -254,7 +249,7 @@ export class AccountService implements OnDestroy {
user.apiKey = key; user.apiKey = key;
localStorage.setItem(this.userKey, JSON.stringify(user)); localStorage.setItem(this.userKey, JSON.stringify(user));
this.currentUserSource.next(user); this.currentUserSource.next(user);
this.currentUser = user; this.currentUser = user;
} }
@ -270,7 +265,7 @@ export class AccountService implements OnDestroy {
this.currentUser.token = user.token; this.currentUser.token = user.token;
this.currentUser.refreshToken = user.refreshToken; this.currentUser.refreshToken = user.refreshToken;
} }
this.setCurrentUser(this.currentUser); this.setCurrentUser(this.currentUser);
return user; return user;
})); }));

View File

@ -1,16 +1,17 @@
import { Injectable, OnDestroy } from '@angular/core'; import {DestroyRef, inject, Injectable, OnDestroy} from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { ThemeService } from './theme.service'; import { ThemeService } from './theme.service';
import { RecentlyAddedItem } from '../_models/recently-added-item'; import { RecentlyAddedItem } from '../_models/recently-added-item';
import { AccountService } from './account.service'; import { AccountService } from './account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ImageService implements OnDestroy { export class ImageService {
private readonly destroyRef = inject(DestroyRef);
baseUrl = environment.apiUrl; baseUrl = environment.apiUrl;
apiKey: string = ''; apiKey: string = '';
encodedKey: string = ''; encodedKey: string = '';
@ -19,10 +20,8 @@ export class ImageService implements OnDestroy {
public resetCoverImage = 'assets/images/image-reset-cover-min.png'; public resetCoverImage = 'assets/images/image-reset-cover-min.png';
public errorWebLinkImage = 'assets/images/broken-white-32x32.png'; public errorWebLinkImage = 'assets/images/broken-white-32x32.png';
private onDestroy: Subject<void> = new Subject();
constructor(private accountService: AccountService, private themeService: ThemeService) { constructor(private accountService: AccountService, private themeService: ThemeService) {
this.themeService.currentTheme$.pipe(takeUntil(this.onDestroy)).subscribe(theme => { this.themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(theme => {
if (this.themeService.isDarkTheme()) { if (this.themeService.isDarkTheme()) {
this.placeholderImage = 'assets/images/image-placeholder.dark-min.png'; this.placeholderImage = 'assets/images/image-placeholder.dark-min.png';
this.errorImage = 'assets/images/error-placeholder2.dark-min.png'; this.errorImage = 'assets/images/error-placeholder2.dark-min.png';
@ -34,7 +33,7 @@ export class ImageService implements OnDestroy {
} }
}); });
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
if (user) { if (user) {
this.apiKey = user.apiKey; this.apiKey = user.apiKey;
this.encodedKey = encodeURIComponent(this.apiKey); this.encodedKey = encodeURIComponent(this.apiKey);
@ -42,11 +41,6 @@ export class ImageService implements OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
getRecentlyAddedItem(item: RecentlyAddedItem) { getRecentlyAddedItem(item: RecentlyAddedItem) {
if (item.chapterId === 0) { if (item.chapterId === 0) {
return this.getVolumeCoverImage(item.volumeId); return this.getVolumeCoverImage(item.volumeId);
@ -56,8 +50,8 @@ export class ImageService implements OnDestroy {
/** /**
* Returns the entity type from a cover image url. Undefied if not applicable * Returns the entity type from a cover image url. Undefied if not applicable
* @param url * @param url
* @returns * @returns
*/ */
getEntityTypeFromUrl(url: string) { getEntityTypeFromUrl(url: string) {
if (url.indexOf('?') < 0) return undefined; if (url.indexOf('?') < 0) return undefined;

View File

@ -1,5 +1,5 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import {DestroyRef, inject, Injectable} from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
@ -19,6 +19,7 @@ import { TextResonse } from '../_types/text-response';
import { AccountService } from './account.service'; import { AccountService } from './account.service';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { OnDestroy } from '@angular/core'; import { OnDestroy } from '@angular/core';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
export const CHAPTER_ID_DOESNT_EXIST = -1; export const CHAPTER_ID_DOESNT_EXIST = -1;
export const CHAPTER_ID_NOT_FETCHED = -2; export const CHAPTER_ID_NOT_FETCHED = -2;
@ -26,29 +27,25 @@ export const CHAPTER_ID_NOT_FETCHED = -2;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ReaderService implements OnDestroy { export class ReaderService {
private readonly destroyRef = inject(DestroyRef);
baseUrl = environment.apiUrl; baseUrl = environment.apiUrl;
encodedKey: string = ''; encodedKey: string = '';
private onDestroy: Subject<void> = new Subject();
// Override background color for reader and restore it onDestroy // Override background color for reader and restore it onDestroy
private originalBodyColor!: string; private originalBodyColor!: string;
constructor(private httpClient: HttpClient, private router: Router, constructor(private httpClient: HttpClient, private router: Router,
private location: Location, private utilityService: UtilityService, private location: Location, private utilityService: UtilityService,
private filterUtilitySerivce: FilterUtilitiesService, private accountService: AccountService) { private filterUtilityService: FilterUtilitiesService, private accountService: AccountService) {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
if (user) { if (user) {
this.encodedKey = encodeURIComponent(user.apiKey); this.encodedKey = encodeURIComponent(user.apiKey);
} }
}); });
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) { getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) {
if (format === undefined) format = MangaFormat.ARCHIVE; if (format === undefined) format = MangaFormat.ARCHIVE;
@ -77,7 +74,7 @@ export class ReaderService implements OnDestroy {
getAllBookmarks(filter: SeriesFilter | undefined) { getAllBookmarks(filter: SeriesFilter | undefined) {
let params = new HttpParams(); let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, undefined, undefined); params = this.utilityService.addPaginationIfExists(params, undefined, undefined);
const data = this.filterUtilitySerivce.createSeriesFilter(filter); const data = this.filterUtilityService.createSeriesFilter(filter);
return this.httpClient.post<PageBookmark[]>(this.baseUrl + 'reader/all-bookmarks', data); return this.httpClient.post<PageBookmark[]>(this.baseUrl + 'reader/all-bookmarks', data);
} }

View File

@ -1,6 +1,15 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Inject, Injectable, OnDestroy, Renderer2, RendererFactory2, SecurityContext } from '@angular/core'; import {
DestroyRef,
inject,
Inject,
Injectable,
OnDestroy,
Renderer2,
RendererFactory2,
SecurityContext
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { map, ReplaySubject, Subject, takeUntil, take } from 'rxjs'; import { map, ReplaySubject, Subject, takeUntil, take } from 'rxjs';
@ -10,13 +19,15 @@ import { NotificationProgressEvent } from '../_models/events/notification-progre
import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme'; import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme';
import { TextResonse } from '../_types/text-response'; import { TextResonse } from '../_types/text-response';
import { EVENTS, MessageHubService } from './message-hub.service'; import { EVENTS, MessageHubService } from './message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ThemeService implements OnDestroy { export class ThemeService {
private readonly destroyRef = inject(DestroyRef);
public defaultTheme: string = 'dark'; public defaultTheme: string = 'dark';
public defaultBookTheme: string = 'Dark'; public defaultBookTheme: string = 'Dark';
@ -25,13 +36,12 @@ export class ThemeService implements OnDestroy {
private themesSource = new ReplaySubject<SiteTheme[]>(1); private themesSource = new ReplaySubject<SiteTheme[]>(1);
public themes$ = this.themesSource.asObservable(); public themes$ = this.themesSource.asObservable();
/** /**
* Maintain a cache of themes. SignalR will inform us if we need to refresh cache * Maintain a cache of themes. SignalR will inform us if we need to refresh cache
*/ */
private themeCache: Array<SiteTheme> = []; private themeCache: Array<SiteTheme> = [];
private readonly onDestroy = new Subject<void>();
private renderer: Renderer2; private renderer: Renderer2;
private baseUrl = environment.apiUrl; private baseUrl = environment.apiUrl;
@ -42,7 +52,7 @@ export class ThemeService implements OnDestroy {
this.getThemes(); this.getThemes();
messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(message => { messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => {
if (message.event !== EVENTS.NotificationProgress) return; if (message.event !== EVENTS.NotificationProgress) return;
const notificationEvent = (message.payload as NotificationProgressEvent); const notificationEvent = (message.payload as NotificationProgressEvent);
@ -56,31 +66,26 @@ export class ThemeService implements OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
getColorScheme() { getColorScheme() {
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim(); return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
} }
/** /**
* --theme-color from theme. Updates the meta tag * --theme-color from theme. Updates the meta tag
* @returns * @returns
*/ */
getThemeColor() { getThemeColor() {
return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim(); return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim();
} }
/** /**
* --msapplication-TileColor from theme. Updates the meta tag * --msapplication-TileColor from theme. Updates the meta tag
* @returns * @returns
*/ */
getTileColor() { getTileColor() {
return getComputedStyle(this.document.body).getPropertyValue('--title-color').trim(); return getComputedStyle(this.document.body).getPropertyValue('--title-color').trim();
} }
getCssVariable(variable: string) { getCssVariable(variable: string) {
return getComputedStyle(this.document.body).getPropertyValue(variable).trim(); return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
} }

View File

@ -11,7 +11,7 @@ import { AccountService } from 'src/app/_services/account.service';
}) })
export class ResetPasswordModalComponent { export class ResetPasswordModalComponent {
@Input() member!: Member; @Input({required: true}) member!: Member;
errorMessage = ''; errorMessage = '';
resetPasswordForm: FormGroup = new FormGroup({ resetPasswordForm: FormGroup = new FormGroup({
password: new FormControl('', [Validators.required]), password: new FormControl('', [Validators.required]),

View File

@ -13,8 +13,8 @@ import { AccountService } from 'src/app/_services/account.service';
}) })
export class EditUserComponent implements OnInit { export class EditUserComponent implements OnInit {
@Input() member!: Member; @Input({required: true}) member!: Member;
selectedRoles: Array<string> = []; selectedRoles: Array<string> = [];
selectedLibraries: Array<number> = []; selectedLibraries: Array<number> = [];
selectedRestriction!: AgeRestriction; selectedRestriction!: AgeRestriction;

View File

@ -1,10 +1,22 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output, QueryList, ViewChildren, inject } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
OnInit,
Output,
QueryList,
ViewChildren,
inject,
DestroyRef
} from '@angular/core';
import { BehaviorSubject, Observable, Subject, combineLatest, filter, map, shareReplay, takeUntil } from 'rxjs'; import { BehaviorSubject, Observable, Subject, combineLatest, filter, map, shareReplay, takeUntil } from 'rxjs';
import { SortEvent, SortableHeader, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive'; import { SortEvent, SortableHeader, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { KavitaMediaError } from '../_models/media-error'; import { KavitaMediaError } from '../_models/media-error';
import { ServerService } from 'src/app/_services/server.service'; import { ServerService } from 'src/app/_services/server.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-manage-alerts', selector: 'app-manage-alerts',
@ -16,12 +28,12 @@ export class ManageAlertsComponent implements OnInit {
@Output() alertCount = new EventEmitter<number>(); @Output() alertCount = new EventEmitter<number>();
@ViewChildren(SortableHeader<KavitaMediaError>) headers!: QueryList<SortableHeader<KavitaMediaError>>; @ViewChildren(SortableHeader<KavitaMediaError>) headers!: QueryList<SortableHeader<KavitaMediaError>>;
private readonly serverService = inject(ServerService); private readonly serverService = inject(ServerService);
private readonly messageHub = inject(MessageHubService); private readonly messageHub = inject(MessageHubService);
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
private readonly onDestroy = new Subject<void>(); private readonly destroyRef = inject(DestroyRef);
messageHubUpdate$ = this.messageHub.messages$.pipe(takeUntil(this.onDestroy), filter(m => m.event === EVENTS.ScanSeries), shareReplay()); messageHubUpdate$ = this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), filter(m => m.event === EVENTS.ScanSeries), shareReplay());
currentSort = new BehaviorSubject<SortEvent<KavitaMediaError>>({column: 'extension', direction: 'asc'}); currentSort = new BehaviorSubject<SortEvent<KavitaMediaError>>({column: 'extension', direction: 'asc'});
currentSort$: Observable<SortEvent<KavitaMediaError>> = this.currentSort.asObservable(); currentSort$: Observable<SortEvent<KavitaMediaError>> = this.currentSort.asObservable();
@ -30,7 +42,7 @@ export class ManageAlertsComponent implements OnInit {
formGroup = new FormGroup({ formGroup = new FormGroup({
filter: new FormControl('', []) filter: new FormControl('', [])
}); });
constructor() {} constructor() {}

View File

@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -10,6 +18,7 @@ import { ScanSeriesEvent } from 'src/app/_models/events/scan-series-event';
import { Library } from 'src/app/_models/library'; import { Library } from 'src/app/_models/library';
import { LibraryService } from 'src/app/_services/library.service'; import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service'; import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-manage-library', selector: 'app-manage-library',
@ -17,7 +26,7 @@ import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hu
styleUrls: ['./manage-library.component.scss'], styleUrls: ['./manage-library.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ManageLibraryComponent implements OnInit, OnDestroy { export class ManageLibraryComponent implements OnInit {
libraries: Library[] = []; libraries: Library[] = [];
loading = false; loading = false;
@ -26,10 +35,9 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
*/ */
deletionInProgress: boolean = false; deletionInProgress: boolean = false;
libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`; libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`;
private readonly destroyRef = inject(DestroyRef);
private readonly onDestroy = new Subject<void>(); constructor(private modalService: NgbModal, private libraryService: LibraryService,
constructor(private modalService: NgbModal, private libraryService: LibraryService,
private toastr: ToastrService, private confirmService: ConfirmService, private toastr: ToastrService, private confirmService: ConfirmService,
private hubService: MessageHubService, private readonly cdRef: ChangeDetectorRef) { } private hubService: MessageHubService, private readonly cdRef: ChangeDetectorRef) { }
@ -37,10 +45,10 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
this.getLibraries(); this.getLibraries();
// when a progress event comes in, show it on the UI next to library // when a progress event comes in, show it on the UI next to library
this.hubService.messages$.pipe(takeUntil(this.onDestroy), this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef),
filter(event => event.event === EVENTS.ScanSeries || event.event === EVENTS.NotificationProgress), filter(event => event.event === EVENTS.ScanSeries || event.event === EVENTS.NotificationProgress),
distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) => distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) =>
this.hasMessageChanged(prev, curr))) this.hasMessageChanged(prev, curr)))
.subscribe((event: Message<ScanSeriesEvent | NotificationProgressEvent>) => { .subscribe((event: Message<ScanSeriesEvent | NotificationProgressEvent>) => {
let libId = 0; let libId = 0;
if (event.event === EVENTS.ScanSeries) { if (event.event === EVENTS.ScanSeries) {
@ -62,11 +70,6 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
hasMessageChanged(prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) { hasMessageChanged(prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) {
if (curr.event !== prev.event) return true; if (curr.event !== prev.event) return true;
if (curr.event === EVENTS.ScanSeries) { if (curr.event === EVENTS.ScanSeries) {
@ -91,7 +94,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
editLibrary(library: Library) { editLibrary(library: Library) {
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' }); const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
modalRef.componentInstance.library = library; modalRef.componentInstance.library = library;
modalRef.closed.pipe(takeUntil(this.onDestroy)).subscribe(refresh => { modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => {
if (refresh) { if (refresh) {
this.getLibraries(); this.getLibraries();
} }
@ -100,7 +103,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
addLibrary() { addLibrary() {
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' }); const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
modalRef.closed.pipe(takeUntil(this.onDestroy)).subscribe(refresh => { modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => {
if (refresh) { if (refresh) {
this.getLibraries(); this.getLibraries();
} }

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
HostListener,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -16,6 +25,7 @@ import { ActionService } from 'src/app/_services/action.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service'; import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { MessageHubService, Message, EVENTS } from 'src/app/_services/message-hub.service'; import { MessageHubService, Message, EVENTS } from 'src/app/_services/message-hub.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@ -25,19 +35,19 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./all-series.component.scss'], styleUrls: ['./all-series.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AllSeriesComponent implements OnInit, OnDestroy { export class AllSeriesComponent implements OnInit {
title: string = 'All Series'; title: string = 'All Series';
series: Series[] = []; series: Series[] = [];
loadingSeries = false; loadingSeries = false;
pagination!: Pagination; pagination!: Pagination;
filter: SeriesFilter | undefined = undefined; filter: SeriesFilter | undefined = undefined;
onDestroy: Subject<void> = new Subject<void>();
filterSettings: FilterSettings = new FilterSettings(); filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter(); filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActiveCheck!: SeriesFilter; filterActiveCheck!: SeriesFilter;
filterActive: boolean = false; filterActive: boolean = false;
jumpbarKeys: Array<JumpKey> = []; jumpbarKeys: Array<JumpKey> = [];
private readonly destroyRef = inject(DestroyRef);
bulkActionCallback = (action: ActionItem<any>, data: any) => { bulkActionCallback = (action: ActionItem<any>, data: any) => {
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series'); const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
@ -69,7 +79,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
this.loadPage(); this.loadPage();
this.bulkSelectionService.deselectAll(); this.bulkSelectionService.deselectAll();
}); });
break; break;
case Action.MarkAsUnread: case Action.MarkAsUnread:
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => { this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
@ -87,13 +97,13 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
} }
} }
constructor(private router: Router, private seriesService: SeriesService, constructor(private router: Router, private seriesService: SeriesService,
private titleService: Title, private actionService: ActionService, private titleService: Title, private actionService: ActionService,
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
private utilityService: UtilityService, private route: ActivatedRoute, private utilityService: UtilityService, private route: ActivatedRoute,
private filterUtilityService: FilterUtilitiesService, private jumpbarService: JumpbarService, private filterUtilityService: FilterUtilitiesService, private jumpbarService: JumpbarService,
private readonly cdRef: ChangeDetectorRef) { private readonly cdRef: ChangeDetectorRef) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.title = this.route.snapshot.queryParamMap.get('title') || 'All Series'; this.title = this.route.snapshot.queryParamMap.get('title') || 'All Series';
@ -106,17 +116,12 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.hubService.messages$.pipe(debounceTime(6000), takeUntil(this.onDestroy)).subscribe((event: Message<any>) => { this.hubService.messages$.pipe(debounceTime(6000), takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<any>) => {
if (event.event !== EVENTS.SeriesAdded) return; if (event.event !== EVENTS.SeriesAdded) return;
this.loadPage(); this.loadPage();
}); });
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event']) @HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) { handleKeypress(event: KeyboardEvent) {
if (event.key === KEY_CODES.SHIFT) { if (event.key === KEY_CODES.SHIFT) {
@ -130,11 +135,11 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
this.bulkSelectionService.isShiftDown = false; this.bulkSelectionService.isShiftDown = false;
} }
} }
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent) {
this.filter = data.filter; this.filter = data.filter;
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter); if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
this.loadPage(); this.loadPage();
} }

View File

@ -1,4 +1,18 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, Renderer2, RendererStyleFlags2, ViewChild } from '@angular/core'; import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
HostListener,
inject,
Inject,
OnDestroy,
OnInit,
Renderer2,
RendererStyleFlags2,
ViewChild
} from '@angular/core';
import {DOCUMENT, Location} from '@angular/common'; import {DOCUMENT, Location} from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
@ -28,6 +42,7 @@ import { User } from 'src/app/_models/user';
import { ThemeService } from 'src/app/_services/theme.service'; import { ThemeService } from 'src/app/_services/theme.service';
import { ScrollService } from 'src/app/_services/scroll.service'; import { ScrollService } from 'src/app/_services/scroll.service';
import { PAGING_DIRECTION } from 'src/app/manga-reader/_models/reader-enums'; import { PAGING_DIRECTION } from 'src/app/manga-reader/_models/reader-enums';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID { enum TabID {
@ -260,7 +275,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
writingStyle: WritingStyle = WritingStyle.Horizontal; writingStyle: WritingStyle = WritingStyle.Horizontal;
private readonly onDestroy = new Subject<void>(); private readonly destroyRef = inject(DestroyRef);
@ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef<HTMLDivElement>; @ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef<HTMLDivElement>;
/** /**
@ -453,7 +468,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
fromEvent(this.reader.nativeElement, 'scroll') fromEvent(this.reader.nativeElement, 'scroll')
.pipe( .pipe(
debounceTime(200), debounceTime(200),
takeUntil(this.onDestroy)) takeUntilDestroyed(this.destroyRef))
.subscribe((event) => { .subscribe((event) => {
if (this.isLoading) return; if (this.isLoading) return;
@ -509,9 +524,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.navService.showNavBar(); this.navService.showNavBar();
this.navService.showSideNav(); this.navService.showSideNav();
this.onDestroy.next();
this.onDestroy.complete();
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -1,5 +1,16 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { Subject, take, takeUntil } from 'rxjs'; import { Subject, take, takeUntil } from 'rxjs';
import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode'; import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode';
@ -15,6 +26,7 @@ import { BookBlackTheme } from '../../_models/book-black-theme';
import { BookDarkTheme } from '../../_models/book-dark-theme'; import { BookDarkTheme } from '../../_models/book-dark-theme';
import { BookWhiteTheme } from '../../_models/book-white-theme'; import { BookWhiteTheme } from '../../_models/book-white-theme';
import { BookPaperTheme } from '../../_models/book-paper-theme'; import { BookPaperTheme } from '../../_models/book-paper-theme';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
* Used for book reader. Do not use for other components * Used for book reader. Do not use for other components
@ -74,7 +86,7 @@ const mobileBreakpointMarginOverride = 700;
styleUrls: ['./reader-settings.component.scss'], styleUrls: ['./reader-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReaderSettingsComponent implements OnInit, OnDestroy { export class ReaderSettingsComponent implements OnInit {
/** /**
* Outputs when clickToPaginate is changed * Outputs when clickToPaginate is changed
*/ */
@ -134,9 +146,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
* System provided themes * System provided themes
*/ */
themes: Array<BookTheme> = bookColorThemes; themes: Array<BookTheme> = bookColorThemes;
private readonly destroyRef = inject(DestroyRef);
private onDestroy: Subject<void> = new Subject();
get BookPageLayoutMode(): typeof BookPageLayoutMode { get BookPageLayoutMode(): typeof BookPageLayoutMode {
@ -191,7 +201,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, [])); this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(fontName => { this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(fontName => {
const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family; const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family;
if (familyName === 'default') { if (familyName === 'default') {
this.pageStyles['font-family'] = 'inherit'; this.pageStyles['font-family'] = 'inherit';
@ -203,24 +213,24 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
}); });
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, [])); this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
this.settingsForm.get('bookReaderFontSize')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => { this.settingsForm.get('bookReaderFontSize')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['font-size'] = value + '%'; this.pageStyles['font-size'] = value + '%';
this.styleUpdate.emit(this.pageStyles); this.styleUpdate.emit(this.pageStyles);
}); });
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, [])); this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => { this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.clickToPaginateChanged.emit(value); this.clickToPaginateChanged.emit(value);
}); });
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, [])); this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => { this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['line-height'] = value + '%'; this.pageStyles['line-height'] = value + '%';
this.styleUpdate.emit(this.pageStyles); this.styleUpdate.emit(this.pageStyles);
}); });
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, [])); this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => { this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['margin-left'] = value + 'vw'; this.pageStyles['margin-left'] = value + 'vw';
this.pageStyles['margin-right'] = value + 'vw'; this.pageStyles['margin-right'] = value + 'vw';
this.styleUpdate.emit(this.pageStyles); this.styleUpdate.emit(this.pageStyles);
@ -229,12 +239,12 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, [])); 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.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((layoutMode: BookPageLayoutMode) => {
this.layoutModeUpdate.emit(layoutMode); this.layoutModeUpdate.emit(layoutMode);
}); });
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user.preferences.bookReaderImmersiveMode, [])); this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user.preferences.bookReaderImmersiveMode, []));
this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((immersiveMode: boolean) => { this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((immersiveMode: boolean) => {
if (immersiveMode) { if (immersiveMode) {
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true); this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true);
} }
@ -261,12 +271,6 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
resetSettings() { resetSettings() {
if (this.user) { 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.user.preferences.bookReaderFontFamily, this.user.preferences.bookReaderFontSize + '%', this.user.preferences.bookReaderMargin + 'vw', this.user.preferences.bookReaderLineSpacing + '%');

View File

@ -10,9 +10,9 @@ import { BookChapterItem } from '../../_models/book-chapter-item';
}) })
export class TableOfContentsComponent implements OnDestroy { export class TableOfContentsComponent implements OnDestroy {
@Input() chapterId!: number; @Input({required: true}) chapterId!: number;
@Input() pageNum!: number; @Input({required: true}) pageNum!: number;
@Input() currentPageAnchor!: string; @Input({required: true}) currentPageAnchor!: string;
@Input() chapters:Array<BookChapterItem> = []; @Input() chapters:Array<BookChapterItem> = [];
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter(); @Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();

View File

@ -15,7 +15,7 @@ import { CollectionTagService } from 'src/app/_services/collection-tag.service';
}) })
export class BulkAddToCollectionComponent implements OnInit, AfterViewInit { export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
@Input() title!: string; @Input({required: true}) title!: string;
/** /**
* Series Ids to add to Collection Tag * Series Ids to add to Collection Tag
*/ */
@ -33,14 +33,14 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>; @ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService, constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService,
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { } private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.listForm.addControl('title', new FormControl(this.title, [])); this.listForm.addControl('title', new FormControl(this.title, []));
this.listForm.addControl('filterQuery', new FormControl('', [])); this.listForm.addControl('filterQuery', new FormControl('', []));
this.loading = true; this.loading = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.collectionService.allTags().subscribe(tags => { this.collectionService.allTags().subscribe(tags => {
@ -77,7 +77,7 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
this.toastr.success('Series added to ' + tag.title + ' collection'); this.toastr.success('Series added to ' + tag.title + ' collection');
this.modal.close(); this.modal.close();
}); });
} }
filterList = (listItem: ReadingList) => { filterList = (listItem: ReadingList) => {

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
@ -14,6 +23,7 @@ import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service'; import { LibraryService } from 'src/app/_services/library.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import { UploadService } from 'src/app/_services/upload.service'; import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID { enum TabID {
@ -28,9 +38,9 @@ enum TabID {
styleUrls: ['./edit-collection-tags.component.scss'], styleUrls: ['./edit-collection-tags.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EditCollectionTagsComponent implements OnInit, OnDestroy { export class EditCollectionTagsComponent implements OnInit {
@Input() tag!: CollectionTag; @Input({required: true}) tag!: CollectionTag;
series: Array<Series> = []; series: Array<Series> = [];
selections!: SelectionModel<Series>; selections!: SelectionModel<Series>;
isLoading: boolean = true; isLoading: boolean = true;
@ -42,8 +52,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
active = TabID.General; active = TabID.General;
imageUrls: Array<string> = []; imageUrls: Array<string> = [];
selectedCover: string = ''; selectedCover: string = '';
private readonly destroyRef = inject(DestroyRef);
private readonly onDestroy = new Subject<void>();
get hasSomeSelected() { get hasSomeSelected() {
return this.selections != null && this.selections.hasSomeSelected(); return this.selections != null && this.selections.hasSomeSelected();
@ -57,7 +66,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
return TabID; return TabID;
} }
constructor(public modal: NgbActiveModal, private seriesService: SeriesService, constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
private collectionService: CollectionTagService, private toastr: ToastrService, private collectionService: CollectionTagService, private toastr: ToastrService,
private confirmSerivce: ConfirmService, private libraryService: LibraryService, private confirmSerivce: ConfirmService, private libraryService: LibraryService,
private imageService: ImageService, private uploadService: UploadService, private imageService: ImageService, private uploadService: UploadService,
@ -76,7 +85,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
}); });
this.collectionTagForm.get('title')?.valueChanges.pipe( this.collectionTagForm.get('title')?.valueChanges.pipe(
debounceTime(100), debounceTime(100),
distinctUntilChanged(), distinctUntilChanged(),
switchMap(name => this.collectionService.tagNameExists(name)), switchMap(name => this.collectionService.tagNameExists(name)),
tap(exists => { tap(exists => {
@ -84,22 +93,17 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
if (!exists || isExistingName) { if (!exists || isExistingName) {
this.collectionTagForm.get('title')?.setErrors(null); this.collectionTagForm.get('title')?.setErrors(null);
} else { } else {
this.collectionTagForm.get('title')?.setErrors({duplicateName: true}) this.collectionTagForm.get('title')?.setErrors({duplicateName: true})
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
).subscribe(); ).subscribe();
this.imageUrls.push(this.imageService.randomize(this.imageService.getCollectionCoverImage(this.tag.id))); this.imageUrls.push(this.imageService.randomize(this.imageService.getCollectionCoverImage(this.tag.id)));
this.loadSeries(); this.loadSeries();
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
onPageChange(pageNum: number) { onPageChange(pageNum: number) {
this.pagination.currentPage = pageNum; this.pagination.currentPage = pageNum;
this.loadSeries(); this.loadSeries();
@ -153,7 +157,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
const unselectedIds = this.selections.unselected().map(s => s.id); const unselectedIds = this.selections.unselected().map(s => s.id);
const tag = this.collectionTagForm.value; const tag = this.collectionTagForm.value;
tag.id = this.tag.id; tag.id = this.tag.id;
if (unselectedIds.length == this.series.length && !await this.confirmSerivce.confirm('Warning! No series are selected, saving will delete the tag. Are you sure you want to continue?')) { if (unselectedIds.length == this.series.length && !await this.confirmSerivce.confirm('Warning! No series are selected, saving will delete the tag. Are you sure you want to continue?')) {
return; return;
} }
@ -162,11 +166,11 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
this.collectionService.updateTag(tag), this.collectionService.updateTag(tag),
this.collectionService.updateSeriesForTag(tag, this.selections.unselected().map(s => s.id)) this.collectionService.updateSeriesForTag(tag, this.selections.unselected().map(s => s.id))
]; ];
if (selectedIndex > 0) { if (selectedIndex > 0) {
apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover)); apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover));
} }
forkJoin(apis).subscribe(() => { forkJoin(apis).subscribe(() => {
this.modal.close({success: true, coverImageUpdated: selectedIndex > 0}); this.modal.close({success: true, coverImageUpdated: selectedIndex > 0});
this.toastr.success('Tag updated'); this.toastr.success('Tag updated');

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { forkJoin, Observable, of, Subject } from 'rxjs'; import { forkJoin, Observable, of, Subject } from 'rxjs';
@ -21,6 +30,7 @@ import { LibraryService } from 'src/app/_services/library.service';
import { MetadataService } from 'src/app/_services/metadata.service'; import { MetadataService } from 'src/app/_services/metadata.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import { UploadService } from 'src/app/_services/upload.service'; import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID { enum TabID {
General = 0, General = 0,
@ -38,9 +48,9 @@ enum TabID {
styleUrls: ['./edit-series-modal.component.scss'], styleUrls: ['./edit-series-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EditSeriesModalComponent implements OnInit, OnDestroy { export class EditSeriesModalComponent implements OnInit {
@Input() series!: Series; @Input({required: true}) series!: Series;
seriesVolumes: any[] = []; seriesVolumes: any[] = [];
isLoadingVolumes = false; isLoadingVolumes = false;
/** /**
@ -56,9 +66,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
editSeriesForm!: FormGroup; editSeriesForm!: FormGroup;
libraryName: string | undefined = undefined; libraryName: string | undefined = undefined;
size: number = 0; size: number = 0;
private readonly onDestroy = new Subject<void>(); private readonly destroyRef = inject(DestroyRef);
// Typeaheads // Typeaheads
ageRatingSettings: TypeaheadSettings<AgeRatingDto> = new TypeaheadSettings(); ageRatingSettings: TypeaheadSettings<AgeRatingDto> = new TypeaheadSettings();
@ -117,16 +125,16 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
private libraryService: LibraryService, private libraryService: LibraryService,
private collectionService: CollectionTagService, private collectionService: CollectionTagService,
private uploadService: UploadService, private uploadService: UploadService,
private metadataService: MetadataService, private metadataService: MetadataService,
private readonly cdRef: ChangeDetectorRef) { } private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id)); this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => { this.libraryService.getLibraryNames().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(names => {
this.libraryName = names[this.series.libraryId]; this.libraryName = names[this.series.libraryId];
}); });
this.initSeries = Object.assign({}, this.series); this.initSeries = Object.assign({}, this.series);
this.editSeriesForm = this.fb.group({ this.editSeriesForm = this.fb.group({
@ -180,41 +188,41 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.editSeriesForm.get('name')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { this.editSeriesForm.get('name')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.series.nameLocked = true; this.series.nameLocked = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.editSeriesForm.get('sortName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { this.editSeriesForm.get('sortName')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.series.sortNameLocked = true; this.series.sortNameLocked = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.editSeriesForm.get('localizedName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { this.editSeriesForm.get('localizedName')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.series.localizedNameLocked = true; this.series.localizedNameLocked = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.editSeriesForm.get('summary')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { this.editSeriesForm.get('summary')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.metadata.summaryLocked = true; this.metadata.summaryLocked = true;
this.metadata.summary = val; this.metadata.summary = val;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.editSeriesForm.get('ageRating')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { this.editSeriesForm.get('ageRating')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.metadata.ageRating = parseInt(val + '', 10); this.metadata.ageRating = parseInt(val + '', 10);
this.metadata.ageRatingLocked = true; this.metadata.ageRatingLocked = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.metadata.publicationStatus = parseInt(val + '', 10); this.metadata.publicationStatus = parseInt(val + '', 10);
this.metadata.publicationStatusLocked = true; this.metadata.publicationStatusLocked = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.editSeriesForm.get('releaseYear')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => { this.editSeriesForm.get('releaseYear')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.metadata.releaseYear = parseInt(val + '', 10); this.metadata.releaseYear = parseInt(val + '', 10);
this.metadata.releaseYearLocked = true; this.metadata.releaseYearLocked = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -257,10 +265,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
setupTypeaheads() { setupTypeaheads() {
forkJoin([ forkJoin([
@ -490,7 +494,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
.filter(v => v !== null && v !== '') .filter(v => v !== null && v !== '')
.join(','); .join(',');
const apis = [ const apis = [
this.seriesService.updateMetadata(this.metadata, this.collectionTags) this.seriesService.updateMetadata(this.metadata, this.collectionTags)

View File

@ -1,7 +1,17 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { BulkSelectionService } from '../bulk-selection.service'; import { BulkSelectionService } from '../bulk-selection.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-bulk-operations', selector: 'app-bulk-operations',
@ -9,16 +19,15 @@ import { BulkSelectionService } from '../bulk-selection.service';
styleUrls: ['./bulk-operations.component.scss'], styleUrls: ['./bulk-operations.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class BulkOperationsComponent implements OnInit, OnDestroy { export class BulkOperationsComponent implements OnInit {
@Input() actionCallback!: (action: ActionItem<any>, data: any) => void; @Input({required: true}) actionCallback!: (action: ActionItem<any>, data: any) => void;
topOffset: number = 56; topOffset: number = 56;
hasMarkAsRead: boolean = false; hasMarkAsRead: boolean = false;
hasMarkAsUnread: boolean = false; hasMarkAsUnread: boolean = false;
actions: Array<ActionItem<any>> = []; actions: Array<ActionItem<any>> = [];
private readonly destroyRef = inject(DestroyRef);
private onDestory: Subject<void> = new Subject();
get Action() { get Action() {
return Action; return Action;
@ -28,7 +37,7 @@ export class BulkOperationsComponent implements OnInit, OnDestroy {
private actionFactoryService: ActionFactoryService) { } private actionFactoryService: ActionFactoryService) { }
ngOnInit(): void { ngOnInit(): void {
this.bulkSelectionService.actions$.pipe(takeUntil(this.onDestory)).subscribe(actions => { this.bulkSelectionService.actions$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(actions => {
// We need to do a recursive callback apply // We need to do a recursive callback apply
this.actions = this.actionFactoryService.applyCallbackToList(actions, this.actionCallback.bind(this)); this.actions = this.actionFactoryService.applyCallbackToList(actions, this.actionCallback.bind(this));
this.hasMarkAsRead = this.actionFactoryService.hasAction(this.actions, Action.MarkAsRead); this.hasMarkAsRead = this.actionFactoryService.hasAction(this.actions, Action.MarkAsRead);
@ -37,11 +46,6 @@ export class BulkOperationsComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestory.next();
this.onDestory.complete();
}
handleActionCallback(action: ActionItem<any>, data: any) { handleActionCallback(action: ActionItem<any>, data: any) {
this.actionCallback(action, data); this.actionCallback(action, data);
} }

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
@ -24,9 +33,10 @@ import { MetadataService } from 'src/app/_services/metadata.service';
import { ReaderService } from 'src/app/_services/reader.service'; import { ReaderService } from 'src/app/_services/reader.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import { UploadService } from 'src/app/_services/upload.service'; import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID { enum TabID {
General = 0, General = 0,
Metadata = 1, Metadata = 1,
Cover = 2, Cover = 2,
Files = 3 Files = 3
@ -38,13 +48,14 @@ enum TabID {
styleUrls: ['./card-detail-drawer.component.scss'], styleUrls: ['./card-detail-drawer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CardDetailDrawerComponent implements OnInit, OnDestroy { export class CardDetailDrawerComponent implements OnInit {
@Input() parentName = ''; @Input() parentName = '';
@Input() seriesId: number = 0; @Input() seriesId: number = 0;
@Input() libraryId: number = 0; @Input() libraryId: number = 0;
@Input() data!: Volume | Chapter; @Input({required: true}) data!: Volume | Chapter;
private readonly destroyRef = inject(DestroyRef);
/** /**
* If this is a volume, this will be first chapter for said volume. * If this is a volume, this will be first chapter for said volume.
*/ */
@ -63,7 +74,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
actions: ActionItem<any>[] = []; actions: ActionItem<any>[] = [];
chapterActions: ActionItem<Chapter>[] = []; chapterActions: ActionItem<Chapter>[] = [];
libraryType: LibraryType = LibraryType.Manga; libraryType: LibraryType = LibraryType.Manga;
tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Info', disabled: false}]; tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Info', disabled: false}];
@ -74,8 +85,6 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
download$: Observable<Download> | null = null; download$: Observable<Download> | null = null;
downloadInProgress: boolean = false; downloadInProgress: boolean = false;
private readonly onDestroy = new Subject<void>();
get MangaFormat() { get MangaFormat() {
return MangaFormat; return MangaFormat;
@ -97,16 +106,15 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
return TabID; return TabID;
} }
constructor(public utilityService: UtilityService, constructor(public utilityService: UtilityService,
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService, public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
private accountService: AccountService, private actionFactoryService: ActionFactoryService, private accountService: AccountService, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, private router: Router, private libraryService: LibraryService, private actionService: ActionService, private router: Router, private libraryService: LibraryService,
private seriesService: SeriesService, private readerService: ReaderService, public metadataService: MetadataService, private seriesService: SeriesService, private readerService: ReaderService,
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef, public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef) {
private deviceSerivce: DeviceService) {
this.isAdmin$ = this.accountService.currentUser$.pipe( this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false), map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay() shareReplay()
); );
} }
@ -133,7 +141,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)) this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this))
.filter(item => item.action !== Action.Edit); .filter(item => item.action !== Action.Edit);
this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []}); this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []});
if (this.isChapter) { if (this.isChapter) {
const chapter = this.utilityService.asChapter(this.data); const chapter = this.utilityService.asChapter(this.data);
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, chapter); this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, chapter);
@ -146,7 +154,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}); var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
this.chapters.forEach((c: Chapter) => { this.chapters.forEach((c: Chapter) => {
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath)); c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
@ -157,10 +165,6 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
close() { close() {
this.activeOffcanvas.close(); this.activeOffcanvas.close();
@ -193,7 +197,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
if (this.seriesId === 0) { if (this.seriesId === 0) {
return; return;
} }
this.actionService.markChapterAsRead(this.libraryId, this.seriesId, chapter, () => { this.cdRef.markForCheck(); }); this.actionService.markChapterAsRead(this.libraryId, this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
} }

View File

@ -116,7 +116,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
ngOnChanges(): void { ngOnChanges(): void {
this.jumpBarKeysToRender = [...this.jumpBarKeys]; this.jumpBarKeysToRender = [...this.jumpBarKeys];
this.resizeJumpBar(); this.resizeJumpBar();
// Don't resume jump key when there is a custom sort order, as it won't work // Don't resume jump key when there is a custom sort order, as it won't work
if (!this.hasCustomSort()) { if (!this.hasCustomSort()) {
if (!this.hasResumedJumpKey && this.jumpBarKeysToRender.length > 0) { if (!this.hasResumedJumpKey && this.jumpBarKeysToRender.length > 0) {
@ -124,7 +124,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
if (resumeKey === '') return; if (resumeKey === '') return;
const keys = this.jumpBarKeysToRender.filter(k => k.key === resumeKey); const keys = this.jumpBarKeysToRender.filter(k => k.key === resumeKey);
if (keys.length < 1) return; if (keys.length < 1) return;
this.hasResumedJumpKey = true; this.hasResumedJumpKey = true;
setTimeout(() => this.scrollTo(keys[0]), 100); setTimeout(() => this.scrollTo(keys[0]), 100);
} }

View File

@ -1,4 +1,15 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
HostListener,
inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { filter, map, takeUntil } from 'rxjs/operators'; import { filter, map, takeUntil } from 'rxjs/operators';
import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service'; import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service';
@ -19,6 +30,7 @@ import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { ScrollService } from 'src/app/_services/scroll.service'; import { ScrollService } from 'src/app/_services/scroll.service';
import { BulkSelectionService } from '../bulk-selection.service'; import { BulkSelectionService } from '../bulk-selection.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-card-item', selector: 'app-card-item',
@ -26,7 +38,7 @@ import { BulkSelectionService } from '../bulk-selection.service';
styleUrls: ['./card-item.component.scss'], styleUrls: ['./card-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CardItemComponent implements OnInit, OnDestroy { export class CardItemComponent implements OnInit {
/** /**
* Card item url. Will internally handle error and missing covers * Card item url. Will internally handle error and missing covers
@ -59,7 +71,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
/** /**
* This is the entity we are representing. It will be returned if an action is executed. * This is the entity we are representing. It will be returned if an action is executed.
*/ */
@Input() entity!: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem; @Input({required: true}) entity!: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem;
/** /**
* If the entity is selected or not. * If the entity is selected or not.
*/ */
@ -115,17 +127,17 @@ export class CardItemComponent implements OnInit, OnDestroy {
selectionInProgress: boolean = false; selectionInProgress: boolean = false;
private user: User | undefined; private user: User | undefined;
private readonly destroyRef = inject(DestroyRef);
get MangaFormat(): typeof MangaFormat { get MangaFormat(): typeof MangaFormat {
return MangaFormat; return MangaFormat;
} }
private readonly onDestroy = new Subject<void>();
constructor(public imageService: ImageService, private libraryService: LibraryService, constructor(public imageService: ImageService, private libraryService: LibraryService,
public utilityService: UtilityService, private downloadService: DownloadService, public utilityService: UtilityService, private downloadService: DownloadService,
public bulkSelectionService: BulkSelectionService, public bulkSelectionService: BulkSelectionService,
private messageHub: MessageHubService, private accountService: AccountService, private messageHub: MessageHubService, private accountService: AccountService,
private scrollService: ScrollService, private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private readonly cdRef: ChangeDetectorRef,
private actionFactoryService: ActionFactoryService) {} private actionFactoryService: ActionFactoryService) {}
@ -136,14 +148,14 @@ export class CardItemComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
if (this.suppressLibraryLink === false) { if (!this.suppressLibraryLink) {
if (this.entity !== undefined && this.entity.hasOwnProperty('libraryId')) { if (this.entity !== undefined && this.entity.hasOwnProperty('libraryId')) {
this.libraryId = (this.entity as Series).libraryId; this.libraryId = (this.entity as Series).libraryId;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
if (this.libraryId !== undefined && this.libraryId > 0) { if (this.libraryId !== undefined && this.libraryId > 0) {
this.libraryService.getLibraryName(this.libraryId).pipe(takeUntil(this.onDestroy)).subscribe(name => { this.libraryService.getLibraryName(this.libraryId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(name => {
this.libraryName = name; this.libraryName = name;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
@ -176,18 +188,18 @@ export class CardItemComponent implements OnInit, OnDestroy {
} }
this.filterSendTo(); this.filterSendTo();
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
this.user = user; this.user = user;
}); });
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate), this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
map(evt => evt.payload as UserProgressUpdateEvent), takeUntil(this.onDestroy)).subscribe(updateEvent => { map(evt => evt.payload as UserProgressUpdateEvent), takeUntilDestroyed(this.destroyRef)).subscribe(updateEvent => {
if (this.user === undefined || this.user.username !== updateEvent.username) return; if (this.user === undefined || this.user.username !== updateEvent.username) return;
if (this.utilityService.isChapter(this.entity) && updateEvent.chapterId !== this.entity.id) return; if (this.utilityService.isChapter(this.entity) && updateEvent.chapterId !== this.entity.id) return;
if (this.utilityService.isVolume(this.entity) && updateEvent.volumeId !== this.entity.id) return; if (this.utilityService.isVolume(this.entity) && updateEvent.volumeId !== this.entity.id) return;
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return; if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
// For volume or Series, we can't just take the event // For volume or Series, we can't just take the event
if (this.utilityService.isChapter(this.entity)) { if (this.utilityService.isChapter(this.entity)) {
const c = this.utilityService.asChapter(this.entity); const c = this.utilityService.asChapter(this.entity);
c.pagesRead = updateEvent.pagesRead; c.pagesRead = updateEvent.pagesRead;
@ -212,7 +224,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}); });
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntil(this.onDestroy), map((events) => { this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
if(this.utilityService.isSeries(this.entity)) return events.find(e => e.entityType === 'series' && e.subTitle === this.downloadService.downloadSubtitle('series', (this.entity as Series))) || null; if(this.utilityService.isSeries(this.entity)) return events.find(e => e.entityType === 'series' && e.subTitle === this.downloadService.downloadSubtitle('series', (this.entity as Series))) || null;
if(this.utilityService.isVolume(this.entity)) return events.find(e => e.entityType === 'volume' && e.subTitle === this.downloadService.downloadSubtitle('volume', (this.entity as Volume))) || null; if(this.utilityService.isVolume(this.entity)) return events.find(e => e.entityType === 'volume' && e.subTitle === this.downloadService.downloadSubtitle('volume', (this.entity as Volume))) || null;
if(this.utilityService.isChapter(this.entity)) return events.find(e => e.entityType === 'chapter' && e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null; if(this.utilityService.isChapter(this.entity)) return events.find(e => e.entityType === 'chapter' && e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null;
@ -223,10 +235,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('touchmove', ['$event']) @HostListener('touchmove', ['$event'])
onTouchMove(event: TouchEvent) { onTouchMove(event: TouchEvent) {

View File

@ -14,7 +14,7 @@ export class DownloadIndicatorComponent {
/** /**
* Observable that represents when the download completes * Observable that represents when the download completes
*/ */
@Input() download$!: Observable<Download | DownloadEvent | null> | null; @Input({required: true}) download$!: Observable<Download | DownloadEvent | null> | null;
constructor() { } constructor() { }
} }

View File

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs'; import { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs';
import { UtilityService } from 'src/app/shared/_services/utility.service'; import { UtilityService } from 'src/app/shared/_services/utility.service';
@ -10,6 +20,7 @@ import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service'; import { LibraryService } from 'src/app/_services/library.service';
import { SearchService } from 'src/app/_services/search.service'; import { SearchService } from 'src/app/_services/search.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
interface RelationControl { interface RelationControl {
series: {id: number, name: string} | undefined; // Will add type as well series: {id: number, name: string} | undefined; // Will add type as well
@ -23,11 +34,11 @@ interface RelationControl {
styleUrls: ['./edit-series-relation.component.scss'], styleUrls: ['./edit-series-relation.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EditSeriesRelationComponent implements OnInit, OnDestroy { export class EditSeriesRelationComponent implements OnInit {
@Input() series!: Series; @Input({required: true}) series!: Series;
/** /**
* This will tell the component to save based on it's internal state * This will tell the component to save based on its internal state
*/ */
@Input() save: EventEmitter<void> = new EventEmitter(); @Input() save: EventEmitter<void> = new EventEmitter();
@ -39,15 +50,14 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
libraryNames: {[key:number]: string} = {}; libraryNames: {[key:number]: string} = {};
focusTypeahead = new EventEmitter(); focusTypeahead = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
get RelationKind() { get RelationKind() {
return RelationKind; return RelationKind;
} }
private onDestroy: Subject<void> = new Subject<void>(); constructor(private seriesService: SeriesService, private utilityService: UtilityService,
constructor(private seriesService: SeriesService, private utilityService: UtilityService,
public imageService: ImageService, private libraryService: LibraryService, private searchService: SearchService, public imageService: ImageService, private libraryService: LibraryService, private searchService: SearchService,
private readonly cdRef: ChangeDetectorRef) {} private readonly cdRef: ChangeDetectorRef) {}
@ -74,12 +84,7 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.save.pipe(takeUntil(this.onDestroy)).subscribe(() => this.saveState()); this.save.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.saveState());
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
} }
setupRelationRows(relations: Array<Series>, kind: RelationKind) { setupRelationRows(relations: Array<Series>, kind: RelationKind) {

View File

@ -19,9 +19,9 @@ import { ImageService } from 'src/app/_services/image.service';
}) })
export class EntityInfoCardsComponent implements OnInit, OnDestroy { export class EntityInfoCardsComponent implements OnInit, OnDestroy {
@Input() entity!: Volume | Chapter; @Input({required: true}) entity!: Volume | Chapter;
/** /**
* This will pull extra information * This will pull extra information
*/ */
@Input() includeMetadata: boolean = false; @Input() includeMetadata: boolean = false;
@ -84,19 +84,19 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }
this.totalPages = this.chapter.pages; this.totalPages = this.chapter.pages;
if (!this.isChapter) { if (!this.isChapter) {
this.totalPages = this.utilityService.asVolume(this.entity).pages; this.totalPages = this.utilityService.asVolume(this.entity).pages;
} }
this.totalWordCount = this.chapter.wordCount; this.totalWordCount = this.chapter.wordCount;
if (!this.isChapter) { if (!this.isChapter) {
this.totalWordCount = this.utilityService.asVolume(this.entity).chapters.map(c => c.wordCount).reduce((sum, d) => sum + d); this.totalWordCount = this.utilityService.asVolume(this.entity).chapters.map(c => c.wordCount).reduce((sum, d) => sum + d);
} }
if (this.isChapter) { if (this.isChapter) {
this.readingTime.minHours = this.chapter.minHoursToRead; this.readingTime.minHours = this.chapter.minHoursToRead;
this.readingTime.maxHours = this.chapter.maxHoursToRead; this.readingTime.maxHours = this.chapter.maxHoursToRead;

View File

@ -17,13 +17,13 @@ export class EntityTitleComponent implements OnInit {
*/ */
@Input() libraryType: LibraryType = LibraryType.Manga; @Input() libraryType: LibraryType = LibraryType.Manga;
@Input() seriesName: string = ''; @Input() seriesName: string = '';
@Input() entity!: Volume | Chapter; @Input({required: true}) entity!: Volume | Chapter;
/** /**
* When generating the title, should this prepend 'Volume number' before the Chapter wording * When generating the title, should this prepend 'Volume number' before the Chapter wording
*/ */
@Input() includeVolume: boolean = false; @Input() includeVolume: boolean = false;
/** /**
* When a titleName (aka a title) is avaliable on the entity, show it over Volume X Chapter Y * When a titleName (aka a title) is available on the entity, show it over Volume X Chapter Y
*/ */
@Input() prioritizeTitleName: boolean = true; @Input() prioritizeTitleName: boolean = true;

View File

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { map, Observable, Subject, takeUntil } from 'rxjs'; import { map, Observable, Subject, takeUntil } from 'rxjs';
import { Download } from 'src/app/shared/_models/download'; import { Download } from 'src/app/shared/_models/download';
@ -9,6 +19,7 @@ import { LibraryType } from 'src/app/_models/library';
import { RelationKind } from 'src/app/_models/series-detail/relation-kind'; import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
import { Volume } from 'src/app/_models/volume'; import { Volume } from 'src/app/_models/volume';
import { Action, ActionItem } from 'src/app/_services/action-factory.service'; import { Action, ActionItem } from 'src/app/_services/action-factory.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-list-item', selector: 'app-list-item',
@ -16,12 +27,12 @@ import { Action, ActionItem } from 'src/app/_services/action-factory.service';
styleUrls: ['./list-item.component.scss'], styleUrls: ['./list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ListItemComponent implements OnInit, OnDestroy { export class ListItemComponent implements OnInit {
/** /**
* Volume or Chapter to render * Volume or Chapter to render
*/ */
@Input() entity!: Volume | Chapter; @Input({required: true}) entity!: Volume | Chapter;
/** /**
* Image to show * Image to show
*/ */
@ -59,7 +70,7 @@ export class ListItemComponent implements OnInit, OnDestroy {
*/ */
@Input() includeVolume: boolean = false; @Input() includeVolume: boolean = false;
/** /**
* Show's the title if avaible on entity * Show's the title if available on entity
*/ */
@Input() showTitle: boolean = true; @Input() showTitle: boolean = true;
/** /**
@ -68,24 +79,23 @@ export class ListItemComponent implements OnInit, OnDestroy {
@Input() blur: boolean = false; @Input() blur: boolean = false;
@Output() read: EventEmitter<void> = new EventEmitter<void>(); @Output() read: EventEmitter<void> = new EventEmitter<void>();
private readonly destroyRef = inject(DestroyRef);
actionInProgress: boolean = false; actionInProgress: boolean = false;
summary: string = ''; summary: string = '';
isChapter: boolean = false; isChapter: boolean = false;
download$: Observable<DownloadEvent | null> | null = null; download$: Observable<DownloadEvent | null> | null = null;
downloadInProgress: boolean = false; downloadInProgress: boolean = false;
private readonly onDestroy = new Subject<void>();
get Title() { get Title() {
if (this.isChapter) return (this.entity as Chapter).titleName; if (this.isChapter) return (this.entity as Chapter).titleName;
return ''; return '';
} }
constructor(private utilityService: UtilityService, private downloadService: DownloadService, constructor(private utilityService: UtilityService, private downloadService: DownloadService,
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { } private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
@ -99,21 +109,16 @@ export class ListItemComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntil(this.onDestroy), map((events) => { this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
if(this.utilityService.isVolume(this.entity)) return events.find(e => e.entityType === 'volume' && e.subTitle === this.downloadService.downloadSubtitle('volume', (this.entity as Volume))) || null; if(this.utilityService.isVolume(this.entity)) return events.find(e => e.entityType === 'volume' && e.subTitle === this.downloadService.downloadSubtitle('volume', (this.entity as Volume))) || null;
if(this.utilityService.isChapter(this.entity)) return events.find(e => e.entityType === 'chapter' && e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null; if(this.utilityService.isChapter(this.entity)) return events.find(e => e.entityType === 'chapter' && e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null;
return null; return null;
})); }));
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
performAction(action: ActionItem<any>) { performAction(action: ActionItem<any>) {
if (action.action == Action.Download) { if (action.action == Action.Download) {
if (this.downloadInProgress === true) { if (this.downloadInProgress) {
this.toastr.info('Download is already in progress. Please wait.'); this.toastr.info('Download is already in progress. Please wait.');
return; return;
} }

View File

@ -18,11 +18,11 @@ import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy { export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
@Input() data!: Series; @Input({required: true}) data!: Series;
@Input() libraryId = 0; @Input() libraryId = 0;
@Input() suppressLibraryLink = false; @Input() suppressLibraryLink = false;
/** /**
* If the entity is selected or not. * If the entity is selected or not.
*/ */
@Input() selected: boolean = false; @Input() selected: boolean = false;
/** /**
@ -51,7 +51,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
constructor(private router: Router, private cdRef: ChangeDetectorRef, constructor(private router: Router, private cdRef: ChangeDetectorRef,
private seriesService: SeriesService, private toastr: ToastrService, private seriesService: SeriesService, private toastr: ToastrService,
private modalService: NgbModal, private imageService: ImageService, private modalService: NgbModal, private imageService: ImageService,
private actionFactoryService: ActionFactoryService, private actionFactoryService: ActionFactoryService,
private actionService: ActionService) {} private actionService: ActionService) {}
@ -157,7 +157,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
this.data.pagesRead = 0; this.data.pagesRead = 0;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
this.dataChanged.emit(series); this.dataChanged.emit(series);
}); });
} }

View File

@ -1,4 +1,15 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Input,
OnChanges,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { debounceTime, filter, map, Subject, takeUntil } from 'rxjs'; import { debounceTime, filter, map, Subject, takeUntil } from 'rxjs';
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service'; import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
import { UtilityService } from 'src/app/shared/_services/utility.service'; import { UtilityService } from 'src/app/shared/_services/utility.service';
@ -11,6 +22,7 @@ import { AccountService } from 'src/app/_services/account.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { MetadataService } from 'src/app/_services/metadata.service'; import { MetadataService } from 'src/app/_services/metadata.service';
import { ReaderService } from 'src/app/_services/reader.service'; import { ReaderService } from 'src/app/_services/reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-series-info-cards', selector: 'app-series-info-cards',
@ -18,10 +30,10 @@ import { ReaderService } from 'src/app/_services/reader.service';
styleUrls: ['./series-info-cards.component.scss'], styleUrls: ['./series-info-cards.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy { export class SeriesInfoCardsComponent implements OnInit, OnChanges {
@Input() series!: Series; @Input({required: true}) series!: Series;
@Input() seriesMetadata!: SeriesMetadata; @Input({required: true}) seriesMetadata!: SeriesMetadata;
@Input() hasReadingProgress: boolean = false; @Input() hasReadingProgress: boolean = false;
@Input() readingTimeLeft: HourEstimateRange | undefined; @Input() readingTimeLeft: HourEstimateRange | undefined;
/** /**
@ -31,8 +43,7 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
@Output() goTo: EventEmitter<{queryParamName: FilterQueryParam, filter: any}> = new EventEmitter(); @Output() goTo: EventEmitter<{queryParamName: FilterQueryParam, filter: any}> = new EventEmitter();
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0}; readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
private readonly destroyRef = inject(DestroyRef);
private readonly onDestroy = new Subject<void>();
get MangaFormat() { get MangaFormat() {
return MangaFormat; return MangaFormat;
@ -42,16 +53,16 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
return FilterQueryParam; return FilterQueryParam;
} }
constructor(public utilityService: UtilityService, public metadataService: MetadataService, constructor(public utilityService: UtilityService, public metadataService: MetadataService,
private readerService: ReaderService, private readonly cdRef: ChangeDetectorRef, private readerService: ReaderService, private readonly cdRef: ChangeDetectorRef,
private messageHub: MessageHubService, private accountService: AccountService) { private messageHub: MessageHubService, private accountService: AccountService) {
// Listen for progress events and re-calculate getTimeLeft // Listen for progress events and re-calculate getTimeLeft
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate), this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
map(evt => evt.payload as UserProgressUpdateEvent), map(evt => evt.payload as UserProgressUpdateEvent),
debounceTime(500), debounceTime(500),
takeUntil(this.onDestroy)) takeUntilDestroyed(this.destroyRef))
.subscribe(updateEvent => { .subscribe(updateEvent => {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
if (user === undefined || user.username !== updateEvent.username) return; if (user === undefined || user.username !== updateEvent.username) return;
if (updateEvent.seriesId !== this.series.id) return; if (updateEvent.seriesId !== this.series.id) return;
this.getReadingTimeLeft(); this.getReadingTimeLeft();
@ -73,10 +84,6 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
handleGoTo(queryParamName: FilterQueryParam, filter: any) { handleGoTo(queryParamName: FilterQueryParam, filter: any) {
this.goTo.emit({queryParamName, filter}); this.goTo.emit({queryParamName, filter});

View File

@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@ -13,6 +21,7 @@ import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/acti
import { CollectionTagService } from 'src/app/_services/collection-tag.service'; import { CollectionTagService } from 'src/app/_services/collection-tag.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service'; import { JumpbarService } from 'src/app/_services/jumpbar.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
@ -21,7 +30,7 @@ import { JumpbarService } from 'src/app/_services/jumpbar.service';
styleUrls: ['./all-collections.component.scss'], styleUrls: ['./all-collections.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AllCollectionsComponent implements OnInit, OnDestroy { export class AllCollectionsComponent implements OnInit {
isLoading: boolean = true; isLoading: boolean = true;
collections: CollectionTag[] = []; collections: CollectionTag[] = [];
@ -29,13 +38,14 @@ export class AllCollectionsComponent implements OnInit, OnDestroy {
jumpbarKeys: Array<JumpKey> = []; jumpbarKeys: Array<JumpKey> = [];
trackByIdentity = (index: number, item: CollectionTag) => `${item.id}_${item.title}`; trackByIdentity = (index: number, item: CollectionTag) => `${item.id}_${item.title}`;
isAdmin$: Observable<boolean> = of(false); isAdmin$: Observable<boolean> = of(false);
private readonly onDestroy = new Subject<void>();
filterOpen: EventEmitter<boolean> = new EventEmitter(); filterOpen: EventEmitter<boolean> = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
constructor(private collectionService: CollectionTagService, private router: Router, constructor(private collectionService: CollectionTagService, private router: Router,
private actionFactoryService: ActionFactoryService, private modalService: NgbModal, private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
private titleService: Title, private jumpbarService: JumpbarService, private titleService: Title, private jumpbarService: JumpbarService,
private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService, private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService,
public accountService: AccountService) { public accountService: AccountService) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.router.routeReuseStrategy.shouldReuseRoute = () => false;
@ -46,18 +56,12 @@ export class AllCollectionsComponent implements OnInit, OnDestroy {
this.loadPage(); this.loadPage();
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this)); this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.isAdmin$ = this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), map(user => { this.isAdmin$ = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), map(user => {
if (!user) return false; if (!user) return false;
return this.accountService.hasAdminRole(user); return this.accountService.hasAdminRole(user);
})); }));
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
loadCollection(item: CollectionTag) { loadCollection(item: CollectionTag) {
this.router.navigate(['collections', item.id]); this.router.navigate(['collections', item.id]);
this.loadPage(); this.loadPage();

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Observable, of, ReplaySubject, Subject } from 'rxjs'; import { Observable, of, ReplaySubject, Subject } from 'rxjs';
@ -16,6 +25,7 @@ import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service'; import { LibraryService } from 'src/app/_services/library.service';
import { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service'; import { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@ -23,7 +33,7 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./dashboard.component.scss'], styleUrls: ['./dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DashboardComponent implements OnInit, OnDestroy { export class DashboardComponent implements OnInit {
/** /**
* By default, 0, but if non-zero, will limit all API calls to library id * By default, 0, but if non-zero, will limit all API calls to library id
@ -32,26 +42,25 @@ export class DashboardComponent implements OnInit, OnDestroy {
libraries$: Observable<Library[]> = of([]); libraries$: Observable<Library[]> = of([]);
isLoading = true; isLoading = true;
isAdmin$: Observable<boolean> = of(false); isAdmin$: Observable<boolean> = of(false);
recentlyUpdatedSeries: SeriesGroup[] = []; recentlyUpdatedSeries: SeriesGroup[] = [];
inProgress: Series[] = []; inProgress: Series[] = [];
recentlyAddedSeries: Series[] = []; recentlyAddedSeries: Series[] = [];
private readonly onDestroy = new Subject<void>();
/** /**
* We use this Replay subject to slow the amount of times we reload the UI * We use this Replay subject to slow the amount of times we reload the UI
*/ */
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>(); private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
private readonly destroyRef = inject(DestroyRef);
constructor(public accountService: AccountService, private libraryService: LibraryService, constructor(public accountService: AccountService, private libraryService: LibraryService,
private seriesService: SeriesService, private router: Router, private seriesService: SeriesService, private router: Router,
private titleService: Title, public imageService: ImageService, private titleService: Title, public imageService: ImageService,
private messageHub: MessageHubService, private readonly cdRef: ChangeDetectorRef) { private messageHub: MessageHubService, private readonly cdRef: ChangeDetectorRef) {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => { this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
if (res.event === EVENTS.SeriesAdded) { if (res.event === EVENTS.SeriesAdded) {
const seriesAddedEvent = res.payload as SeriesAddedEvent; const seriesAddedEvent = res.payload as SeriesAddedEvent;
@ -61,7 +70,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
}); });
} else if (res.event === EVENTS.SeriesRemoved) { } else if (res.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = res.payload as SeriesRemovedEvent; const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId); this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId); this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId); this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
@ -73,12 +82,12 @@ export class DashboardComponent implements OnInit, OnDestroy {
}); });
this.isAdmin$ = this.accountService.currentUser$.pipe( this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false), map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay() shareReplay()
); );
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntil(this.onDestroy)).subscribe(() => { this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.loadRecentlyUpdated(); this.loadRecentlyUpdated();
this.loadRecentlyAddedSeries(); this.loadRecentlyAddedSeries();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -98,11 +107,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.reloadSeries(); this.reloadSeries();
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
reloadSeries() { reloadSeries() {
this.loadOnDeck(); this.loadOnDeck();
this.loadRecentlyUpdated(); this.loadRecentlyUpdated();
@ -127,7 +131,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
if (this.libraryId > 0) { if (this.libraryId > 0) {
api = this.seriesService.getOnDeck(this.libraryId, 1, 30); api = this.seriesService.getOnDeck(this.libraryId, 1, 30);
} }
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => { api.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((updatedSeries) => {
this.inProgress = updatedSeries.result; this.inProgress = updatedSeries.result;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
@ -138,7 +142,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
if (this.libraryId > 0) { if (this.libraryId > 0) {
api = this.seriesService.getRecentlyAdded(this.libraryId, 1, 30); api = this.seriesService.getRecentlyAdded(this.libraryId, 1, 30);
} }
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => { api.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((updatedSeries) => {
this.recentlyAddedSeries = updatedSeries.result; this.recentlyAddedSeries = updatedSeries.result;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
@ -150,7 +154,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
if (this.libraryId > 0) { if (this.libraryId > 0) {
api = this.seriesService.getRecentlyUpdatedSeries(); api = this.seriesService.getRecentlyUpdatedSeries();
} }
api.pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => { api.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(updatedSeries => {
this.recentlyUpdatedSeries = updatedSeries.filter(group => { this.recentlyUpdatedSeries = updatedSeries.filter(group => {
if (this.libraryId === 0) return true; if (this.libraryId === 0) return true;
return group.libraryId === this.libraryId; return group.libraryId === this.libraryId;
@ -183,7 +187,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
params[FilterQueryParam.Page] = 1; params[FilterQueryParam.Page] = 1;
params['title'] = 'Newly Added'; params['title'] = 'Newly Added';
this.router.navigate(['all-series'], {queryParams: params}); this.router.navigate(['all-series'], {queryParams: params});
} }
} }
removeFromArray(arr: Array<any>, element: any) { removeFromArray(arr: Array<any>, element: any) {

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
HostListener,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -20,6 +29,7 @@ import { FilterUtilitiesService } from '../shared/_services/filter-utilities.ser
import { FilterSettings } from '../metadata-filter/filter-settings'; import { FilterSettings } from '../metadata-filter/filter-settings';
import { JumpKey } from '../_models/jumpbar/jump-key'; import { JumpKey } from '../_models/jumpbar/jump-key';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event'; import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-library-detail', selector: 'app-library-detail',
@ -27,7 +37,7 @@ import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
styleUrls: ['./library-detail.component.scss'], styleUrls: ['./library-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LibraryDetailComponent implements OnInit, OnDestroy { export class LibraryDetailComponent implements OnInit {
libraryId!: number; libraryId!: number;
libraryName = ''; libraryName = '';
@ -36,7 +46,6 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
pagination!: Pagination; pagination!: Pagination;
actions: ActionItem<Library>[] = []; actions: ActionItem<Library>[] = [];
filter: SeriesFilter | undefined = undefined; filter: SeriesFilter | undefined = undefined;
onDestroy: Subject<void> = new Subject<void>();
filterSettings: FilterSettings = new FilterSettings(); filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter(); filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false; filterActive: boolean = false;
@ -47,14 +56,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
tabs: Array<{title: string, fragment: string, icon: string}> = [ tabs: Array<{title: string, fragment: string, icon: string}> = [
{title: 'Library', fragment: '', icon: 'fa-landmark'}, {title: 'Library', fragment: '', icon: 'fa-landmark'},
{title: 'Recommended', fragment: 'recomended', icon: 'fa-award'}, {title: 'Recommended', fragment: 'recommended', icon: 'fa-award'},
]; ];
active = this.tabs[0]; active = this.tabs[0];
private readonly destroyRef = inject(DestroyRef);
bulkActionCallback = (action: ActionItem<any>, data: any) => { bulkActionCallback = (action: ActionItem<any>, data: any) => {
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series'); const selectedSeriesIndices = this.bulkSelectionService.getSelectedCardsForSource('series');
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + '')); const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndices.includes(index + ''));
switch (action.action) { switch (action.action) {
case Action.AddToReadingList: case Action.AddToReadingList:
@ -86,7 +95,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
this.bulkSelectionService.deselectAll(); this.bulkSelectionService.deselectAll();
this.loadPage(); this.loadPage();
}); });
break; break;
case Action.MarkAsUnread: case Action.MarkAsUnread:
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => { this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
@ -104,8 +113,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
} }
} }
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService, constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService, private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService, private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService,
private readonly cdRef: ChangeDetectorRef) { private readonly cdRef: ChangeDetectorRef) {
@ -130,7 +139,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
}); });
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this)); this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
this.pagination = this.filterUtilityService.pagination(this.route.snapshot); this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot); [this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId]; if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
@ -143,7 +152,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => { this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
if (event.event === EVENTS.SeriesAdded) { if (event.event === EVENTS.SeriesAdded) {
const seriesAdded = event.payload as SeriesAddedEvent; const seriesAdded = event.payload as SeriesAddedEvent;
if (seriesAdded.libraryId !== this.libraryId) return; if (seriesAdded.libraryId !== this.libraryId) return;
@ -161,8 +170,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.refresh.emit(); this.refresh.emit();
}); });
} else if (event.event === EVENTS.SeriesRemoved) { } else if (event.event === EVENTS.SeriesRemoved) {
const seriesRemoved = event.payload as SeriesRemovedEvent; const seriesRemoved = event.payload as SeriesRemovedEvent;
if (seriesRemoved.libraryId !== this.libraryId) return; if (seriesRemoved.libraryId !== this.libraryId) return;
@ -179,10 +188,6 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event']) @HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) { handleKeypress(event: KeyboardEvent) {
@ -242,9 +247,9 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
this.loadingSeries = true; this.loadingSeries = true;
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck); this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.seriesService.getSeriesForLibrary(0, undefined, undefined, this.filter).pipe(take(1)).subscribe(series => { this.seriesService.getSeriesForLibrary(0, undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result; this.series = series.result;
this.pagination = series.pagination; this.pagination = series.pagination;
this.loadingSeries = false; this.loadingSeries = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -1,10 +1,20 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { filter, map, merge, Observable, shareReplay, Subject, takeUntil } from 'rxjs'; import { filter, map, merge, Observable, shareReplay, Subject, takeUntil } from 'rxjs';
import { Genre } from 'src/app/_models/metadata/genre'; import { Genre } from 'src/app/_models/metadata/genre';
import { Series } from 'src/app/_models/series'; import { Series } from 'src/app/_models/series';
import { MetadataService } from 'src/app/_services/metadata.service'; import { MetadataService } from 'src/app/_services/metadata.service';
import { RecommendationService } from 'src/app/_services/recommendation.service'; import { RecommendationService } from 'src/app/_services/recommendation.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-library-recommended', selector: 'app-library-recommended',
@ -12,9 +22,10 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./library-recommended.component.scss'], styleUrls: ['./library-recommended.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LibraryRecommendedComponent implements OnInit, OnDestroy { export class LibraryRecommendedComponent implements OnInit {
@Input() libraryId: number = 0; @Input() libraryId: number = 0;
private readonly destroyRef = inject(DestroyRef);
quickReads$!: Observable<Series[]>; quickReads$!: Observable<Series[]>;
quickCatchups$!: Observable<Series[]>; quickCatchups$!: Observable<Series[]>;
@ -26,43 +37,36 @@ export class LibraryRecommendedComponent implements OnInit, OnDestroy {
all$!: Observable<any>; all$!: Observable<any>;
private onDestroy: Subject<void> = new Subject(); constructor(private recommendationService: RecommendationService, private seriesService: SeriesService,
constructor(private recommendationService: RecommendationService, private seriesService: SeriesService,
private metadataService: MetadataService) { } private metadataService: MetadataService) { }
ngOnInit(): void { ngOnInit(): void {
this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId, 0, 30) this.quickReads$ = this.recommendationService.getQuickReads(this.libraryId, 0, 30)
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); .pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
this.quickCatchups$ = this.recommendationService.getQuickCatchupReads(this.libraryId, 0, 30) this.quickCatchups$ = this.recommendationService.getQuickCatchupReads(this.libraryId, 0, 30)
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); .pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId, 0, 30) this.highlyRated$ = this.recommendationService.getHighlyRated(this.libraryId, 0, 30)
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); .pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
this.rediscover$ = this.recommendationService.getRediscover(this.libraryId, 0, 30) this.rediscover$ = this.recommendationService.getRediscover(this.libraryId, 0, 30)
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); .pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
this.onDeck$ = this.seriesService.getOnDeck(this.libraryId, 0, 30) this.onDeck$ = this.seriesService.getOnDeck(this.libraryId, 0, 30)
.pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); .pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe( this.genre$ = this.metadataService.getAllGenres([this.libraryId]).pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(genres => genres[Math.floor(Math.random() * genres.length)]), map(genres => genres[Math.floor(Math.random() * genres.length)]),
shareReplay() shareReplay()
); );
this.genre$.subscribe(genre => { this.genre$.subscribe(genre => {
this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id, 0, 30).pipe(takeUntil(this.onDestroy), map(p => p.result), shareReplay()); this.moreIn$ = this.recommendationService.getMoreIn(this.libraryId, genre.id, 0, 30).pipe(takeUntilDestroyed(this.destroyRef), map(p => p.result), shareReplay());
}); });
this.all$ = merge(this.quickReads$, this.quickCatchups$, this.highlyRated$, this.rediscover$, this.onDeck$, this.genre$).pipe(takeUntil(this.onDestroy)); this.all$ = merge(this.quickReads$, this.quickCatchups$, this.highlyRated$, this.rediscover$, this.onDeck$, this.genre$).pipe(takeUntilDestroyed(this.destroyRef));
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
} }
@ -75,7 +79,7 @@ export class LibraryRecommendedComponent implements OnInit, OnDestroy {
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) { if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
return; return;
} }
this.quickReads$ = this.quickReads$.pipe(filter(series => !series.includes(seriesObj))); this.quickReads$ = this.quickReads$.pipe(filter(series => !series.includes(seriesObj)));
this.quickCatchups$ = this.quickCatchups$.pipe(filter(series => !series.includes(seriesObj))); this.quickCatchups$ = this.quickCatchups$.pipe(filter(series => !series.includes(seriesObj)));
} }

View File

@ -1,4 +1,17 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { filter, map, Observable, of, Subject, takeUntil, takeWhile, tap } from 'rxjs'; import { filter, map, Observable, of, Subject, takeUntil, takeWhile, tap } from 'rxjs';
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
import { ReaderService } from 'src/app/_services/reader.service'; import { ReaderService } from 'src/app/_services/reader.service';
@ -7,6 +20,7 @@ import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from '../../_models
import { ReaderSetting } from '../../_models/reader-setting'; import { ReaderSetting } from '../../_models/reader-setting';
import { ImageRenderer } from '../../_models/renderer'; import { ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRightToLeft]; const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRightToLeft];
@ -16,18 +30,18 @@ const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRigh
styleUrls: ['./canvas-renderer.component.scss'], styleUrls: ['./canvas-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy, ImageRenderer { export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>; @Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>; @Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>; @Input({required: true}) bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>; @Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input() imageFit$!: Observable<FITTING_OPTION>; @Input() imageFit$!: Observable<FITTING_OPTION>;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>(); @Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
@ViewChild('content') canvas: ElementRef | undefined; @ViewChild('content') canvas: ElementRef | undefined;
private ctx!: CanvasRenderingContext2D; private ctx!: CanvasRenderingContext2D;
private readonly onDestroy = new Subject<void>();
currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT; currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT;
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
@ -47,13 +61,13 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
*/ */
imageFitClass$!: Observable<string>; imageFitClass$!: Observable<string>;
renderWithCanvas: boolean = false; renderWithCanvas: boolean = false;
constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService, private readerService: ReaderService) { } constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService, private readerService: ReaderService) { }
ngOnInit(): void { ngOnInit(): void {
this.readerSettings$.pipe(takeUntil(this.onDestroy), tap((value: ReaderSetting) => { this.readerSettings$.pipe(takeUntilDestroyed(this.destroyRef), tap((value: ReaderSetting) => {
this.fit = value.fitting; this.fit = value.fitting;
this.pageSplit = value.pageSplit; this.pageSplit = value.pageSplit;
this.layoutMode = value.layoutMode; this.layoutMode = value.layoutMode;
@ -65,19 +79,19 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
})).subscribe(() => {}); })).subscribe(() => {});
this.darkenss$ = this.readerSettings$.pipe( this.darkenss$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'), map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.imageFitClass$ = this.readerSettings$.pipe( this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map((values: ReaderSetting) => values.fitting), map((values: ReaderSetting) => values.fitting),
map(fit => { map(fit => {
if (fit === FITTING_OPTION.WIDTH) return fit; // || this.layoutMode === LayoutMode.Single (so that we can check the wide stuff) if (fit === FITTING_OPTION.WIDTH) return fit; // || this.layoutMode === LayoutMode.Single (so that we can check the wide stuff)
if (this.canvasImage === null) return fit; if (this.canvasImage === null) return fit;
// Would this ever execute given that we perform splitting only in this renderer? // Would this ever execute given that we perform splitting only in this renderer?
if ( if (
this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src)) && this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src)) &&
this.mangaReaderService.shouldRenderAsFitSplit(this.pageSplit) this.mangaReaderService.shouldRenderAsFitSplit(this.pageSplit)
@ -92,7 +106,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
this.bookmark$.pipe( this.bookmark$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(_ => { tap(_ => {
if (this.currentImageSplitPart === SPLIT_PAGE_PART.NO_SPLIT) return; if (this.currentImageSplitPart === SPLIT_PAGE_PART.NO_SPLIT) return;
if (!this.canvas) return; if (!this.canvas) return;
@ -103,8 +117,8 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
).subscribe(() => {}); ).subscribe(() => {});
this.showClickOverlayClass$ = this.showClickOverlay$.pipe( this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''), map(showOverlay => showOverlay ? 'blur' : ''),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
@ -114,10 +128,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
} }
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
reset() { reset() {
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT; this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
@ -126,7 +136,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
updateSplitPage() { updateSplitPage() {
if (this.canvasImage == null) return; if (this.canvasImage == null) return;
const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src)); const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src));
if (!needsSplitting || this.mangaReaderService.isNoSplit(this.pageSplit)) { if (!needsSplitting || this.mangaReaderService.isNoSplit(this.pageSplit)) {
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT; this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
return needsSplitting; return needsSplitting;
@ -171,8 +181,8 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
/** /**
* This renderer does not render when splitting is not needed * This renderer does not render when splitting is not needed
* @param img * @param img
* @returns * @returns
*/ */
renderPage(img: Array<HTMLImageElement | null>) { renderPage(img: Array<HTMLImageElement | null>) {
this.renderWithCanvas = false; this.renderWithCanvas = false;
@ -184,7 +194,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
if (this.layoutMode !== LayoutMode.Single || !ValidSplits.includes(this.pageSplit)) { if (this.layoutMode !== LayoutMode.Single || !ValidSplits.includes(this.pageSplit)) {
return; return;
} }
const needsSplitting = this.updateSplitPage(); const needsSplitting = this.updateSplitPage();
if (!needsSplitting) return; if (!needsSplitting) return;
@ -236,8 +246,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
setCanvasSize() { setCanvasSize() {
if (this.canvasImage == null) return; if (this.canvasImage == null) return;
if (!this.ctx || !this.canvas) { return; } if (!this.ctx || !this.canvas) { return; }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const isSafari = [ const isSafari = [
'iPad Simulator', 'iPad Simulator',
'iPhone Simulator', 'iPhone Simulator',

View File

@ -1,18 +1,18 @@
<ng-container *ngIf="isValid()"> <ng-container *ngIf="isValid()">
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}" <div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle" [style.filter]="(darkness$ | async) ?? '' | safeStyle"
[ngClass]="{'center-double': (shouldRenderDouble$ | async)}"> [ngClass]="{'center-double': (shouldRenderDouble$ | async)}">
<ng-container *ngIf="currentImage"> <ng-container *ngIf="currentImage">
<img alt=" " <img alt=" "
#image [src]="currentImage.src" #image [src]="currentImage.src"
id="image-1" id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}" class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
> >
</ng-container> </ng-container>
<ng-container *ngIf="shouldRenderDouble$ | async"> <ng-container *ngIf="shouldRenderDouble$ | async">
<img alt=" " [src]="currentImage2.src" <img alt=" " [src]="currentImage2.src"
id="image-2" id="image-2"
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"> class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}">
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,5 +1,16 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { Observable, of, Subject, map, takeUntil, tap, zip, shareReplay, filter, combineLatest } from 'rxjs'; import { Observable, of, Subject, map, takeUntil, tap, zip, shareReplay, filter, combineLatest } from 'rxjs';
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; import { ReaderMode } from 'src/app/_models/preferences/reader-mode';
@ -9,6 +20,7 @@ import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting'; import { ReaderSetting } from '../../_models/reader-setting';
import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer'; import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
* Renders 2 pages except on last page, and before a wide image * Renders 2 pages except on last page, and before a wide image
@ -19,22 +31,23 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./double-no-cover-renderer.component.scss'], styleUrls: ['./double-no-cover-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy { export class DoubleNoCoverRendererComponent implements OnInit {
@Input() readerSettings$!: Observable<ReaderSetting>; @Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>; @Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>; @Input({required: true}) bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>; @Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; @Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement; @Input({required: true}) getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>(); @Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
debugMode: DEBUG_MODES = DEBUG_MODES.Logs; debugMode: DEBUG_MODES = DEBUG_MODES.Logs;
imageFitClass$!: Observable<string>; imageFitClass$!: Observable<string>;
showClickOverlayClass$!: Observable<string>; showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>; readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>; layoutClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)'); darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of(''); emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single; layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit; pageSplit: PageSplitOption = PageSplitOption.FitSplit;
@ -47,60 +60,58 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
*/ */
currentImage = new Image(); currentImage = new Image();
/** /**
* Used solely for LayoutMode.Double rendering. * Used solely for LayoutMode.Double rendering.
* @remarks Used for rendering to screen. * @remarks Used for rendering to screen.
*/ */
currentImage2 = new Image(); currentImage2 = new Image();
/** /**
* Determines if we should render a double page. * Determines if we should render a double page.
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image * The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
* and the next page is not a wide image (as only non-wides should be shown next to each other). * and the next page is not a wide image (as only non-wides should be shown next to each other).
* @remarks This will always fail if the window's width is greater than the height * @remarks This will always fail if the window's width is greater than the height
*/ */
shouldRenderDouble$!: Observable<boolean>; shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>(); get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService, constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
@Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { }
ngOnInit(): void { ngOnInit(): void {
this.readerModeClass$ = this.readerSettings$.pipe( this.readerModeClass$ = this.readerSettings$.pipe(
map(values => values.readerMode), map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.darkenss$ = this.readerSettings$.pipe( this.darkness$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'), map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.emulateBookClass$ = this.readerSettings$.pipe( this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook), map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''), map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.showClickOverlayClass$ = this.showClickOverlay$.pipe( this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''), map(showOverlay => showOverlay ? 'blur' : ''),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.pageNum$.pipe( this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(pageInfo => { tap(pageInfo => {
this.pageNum = pageInfo.pageNum; this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages; this.maxPages = pageInfo.maxPages;
@ -114,20 +125,20 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
).subscribe(() => {}); ).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe( this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map((_) => this.shouldRenderDouble()), map((_) => this.shouldRenderDouble()),
filter(_ => this.isValid()), filter(_ => this.isValid()),
); );
this.imageFitClass$ = this.readerSettings$.pipe( this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(values => values.fitting), map(values => values.fitting),
filter(_ => this.isValid()), filter(_ => this.isValid()),
shareReplay() shareReplay()
); );
this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe( this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map((value) => { map((value) => {
if (value[0] && value[1].fitting === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset'; if (value[0] && value[1].fitting === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset';
if (value[0] && value[1].fitting === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset'; if (value[0] && value[1].fitting === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset';
@ -139,7 +150,7 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
this.readerSettings$.pipe( this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(values => { tap(values => {
this.layoutMode = values.layoutMode; this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit; this.pageSplit = values.pageSplit;
@ -148,7 +159,7 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
).subscribe(() => {}); ).subscribe(() => {});
this.bookmark$.pipe( this.bookmark$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(_ => { tap(_ => {
const elements = []; const elements = [];
const image1 = this.document.querySelector('#image-1'); const image1 = this.document.querySelector('#image-1');
@ -156,18 +167,13 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
const image2 = this.document.querySelector('#image-2'); const image2 = this.document.querySelector('#image-2');
if (image2 != null) elements.push(image2); if (image2 != null) elements.push(image2);
this.mangaReaderService.applyBookmarkEffect(elements); this.mangaReaderService.applyBookmarkEffect(elements);
}), }),
filter(_ => this.isValid()), filter(_ => this.isValid()),
).subscribe(() => {}); ).subscribe(() => {});
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() { shouldRenderDouble() {
if (!this.isValid()) return false; if (!this.isValid()) return false;
@ -202,11 +208,11 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
isValid() { isValid() {
return this.layoutMode === LayoutMode.DoubleNoCover; return this.layoutMode === LayoutMode.DoubleNoCover;
} }
renderPage(img: Array<HTMLImageElement | null>): void { renderPage(img: Array<HTMLImageElement | null>): void {
if (img === null || img.length === 0 || img[0] === null) return; if (img === null || img.length === 0 || img[0] === null) return;
if (!this.isValid()) return; if (!this.isValid()) return;
// First load, switching from double manga -> double, this is 0 and thus not rendering // First load, switching from double manga -> double, this is 0 and thus not rendering
if (!this.shouldRenderDouble() && (this.currentImage.height || img[0].height) > 0) { if (!this.shouldRenderDouble() && (this.currentImage.height || img[0].height) > 0) {
this.imageHeight.emit(this.currentImage.height || img[0].height); this.imageHeight.emit(this.currentImage.height || img[0].height);
@ -297,7 +303,7 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
if (!(this.debugMode & DEBUG_MODES.Logs)) return; if (!(this.debugMode & DEBUG_MODES.Logs)) return;
if (extraData !== undefined) { if (extraData !== undefined) {
console.log(message, extraData); console.log(message, extraData);
} else { } else {
console.log(message); console.log(message);
} }

View File

@ -1,18 +1,18 @@
<ng-container *ngIf="isValid()"> <ng-container *ngIf="isValid()">
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}" <div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle" [style.filter]="(darkness$ | async) ?? '' | safeStyle"
[ngClass]="{'center-double': (shouldRenderDouble$ | async)}"> [ngClass]="{'center-double': (shouldRenderDouble$ | async)}">
<ng-container *ngIf="currentImage"> <ng-container *ngIf="currentImage">
<img alt=" " <img alt=" "
#image [src]="currentImage.src" #image [src]="currentImage.src"
id="image-1" id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}" class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
> >
</ng-container> </ng-container>
<ng-container *ngIf="shouldRenderDouble$ | async"> <ng-container *ngIf="shouldRenderDouble$ | async">
<img alt=" " [src]="currentImage2.src" <img alt=" " [src]="currentImage2.src"
id="image-2" id="image-2"
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"> class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}">
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,5 +1,16 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { Observable, of, Subject, map, takeUntil, tap, shareReplay, filter, combineLatest } from 'rxjs'; import { Observable, of, Subject, map, takeUntil, tap, shareReplay, filter, combineLatest } from 'rxjs';
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; import { ReaderMode } from 'src/app/_models/preferences/reader-mode';
@ -9,6 +20,7 @@ import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting'; import { ReaderSetting } from '../../_models/reader-setting';
import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer'; import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
* Renders 2 pages except on first page, last page, and before a wide image * Renders 2 pages except on first page, last page, and before a wide image
@ -19,22 +31,23 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./double-renderer.component.scss'], styleUrls: ['./double-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer { export class DoubleRendererComponent implements OnInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>; @Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>; @Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>; @Input({required: true}) bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>; @Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; @Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement; @Input({required: true}) getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>(); @Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
debugMode: DEBUG_MODES = DEBUG_MODES.None; debugMode: DEBUG_MODES = DEBUG_MODES.None;
imageFitClass$!: Observable<string>; imageFitClass$!: Observable<string>;
showClickOverlayClass$!: Observable<string>; showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>; readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>; layoutClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)'); darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of(''); emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single; layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit; pageSplit: PageSplitOption = PageSplitOption.FitSplit;
@ -47,60 +60,58 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
*/ */
currentImage = new Image(); currentImage = new Image();
/** /**
* Used solely for LayoutMode.Double rendering. * Used solely for LayoutMode.Double rendering.
* @remarks Used for rendering to screen. * @remarks Used for rendering to screen.
*/ */
currentImage2 = new Image(); currentImage2 = new Image();
/** /**
* Determines if we should render a double page. * Determines if we should render a double page.
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image * The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
* and the next page is not a wide image (as only non-wides should be shown next to each other). * and the next page is not a wide image (as only non-wides should be shown next to each other).
* @remarks This will always fail if the window's width is greater than the height * @remarks This will always fail if the window's width is greater than the height
*/ */
shouldRenderDouble$!: Observable<boolean>; shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>(); get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService, constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
@Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { }
ngOnInit(): void { ngOnInit(): void {
this.readerModeClass$ = this.readerSettings$.pipe( this.readerModeClass$ = this.readerSettings$.pipe(
map(values => values.readerMode), map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.darkenss$ = this.readerSettings$.pipe( this.darkness$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'), map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.emulateBookClass$ = this.readerSettings$.pipe( this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook), map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''), map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.showClickOverlayClass$ = this.showClickOverlay$.pipe( this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''), map(showOverlay => showOverlay ? 'blur' : ''),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.pageNum$.pipe( this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(pageInfo => { tap(pageInfo => {
this.pageNum = pageInfo.pageNum; this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages; this.maxPages = pageInfo.maxPages;
@ -114,20 +125,20 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {}); ).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe( this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map((_) => this.shouldRenderDouble()), map((_) => this.shouldRenderDouble()),
filter(_ => this.isValid()), filter(_ => this.isValid()),
); );
this.imageFitClass$ = this.readerSettings$.pipe( this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(values => values.fitting), map(values => values.fitting),
filter(_ => this.isValid()), filter(_ => this.isValid()),
shareReplay() shareReplay()
); );
this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe( this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map((value) => { map((value) => {
if (value[0] && value[1].fitting === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset'; if (value[0] && value[1].fitting === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset';
if (value[0] && value[1].fitting === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset'; if (value[0] && value[1].fitting === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset';
@ -140,7 +151,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
this.readerSettings$.pipe( this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(values => { tap(values => {
this.layoutMode = values.layoutMode; this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit; this.pageSplit = values.pageSplit;
@ -149,7 +160,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {}); ).subscribe(() => {});
this.bookmark$.pipe( this.bookmark$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(_ => { tap(_ => {
const elements = []; const elements = [];
const image1 = this.document.querySelector('#image-1'); const image1 = this.document.querySelector('#image-1');
@ -157,17 +168,13 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
const image2 = this.document.querySelector('#image-2'); const image2 = this.document.querySelector('#image-2');
if (image2 != null) elements.push(image2); if (image2 != null) elements.push(image2);
this.mangaReaderService.applyBookmarkEffect(elements); this.mangaReaderService.applyBookmarkEffect(elements);
}), }),
filter(_ => this.isValid()), filter(_ => this.isValid()),
).subscribe(() => {}); ).subscribe(() => {});
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() { shouldRenderDouble() {
if (!this.isValid()) return false; if (!this.isValid()) return false;
@ -203,11 +210,11 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
isValid() { isValid() {
return this.layoutMode === LayoutMode.Double; return this.layoutMode === LayoutMode.Double;
} }
renderPage(img: Array<HTMLImageElement | null>): void { renderPage(img: Array<HTMLImageElement | null>): void {
if (img === null || img.length === 0 || img[0] === null) return; if (img === null || img.length === 0 || img[0] === null) return;
if (!this.isValid()) return; if (!this.isValid()) return;
// First load, switching from double manga -> double, this is 0 and thus not rendering // First load, switching from double manga -> double, this is 0 and thus not rendering
if (!this.shouldRenderDouble() && (this.currentImage.height || img[0].height) > 0) { if (!this.shouldRenderDouble() && (this.currentImage.height || img[0].height) > 0) {
this.imageHeight.emit(this.currentImage.height || img[0].height); this.imageHeight.emit(this.currentImage.height || img[0].height);
@ -291,7 +298,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
if (!(this.debugMode & DEBUG_MODES.Logs)) return; if (!(this.debugMode & DEBUG_MODES.Logs)) return;
if (extraData !== undefined) { if (extraData !== undefined) {
console.log(message, extraData); console.log(message, extraData);
} else { } else {
console.log(message); console.log(message);
} }

View File

@ -1,18 +1,18 @@
<ng-container *ngIf="isValid()"> <ng-container *ngIf="isValid()">
<div class="image-container {{layoutClass$ | async}} {{emulateBookClass$ | async}}" <div class="image-container {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle" [style.filter]="(darkness$ | async) ?? '' | safeStyle"
[ngClass]="{'center-double': (shouldRenderDouble$ | async), 'reverse': (shouldRenderDouble$ | async)}"> [ngClass]="{'center-double': (shouldRenderDouble$ | async), 'reverse': (shouldRenderDouble$ | async)}">
<ng-container *ngIf="leftImage"> <ng-container *ngIf="leftImage">
<img alt=" " <img alt=" "
#image [src]="leftImage.src" #image [src]="leftImage.src"
id="image-1" id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}" class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
> >
</ng-container> </ng-container>
<ng-container *ngIf="shouldRenderDouble$ | async"> <ng-container *ngIf="shouldRenderDouble$ | async">
<img alt=" " [src]="rightImage.src" <img alt=" " [src]="rightImage.src"
id="image-2" id="image-2"
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"> <!--reverse--> class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"> <!--reverse-->
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,5 +1,16 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { Observable, of, Subject, map, takeUntil, tap, zip, shareReplay, filter, combineLatest } from 'rxjs'; import { Observable, of, Subject, map, takeUntil, tap, zip, shareReplay, filter, combineLatest } from 'rxjs';
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; import { ReaderMode } from 'src/app/_models/preferences/reader-mode';
@ -9,10 +20,11 @@ import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting'; import { ReaderSetting } from '../../_models/reader-setting';
import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer'; import { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
* This is aimed at manga. Double page renderer but where if we have page = 10, you will see * This is aimed at manga. Double page renderer but where if we have page = 10, you will see
* page 11 page 10. * page 11 page 10.
*/ */
@Component({ @Component({
selector: 'app-double-reverse-renderer', selector: 'app-double-reverse-renderer',
@ -20,16 +32,17 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./double-reverse-renderer.component.scss'], styleUrls: ['./double-reverse-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageRenderer { export class DoubleReverseRendererComponent implements OnInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>; @Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>; @Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>; @Input({required: true}) bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>; @Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; @Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement; @Input({required: true}) getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>(); @Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
debugMode: DEBUG_MODES = DEBUG_MODES.None; debugMode: DEBUG_MODES = DEBUG_MODES.None;
@ -37,7 +50,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
showClickOverlayClass$!: Observable<string>; showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>; readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>; layoutClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)'); darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of(''); emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single; layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit; pageSplit: PageSplitOption = PageSplitOption.FitSplit;
@ -57,52 +70,50 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
/** /**
* Determines if we should render a double page. * Determines if we should render a double page.
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image * The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
* and the next page is not a wide image (as only non-wides should be shown next to each other). * and the next page is not a wide image (as only non-wides should be shown next to each other).
* @remarks This will always fail if the window's width is greater than the height * @remarks This will always fail if the window's width is greater than the height
*/ */
shouldRenderDouble$!: Observable<boolean>; shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>(); get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService, constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
@Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { } @Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { }
ngOnInit(): void { ngOnInit(): void {
this.readerModeClass$ = this.readerSettings$.pipe( this.readerModeClass$ = this.readerSettings$.pipe(
filter(_ => this.isValid()), filter(_ => this.isValid()),
map(values => values.readerMode), map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.darkenss$ = this.readerSettings$.pipe( this.darkness$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'), map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.emulateBookClass$ = this.readerSettings$.pipe( this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook), map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''), map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.showClickOverlayClass$ = this.showClickOverlay$.pipe( this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''), map(showOverlay => showOverlay ? 'blur' : ''),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.pageNum$.pipe( this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(pageInfo => { tap(pageInfo => {
this.pageNum = pageInfo.pageNum; this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages; this.maxPages = pageInfo.maxPages;
@ -114,21 +125,21 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
).subscribe(() => {}); ).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe( this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(() => this.shouldRenderDouble()), map(() => this.shouldRenderDouble()),
filter(() => this.isValid()), filter(() => this.isValid()),
shareReplay() shareReplay()
); );
this.imageFitClass$ = this.readerSettings$.pipe( this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(values => values.fitting), map(values => values.fitting),
filter(_ => this.isValid()), filter(_ => this.isValid()),
shareReplay() shareReplay()
); );
this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe( this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map((value) => { map((value) => {
if (value[0] && value[1].fitting === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset'; if (value[0] && value[1].fitting === FITTING_OPTION.WIDTH) return 'fit-to-width-double-offset';
if (value[0] && value[1].fitting === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset'; if (value[0] && value[1].fitting === FITTING_OPTION.HEIGHT) return 'fit-to-height-double-offset';
@ -141,7 +152,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
this.readerSettings$.pipe( this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(values => { tap(values => {
this.layoutMode = values.layoutMode; this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit; this.pageSplit = values.pageSplit;
@ -150,7 +161,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
).subscribe(() => {}); ).subscribe(() => {});
this.bookmark$.pipe( this.bookmark$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(_ => { tap(_ => {
const elements = []; const elements = [];
const image1 = this.document.querySelector('#image-1'); const image1 = this.document.querySelector('#image-1');
@ -165,11 +176,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
).subscribe(() => {}); ).subscribe(() => {});
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() { shouldRenderDouble() {
if (!this.isValid()) return false; if (!this.isValid()) return false;
@ -204,7 +210,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
isValid() { isValid() {
return this.layoutMode === LayoutMode.DoubleReversed; return this.layoutMode === LayoutMode.DoubleReversed;
} }
renderPage(img: Array<HTMLImageElement | null>): void { renderPage(img: Array<HTMLImageElement | null>): void {
if (img === null || img.length === 0 || img[0] === null) return; if (img === null || img.length === 0 || img[0] === null) return;
if (!this.isValid()) return; if (!this.isValid()) return;
@ -305,7 +311,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
if (!(this.debugMode & DEBUG_MODES.Logs)) return; if (!(this.debugMode & DEBUG_MODES.Logs)) return;
if (extraData !== undefined) { if (extraData !== undefined) {
console.log(message, extraData); console.log(message, extraData);
} else { } else {
console.log(message); console.log(message);
} }

View File

@ -1,5 +1,20 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
EventEmitter,
inject,
Inject,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
SimpleChanges
} from '@angular/core';
import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs'; import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators'; import { debounceTime, takeUntil } from 'rxjs/operators';
import { ScrollService } from 'src/app/_services/scroll.service'; import { ScrollService } from 'src/app/_services/scroll.service';
@ -7,6 +22,7 @@ import { ReaderService } from '../../../_services/reader.service';
import { PAGING_DIRECTION } from '../../_models/reader-enums'; import { PAGING_DIRECTION } from '../../_models/reader-enums';
import { WebtoonImage } from '../../_models/webtoon-image'; import { WebtoonImage } from '../../_models/webtoon-image';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
* How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load * How much additional space should pass, past the original bottom of the document height before we trigger the next chapter load
@ -58,7 +74,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* Method to generate the src for Image loading * Method to generate the src for Image loading
*/ */
@Input() urlProvider!: (page: number) => string; @Input({required: true}) urlProvider!: (page: number) => string;
@Output() pageNumberChange: EventEmitter<number> = new EventEmitter<number>(); @Output() pageNumberChange: EventEmitter<number> = new EventEmitter<number>();
@Output() loadNextChapter: EventEmitter<void> = new EventEmitter<void>(); @Output() loadNextChapter: EventEmitter<void> = new EventEmitter<void>();
@Output() loadPrevChapter: EventEmitter<void> = new EventEmitter<void>(); @Output() loadPrevChapter: EventEmitter<void> = new EventEmitter<void>();
@ -66,6 +82,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
@Input() goToPage: BehaviorSubject<number> | undefined; @Input() goToPage: BehaviorSubject<number> | undefined;
@Input() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>(); @Input() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>();
@Input() fullscreenToggled: ReplaySubject<boolean> = new ReplaySubject<boolean>(); @Input() fullscreenToggled: ReplaySubject<boolean> = new ReplaySubject<boolean>();
private readonly destroyRef = inject(DestroyRef);
readerElemRef!: ElementRef<HTMLDivElement>; readerElemRef!: ElementRef<HTMLDivElement>;
@ -110,7 +127,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* If the user has scrolled all the way to the bottom. This is used solely for continuous reading * If the user has scrolled all the way to the bottom. This is used solely for continuous reading
*/ */
atBottom: boolean = false; atBottom: boolean = false;
/** /**
* If the user has scrolled all the way to the top. This is used solely for continuous reading * If the user has scrolled all the way to the top. This is used solely for continuous reading
*/ */
@ -149,10 +166,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return this.webtoonImageWidth > (innerWidth || document.body.clientWidth); return this.webtoonImageWidth > (innerWidth || document.body.clientWidth);
} }
constructor(private readerService: ReaderService, private renderer: Renderer2,
private readonly onDestroy = new Subject<void>();
constructor(private readerService: ReaderService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) { private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) {
// This will always exist at this point in time since this is used within manga reader // This will always exist at this point in time since this is used within manga reader
@ -172,18 +186,16 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.intersectionObserver.disconnect(); this.intersectionObserver.disconnect();
this.onDestroy.next();
this.onDestroy.complete();
} }
/** /**
* Responsible for binding the scroll handler to the correct event. On non-fullscreen, body is correct. However, on fullscreen, we must use the reader as that is what * Responsible for binding the scroll handler to the correct event. On non-fullscreen, body is correct. However, on fullscreen, we must use the reader as that is what
* gets promoted to fullscreen. * gets promoted to fullscreen.
*/ */
initScrollHandler() { initScrollHandler() {
console.log('Setting up Scroll handler on ', this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body); console.log('Setting up Scroll handler on ', this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body);
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll') fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll')
.pipe(debounceTime(20), takeUntil(this.onDestroy)) .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.handleScrollEvent(event)); .subscribe((event) => this.handleScrollEvent(event));
} }
@ -193,7 +205,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.recalculateImageWidth(); this.recalculateImageWidth();
if (this.goToPage) { if (this.goToPage) {
this.goToPage.pipe(takeUntil(this.onDestroy)).subscribe(page => { this.goToPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {
const isSamePage = this.pageNum === page; const isSamePage = this.pageNum === page;
if (isSamePage) { return; } if (isSamePage) { return; }
this.debugLog('[GoToPage] jump has occured from ' + this.pageNum + ' to ' + page); this.debugLog('[GoToPage] jump has occured from ' + this.pageNum + ' to ' + page);
@ -209,11 +221,11 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} }
if (this.bookmarkPage) { if (this.bookmarkPage) {
this.bookmarkPage.pipe(takeUntil(this.onDestroy)).subscribe(page => { this.bookmarkPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {
const image = document.querySelector('img[id^="page-' + page + '"]'); const image = document.querySelector('img[id^="page-' + page + '"]');
if (image) { if (image) {
this.renderer.addClass(image, 'bookmark-effect'); this.renderer.addClass(image, 'bookmark-effect');
setTimeout(() => { setTimeout(() => {
this.renderer.removeClass(image, 'bookmark-effect'); this.renderer.removeClass(image, 'bookmark-effect');
}, 1000); }, 1000);
@ -222,7 +234,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} }
if (this.fullscreenToggled) { if (this.fullscreenToggled) {
this.fullscreenToggled.pipe(takeUntil(this.onDestroy)).subscribe(isFullscreen => { this.fullscreenToggled.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(isFullscreen => {
this.debugLog('[FullScreen] Fullscreen mode: ', isFullscreen); this.debugLog('[FullScreen] Fullscreen mode: ', isFullscreen);
this.isFullscreenMode = isFullscreen; this.isFullscreenMode = isFullscreen;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -251,13 +263,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} }
return (offset return (offset
|| document.body.scrollTop || document.body.scrollTop
|| document.documentElement.scrollTop || document.documentElement.scrollTop
|| 0); || 0);
} }
/** /**
* On scroll in document, calculate if the user/javascript has scrolled to the current image element (and it's visible), update that scrolling has ended completely, * On scroll in document, calculate if the user/javascript has scrolled to the current image element (and it's visible), update that scrolling has ended completely,
* and calculate the direction the scrolling is occuring. This is not used for prefetching. * and calculate the direction the scrolling is occuring. This is not used for prefetching.
* @param event Scroll Event * @param event Scroll Event
*/ */
@ -279,7 +291,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (!this.isScrolling) { if (!this.isScrolling) {
// Use offset of the image against the scroll container to test if the most of the image is visible on the screen. We can use this // Use offset of the image against the scroll container to test if the most of the image is visible on the screen. We can use this
// to mark the current page and separate the prefetching code. // to mark the current page and separate the prefetching code.
const midlineImages = Array.from(document.querySelectorAll('img[id^="page-"]')) const midlineImages = Array.from(document.querySelectorAll('img[id^="page-"]'))
.filter(entry => this.shouldElementCountAsCurrentPage(entry)); .filter(entry => this.shouldElementCountAsCurrentPage(entry));
@ -336,7 +348,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2); document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) { } else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
// This if statement will fire once we scroll into the spacer at all // This if statement will fire once we scroll into the spacer at all
this.loadNextChapter.emit(); this.loadNextChapter.emit();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -345,12 +357,12 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
// < 5 because debug mode and FF (mobile) can report non 0, despite being at 0 // < 5 because debug mode and FF (mobile) can report non 0, despite being at 0
if (this.getScrollTop() < 5 && this.pageNum === 0 && !this.atTop) { if (this.getScrollTop() < 5 && this.pageNum === 0 && !this.atTop) {
this.atBottom = false; this.atBottom = false;
this.atTop = true; this.atTop = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
// Scroll user back to original location // Scroll user back to original location
this.previousScrollHeightMinusTop = document.body.scrollHeight - document.body.scrollTop; this.previousScrollHeightMinusTop = document.body.scrollHeight - document.body.scrollTop;
const reader = this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body; const reader = this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body;
requestAnimationFrame(() => this.scrollService.scrollTo((SPACER_SCROLL_INTO_PX / 2), reader)); requestAnimationFrame(() => this.scrollService.scrollTo((SPACER_SCROLL_INTO_PX / 2), reader));
} else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) { } else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) {
@ -362,7 +374,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} }
/** /**
* *
* @returns Height, Width * @returns Height, Width
*/ */
getInnerDimensions() { getInnerDimensions() {
@ -377,10 +389,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} }
/** /**
* Is any part of the element visible in the scrollport. Does not take into account * Is any part of the element visible in the scrollport. Does not take into account
* style properites, just scroll port visibility. * style properites, just scroll port visibility.
* @param elem * @param elem
* @returns * @returns
*/ */
isElementVisible(elem: Element) { isElementVisible(elem: Element) {
if (elem === null || elem === undefined) { return false; } if (elem === null || elem === undefined) { return false; }
@ -391,8 +403,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
const [innerHeight, innerWidth] = this.getInnerDimensions(); const [innerHeight, innerWidth] = this.getInnerDimensions();
return (rect.bottom >= 0 && return (rect.bottom >= 0 &&
rect.right >= 0 && rect.right >= 0 &&
rect.top <= (innerHeight || document.body.clientHeight) && rect.top <= (innerHeight || document.body.clientHeight) &&
rect.left <= (innerWidth || document.body.clientWidth) rect.left <= (innerWidth || document.body.clientWidth)
); );
@ -400,7 +412,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* Is any part of the element visible in the scrollport and is it above the midline trigger. * Is any part of the element visible in the scrollport and is it above the midline trigger.
* The midline trigger does not mean it is half of the screen. It may be top 25%. * The midline trigger does not mean it is half of the screen. It may be top 25%.
* @param elem HTML Element * @param elem HTML Element
* @returns If above midline * @returns If above midline
*/ */
@ -412,8 +424,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
const [innerHeight, innerWidth] = this.getInnerDimensions(); const [innerHeight, innerWidth] = this.getInnerDimensions();
if (rect.bottom >= 0 && if (rect.bottom >= 0 &&
rect.right >= 0 && rect.right >= 0 &&
rect.top <= (innerHeight || document.body.clientHeight) && rect.top <= (innerHeight || document.body.clientHeight) &&
rect.left <= (innerWidth || document.body.clientWidth) rect.left <= (innerWidth || document.body.clientWidth)
) { ) {
@ -444,7 +456,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* Callback for an image onLoad. At this point the image is already rendered in DOM (may not be visible) * Callback for an image onLoad. At this point the image is already rendered in DOM (may not be visible)
* This will be used to scroll to current page for intial load * This will be used to scroll to current page for intial load
* @param event * @param event
*/ */
onImageLoad(event: any) { onImageLoad(event: any) {
const imagePage = this.readerService.imageUrlToPageNum(event.target.src); const imagePage = this.readerService.imageUrlToPageNum(event.target.src);
@ -468,13 +480,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.debugLog('[Image Load] ! Loaded current page !', this.pageNum); this.debugLog('[Image Load] ! Loaded current page !', this.pageNum);
this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum); this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum);
// There needs to be a bit of time before we scroll // There needs to be a bit of time before we scroll
if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) { if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) {
this.scrollToCurrentPage(); this.scrollToCurrentPage();
} else { } else {
this.initFinished = true; this.initFinished = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
this.allImagesLoaded = true; this.allImagesLoaded = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
@ -530,7 +542,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.currentPageElem = document.querySelector('img#page-' + this.pageNum); this.currentPageElem = document.querySelector('img#page-' + this.pageNum);
if (!this.currentPageElem) { return; } if (!this.currentPageElem) { return; }
this.debugLog('[GoToPage] Scrolling to page', this.pageNum); this.debugLog('[GoToPage] Scrolling to page', this.pageNum);
// Update prevScrollPosition, so the next scroll event properly calculates direction // Update prevScrollPosition, so the next scroll event properly calculates direction
this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top; this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
this.isScrolling = true; this.isScrolling = true;
@ -553,7 +565,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
} }
this.debugLog('\t[PREFETCH] Prefetching ', page); this.debugLog('\t[PREFETCH] Prefetching ', page);
const data = this.webtoonImages.value.concat({src: this.urlProvider(page), page}); const data = this.webtoonImages.value.concat({src: this.urlProvider(page), page});
data.sort((a: WebtoonImage, b: WebtoonImage) => { data.sort((a: WebtoonImage, b: WebtoonImage) => {
@ -582,9 +594,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* Finds the ranges of indecies to load from backend. totalPages - 1 is due to backend will automatically return last page for any page number * Finds the ranges of indecies to load from backend. totalPages - 1 is due to backend will automatically return last page for any page number
* above totalPages. Webtoon reader might ask for that which results in duplicate last pages. * above totalPages. Webtoon reader might ask for that which results in duplicate last pages.
* @param pageNum * @param pageNum
* @returns * @returns
*/ */
calculatePrefetchIndecies(pageNum: number = -1) { calculatePrefetchIndecies(pageNum: number = -1) {
if (pageNum == -1) { if (pageNum == -1) {
@ -646,7 +658,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (this.debugLogFilter.filter(str => message.replace('\t', '').startsWith(str)).length > 0) return; if (this.debugLogFilter.filter(str => message.replace('\t', '').startsWith(str)).length > 0) return;
if (extraData !== undefined) { if (extraData !== undefined) {
console.log(message, extraData); console.log(message, extraData);
} else { } else {
console.log(message); console.log(message);
} }

View File

@ -1,4 +1,18 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
EventEmitter,
HostListener,
inject,
Inject,
OnDestroy,
OnInit,
SimpleChanges,
ViewChild
} from '@angular/core';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, debounceTime, distinctUntilChanged, forkJoin, fromEvent, map, merge, Observable, ReplaySubject, Subject, take, takeUntil, tap } from 'rxjs'; import { BehaviorSubject, debounceTime, distinctUntilChanged, forkJoin, fromEvent, map, merge, Observable, ReplaySubject, Subject, take, takeUntil, tap } from 'rxjs';
@ -33,6 +47,7 @@ import { SingleRendererComponent } from '../single-renderer/single-renderer.comp
import { ChapterInfo } from '../../_models/chapter-info'; import { ChapterInfo } from '../../_models/chapter-info';
import { DoubleNoCoverRendererComponent } from '../double-renderer-no-cover/double-no-cover-renderer.component'; import { DoubleNoCoverRendererComponent } from '../double-renderer-no-cover/double-no-cover-renderer.component';
import { SwipeEvent } from 'src/app/ng-swipe/ag-swipe.core'; import { SwipeEvent } from 'src/app/ng-swipe/ag-swipe.core';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
const PREFETCH_PAGES = 10; const PREFETCH_PAGES = 10;
@ -53,7 +68,7 @@ enum ChapterInfoPosition {
enum KeyDirection { enum KeyDirection {
Right = 0, Right = 0,
Left = 1, Left = 1,
Up = 2, Up = 2,
Down = 3 Down = 3
} }
@ -98,7 +113,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(DoubleRendererComponent, { static: false }) doubleRenderer!: DoubleRendererComponent; @ViewChild(DoubleRendererComponent, { static: false }) doubleRenderer!: DoubleRendererComponent;
@ViewChild(DoubleReverseRendererComponent, { static: false }) doubleReverseRenderer!: DoubleReverseRendererComponent; @ViewChild(DoubleReverseRendererComponent, { static: false }) doubleReverseRenderer!: DoubleReverseRendererComponent;
@ViewChild(DoubleNoCoverRendererComponent, { static: false }) doubleNoCoverRenderer!: DoubleNoCoverRendererComponent; @ViewChild(DoubleNoCoverRendererComponent, { static: false }) doubleNoCoverRenderer!: DoubleNoCoverRendererComponent;
private readonly destroyRef = inject(DestroyRef);
libraryId!: number; libraryId!: number;
seriesId!: number; seriesId!: number;
@ -155,7 +170,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
isLoading = true; isLoading = true;
hasBookmarkRights: boolean = false; // TODO: This can be an observable hasBookmarkRights: boolean = false; // TODO: This can be an observable
getPageFn!: (pageNum: number) => HTMLImageElement; getPageFn!: (pageNum: number) => HTMLImageElement;
@ -165,9 +180,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* @remarks Used for rendering to screen. * @remarks Used for rendering to screen.
*/ */
canvasImage = new Image(); canvasImage = new Image();
/** /**
* Dictates if we use render with canvas or with image. * Dictates if we use render with canvas or with image.
* @remarks This is only for Splitting. * @remarks This is only for Splitting.
*/ */
//renderWithCanvas: boolean = false; //renderWithCanvas: boolean = false;
@ -314,7 +329,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
prevIsVerticalScrollLeft = true; prevIsVerticalScrollLeft = true;
/** /**
* Has the user scrolled to the far right side. This is used for swipe to next page and must ensure user is at end of scroll then on next swipe, will move pages. * Has the user scrolled to the far right side. This is used for swipe to next page and must ensure user is at end of scroll then on next swipe, will move pages.
*/ */
hasHitRightScroll = false; hasHitRightScroll = false;
/** /**
@ -345,7 +360,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private pageNumSubject: Subject<{pageNum: number, maxPages: number}> = new ReplaySubject(); private pageNumSubject: Subject<{pageNum: number, maxPages: number}> = new ReplaySubject();
pageNum$: Observable<{pageNum: number, maxPages: number}> = this.pageNumSubject.asObservable(); pageNum$: Observable<{pageNum: number, maxPages: number}> = this.pageNumSubject.asObservable();
bookmarkPageHandler = this.bookmarkPage.bind(this); bookmarkPageHandler = this.bookmarkPage.bind(this);
@ -354,8 +369,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return this.readerService.getPageUrl(chapterId, pageNum); return this.readerService.getPageUrl(chapterId, pageNum);
} }
private readonly onDestroy = new Subject<void>();
get PageNumber() { get PageNumber() {
return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0); return Math.max(Math.min(this.pageNum, this.maxPages - 1), 0);
} }
@ -373,7 +386,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.FittingOption !== FITTING_OPTION.HEIGHT) { if (this.FittingOption !== FITTING_OPTION.HEIGHT) {
return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px'; return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px';
} }
return this.readingArea?.nativeElement?.clientHeight + 'px'; return this.readingArea?.nativeElement?.clientHeight + 'px';
} }
@ -428,8 +441,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
public readerService: ReaderService, private formBuilder: FormBuilder, private navService: NavService, public readerService: ReaderService, private formBuilder: FormBuilder, private navService: NavService,
private toastr: ToastrService, private memberService: MemberService, private toastr: ToastrService, private memberService: MemberService,
public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
private modalService: NgbModal, private readonly cdRef: ChangeDetectorRef, private modalService: NgbModal, private readonly cdRef: ChangeDetectorRef,
public mangaReaderService: ManagaReaderService) { public mangaReaderService: ManagaReaderService) {
this.navService.hideNavBar(); this.navService.hideNavBar();
this.navService.hideSideNav(); this.navService.hideSideNav();
@ -494,18 +507,18 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// We need a mergeMap when page changes // We need a mergeMap when page changes
this.readerSettings$ = merge(this.generalSettingsForm.valueChanges, this.pagingDirection$, this.readerMode$).pipe( this.readerSettings$ = merge(this.generalSettingsForm.valueChanges, this.pagingDirection$, this.readerMode$).pipe(
map(_ => this.createReaderSettingsUpdate()), map(_ => this.createReaderSettingsUpdate()),
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
); );
this.updateForm(); this.updateForm();
this.pagingDirection$.pipe( this.pagingDirection$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
tap(dir => { tap(dir => {
this.pagingDirection = dir; this.pagingDirection = dir;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
).subscribe(() => {}); ).subscribe(() => {});
this.readerMode$.pipe( this.readerMode$.pipe(
@ -513,12 +526,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
tap(mode => { tap(mode => {
this.readerMode = mode; this.readerMode = mode;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
).subscribe(() => {}); ).subscribe(() => {});
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
const changeOccurred = parseInt(val, 10) !== this.layoutMode; const changeOccurred = parseInt(val, 10) !== this.layoutMode;
this.layoutMode = parseInt(val, 10); this.layoutMode = parseInt(val, 10);
@ -544,7 +557,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
}); });
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => { this.generalSettingsForm.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((changes: SimpleChanges) => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
this.pageSplitOption = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10); this.pageSplitOption = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10);
@ -570,7 +583,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
ngAfterViewInit() { ngAfterViewInit() {
fromEvent(this.readingArea.nativeElement, 'scroll').pipe(debounceTime(20), takeUntil(this.onDestroy)).subscribe(evt => { fromEvent(this.readingArea.nativeElement, 'scroll').pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef)).subscribe(evt => {
if (this.readerMode === ReaderMode.Webtoon) return; if (this.readerMode === ReaderMode.Webtoon) return;
if (this.readerMode === ReaderMode.LeftRight && this.FittingOption === FITTING_OPTION.HEIGHT) { if (this.readerMode === ReaderMode.LeftRight && this.FittingOption === FITTING_OPTION.HEIGHT) {
this.rightPaginationOffset = (this.readingArea.nativeElement.scrollLeft) * -1; this.rightPaginationOffset = (this.readingArea.nativeElement.scrollLeft) * -1;
@ -581,12 +594,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
fromEvent(this.readingArea.nativeElement, 'click').pipe(debounceTime(200), takeUntil(this.onDestroy)).subscribe((event: MouseEvent | any) => { fromEvent(this.readingArea.nativeElement, 'click').pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe((event: MouseEvent | any) => {
if (event.detail > 1) return; if (event.detail > 1) return;
this.toggleMenu(); this.toggleMenu();
}); });
fromEvent(this.readingArea.nativeElement, 'scroll').pipe(debounceTime(200), takeUntil(this.onDestroy)).subscribe((event: MouseEvent | any) => { fromEvent(this.readingArea.nativeElement, 'scroll').pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe((event: MouseEvent | any) => {
this.prevScrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0; this.prevScrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0;
this.prevScrollTop = this.readingArea?.nativeElement?.scrollTop || 0; this.prevScrollTop = this.readingArea?.nativeElement?.scrollTop || 0;
this.hasScrolledX = true; this.hasScrolledX = true;
@ -598,8 +611,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readerService.resetOverrideStyles(); this.readerService.resetOverrideStyles();
this.navService.showNavBar(); this.navService.showNavBar();
this.navService.showSideNav(); this.navService.showSideNav();
this.onDestroy.next();
this.onDestroy.complete();
this.showBookmarkEffectEvent.complete(); this.showBookmarkEffectEvent.complete();
if (this.goToPageEvent !== undefined) this.goToPageEvent.complete(); if (this.goToPageEvent !== undefined) this.goToPageEvent.complete();
} }
@ -607,7 +618,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event']) @HostListener('window:orientationchange', ['$event'])
onResize() { onResize() {
this.disableDoubleRendererIfScreenTooSmall(); this.disableDoubleRendererIfScreenTooSmall();
} }
@ -692,7 +703,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return; return;
}; };
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return; if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single); this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
this.generalSettingsForm.get('layoutMode')?.disable(); this.generalSettingsForm.get('layoutMode')?.disable();
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout'); this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
@ -704,13 +715,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* @param pageNum Page Number to load * @param pageNum Page Number to load
* @param forceNew Forces to fetch a new image * @param forceNew Forces to fetch a new image
* @param chapterId ChapterId to fetch page from. Defaults to current chapterId. Not used when in bookmark mode * @param chapterId ChapterId to fetch page from. Defaults to current chapterId. Not used when in bookmark mode
* @returns * @returns
*/ */
getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) { getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) {
let img = undefined; let img = undefined;
if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum); if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
&& (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1) && (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1)
); );
if (!img || forceNew) { if (!img || forceNew) {
@ -721,7 +732,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return img; return img;
} }
isHorizontalScrollLeft() { isHorizontalScrollLeft() {
const scrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0; const scrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0;
@ -736,11 +747,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const scrollTop = this.readingArea?.nativeElement?.scrollTop || 0; const scrollTop = this.readingArea?.nativeElement?.scrollTop || 0;
return scrollTop < this.ReadingAreaHeight; return scrollTop < this.ReadingAreaHeight;
} }
/** /**
* Is there any room to scroll in the direction we are giving? If so, return false. Otherwise return true. * Is there any room to scroll in the direction we are giving? If so, return false. Otherwise return true.
* @param direction * @param direction
* @returns * @returns
*/ */
checkIfPaginationAllowed(direction: KeyDirection) { checkIfPaginationAllowed(direction: KeyDirection) {
if (this.readingArea === undefined || this.readingArea.nativeElement === undefined) return true; if (this.readingArea === undefined || this.readingArea.nativeElement === undefined) return true;
@ -782,7 +793,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return true; return true;
} }
init() { init() {
this.nextChapterId = CHAPTER_ID_NOT_FETCHED; this.nextChapterId = CHAPTER_ID_NOT_FETCHED;
@ -887,7 +898,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else { } else {
// Fetch the first page of next chapter // Fetch the first page of next chapter
this.getPage(0, this.nextChapterId); this.getPage(0, this.nextChapterId);
} }
}); });
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => { this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
@ -976,11 +987,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.hasHitBottomTopScroll = false; this.hasHitBottomTopScroll = false;
this.hasHitZeroTopScroll = false; this.hasHitZeroTopScroll = false;
} }
/** /**
* This executes BEFORE fromEvent('scroll') * This executes BEFORE fromEvent('scroll')
* @param event * @param event
* @returns * @returns
*/ */
onSwipeMove(_: SwipeEvent) { onSwipeMove(_: SwipeEvent) {
this.prevScrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0; this.prevScrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0;
@ -989,7 +1000,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
triggerSwipePagination(direction: KeyDirection) { triggerSwipePagination(direction: KeyDirection) {
if (!this.generalSettingsForm.get('swipeToPaginate')?.value) return; if (!this.generalSettingsForm.get('swipeToPaginate')?.value) return;
switch(direction) { switch(direction) {
case KeyDirection.Down: case KeyDirection.Down:
this.nextPage(); this.nextPage();
@ -1004,7 +1015,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage(); this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage();
break; break;
} }
} }
onSwipeEnd(event: SwipeEvent) { onSwipeEnd(event: SwipeEvent) {
@ -1117,17 +1128,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.isLoading = true; this.isLoading = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD); this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD);
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.singleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.singleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
this.doubleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.doubleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.FORWARD) this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.FORWARD)
); );
const notInSplit = this.canvasRenderer.shouldMovePrev(); const notInSplit = this.canvasRenderer.shouldMovePrev();
if ((this.pageNum + pageAmount >= this.maxPages && notInSplit)) { if ((this.pageNum + pageAmount >= this.maxPages && notInSplit)) {
// Move to next volume/chapter automatically // Move to next volume/chapter automatically
this.loadNextChapter(); this.loadNextChapter();
return; return;
@ -1142,7 +1153,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
} }
this.resetSwipeModifiers(); this.resetSwipeModifiers();
this.isLoading = true; this.isLoading = true;
@ -1151,8 +1162,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS); this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
this.singleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), this.singleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
this.doubleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), this.doubleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS), this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS) this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS)
@ -1165,7 +1176,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.loadPrevChapter(); this.loadPrevChapter();
return; return;
} }
this.setPageNum(this.pageNum - pageAmount); this.setPageNum(this.pageNum - pageAmount);
this.loadPage(); this.loadPage();
} }
@ -1179,13 +1190,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.canvasImage.addEventListener('load', () => { this.canvasImage.addEventListener('load', () => {
this.currentImage.next(this.canvasImage); this.currentImage.next(this.canvasImage);
}, false); }, false);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
loadNextChapter() { loadNextChapter() {
if (this.nextPageDisabled || this.nextChapterDisabled || this.bookmarkMode) { if (this.nextPageDisabled || this.nextChapterDisabled || this.bookmarkMode) {
this.toastr.info('No Next Chapter'); this.toastr.info('No Next Chapter');
this.isLoading = false; this.isLoading = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -1203,11 +1214,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
loadPrevChapter() { loadPrevChapter() {
if (this.prevPageDisabled || this.prevChapterDisabled || this.bookmarkMode) { if (this.prevPageDisabled || this.prevChapterDisabled || this.bookmarkMode) {
this.toastr.info('No Previous Chapter'); this.toastr.info('No Previous Chapter');
this.isLoading = false; this.isLoading = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
return; return;
} }
this.continuousChaptersStack.pop(); this.continuousChaptersStack.pop();
const prevChapter = this.continuousChaptersStack.peek(); const prevChapter = this.continuousChaptersStack.peek();
@ -1254,12 +1265,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
renderPage() { renderPage() {
const page = [this.canvasImage]; const page = [this.canvasImage];
this.canvasRenderer?.renderPage(page); this.canvasRenderer?.renderPage(page);
this.singleRenderer?.renderPage(page); this.singleRenderer?.renderPage(page);
this.doubleRenderer?.renderPage(page); this.doubleRenderer?.renderPage(page);
this.doubleNoCoverRenderer?.renderPage(page); this.doubleNoCoverRenderer?.renderPage(page);
@ -1331,7 +1342,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/ */
loadPage() { loadPage() {
if (this.readerMode === ReaderMode.Webtoon) return; if (this.readerMode === ReaderMode.Webtoon) return;
this.isLoading = true; this.isLoading = true;
this.setCanvasImage(); this.setCanvasImage();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -1339,7 +1350,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.renderPage(); this.renderPage();
this.isLoading = false; this.isLoading = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.prefetch(); this.prefetch();
} }
@ -1423,12 +1434,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
/** /**
* Loads the first 5 images (throwaway cache) from the given chapterId * Loads the first 5 images (throwaway cache) from the given chapterId
* @param chapterId * @param chapterId
* @param direction Used to indicate if the chapter is behind or ahead of curent chapter * @param direction Used to indicate if the chapter is behind or ahead of curent chapter
*/ */
prefetchStartOfChapter(chapterId: number, direction: PAGING_DIRECTION) { prefetchStartOfChapter(chapterId: number, direction: PAGING_DIRECTION) {
let pages = []; let pages = [];
if (direction === PAGING_DIRECTION.BACKWARDS) { if (direction === PAGING_DIRECTION.BACKWARDS) {
if (this.continuousChapterInfos[ChapterInfoPosition.Previous] === undefined) return; if (this.continuousChapterInfos[ChapterInfoPosition.Previous] === undefined) return;
const n = this.continuousChapterInfos[ChapterInfoPosition.Previous]!.pages; const n = this.continuousChapterInfos[ChapterInfoPosition.Previous]!.pages;
@ -1436,7 +1447,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else { } else {
pages = [0, 1, 2, 3, 4]; pages = [0, 1, 2, 3, 4];
} }
let images = []; let images = [];
pages.forEach((_, i: number) => { pages.forEach((_, i: number) => {
let img = new Image(); let img = new Image();
@ -1564,7 +1575,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.bookmarkMode) return; if (this.bookmarkMode) return;
const pageNum = this.pageNum; const pageNum = this.pageNum;
const isDouble = Math.max(this.canvasRenderer.getBookmarkPageCount(), this.singleRenderer.getBookmarkPageCount(), const isDouble = Math.max(this.canvasRenderer.getBookmarkPageCount(), this.singleRenderer.getBookmarkPageCount(),
this.doubleRenderer.getBookmarkPageCount(), this.doubleReverseRenderer.getBookmarkPageCount(), this.doubleNoCoverRenderer.getBookmarkPageCount()) > 1; this.doubleRenderer.getBookmarkPageCount(), this.doubleReverseRenderer.getBookmarkPageCount(), this.doubleNoCoverRenderer.getBookmarkPageCount()) > 1;
if (this.CurrentPageBookmarked) { if (this.CurrentPageBookmarked) {
@ -1640,5 +1651,5 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
}) })
}); });
} }
} }

View File

@ -1,12 +1,12 @@
<ng-container *ngIf="isValid() && !this.mangaReaderService.shouldSplit(this.currentImage, this.pageSplit)"> <ng-container *ngIf="isValid() && !this.mangaReaderService.shouldSplit(this.currentImage, this.pageSplit)">
<div class="image-container {{imageFitClass$ | async}} {{emulateBookClass$ | async}}" <div class="image-container {{imageFitClass$ | async}} {{emulateBookClass$ | async}}"
[style.filter]="(darkenss$ | async) ?? '' | safeStyle" [style.height]="(imageContainerHeight$ | async) ?? '' | safeStyle"> [style.filter]="(darkness$ | async) ?? '' | safeStyle" [style.height]="(imageContainerHeight$ | async) ?? '' | safeStyle">
<ng-container *ngIf="currentImage"> <ng-container *ngIf="currentImage">
<img alt=" " <img alt=" "
#image [src]="currentImage.src" #image [src]="currentImage.src"
id="image-1" id="image-1"
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}" class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
> >
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,5 +1,16 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { combineLatest, filter, map, Observable, of, shareReplay, Subject, takeUntil, tap } from 'rxjs'; import { combineLatest, filter, map, Observable, of, shareReplay, Subject, takeUntil, tap } from 'rxjs';
import { PageSplitOption } from 'src/app/_models/preferences/page-split-option'; import { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
import { ReaderMode } from 'src/app/_models/preferences/reader-mode'; import { ReaderMode } from 'src/app/_models/preferences/reader-mode';
@ -9,6 +20,7 @@ import { FITTING_OPTION, PAGING_DIRECTION } from '../../_models/reader-enums';
import { ReaderSetting } from '../../_models/reader-setting'; import { ReaderSetting } from '../../_models/reader-setting';
import { ImageRenderer } from '../../_models/renderer'; import { ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service'; import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-single-renderer', selector: 'app-single-renderer',
@ -16,21 +28,22 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./single-renderer.component.scss'], styleUrls: ['./single-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer { export class SingleRendererComponent implements OnInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>; @Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>; @Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>; @Input({required: true}) bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>; @Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>; @Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>(); @Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
imageFitClass$!: Observable<string>; imageFitClass$!: Observable<string>;
imageContainerHeight$!: Observable<string>; imageContainerHeight$!: Observable<string>;
showClickOverlayClass$!: Observable<string>; showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>; readerModeClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)'); darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$!: Observable<string>; emulateBookClass$!: Observable<string>;
currentImage!: HTMLImageElement; currentImage!: HTMLImageElement;
layoutMode: LayoutMode = LayoutMode.Single; layoutMode: LayoutMode = LayoutMode.Single;
@ -39,31 +52,29 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
pageNum: number = 0; pageNum: number = 0;
maxPages: number = 1; maxPages: number = 1;
private readonly onDestroy = new Subject<void>(); get ReaderMode() {return ReaderMode;}
get LayoutMode() {return LayoutMode;}
get ReaderMode() {return ReaderMode;} constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
get LayoutMode() {return LayoutMode;}
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
@Inject(DOCUMENT) private document: Document) { } @Inject(DOCUMENT) private document: Document) { }
ngOnInit(): void { ngOnInit(): void {
this.readerModeClass$ = this.readerSettings$.pipe( this.readerModeClass$ = this.readerSettings$.pipe(
map(values => values.readerMode), map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'), map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.emulateBookClass$ = this.readerSettings$.pipe( this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook), map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''), map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.imageContainerHeight$ = this.readerSettings$.pipe( this.imageContainerHeight$ = this.readerSettings$.pipe(
map(values => values.fitting), map(values => values.fitting),
map(mode => { map(mode => {
if ( mode !== FITTING_OPTION.HEIGHT) return ''; if ( mode !== FITTING_OPTION.HEIGHT) return '';
@ -76,31 +87,31 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
return 'calc(100vh)' return 'calc(100vh)'
}), }),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.pageNum$.pipe( this.pageNum$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(pageInfo => { tap(pageInfo => {
this.pageNum = pageInfo.pageNum; this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages; this.maxPages = pageInfo.maxPages;
}), }),
).subscribe(() => {}); ).subscribe(() => {});
this.darkenss$ = this.readerSettings$.pipe( this.darkness$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'), map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.showClickOverlayClass$ = this.showClickOverlay$.pipe( this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''), map(showOverlay => showOverlay ? 'blur' : ''),
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
filter(_ => this.isValid()), filter(_ => this.isValid()),
); );
this.readerSettings$.pipe( this.readerSettings$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(values => { tap(values => {
this.layoutMode = values.layoutMode; this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit; this.pageSplit = values.pageSplit;
@ -109,7 +120,7 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {}); ).subscribe(() => {});
this.bookmark$.pipe( this.bookmark$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
tap(_ => { tap(_ => {
const elements = []; const elements = [];
const image1 = this.document.querySelector('#image-1'); const image1 = this.document.querySelector('#image-1');
@ -134,7 +145,7 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
}), }),
shareReplay(), shareReplay(),
filter(_ => this.isValid()), filter(_ => this.isValid()),
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
); );
} }
@ -142,15 +153,10 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
return this.layoutMode === LayoutMode.Single; return this.layoutMode === LayoutMode.Single;
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
renderPage(img: Array<HTMLImageElement | null>): void { renderPage(img: Array<HTMLImageElement | null>): void {
if (img === null || img.length === 0 || img[0] === null) return; if (img === null || img.length === 0 || img[0] === null) return;
if (!this.isValid()) return; if (!this.isValid()) return;
this.currentImage = img[0]; this.currentImage = img[0];
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.imageHeight.emit(this.currentImage.height); this.imageHeight.emit(this.currentImage.height);

View File

@ -1,4 +1,15 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild, DestroyRef,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs'; import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
@ -20,6 +31,7 @@ import { LibraryService } from '../_services/library.service';
import { MetadataService } from '../_services/metadata.service'; import { MetadataService } from '../_services/metadata.service';
import { ToggleService } from '../_services/toggle.service'; import { ToggleService } from '../_services/toggle.service';
import { FilterSettings } from './filter-settings'; import { FilterSettings } from './filter-settings';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-metadata-filter', selector: 'app-metadata-filter',
@ -27,7 +39,7 @@ import { FilterSettings } from './filter-settings';
styleUrls: ['./metadata-filter.component.scss'], styleUrls: ['./metadata-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MetadataFilterComponent implements OnInit, OnDestroy { export class MetadataFilterComponent implements OnInit {
/** /**
* This toggles the opening/collapsing of the metadata filter code * This toggles the opening/collapsing of the metadata filter code
@ -39,11 +51,12 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
*/ */
@Input() filteringDisabled: boolean = false; @Input() filteringDisabled: boolean = false;
@Input() filterSettings!: FilterSettings; @Input({required: true}) filterSettings!: FilterSettings;
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter(); @Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse; @ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
private readonly destroyRef = inject(DestroyRef);
formatSettings: TypeaheadSettings<FilterItem<MangaFormat>> = new TypeaheadSettings(); formatSettings: TypeaheadSettings<FilterItem<MangaFormat>> = new TypeaheadSettings();
@ -58,7 +71,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
resetTypeaheads: ReplaySubject<boolean> = new ReplaySubject(1); resetTypeaheads: ReplaySubject<boolean> = new ReplaySubject(1);
/** /**
* Controls the visiblity of extended controls that sit below the main header. * Controls the visibility of extended controls that sit below the main header.
*/ */
filteringCollapsed: boolean = true; filteringCollapsed: boolean = true;
@ -76,9 +89,6 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
fullyLoaded: boolean = false; fullyLoaded: boolean = false;
private onDestroy: Subject<void> = new Subject();
get PersonRole(): typeof PersonRole { get PersonRole(): typeof PersonRole {
return PersonRole; return PersonRole;
} }
@ -87,7 +97,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
return SortField; return SortField;
} }
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private utilityService: UtilityService, constructor(private libraryService: LibraryService, private metadataService: MetadataService, private utilityService: UtilityService,
private collectionTagService: CollectionTagService, public toggleService: ToggleService, private collectionTagService: CollectionTagService, public toggleService: ToggleService,
private readonly cdRef: ChangeDetectorRef, private filterUtilitySerivce: FilterUtilitiesService) { private readonly cdRef: ChangeDetectorRef, private filterUtilitySerivce: FilterUtilitiesService) {
} }
@ -99,13 +109,13 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
} }
if (this.filterOpen) { if (this.filterOpen) {
this.filterOpen.pipe(takeUntil(this.onDestroy)).subscribe(openState => { this.filterOpen.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(openState => {
this.filteringCollapsed = !openState; this.filteringCollapsed = !openState;
this.toggleService.set(!this.filteringCollapsed); this.toggleService.set(!this.filteringCollapsed);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }
this.filter = this.filterUtilitySerivce.createSeriesFilter(); this.filter = this.filterUtilitySerivce.createSeriesFilter();
this.readProgressGroup = new FormGroup({ this.readProgressGroup = new FormGroup({
read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []), read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []),
@ -126,7 +136,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
max: new FormControl({value: undefined, disabled: this.filterSettings.releaseYearDisabled}, [Validators.min(1000), Validators.max(9999)]) max: new FormControl({value: undefined, disabled: this.filterSettings.releaseYearDisabled}, [Validators.min(1000), Validators.max(9999)])
}); });
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(changes => { this.readProgressGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(changes => {
this.filter.readStatus.read = this.readProgressGroup.get('read')?.value; this.filter.readStatus.read = this.readProgressGroup.get('read')?.value;
this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value; this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value;
this.filter.readStatus.notRead = this.readProgressGroup.get('notRead')?.value; this.filter.readStatus.notRead = this.readProgressGroup.get('notRead')?.value;
@ -148,7 +158,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.sortGroup.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(changes => { this.sortGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(changes => {
if (this.filter.sortOptions == null) { if (this.filter.sortOptions == null) {
this.filter.sortOptions = { this.filter.sortOptions = {
isAscending: this.isAscendingSort, isAscending: this.isAscendingSort,
@ -161,8 +171,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe( this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
map(val => (val || '').trim()), map(val => (val || '').trim()),
distinctUntilChanged(), distinctUntilChanged(),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(changes => { .subscribe(changes => {
this.filter.seriesNameQuery = changes; // TODO: See if we can make this into observable this.filter.seriesNameQuery = changes; // TODO: See if we can make this into observable
@ -170,8 +180,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}); });
this.releaseYearRange.valueChanges.pipe( this.releaseYearRange.valueChanges.pipe(
distinctUntilChanged(), distinctUntilChanged(),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(changes => { .subscribe(changes => {
this.filter.releaseYearRange = {min: this.releaseYearRange.get('min')?.value, max: this.releaseYearRange.get('max')?.value}; this.filter.releaseYearRange = {min: this.releaseYearRange.get('min')?.value, max: this.releaseYearRange.get('max')?.value};
@ -188,11 +198,6 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
getPersonsSettings(role: PersonRole) { getPersonsSettings(role: PersonRole) {
return this.peopleSettings[role]; return this.peopleSettings[role];
} }
@ -203,7 +208,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read); this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read);
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead); this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead);
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets.readStatus.inProgress); this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets.readStatus.inProgress);
if (this.filterSettings.presets.sortOptions) { if (this.filterSettings.presets.sortOptions) {
this.sortGroup.get('sortField')?.setValue(this.filterSettings.presets.sortOptions.sortField); this.sortGroup.get('sortField')?.setValue(this.filterSettings.presets.sortOptions.sortField);
this.isAscendingSort = this.filterSettings.presets.sortOptions.isAscending; this.isAscendingSort = this.filterSettings.presets.sortOptions.isAscending;
@ -248,7 +253,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.formatSettings.id = 'format'; this.formatSettings.id = 'format';
this.formatSettings.unique = true; this.formatSettings.unique = true;
this.formatSettings.addIfNonExisting = false; this.formatSettings.addIfNonExisting = false;
this.formatSettings.fetchFn = (filter: string) => of(mangaFormatFilters).pipe(map(items => this.formatSettings.compareFn(items, filter))); this.formatSettings.fetchFn = (filter: string) => of(mangaFormatFilters).pipe(map(items => this.formatSettings.compareFn(items, filter)));
this.formatSettings.compareFn = (options: FilterItem<MangaFormat>[], filter: string) => { this.formatSettings.compareFn = (options: FilterItem<MangaFormat>[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter)); return options.filter(m => this.utilityService.filter(m.title, filter));
} }
@ -271,7 +276,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.librarySettings.addIfNonExisting = false; this.librarySettings.addIfNonExisting = false;
this.librarySettings.fetchFn = (filter: string) => { this.librarySettings.fetchFn = (filter: string) => {
return this.libraryService.getLibraries() return this.libraryService.getLibraries()
.pipe(map(items => this.librarySettings.compareFn(items, filter))); .pipe(map(items => this.librarySettings.compareFn(items, filter)));
}; };
this.librarySettings.compareFn = (options: Library[], filter: string) => { this.librarySettings.compareFn = (options: Library[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.name, filter)); return options.filter(m => this.utilityService.filter(m.name, filter));
@ -298,7 +303,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.genreSettings.addIfNonExisting = false; this.genreSettings.addIfNonExisting = false;
this.genreSettings.fetchFn = (filter: string) => { this.genreSettings.fetchFn = (filter: string) => {
return this.metadataService.getAllGenres(this.filter.libraries) return this.metadataService.getAllGenres(this.filter.libraries)
.pipe(map(items => this.genreSettings.compareFn(items, filter))); .pipe(map(items => this.genreSettings.compareFn(items, filter)));
}; };
this.genreSettings.compareFn = (options: Genre[], filter: string) => { this.genreSettings.compareFn = (options: Genre[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter)); return options.filter(m => this.utilityService.filter(m.title, filter));
@ -324,12 +329,12 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.ageRatingSettings.unique = true; this.ageRatingSettings.unique = true;
this.ageRatingSettings.addIfNonExisting = false; this.ageRatingSettings.addIfNonExisting = false;
this.ageRatingSettings.fetchFn = (filter: string) => this.metadataService.getAllAgeRatings(this.filter.libraries) this.ageRatingSettings.fetchFn = (filter: string) => this.metadataService.getAllAgeRatings(this.filter.libraries)
.pipe(map(items => this.ageRatingSettings.compareFn(items, filter))); .pipe(map(items => this.ageRatingSettings.compareFn(items, filter)));
this.ageRatingSettings.compareFn = (options: AgeRatingDto[], filter: string) => { this.ageRatingSettings.compareFn = (options: AgeRatingDto[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter)); return options.filter(m => this.utilityService.filter(m.title, filter));
} }
this.ageRatingSettings.selectionCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => { this.ageRatingSettings.selectionCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
return a.title == b.title; return a.title == b.title;
@ -352,8 +357,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.publicationStatusSettings.unique = true; this.publicationStatusSettings.unique = true;
this.publicationStatusSettings.addIfNonExisting = false; this.publicationStatusSettings.addIfNonExisting = false;
this.publicationStatusSettings.fetchFn = (filter: string) => this.metadataService.getAllPublicationStatus(this.filter.libraries) this.publicationStatusSettings.fetchFn = (filter: string) => this.metadataService.getAllPublicationStatus(this.filter.libraries)
.pipe(map(items => this.publicationStatusSettings.compareFn(items, filter))); .pipe(map(items => this.publicationStatusSettings.compareFn(items, filter)));
this.publicationStatusSettings.compareFn = (options: PublicationStatusDto[], filter: string) => { this.publicationStatusSettings.compareFn = (options: PublicationStatusDto[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter)); return options.filter(m => this.utilityService.filter(m.title, filter));
} }
@ -382,8 +387,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
return options.filter(m => this.utilityService.filter(m.title, filter)); return options.filter(m => this.utilityService.filter(m.title, filter));
} }
this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags(this.filter.libraries) this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags(this.filter.libraries)
.pipe(map(items => this.tagsSettings.compareFn(items, filter))); .pipe(map(items => this.tagsSettings.compareFn(items, filter)));
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => { this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
return a.id == b.id; return a.id == b.id;
} }
@ -408,7 +413,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
return options.filter(m => this.utilityService.filter(m.title, filter)); return options.filter(m => this.utilityService.filter(m.title, filter));
} }
this.languageSettings.fetchFn = (filter: string) => this.metadataService.getAllLanguages(this.filter.libraries) this.languageSettings.fetchFn = (filter: string) => this.metadataService.getAllLanguages(this.filter.libraries)
.pipe(map(items => this.languageSettings.compareFn(items, filter))); .pipe(map(items => this.languageSettings.compareFn(items, filter)));
this.languageSettings.selectionCompareFn = (a: Language, b: Language) => { this.languageSettings.selectionCompareFn = (a: Language, b: Language) => {
return a.isoCode == b.isoCode; return a.isoCode == b.isoCode;
@ -461,7 +466,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
return true; return true;
})); }));
} }
this.peopleSettings[role] = personSettings; this.peopleSettings[role] = personSettings;
return of(true); return of(true);
@ -472,7 +477,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
return forkJoin([ return forkJoin([
this.updateFromPreset('writers', this.filter.writers, this.filterSettings.presets?.writers, PersonRole.Writer), this.updateFromPreset('writers', this.filter.writers, this.filterSettings.presets?.writers, PersonRole.Writer),
this.updateFromPreset('character', this.filter.character, this.filterSettings.presets?.character, PersonRole.Character), this.updateFromPreset('character', this.filter.character, this.filterSettings.presets?.character, PersonRole.Character),
this.updateFromPreset('colorist', this.filter.colorist, this.filterSettings.presets?.colorist, PersonRole.Colorist), this.updateFromPreset('colorist', this.filter.colorist, this.filterSettings.presets?.colorist, PersonRole.Colorist),
this.updateFromPreset('cover-artist', this.filter.coverArtist, this.filterSettings.presets?.coverArtist, PersonRole.CoverArtist), this.updateFromPreset('cover-artist', this.filter.coverArtist, this.filterSettings.presets?.coverArtist, PersonRole.CoverArtist),
this.updateFromPreset('editor', this.filter.editor, this.filterSettings.presets?.editor, PersonRole.Editor), this.updateFromPreset('editor', this.filter.editor, this.filterSettings.presets?.editor, PersonRole.Editor),
@ -486,7 +491,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
})); }));
} }
fetchPeople(role: PersonRole, filter: string) { fetchPeople(role: PersonRole, filter: string) {
return this.metadataService.getAllPeople(this.filter.libraries).pipe(map(people => { return this.metadataService.getAllPeople(this.filter.libraries).pipe(map(people => {
return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter)); return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter));
})); }));

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { map, shareReplay, takeUntil } from 'rxjs/operators'; import { map, shareReplay, takeUntil } from 'rxjs/operators';
@ -13,6 +22,7 @@ import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event'
import { User } from 'src/app/_models/user'; import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service'; import { AccountService } from 'src/app/_services/account.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service'; import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-nav-events-toggle', selector: 'app-nav-events-toggle',
@ -21,12 +31,11 @@ import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hu
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EventsWidgetComponent implements OnInit, OnDestroy { export class EventsWidgetComponent implements OnInit, OnDestroy {
@Input() user!: User; @Input({required: true}) user!: User;
private readonly destroyRef = inject(DestroyRef);
isAdmin$: Observable<boolean> = of(false); isAdmin$: Observable<boolean> = of(false);
private readonly onDestroy = new Subject<void>();
/** /**
* Progress events (Event Type: 'started', 'ended', 'updated' that have progress property) * Progress events (Event Type: 'started', 'ended', 'updated' that have progress property)
*/ */
@ -53,21 +62,19 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
return EVENTS; return EVENTS;
} }
constructor(public messageHub: MessageHubService, private modalService: NgbModal, constructor(public messageHub: MessageHubService, private modalService: NgbModal,
private accountService: AccountService, private confirmService: ConfirmService, private accountService: AccountService, private confirmService: ConfirmService,
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) { private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.progressEventsSource.complete(); this.progressEventsSource.complete();
this.singleUpdateSource.complete(); this.singleUpdateSource.complete();
this.errorSource.complete(); this.errorSource.complete();
} }
ngOnInit(): void { ngOnInit(): void {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => { this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
if (event.event === EVENTS.NotificationProgress) { if (event.event === EVENTS.NotificationProgress) {
this.processNotificationProgressEvent(event); this.processNotificationProgressEvent(event);
} else if (event.event === EVENTS.Error) { } else if (event.event === EVENTS.Error) {
@ -86,8 +93,8 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
}); });
this.isAdmin$ = this.accountService.currentUser$.pipe( this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false), map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay() shareReplay()
); );
} }
@ -187,11 +194,11 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
let data = []; let data = [];
if (messageEvent.name === EVENTS.Info) { if (messageEvent.name === EVENTS.Info) {
data = this.infoSource.getValue(); data = this.infoSource.getValue();
data = data.filter(m => m !== messageEvent); data = data.filter(m => m !== messageEvent);
this.infoSource.next(data); this.infoSource.next(data);
} else { } else {
data = this.errorSource.getValue(); data = this.errorSource.getValue();
data = data.filter(m => m !== messageEvent); data = data.filter(m => m !== messageEvent);
this.errorSource.next(data); this.errorSource.next(data);
} }
this.activeEvents = Math.max(this.activeEvents - 1, 0); this.activeEvents = Math.max(this.activeEvents - 1, 0);

View File

@ -3,7 +3,7 @@
<div class="search"> <div class="search">
<input #input [id]="id" type="text" inputmode="search" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder" <input #input [id]="id" type="text" inputmode="search" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
aria-haspopup="listbox" aria-owns="dropdown" aria-expanded="hasFocus && (grouppedData.persons.length || grouppedData.collections.length || grouppedData.series.length || grouppedData.persons.length || grouppedData.tags.length || grouppedData.genres.length)" aria-haspopup="listbox" aria-owns="dropdown" aria-expanded="hasFocus && (grouppedData.persons.length || grouppedData.collections.length || grouppedData.series.length || grouppedData.persons.length || grouppedData.tags.length || grouppedData.genres.length)"
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search" aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search"
> >
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading"> <div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
@ -13,90 +13,90 @@
</div> </div>
<div class="dropdown" *ngIf="hasFocus"> <div class="dropdown" *ngIf="hasFocus">
<ul class="list-group" role="listbox" id="dropdown"> <ul class="list-group" role="listbox" id="dropdown">
<ng-container *ngIf="seriesTemplate !== undefined && grouppedData.series.length > 0"> <ng-container *ngIf="seriesTemplate !== undefined && groupedData.series.length > 0">
<li class="list-group-item section-header"><h5 id="series-group">Series</h5></li> <li class="list-group-item section-header"><h5 id="series-group">Series</h5></li>
<ul class="list-group results" role="group" aria-describedby="series-group"> <ul class="list-group results" role="group" aria-describedby="series-group">
<li *ngFor="let option of grouppedData.series; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.series; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" aria-labelledby="series-group" role="option"> class="list-group-item" aria-labelledby="series-group" role="option">
<ng-container [ngTemplateOutlet]="seriesTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="seriesTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="collectionTemplate !== undefined && grouppedData.collections.length > 0"> <ng-container *ngIf="collectionTemplate !== undefined && groupedData.collections.length > 0">
<li class="list-group-item section-header"><h5>Collections</h5></li> <li class="list-group-item section-header"><h5>Collections</h5></li>
<ul class="list-group results"> <ul class="list-group results">
<li *ngFor="let option of grouppedData.collections; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.collections; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option"> class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="collectionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="collectionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="readingListTemplate !== undefined && grouppedData.readingLists.length > 0"> <ng-container *ngIf="readingListTemplate !== undefined && groupedData.readingLists.length > 0">
<li class="list-group-item section-header"><h5>Reading Lists</h5></li> <li class="list-group-item section-header"><h5>Reading Lists</h5></li>
<ul class="list-group results"> <ul class="list-group results">
<li *ngFor="let option of grouppedData.readingLists; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.readingLists; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option"> class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="readingListTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="readingListTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="libraryTemplate !== undefined && grouppedData.libraries.length > 0"> <ng-container *ngIf="libraryTemplate !== undefined && groupedData.libraries.length > 0">
<li class="list-group-item section-header"><h5 id="libraries-group">Libraries</h5></li> <li class="list-group-item section-header"><h5 id="libraries-group">Libraries</h5></li>
<ul class="list-group results" role="group" aria-describedby="libraries-group"> <ul class="list-group results" role="group" aria-describedby="libraries-group">
<li *ngFor="let option of grouppedData.libraries; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.libraries; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" aria-labelledby="libraries-group" role="option"> class="list-group-item" aria-labelledby="libraries-group" role="option">
<ng-container [ngTemplateOutlet]="libraryTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="libraryTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="genreTemplate !== undefined && grouppedData.genres.length > 0"> <ng-container *ngIf="genreTemplate !== undefined && groupedData.genres.length > 0">
<li class="list-group-item section-header"><h5>Genres</h5></li> <li class="list-group-item section-header"><h5>Genres</h5></li>
<ul class="list-group results"> <ul class="list-group results">
<li *ngFor="let option of grouppedData.genres; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.genres; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option"> class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="genreTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="genreTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="tagTemplate !== undefined && grouppedData.tags.length > 0"> <ng-container *ngIf="tagTemplate !== undefined && groupedData.tags.length > 0">
<li class="list-group-item section-header"><h5>Tags</h5></li> <li class="list-group-item section-header"><h5>Tags</h5></li>
<ul class="list-group results"> <ul class="list-group results">
<li *ngFor="let option of grouppedData.tags; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.tags; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option"> class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="tagTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="tagTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="personTemplate !== undefined && grouppedData.persons.length > 0"> <ng-container *ngIf="personTemplate !== undefined && groupedData.persons.length > 0">
<li class="list-group-item section-header"><h5>People</h5></li> <li class="list-group-item section-header"><h5>People</h5></li>
<ul class="list-group results"> <ul class="list-group results">
<li *ngFor="let option of grouppedData.persons; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.persons; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option"> class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="personTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="personTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="chapterTemplate !== undefined && grouppedData.chapters.length > 0"> <ng-container *ngIf="chapterTemplate !== undefined && groupedData.chapters.length > 0">
<li class="list-group-item section-header"><h5>Chapters</h5></li> <li class="list-group-item section-header"><h5>Chapters</h5></li>
<ul class="list-group results"> <ul class="list-group results">
<li *ngFor="let option of grouppedData.chapters; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.chapters; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option"> class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="chapterTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="chapterTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
<ng-container *ngIf="fileTemplate !== undefined && grouppedData.files.length > 0"> <ng-container *ngIf="fileTemplate !== undefined && groupedData.files.length > 0">
<li class="list-group-item section-header"><h5>Files</h5></li> <li class="list-group-item section-header"><h5>Files</h5></li>
<ul class="list-group results"> <ul class="list-group results">
<li *ngFor="let option of grouppedData.files; let index = index;" (click)="handleResultlick(option)" tabindex="0" <li *ngFor="let option of groupedData.files; let index = index;" (click)="handleResultlick(option)" tabindex="0"
class="list-group-item" role="option"> class="list-group-item" role="option">
<ng-container [ngTemplateOutlet]="fileTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container> <ng-container [ngTemplateOutlet]="fileTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li> </li>

View File

@ -1,9 +1,25 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, TemplateRef, ViewChild } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild, DestroyRef,
ElementRef,
EventEmitter,
HostListener,
inject,
Input,
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewChild
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators'; import { debounceTime, takeUntil } from 'rxjs/operators';
import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { SearchResultGroup } from 'src/app/_models/search/search-result-group'; import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-grouped-typeahead', selector: 'app-grouped-typeahead',
@ -11,7 +27,7 @@ import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
styleUrls: ['./grouped-typeahead.component.scss'], styleUrls: ['./grouped-typeahead.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class GroupedTypeaheadComponent implements OnInit, OnDestroy { export class GroupedTypeaheadComponent implements OnInit {
/** /**
* Unique id to tie with a label element * Unique id to tie with a label element
*/ */
@ -24,7 +40,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
* Initial value of the search model * Initial value of the search model
*/ */
@Input() initialValue: string = ''; @Input() initialValue: string = '';
@Input() grouppedData: SearchResultGroup = new SearchResultGroup(); @Input() groupedData: SearchResultGroup = new SearchResultGroup();
/** /**
* Placeholder for the input * Placeholder for the input
*/ */
@ -62,7 +78,8 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
@ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>; @ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>;
@ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>; @ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>;
@ContentChild('chapterTemplate') chapterTemplate!: TemplateRef<any>; @ContentChild('chapterTemplate') chapterTemplate!: TemplateRef<any>;
private readonly destroyRef = inject(DestroyRef);
hasFocus: boolean = false; hasFocus: boolean = false;
isLoading: boolean = false; isLoading: boolean = false;
@ -70,16 +87,14 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
prevSearchTerm: string = ''; prevSearchTerm: string = '';
private onDestroy: Subject<void> = new Subject();
get searchTerm() { get searchTerm() {
return this.typeaheadForm.get('typeahead')?.value || ''; return this.typeaheadForm.get('typeahead')?.value || '';
} }
get hasData() { get hasData() {
return !(this.noResultsTemplate != undefined && !this.grouppedData.persons.length && !this.grouppedData.collections.length return !(this.noResultsTemplate != undefined && !this.groupedData.persons.length && !this.groupedData.collections.length
&& !this.grouppedData.series.length && !this.grouppedData.persons.length && !this.grouppedData.tags.length && !this.grouppedData.genres.length && !this.grouppedData.libraries.length && !this.groupedData.series.length && !this.groupedData.persons.length && !this.groupedData.tags.length && !this.groupedData.genres.length && !this.groupedData.libraries.length
&& !this.grouppedData.files.length && !this.grouppedData.chapters.length); && !this.groupedData.files.length && !this.groupedData.chapters.length);
} }
@ -92,7 +107,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
} }
@HostListener('window:keydown', ['$event']) @HostListener('window:keydown', ['$event'])
handleKeyPress(event: KeyboardEvent) { handleKeyPress(event: KeyboardEvent) {
if (!this.hasFocus) { return; } if (!this.hasFocus) { return; }
switch(event.key) { switch(event.key) {
@ -109,7 +124,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
this.typeaheadForm.addControl('typeahead', new FormControl(this.initialValue, [])); this.typeaheadForm.addControl('typeahead', new FormControl(this.initialValue, []));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.typeaheadForm.valueChanges.pipe(debounceTime(this.debounceTime), takeUntil(this.onDestroy)).subscribe(change => { this.typeaheadForm.valueChanges.pipe(debounceTime(this.debounceTime), takeUntilDestroyed(this.destroyRef)).subscribe(change => {
const value = this.typeaheadForm.get('typeahead')?.value; const value = this.typeaheadForm.get('typeahead')?.value;
if (value != undefined && value != '' && !this.hasFocus) { if (value != undefined && value != '' && !this.hasFocus) {
@ -127,17 +142,12 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
onInputFocus(event: any) { onInputFocus(event: any) {
if (event) { if (event) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
} }
this.openDropdown(); this.openDropdown();
return this.hasFocus; return this.hasFocus;
} }

View File

@ -17,7 +17,7 @@
[minQueryLength]="2" [minQueryLength]="2"
initialValue="" initialValue=""
placeholder="Search…" placeholder="Search…"
[grouppedData]="searchResults" [groupedData]="searchResults"
(inputChanged)="onChangeSearch($event)" (inputChanged)="onChangeSearch($event)"
(clearField)="clearSearch()" (clearField)="clearSearch()"
(focusChanged)="focusUpdate($event)" (focusChanged)="focusUpdate($event)"

View File

@ -1,5 +1,15 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
inject,
Inject,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { fromEvent, Subject } from 'rxjs'; import { fromEvent, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, takeUntil, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, filter, takeUntil, tap } from 'rxjs/operators';
@ -17,6 +27,7 @@ import { ImageService } from 'src/app/_services/image.service';
import { NavService } from 'src/app/_services/nav.service'; import { NavService } from 'src/app/_services/nav.service';
import { ScrollService } from 'src/app/_services/scroll.service'; import { ScrollService } from 'src/app/_services/scroll.service';
import { SearchService } from 'src/app/_services/search.service'; import { SearchService } from 'src/app/_services/search.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-nav-header', selector: 'app-nav-header',
@ -24,9 +35,10 @@ import { SearchService } from 'src/app/_services/search.service';
styleUrls: ['./nav-header.component.scss'], styleUrls: ['./nav-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class NavHeaderComponent implements OnInit, OnDestroy { export class NavHeaderComponent implements OnInit {
@ViewChild('search') searchViewRef!: any; @ViewChild('search') searchViewRef!: any;
private readonly destroyRef = inject(DestroyRef);
isLoading = false; isLoading = false;
debounceTime = 300; debounceTime = 300;
@ -48,7 +60,6 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
backToTopNeeded = false; backToTopNeeded = false;
searchFocused: boolean = false; searchFocused: boolean = false;
scrollElem: HTMLElement; scrollElem: HTMLElement;
private readonly onDestroy = new Subject<void>();
constructor(public accountService: AccountService, private router: Router, public navService: NavService, constructor(public accountService: AccountService, private router: Router, public navService: NavService,
public imageService: ImageService, @Inject(DOCUMENT) private document: Document, public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
@ -57,7 +68,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.scrollService.scrollContainer$.pipe(distinctUntilChanged(), takeUntil(this.onDestroy), tap((scrollContainer) => { this.scrollService.scrollContainer$.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), tap((scrollContainer) => {
if (scrollContainer === 'body' || scrollContainer === undefined) { if (scrollContainer === 'body' || scrollContainer === undefined) {
this.scrollElem = this.document.body; this.scrollElem = this.document.body;
fromEvent(this.document.body, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(this.document.body)); fromEvent(this.document.body, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(this.document.body));
@ -86,11 +97,6 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
logout() { logout() {
this.accountService.logout(); this.accountService.logout();
this.navService.hideNavBar(); this.navService.hideNavBar();
@ -109,7 +115,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
this.searchTerm = val.trim(); this.searchTerm = val.trim();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.searchService.search(val.trim()).pipe(takeUntil(this.onDestroy)).subscribe(results => { this.searchService.search(val.trim()).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(results => {
this.searchResults = results; this.searchResults = results;
this.isLoading = false; this.isLoading = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -1,8 +0,0 @@
import { RelationshipPipe } from './relationship.pipe';
describe('RelationshipPipe', () => {
it('create an instance', () => {
const pipe = new RelationshipPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -54,7 +54,7 @@ export class DraggableOrderedListComponent {
} }
updateIndex(previousIndex: number, item: any) { updateIndex(previousIndex: number, item: any) {
// get the new value of the input // get the new value of the input
var inputElem = <HTMLInputElement>document.querySelector('#reorder-' + previousIndex); var inputElem = <HTMLInputElement>document.querySelector('#reorder-' + previousIndex);
const newIndex = parseInt(inputElem.value, 10); const newIndex = parseInt(inputElem.value, 10);
if (previousIndex === newIndex) return; if (previousIndex === newIndex) return;

View File

@ -12,7 +12,7 @@ import { ImageService } from 'src/app/_services/image.service';
}) })
export class ReadingListItemComponent { export class ReadingListItemComponent {
@Input() item!: ReadingListItem; @Input({required: true}) item!: ReadingListItem;
@Input() position: number = 0; @Input() position: number = 0;
@Input() libraryTypes: {[key: number]: LibraryType} = {}; @Input() libraryTypes: {[key: number]: LibraryType} = {};
/** /**

View File

@ -20,7 +20,7 @@ export enum ADD_FLOW {
}) })
export class AddToListModalComponent implements OnInit, AfterViewInit { export class AddToListModalComponent implements OnInit, AfterViewInit {
@Input() title!: string; @Input({required: true}) title!: string;
/** /**
* Only used in Series flow * Only used in Series flow
*/ */
@ -49,7 +49,7 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
/** /**
* Determines which Input is required and which API is used to associate to the Reading List * Determines which Input is required and which API is used to associate to the Reading List
*/ */
@Input() type!: ADD_FLOW; @Input({required: true}) type!: ADD_FLOW;
/** /**
* All existing reading lists sorted by recent use date * All existing reading lists sorted by recent use date
@ -71,14 +71,14 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
this.listForm.addControl('title', new FormControl(this.title, [])); this.listForm.addControl('title', new FormControl(this.title, []));
this.listForm.addControl('filterQuery', new FormControl('', [])); this.listForm.addControl('filterQuery', new FormControl('', []));
this.loading = true; this.loading = true;
this.readingListService.getReadingLists(false, true).subscribe(lists => { this.readingListService.getReadingLists(false, true).subscribe(lists => {
this.lists = lists.result; this.lists = lists.result;
this.loading = false; this.loading = false;
}); });
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -130,6 +130,6 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
this.modal.close(); this.modal.close();
}); });
} }
} }
} }

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms'; import { FormGroup, FormControl, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
@ -9,6 +18,7 @@ import { AccountService } from 'src/app/_services/account.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { ReadingListService } from 'src/app/_services/reading-list.service'; import { ReadingListService } from 'src/app/_services/reading-list.service';
import { UploadService } from 'src/app/_services/upload.service'; import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID { enum TabID {
General = 'General', General = 'General',
@ -21,9 +31,10 @@ enum TabID {
styleUrls: ['./edit-reading-list-modal.component.scss'], styleUrls: ['./edit-reading-list-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EditReadingListModalComponent implements OnInit, OnDestroy { export class EditReadingListModalComponent implements OnInit {
@Input() readingList!: ReadingList; @Input({required: true}) readingList!: ReadingList;
private readonly destroyRef = inject(DestroyRef);
reviewGroup!: FormGroup; reviewGroup!: FormGroup;
coverImageIndex: number = 0; coverImageIndex: number = 0;
@ -35,13 +46,11 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
imageUrls: Array<string> = []; imageUrls: Array<string> = [];
active = TabID.General; active = TabID.General;
private readonly onDestroy = new Subject<void>();
get Breakpoint() { return Breakpoint; } get Breakpoint() { return Breakpoint; }
get TabID() { return TabID; } get TabID() { return TabID; }
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService, constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
public utilityService: UtilityService, private uploadService: UploadService, private toastr: ToastrService, public utilityService: UtilityService, private uploadService: UploadService, private toastr: ToastrService,
private imageService: ImageService, private readonly cdRef: ChangeDetectorRef, public accountService: AccountService) { } private imageService: ImageService, private readonly cdRef: ChangeDetectorRef, public accountService: AccountService) { }
ngOnInit(): void { ngOnInit(): void {
@ -58,7 +67,7 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
this.coverImageLocked = this.readingList.coverImageLocked; this.coverImageLocked = this.readingList.coverImageLocked;
this.reviewGroup.get('title')?.valueChanges.pipe( this.reviewGroup.get('title')?.valueChanges.pipe(
debounceTime(100), debounceTime(100),
distinctUntilChanged(), distinctUntilChanged(),
switchMap(name => this.readingListService.nameExists(name)), switchMap(name => this.readingListService.nameExists(name)),
tap(exists => { tap(exists => {
@ -66,11 +75,11 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
if (!exists || isExistingName) { if (!exists || isExistingName) {
this.reviewGroup.get('title')?.setErrors(null); this.reviewGroup.get('title')?.setErrors(null);
} else { } else {
this.reviewGroup.get('title')?.setErrors({duplicateName: true}) this.reviewGroup.get('title')?.setErrors({duplicateName: true})
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
).subscribe(); ).subscribe();
this.imageUrls.push(this.imageService.randomize(this.imageService.getReadingListCoverImage(this.readingList.id))); this.imageUrls.push(this.imageService.randomize(this.imageService.getReadingListCoverImage(this.readingList.id)));
@ -83,11 +92,6 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
} }
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
close() { close() {
this.ngModal.dismiss(undefined); this.ngModal.dismiss(undefined);
} }
@ -101,11 +105,11 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
model.endingMonth = model.endingMonth || 0; model.endingMonth = model.endingMonth || 0;
model.endingYear = model.endingYear || 0; model.endingYear = model.endingYear || 0;
const apis = [this.readingListService.update(model)]; const apis = [this.readingListService.update(model)];
if (this.selectedCover !== '') { if (this.selectedCover !== '') {
apis.push(this.uploadService.updateReadingListCoverImage(this.readingList.id, this.selectedCover)) apis.push(this.uploadService.updateReadingListCoverImage(this.readingList.id, this.selectedCover))
} }
forkJoin(apis).subscribe(results => { forkJoin(apis).subscribe(results => {
this.readingList.title = model.title; this.readingList.title = model.title;
this.readingList.summary = model.summary; this.readingList.summary = model.summary;

View File

@ -13,8 +13,8 @@ import { AccountService } from 'src/app/_services/account.service';
}) })
export class AddEmailToAccountMigrationModalComponent implements OnInit { export class AddEmailToAccountMigrationModalComponent implements OnInit {
@Input() username!: string; @Input({required: true}) username!: string;
@Input() password!: string; @Input({required: true}) password!: string;
isSaving: boolean = false; isSaving: boolean = false;
registerForm: FormGroup = new FormGroup({}); registerForm: FormGroup = new FormGroup({});
@ -22,7 +22,7 @@ export class AddEmailToAccountMigrationModalComponent implements OnInit {
emailLinkUrl: SafeUrl | undefined; emailLinkUrl: SafeUrl | undefined;
error: string = ''; error: string = '';
constructor(private accountService: AccountService, private modal: NgbActiveModal, constructor(private accountService: AccountService, private modal: NgbActiveModal,
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) {
} }

View File

@ -1,5 +1,18 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, Inject, ChangeDetectionStrategy, ChangeDetectorRef, AfterContentChecked, inject } from '@angular/core'; import {
Component,
ElementRef,
HostListener,
OnDestroy,
OnInit,
ViewChild,
Inject,
ChangeDetectionStrategy,
ChangeDetectorRef,
AfterContentChecked,
inject,
DestroyRef
} from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms'; import { FormGroup, FormControl } from '@angular/forms';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -42,6 +55,7 @@ import { ScrollService } from 'src/app/_services/scroll.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import { ReviewSeriesModalComponent } from '../../_modals/review-series-modal/review-series-modal.component'; import { ReviewSeriesModalComponent } from '../../_modals/review-series-modal/review-series-modal.component';
import { PageLayoutMode } from 'src/app/_models/page-layout-mode'; import { PageLayoutMode } from 'src/app/_models/page-layout-mode';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
interface RelatedSeris { interface RelatedSeris {
series: Series; series: Series;
@ -68,10 +82,11 @@ interface StoryLineItem {
styleUrls: ['./series-detail.component.scss'], styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChecked { export class SeriesDetailComponent implements OnInit, AfterContentChecked {
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined; @ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined; @ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
private readonly destroyRef = inject(DestroyRef);
/** /**
* Series Id. Set at load before UI renders * Series Id. Set at load before UI renders
@ -117,7 +132,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
downloadInProgress: boolean = false; downloadInProgress: boolean = false;
itemSize: number = 10; // when 10 done, 16 loads itemSize: number = 10; // when 10 done, 16 loads
/** /**
* Track by function for Volume to tell when to refresh card data * Track by function for Volume to tell when to refresh card data
*/ */
@ -207,9 +222,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
} }
} }
private onDestroy: Subject<void> = new Subject();
get LibraryType() { get LibraryType() {
return LibraryType; return LibraryType;
} }
@ -301,7 +313,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
return; return;
} }
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => { this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
if (event.event === EVENTS.SeriesRemoved) { if (event.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = event.payload as SeriesRemovedEvent; const seriesRemovedEvent = event.payload as SeriesRemovedEvent;
if (seriesRemovedEvent.seriesId === this.seriesId) { if (seriesRemovedEvent.seriesId === this.seriesId) {
@ -322,18 +334,13 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
this.changeDetectionRef.markForCheck(); this.changeDetectionRef.markForCheck();
this.loadSeries(this.seriesId); this.loadSeries(this.seriesId);
this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((val: PageLayoutMode | null) => { this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((val: PageLayoutMode | null) => {
if (val == null) return; if (val == null) return;
this.renderMode = val; this.renderMode = val;
this.changeDetectionRef.markForCheck(); this.changeDetectionRef.markForCheck();
}); });
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event']) @HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) { handleKeypress(event: KeyboardEvent) {
if (event.key === KEY_CODES.SHIFT) { if (event.key === KEY_CODES.SHIFT) {
@ -543,7 +550,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
if (detail == null) return; if (detail == null) return;
this.unreadCount = detail.unreadCount; this.unreadCount = detail.unreadCount;
this.totalCount = detail.totalCount; this.totalCount = detail.totalCount;
this.hasSpecials = detail.specials.length > 0; this.hasSpecials = detail.specials.length > 0;
this.specials = detail.specials; this.specials = detail.specials;
@ -792,7 +799,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
} else { } else {
this.actionService.addMultipleSeriesToWantToReadList([this.series.id]); this.actionService.addMultipleSeriesToWantToReadList([this.series.id]);
} }
this.isWantToRead = !this.isWantToRead; this.isWantToRead = !this.isWantToRead;
this.changeDetectionRef.markForCheck(); this.changeDetectionRef.markForCheck();
} }

View File

@ -20,13 +20,13 @@ import { ImageService } from 'src/app/_services/image.service';
}) })
export class SeriesMetadataDetailComponent implements OnChanges { export class SeriesMetadataDetailComponent implements OnChanges {
@Input() seriesMetadata!: SeriesMetadata; @Input({required: true}) seriesMetadata!: SeriesMetadata;
@Input() hasReadingProgress: boolean = false; @Input() hasReadingProgress: boolean = false;
/** /**
* Reading lists with a connection to the Series * Reading lists with a connection to the Series
*/ */
@Input() readingLists: Array<ReadingList> = []; @Input() readingLists: Array<ReadingList> = [];
@Input() series!: Series; @Input({required: true}) series!: Series;
isCollapsed: boolean = true; isCollapsed: boolean = true;
hasExtendedProperties: boolean = false; hasExtendedProperties: boolean = false;

View File

@ -12,7 +12,7 @@ import { SeriesService } from 'src/app/_services/series.service';
}) })
export class ReviewSeriesModalComponent implements OnInit { export class ReviewSeriesModalComponent implements OnInit {
@Input() series!: Series; @Input({required: true}) series!: Series;
reviewGroup!: FormGroup; reviewGroup!: FormGroup;
constructor(public modal: NgbActiveModal, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {} constructor(public modal: NgbActiveModal, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}

View File

@ -1,9 +1,21 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, ViewChild } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
inject,
Input,
OnChanges,
OnDestroy,
Renderer2,
ViewChild
} from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { CoverUpdateEvent } from 'src/app/_models/events/cover-update-event'; import { CoverUpdateEvent } from 'src/app/_models/events/cover-update-event';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
* This is used for images with placeholder fallback. * This is used for images with placeholder fallback.
@ -14,12 +26,12 @@ import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service
styleUrls: ['./image.component.scss'], styleUrls: ['./image.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ImageComponent implements OnChanges, OnDestroy { export class ImageComponent implements OnChanges {
/** /**
* Source url to load image * Source url to load image
*/ */
@Input() imageUrl!: string; @Input({required: true}) imageUrl!: string;
/** /**
* Width of the image. If not defined, will not be applied * Width of the image. If not defined, will not be applied
*/ */
@ -54,11 +66,10 @@ export class ImageComponent implements OnChanges, OnDestroy {
@Input() processEvents: boolean = true; @Input() processEvents: boolean = true;
@ViewChild('img', {static: true}) imgElem!: ElementRef<HTMLImageElement>; @ViewChild('img', {static: true}) imgElem!: ElementRef<HTMLImageElement>;
private readonly destroyRef = inject(DestroyRef);
private readonly onDestroy = new Subject<void>();
constructor(public imageService: ImageService, private renderer: Renderer2, private hubService: MessageHubService, private changeDetectionRef: ChangeDetectorRef) { constructor(public imageService: ImageService, private renderer: Renderer2, private hubService: MessageHubService, private changeDetectionRef: ChangeDetectorRef) {
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => { this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
if (!this.processEvents) return; if (!this.processEvents) return;
if (res.event === EVENTS.CoverUpdate) { if (res.event === EVENTS.CoverUpdate) {
const updateEvent = res.payload as CoverUpdateEvent; const updateEvent = res.payload as CoverUpdateEvent;
@ -111,9 +122,4 @@ export class ImageComponent implements OnChanges, OnDestroy {
} }
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
} }

View File

@ -9,7 +9,7 @@ import { Person } from '../../_models/metadata/person';
}) })
export class PersonBadgeComponent { export class PersonBadgeComponent {
@Input() person!: Person; @Input({required: true}) person!: Person;
constructor() { } constructor() { }
} }

View File

@ -10,7 +10,7 @@ export class ReadMoreComponent implements OnChanges {
/** /**
* String to apply readmore on * String to apply readmore on
*/ */
@Input() text!: string; @Input({required: true}) text!: string;
/** /**
* Max length before apply read more. Defaults to 250 characters. * Max length before apply read more. Defaults to 250 characters.
*/ */

View File

@ -12,7 +12,7 @@ import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event'
}) })
export class UpdateNotificationModalComponent { export class UpdateNotificationModalComponent {
@Input() updateData!: UpdateVersionEvent; @Input({required: true}) updateData!: UpdateVersionEvent;
constructor(public modal: NgbActiveModal) { } constructor(public modal: NgbActiveModal) { }

View File

@ -1,9 +1,20 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core'; import {
Component,
DestroyRef,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit,
Output,
TemplateRef
} from '@angular/core';
import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap'; import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { NavService } from 'src/app/_services/nav.service'; import { NavService } from 'src/app/_services/nav.service';
import { ToggleService } from 'src/app/_services/toggle.service'; import { ToggleService } from 'src/app/_services/toggle.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
* This should go on all pages which have the side nav present and is not Settings related. * This should go on all pages which have the side nav present and is not Settings related.
@ -14,7 +25,7 @@ import { ToggleService } from 'src/app/_services/toggle.service';
templateUrl: './side-nav-companion-bar.component.html', templateUrl: './side-nav-companion-bar.component.html',
styleUrls: ['./side-nav-companion-bar.component.scss'] styleUrls: ['./side-nav-companion-bar.component.scss']
}) })
export class SideNavCompanionBarComponent implements OnInit, OnDestroy { export class SideNavCompanionBarComponent implements OnInit {
/** /**
* If the page should show a filter * If the page should show a filter
*/ */
@ -34,7 +45,7 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
*/ */
@Input() filterActive: boolean = false; @Input() filterActive: boolean = false;
@Input() extraDrawer!: TemplateRef<any>; @Input() extraDrawer!: TemplateRef<any>;
@Output() filterOpen: EventEmitter<boolean> = new EventEmitter(); @Output() filterOpen: EventEmitter<boolean> = new EventEmitter();
@ -42,9 +53,9 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
isFilterOpen = false; isFilterOpen = false;
isExtrasOpen = false; isExtrasOpen = false;
private onDestroy: Subject<void> = new Subject(); private readonly destroyRef = inject(DestroyRef);
constructor(private navService: NavService, private utilityService: UtilityService, public toggleService: ToggleService, constructor(private navService: NavService, private utilityService: UtilityService, public toggleService: ToggleService,
private offcanvasService: NgbOffcanvas) { private offcanvasService: NgbOffcanvas) {
} }
@ -52,7 +63,7 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
this.isFilterOpen = this.filterOpenByDefault; this.isFilterOpen = this.filterOpenByDefault;
// If user opens side nav while filter is open on mobile, then collapse filter (as it doesn't render well) TODO: Change this when we have new drawer // If user opens side nav while filter is open on mobile, then collapse filter (as it doesn't render well) TODO: Change this when we have new drawer
this.navService.sideNavCollapsed$.pipe(takeUntil(this.onDestroy)).subscribe(sideNavCollapsed => { this.navService.sideNavCollapsed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(sideNavCollapsed => {
if (this.isFilterOpen && sideNavCollapsed && this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet) { if (this.isFilterOpen && sideNavCollapsed && this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet) {
this.isFilterOpen = false; this.isFilterOpen = false;
this.filterOpen.emit(this.isFilterOpen); this.filterOpen.emit(this.isFilterOpen);
@ -60,11 +71,6 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
toggleFilter() { toggleFilter() {
this.isFilterOpen = !this.isFilterOpen; this.isFilterOpen = !this.isFilterOpen;
this.filterOpen.emit(this.isFilterOpen); this.filterOpen.emit(this.isFilterOpen);

View File

@ -1,7 +1,17 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { filter, map, Subject, takeUntil } from 'rxjs'; import { filter, map, Subject, takeUntil } from 'rxjs';
import { NavService } from 'src/app/_services/nav.service'; import { NavService } from 'src/app/_services/nav.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
@ -10,7 +20,7 @@ import { NavService } from 'src/app/_services/nav.service';
styleUrls: ['./side-nav-item.component.scss'], styleUrls: ['./side-nav-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SideNavItemComponent implements OnInit, OnDestroy { export class SideNavItemComponent implements OnInit {
/** /**
* Icon to display next to item. ie) 'fa-home' * Icon to display next to item. ie) 'fa-home'
*/ */
@ -31,35 +41,30 @@ export class SideNavItemComponent implements OnInit, OnDestroy {
@Input() external: boolean = false; @Input() external: boolean = false;
@Input() comparisonMethod: 'startsWith' | 'equals' = 'equals'; @Input() comparisonMethod: 'startsWith' | 'equals' = 'equals';
private readonly destroyRef = inject(DestroyRef);
highlighted = false; highlighted = false;
private onDestroy: Subject<void> = new Subject();
constructor(public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef) { constructor(public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef) {
router.events router.events
.pipe(filter(event => event instanceof NavigationEnd), .pipe(filter(event => event instanceof NavigationEnd),
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd)) map(evt => evt as NavigationEnd))
.subscribe((evt: NavigationEnd) => { .subscribe((evt: NavigationEnd) => {
this.updateHightlight(evt.url.split('?')[0]); this.updateHighlight(evt.url.split('?')[0]);
}); });
} }
ngOnInit(): void { ngOnInit(): void {
setTimeout(() => { setTimeout(() => {
this.updateHightlight(this.router.url.split('?')[0]); this.updateHighlight(this.router.url.split('?')[0]);
}, 100); }, 100);
} }
ngOnDestroy(): void { updateHighlight(page: string) {
this.onDestroy.next();
this.onDestroy.complete();
}
updateHightlight(page: string) {
if (this.link === undefined) { if (this.link === undefined) {
this.highlighted = false; this.highlighted = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -14,6 +22,7 @@ import { Action, ActionFactoryService, ActionItem } from '../../../_services/act
import { ActionService } from '../../../_services/action.service'; import { ActionService } from '../../../_services/action.service';
import { LibraryService } from '../../../_services/library.service'; import { LibraryService } from '../../../_services/library.service';
import { NavService } from '../../../_services/nav.service'; import { NavService } from '../../../_services/nav.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-side-nav', selector: 'app-side-nav',
@ -21,8 +30,9 @@ import { NavService } from '../../../_services/nav.service';
styleUrls: ['./side-nav.component.scss'], styleUrls: ['./side-nav.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SideNavComponent implements OnInit, OnDestroy { export class SideNavComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
libraries: Library[] = []; libraries: Library[] = [];
actions: ActionItem<Library>[] = []; actions: ActionItem<Library>[] = [];
readingListActions = [{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}]; readingListActions = [{action: Action.Import, title: 'Import CBL', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
@ -32,18 +42,16 @@ export class SideNavComponent implements OnInit, OnDestroy {
return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0; return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0;
} }
private onDestroy: Subject<void> = new Subject();
constructor(public accountService: AccountService, private libraryService: LibraryService, constructor(public accountService: AccountService, private libraryService: LibraryService,
public utilityService: UtilityService, private messageHub: MessageHubService, public utilityService: UtilityService, private messageHub: MessageHubService,
private actionFactoryService: ActionFactoryService, private actionService: ActionService, private actionFactoryService: ActionFactoryService, private actionService: ActionService,
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef, public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
private ngbModal: NgbModal, private imageService: ImageService) { private ngbModal: NgbModal, private imageService: ImageService) {
this.router.events.pipe( this.router.events.pipe(
filter(event => event instanceof NavigationEnd), filter(event => event instanceof NavigationEnd),
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd), map(evt => evt as NavigationEnd),
filter(() => this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet)) filter(() => this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet))
.subscribe((evt: NavigationEnd) => { .subscribe((evt: NavigationEnd) => {
@ -68,7 +76,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.messageHub.messages$.pipe(takeUntil(this.onDestroy), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => { this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => {
this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => { this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => {
this.libraries = [...libraries]; this.libraries = [...libraries];
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -76,11 +84,6 @@ export class SideNavComponent implements OnInit, OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
handleAction(action: ActionItem<Library>, library: Library) { handleAction(action: ActionItem<Library>, library: Library) {
switch (action.action) { switch (action.action) {
case(Action.Scan): case(Action.Scan):
@ -129,4 +132,4 @@ export class SideNavComponent implements OnInit, OnDestroy {
this.navService.toggleSideNav(); this.navService.toggleSideNav();
} }
} }

View File

@ -1,4 +1,13 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms'; import { FormGroup, FormControl, Validators } from '@angular/forms';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
@ -11,6 +20,7 @@ import { Library, LibraryType } from 'src/app/_models/library';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service'; import { LibraryService } from 'src/app/_services/library.service';
import { UploadService } from 'src/app/_services/upload.service'; import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID { enum TabID {
General = 'General', General = 'General',
@ -32,9 +42,10 @@ enum StepID {
styleUrls: ['./library-settings-modal.component.scss'], styleUrls: ['./library-settings-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LibrarySettingsModalComponent implements OnInit, OnDestroy { export class LibrarySettingsModalComponent implements OnInit {
@Input() library!: Library; @Input({required: true}) library!: Library;
private readonly destroyRef = inject(DestroyRef);
active = TabID.General; active = TabID.General;
imageUrls: Array<string> = []; imageUrls: Array<string> = [];
@ -54,17 +65,16 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
selectedFolders: string[] = []; selectedFolders: string[] = [];
madeChanges = false; madeChanges = false;
libraryTypes: string[] = [] libraryTypes: string[] = []
isAddLibrary = false; isAddLibrary = false;
setupStep = StepID.General; setupStep = StepID.General;
private readonly onDestroy = new Subject<void>();
get Breakpoint() { return Breakpoint; } get Breakpoint() { return Breakpoint; }
get TabID() { return TabID; } get TabID() { return TabID; }
get StepID() { return StepID; } get StepID() { return StepID; }
constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal, constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal,
private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService, private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService,
private libraryService: LibraryService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef, private libraryService: LibraryService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef,
private imageService: ImageService) { } private imageService: ImageService) { }
@ -87,7 +97,7 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
} }
this.libraryForm.get('name')?.valueChanges.pipe( this.libraryForm.get('name')?.valueChanges.pipe(
debounceTime(100), debounceTime(100),
distinctUntilChanged(), distinctUntilChanged(),
switchMap(name => this.libraryService.libraryNameExists(name)), switchMap(name => this.libraryService.libraryNameExists(name)),
tap(exists => { tap(exists => {
@ -95,23 +105,17 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
if (!exists || isExistingName) { if (!exists || isExistingName) {
this.libraryForm.get('name')?.setErrors(null); this.libraryForm.get('name')?.setErrors(null);
} else { } else {
this.libraryForm.get('name')?.setErrors({duplicateName: true}) this.libraryForm.get('name')?.setErrors({duplicateName: true})
} }
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
).subscribe(); ).subscribe();
this.setValues(); this.setValues();
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
setValues() { setValues() {
if (this.library !== undefined) { if (this.library !== undefined) {
this.libraryForm.get('name')?.setValue(this.library.name); this.libraryForm.get('name')?.setValue(this.library.name);
@ -159,7 +163,7 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
model.type = parseInt(model.type, 10); model.type = parseInt(model.type, 10);
if (model.type !== this.library.type) { if (model.type !== this.library.type) {
if (!await this.confirmService.confirm(`Changing library type will trigger a new scan with different parsing rules and may lead to if (!await this.confirmService.confirm(`Changing library type will trigger a new scan with different parsing rules and may lead to
series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?`)) return; series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?`)) return;
} }
@ -221,7 +225,7 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
isNextDisabled() { isNextDisabled() {
switch (this.setupStep) { switch (this.setupStep) {
case StepID.General: case StepID.General:
return this.libraryForm.get('name')?.invalid || this.libraryForm.get('type')?.invalid; return this.libraryForm.get('name')?.invalid || this.libraryForm.get('type')?.invalid;
case StepID.Folder: case StepID.Folder:
return this.selectedFolders.length === 0; return this.selectedFolders.length === 0;

View File

@ -1,20 +1,20 @@
import { Component, OnDestroy } from '@angular/core'; import {ChangeDetectionStrategy, Component, DestroyRef, inject} from '@angular/core';
import { FormControl } from '@angular/forms'; import {FormControl} from '@angular/forms';
import { LegendPosition } from '@swimlane/ngx-charts'; import {LegendPosition} from '@swimlane/ngx-charts';
import { Subject, map, takeUntil, Observable } from 'rxjs'; import {map, Observable} from 'rxjs';
import { DayOfWeek, StatisticsService } from 'src/app/_services/statistics.service'; import {DayOfWeek, StatisticsService} from 'src/app/_services/statistics.service';
import { PieDataItem } from '../../_models/pie-data-item'; import {PieDataItem} from '../../_models/pie-data-item';
import { StatCount } from '../../_models/stat-count'; import {StatCount} from '../../_models/stat-count';
import { DayOfWeekPipe } from '../../_pipes/day-of-week.pipe'; import {DayOfWeekPipe} from '../../_pipes/day-of-week.pipe';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-day-breakdown', selector: 'app-day-breakdown',
templateUrl: './day-breakdown.component.html', templateUrl: './day-breakdown.component.html',
styleUrls: ['./day-breakdown.component.scss'] styleUrls: ['./day-breakdown.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DayBreakdownComponent implements OnDestroy { export class DayBreakdownComponent {
private readonly onDestroy = new Subject<void>();
view: [number, number] = [0,0]; view: [number, number] = [0,0];
gradient: boolean = true; gradient: boolean = true;
@ -28,6 +28,7 @@ export class DayBreakdownComponent implements OnDestroy {
formControl: FormControl = new FormControl(true, []); formControl: FormControl = new FormControl(true, []);
dayBreakdown$!: Observable<Array<PieDataItem>>; dayBreakdown$!: Observable<Array<PieDataItem>>;
private readonly destroyRef = inject(DestroyRef);
constructor(private statService: StatisticsService) { constructor(private statService: StatisticsService) {
const dayOfWeekPipe = new DayOfWeekPipe(); const dayOfWeekPipe = new DayOfWeekPipe();
@ -37,13 +38,8 @@ export class DayBreakdownComponent implements OnDestroy {
return {name: dayOfWeekPipe.transform(d.value), value: d.count}; return {name: dayOfWeekPipe.transform(d.value), value: d.count};
}) })
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
} }

View File

@ -1,27 +1,33 @@
import { ChangeDetectionStrategy, Component, OnDestroy, QueryList, ViewChildren } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
OnDestroy,
QueryList,
ViewChildren
} from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { LegendPosition } from '@swimlane/ngx-charts'; import { LegendPosition } from '@swimlane/ngx-charts';
import { Observable, Subject, BehaviorSubject, combineLatest, map, takeUntil, shareReplay } from 'rxjs'; import { Observable, Subject, BehaviorSubject, combineLatest, map, takeUntil, shareReplay } from 'rxjs';
import { MangaFormatPipe } from 'src/app/pipe/manga-format.pipe';
import { StatisticsService } from 'src/app/_services/statistics.service'; import { StatisticsService } from 'src/app/_services/statistics.service';
import { SortableHeader, SortEvent, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive'; import { SortableHeader, SortEvent, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { FileExtension, FileExtensionBreakdown } from '../../_models/file-breakdown'; import { FileExtension, FileExtensionBreakdown } from '../../_models/file-breakdown';
import { PieDataItem } from '../../_models/pie-data-item'; import { PieDataItem } from '../../_models/pie-data-item';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
export interface StackedBarChartDataItem { export interface StackedBarChartDataItem {
name: string, name: string,
series: Array<PieDataItem>; series: Array<PieDataItem>;
} }
const mangaFormatPipe = new MangaFormatPipe();
@Component({ @Component({
selector: 'app-file-breakdown-stats', selector: 'app-file-breakdown-stats',
templateUrl: './file-breakdown-stats.component.html', templateUrl: './file-breakdown-stats.component.html',
styleUrls: ['./file-breakdown-stats.component.scss'], styleUrls: ['./file-breakdown-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FileBreakdownStatsComponent implements OnDestroy { export class FileBreakdownStatsComponent {
@ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>; @ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>;
@ -29,11 +35,12 @@ export class FileBreakdownStatsComponent implements OnDestroy {
files$!: Observable<Array<FileExtension>>; files$!: Observable<Array<FileExtension>>;
vizData$!: Observable<Array<StackedBarChartDataItem>>; vizData$!: Observable<Array<StackedBarChartDataItem>>;
vizData2$!: Observable<Array<PieDataItem>>; vizData2$!: Observable<Array<PieDataItem>>;
private readonly onDestroy = new Subject<void>();
currentSort = new BehaviorSubject<SortEvent<FileExtension>>({column: 'extension', direction: 'asc'}); currentSort = new BehaviorSubject<SortEvent<FileExtension>>({column: 'extension', direction: 'asc'});
currentSort$: Observable<SortEvent<FileExtension>> = this.currentSort.asObservable(); currentSort$: Observable<SortEvent<FileExtension>> = this.currentSort.asObservable();
private readonly destroyRef = inject(DestroyRef);
view: [number, number] = [700, 400]; view: [number, number] = [700, 400];
gradient: boolean = true; gradient: boolean = true;
showLegend: boolean = true; showLegend: boolean = true;
@ -48,7 +55,7 @@ export class FileBreakdownStatsComponent implements OnDestroy {
constructor(private statService: StatisticsService) { constructor(private statService: StatisticsService) {
this.rawData$ = this.statService.getFileBreakdown().pipe(takeUntil(this.onDestroy), shareReplay()); this.rawData$ = this.statService.getFileBreakdown().pipe(takeUntilDestroyed(this.destroyRef), shareReplay());
this.files$ = combineLatest([this.currentSort$, this.rawData$]).pipe( this.files$ = combineLatest([this.currentSort$, this.rawData$]).pipe(
map(([sortConfig, data]) => { map(([sortConfig, data]) => {
@ -61,20 +68,15 @@ export class FileBreakdownStatsComponent implements OnDestroy {
return sortConfig.direction === 'asc' ? res : -res; return sortConfig.direction === 'asc' ? res : -res;
}) : fileBreakdown; }) : fileBreakdown;
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.vizData2$ = this.files$.pipe(takeUntil(this.onDestroy), map(data => data.map(d => { this.vizData2$ = this.files$.pipe(takeUntilDestroyed(this.destroyRef), map(data => data.map(d => {
return {name: d.extension || 'Not Categorized', value: d.totalFiles, extra: d.totalSize}; return {name: d.extension || 'Not Categorized', value: d.totalFiles, extra: d.totalSize};
}))); })));
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
onSort(evt: SortEvent<FileExtension>) { onSort(evt: SortEvent<FileExtension>) {
this.currentSort.next(evt); this.currentSort.next(evt);

View File

@ -1,10 +1,20 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit,
QueryList,
ViewChildren
} from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { LegendPosition } from '@swimlane/ngx-charts'; import { LegendPosition } from '@swimlane/ngx-charts';
import { Observable, Subject, BehaviorSubject, combineLatest, map, takeUntil } from 'rxjs'; import { Observable, Subject, BehaviorSubject, combineLatest, map, takeUntil } from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service'; import { StatisticsService } from 'src/app/_services/statistics.service';
import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive'; import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { PieDataItem } from '../../_models/pie-data-item'; import { PieDataItem } from '../../_models/pie-data-item';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-manga-format-stats', selector: 'app-manga-format-stats',
@ -12,13 +22,13 @@ import { PieDataItem } from '../../_models/pie-data-item';
styleUrls: ['./manga-format-stats.component.scss'], styleUrls: ['./manga-format-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MangaFormatStatsComponent implements OnInit, OnDestroy { export class MangaFormatStatsComponent {
@ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>; @ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>;
private readonly destroyRef = inject(DestroyRef);
formats$!: Observable<Array<PieDataItem>>; formats$!: Observable<Array<PieDataItem>>;
private readonly onDestroy = new Subject<void>();
currentSort = new BehaviorSubject<SortEvent<PieDataItem>>({column: 'value', direction: 'asc'}); currentSort = new BehaviorSubject<SortEvent<PieDataItem>>({column: 'value', direction: 'asc'});
currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable(); currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable();
@ -44,20 +54,10 @@ export class MangaFormatStatsComponent implements OnInit, OnDestroy {
return sortConfig.direction === 'asc' ? res : -res; return sortConfig.direction === 'asc' ? res : -res;
}) : data; }) : data;
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
ngOnInit(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
onSort(evt: SortEvent<PieDataItem>) { onSort(evt: SortEvent<PieDataItem>) {
this.currentSort.next(evt); this.currentSort.next(evt);

View File

@ -1,10 +1,19 @@
import { ChangeDetectionStrategy, Component, OnDestroy, QueryList, ViewChildren } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
OnDestroy,
QueryList,
ViewChildren
} from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { LegendPosition } from '@swimlane/ngx-charts'; import { LegendPosition } from '@swimlane/ngx-charts';
import { Observable, Subject, map, takeUntil, combineLatest, BehaviorSubject } from 'rxjs'; import { Observable, Subject, map, takeUntil, combineLatest, BehaviorSubject } from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service'; import { StatisticsService } from 'src/app/_services/statistics.service';
import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive'; import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { PieDataItem } from '../../_models/pie-data-item'; import { PieDataItem } from '../../_models/pie-data-item';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-publication-status-stats', selector: 'app-publication-status-stats',
@ -12,13 +21,12 @@ import { PieDataItem } from '../../_models/pie-data-item';
styleUrls: ['./publication-status-stats.component.scss'], styleUrls: ['./publication-status-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PublicationStatusStatsComponent implements OnDestroy { export class PublicationStatusStatsComponent {
@ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>; @ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>;
publicationStatues$!: Observable<Array<PieDataItem>>; publicationStatues$!: Observable<Array<PieDataItem>>;
private readonly onDestroy = new Subject<void>();
currentSort = new BehaviorSubject<SortEvent<PieDataItem>>({column: 'value', direction: 'asc'}); currentSort = new BehaviorSubject<SortEvent<PieDataItem>>({column: 'value', direction: 'asc'});
currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable(); currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable();
@ -32,6 +40,8 @@ export class PublicationStatusStatsComponent implements OnDestroy {
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA'] domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
}; };
private readonly destroyRef = inject(DestroyRef);
formControl: FormControl = new FormControl(true, []); formControl: FormControl = new FormControl(true, []);
@ -44,15 +54,10 @@ export class PublicationStatusStatsComponent implements OnDestroy {
return sortConfig.direction === 'asc' ? res : -res; return sortConfig.direction === 'asc' ? res : -res;
}) : data; }) : data;
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
onSort(evt: SortEvent<PieDataItem>) { onSort(evt: SortEvent<PieDataItem>) {
this.currentSort.next(evt); this.currentSort.next(evt);

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; import {ChangeDetectionStrategy, Component, DestroyRef, inject, Input, OnDestroy, OnInit} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { filter, map, Observable, of, shareReplay, Subject, switchMap, takeUntil } from 'rxjs'; import { filter, map, Observable, of, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
import { MangaFormatPipe } from 'src/app/pipe/manga-format.pipe'; import { MangaFormatPipe } from 'src/app/pipe/manga-format.pipe';
@ -7,6 +7,7 @@ import { MemberService } from 'src/app/_services/member.service';
import { StatisticsService } from 'src/app/_services/statistics.service'; import { StatisticsService } from 'src/app/_services/statistics.service';
import { PieDataItem } from '../../_models/pie-data-item'; import { PieDataItem } from '../../_models/pie-data-item';
import { TimePeriods } from '../top-readers/top-readers.component'; import { TimePeriods } from '../top-readers/top-readers.component';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" }; const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
const mangaFormatPipe = new MangaFormatPipe(); const mangaFormatPipe = new MangaFormatPipe();
@ -17,7 +18,7 @@ const mangaFormatPipe = new MangaFormatPipe();
styleUrls: ['./reading-activity.component.scss'], styleUrls: ['./reading-activity.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReadingActivityComponent implements OnInit, OnDestroy { export class ReadingActivityComponent implements OnInit {
/** /**
* Only show for one user * Only show for one user
*/ */
@ -33,10 +34,10 @@ export class ReadingActivityComponent implements OnInit, OnDestroy {
users$: Observable<Member[]> | undefined; users$: Observable<Member[]> | undefined;
data$: Observable<Array<PieDataItem>>; data$: Observable<Array<PieDataItem>>;
timePeriods = TimePeriods; timePeriods = TimePeriods;
private readonly onDestroy = new Subject<void>(); private readonly destroyRef = inject(DestroyRef);
constructor(private statService: StatisticsService, private memberService: MemberService) { constructor(private statService: StatisticsService, private memberService: MemberService) {
this.data$ = this.formGroup.valueChanges.pipe( this.data$ = this.formGroup.valueChanges.pipe(
switchMap(_ => this.statService.getReadCountByDay(this.formGroup.get('users')!.value, this.formGroup.get('days')!.value)), switchMap(_ => this.statService.getReadCountByDay(this.formGroup.get('users')!.value, this.formGroup.get('days')!.value)),
map(data => { map(data => {
const gList = data.reduce((formats, entry) => { const gList = data.reduce((formats, entry) => {
@ -56,26 +57,20 @@ export class ReadingActivityComponent implements OnInit, OnDestroy {
return {name: format, value: 0, series: gList[format].series} return {name: format, value: 0, series: gList[format].series}
}); });
}), }),
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
shareReplay(), shareReplay(),
); );
this.data$.subscribe(); this.data$.subscribe();
} }
ngOnInit(): void { ngOnInit(): void {
this.users$ = (this.isAdmin ? this.memberService.getMembers() : of([])).pipe(filter(_ => this.isAdmin), takeUntil(this.onDestroy), shareReplay()); this.users$ = (this.isAdmin ? this.memberService.getMembers() : of([])).pipe(filter(_ => this.isAdmin), takeUntilDestroyed(this.destroyRef), shareReplay());
this.formGroup.get('users')?.setValue(this.userId, {emitValue: true}); this.formGroup.get('users')?.setValue(this.userId, {emitValue: true});
if (!this.isAdmin) { if (!this.isAdmin) {
this.formGroup.get('users')?.disable(); this.formGroup.get('users')?.disable();
} }
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
} }

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, HostListener, OnDestroy } from '@angular/core'; import {ChangeDetectionStrategy, Component, DestroyRef, HostListener, inject, OnDestroy} from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { BehaviorSubject, map, Observable, shareReplay, Subject, takeUntil } from 'rxjs'; import {BehaviorSubject, map, Observable, ReplaySubject, shareReplay, Subject, takeUntil} from 'rxjs';
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service'; import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { Series } from 'src/app/_models/series'; import { Series } from 'src/app/_models/series';
@ -11,6 +11,7 @@ import { StatisticsService } from 'src/app/_services/statistics.service';
import { PieDataItem } from '../../_models/pie-data-item'; import { PieDataItem } from '../../_models/pie-data-item';
import { ServerStatistics } from '../../_models/server-statistics'; import { ServerStatistics } from '../../_models/server-statistics';
import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component'; import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-server-stats', selector: 'app-server-stats',
@ -18,7 +19,7 @@ import { GenericListModalComponent } from '../_modals/generic-list-modal/generic
styleUrls: ['./server-stats.component.scss'], styleUrls: ['./server-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ServerStatsComponent implements OnDestroy { export class ServerStatsComponent {
releaseYears$!: Observable<Array<PieDataItem>>; releaseYears$!: Observable<Array<PieDataItem>>;
mostActiveUsers$!: Observable<Array<PieDataItem>>; mostActiveUsers$!: Observable<Array<PieDataItem>>;
@ -27,41 +28,42 @@ export class ServerStatsComponent implements OnDestroy {
recentlyRead$!: Observable<Array<PieDataItem>>; recentlyRead$!: Observable<Array<PieDataItem>>;
stats$!: Observable<ServerStatistics>; stats$!: Observable<ServerStatistics>;
seriesImage: (data: PieDataItem) => string; seriesImage: (data: PieDataItem) => string;
private readonly onDestroy = new Subject<void>();
openSeries = (data: PieDataItem) => { openSeries = (data: PieDataItem) => {
const series = data.extra as Series; const series = data.extra as Series;
this.router.navigate(['library', series.libraryId, 'series', series.id]); this.router.navigate(['library', series.libraryId, 'series', series.id]);
} }
breakpointSubject = new BehaviorSubject<Breakpoint>(1); breakpointSubject = new ReplaySubject<Breakpoint>(1);
breakpoint$: Observable<Breakpoint> = this.breakpointSubject.asObservable(); breakpoint$: Observable<Breakpoint> = this.breakpointSubject.asObservable();
private readonly destroyRef = inject(DestroyRef);
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event']) @HostListener('window:orientationchange', ['$event'])
onResize() { onResize() {
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint()); this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
} }
get Breakpoint() { return Breakpoint; } get Breakpoint() { return Breakpoint; }
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService, constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
private metadataService: MetadataService, private modalService: NgbModal, private utilityService: UtilityService) { private metadataService: MetadataService, private modalService: NgbModal, private utilityService: UtilityService) {
this.seriesImage = (data: PieDataItem) => { this.seriesImage = (data: PieDataItem) => {
if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id); if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id);
return ''; return '';
} }
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint()); this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
this.stats$ = this.statService.getServerStatistics().pipe(takeUntil(this.onDestroy), shareReplay()); this.stats$ = this.statService.getServerStatistics().pipe(takeUntilDestroyed(this.destroyRef), shareReplay());
this.releaseYears$ = this.statService.getTopYears().pipe(takeUntil(this.onDestroy)); this.releaseYears$ = this.statService.getTopYears().pipe(takeUntilDestroyed(this.destroyRef));
this.mostActiveUsers$ = this.stats$.pipe( this.mostActiveUsers$ = this.stats$.pipe(
map(d => d.mostActiveUsers), map(d => d.mostActiveUsers),
map(userCounts => userCounts.map(count => { map(userCounts => userCounts.map(count => {
return {name: count.value.username, value: count.count}; return {name: count.value.username, value: count.count};
})), })),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.mostActiveLibrary$ = this.stats$.pipe( this.mostActiveLibrary$ = this.stats$.pipe(
@ -69,7 +71,7 @@ export class ServerStatsComponent implements OnDestroy {
map(counts => counts.map(count => { map(counts => counts.map(count => {
return {name: count.value.name, value: count.count}; return {name: count.value.name, value: count.count};
})), })),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.mostActiveSeries$ = this.stats$.pipe( this.mostActiveSeries$ = this.stats$.pipe(
@ -77,7 +79,7 @@ export class ServerStatsComponent implements OnDestroy {
map(counts => counts.map(count => { map(counts => counts.map(count => {
return {name: count.value.name, value: count.count, extra: count.value}; return {name: count.value.name, value: count.count, extra: count.value};
})), })),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
this.recentlyRead$ = this.stats$.pipe( this.recentlyRead$ = this.stats$.pipe(
@ -85,15 +87,10 @@ export class ServerStatsComponent implements OnDestroy {
map(counts => counts.map(count => { map(counts => counts.map(count => {
return {name: count.name, value: -1, extra: count}; return {name: count.name, value: -1, extra: count};
})), })),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
openGenreList() { openGenreList() {
this.metadataService.getAllGenres().subscribe(genres => { this.metadataService.getAllGenres().subscribe(genres => {
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true }); const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
@ -130,6 +127,6 @@ export class ServerStatsComponent implements OnDestroy {
}); });
} }
} }

View File

@ -23,7 +23,7 @@ export class StatListComponent {
* Optional data to put in tooltip * Optional data to put in tooltip
*/ */
@Input() description: string = ''; @Input() description: string = '';
@Input() data$!: Observable<PieDataItem[]>; @Input({required: true}) data$!: Observable<PieDataItem[]>;
@Input() image: ((data: PieDataItem) => string) | undefined = undefined; @Input() image: ((data: PieDataItem) => string) | undefined = undefined;
/** /**
* Optional callback handler when an item is clicked * Optional callback handler when an item is clicked
@ -31,7 +31,7 @@ export class StatListComponent {
@Input() handleClick: ((data: PieDataItem) => void) | undefined = undefined; @Input() handleClick: ((data: PieDataItem) => void) | undefined = undefined;
doClick(item: PieDataItem) { doClick(item: PieDataItem) {
if (!this.handleClick) return; if (!this.handleClick) return;
this.handleClick(item); this.handleClick(item);
} }

View File

@ -1,8 +1,17 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms'; import { FormGroup, FormControl } from '@angular/forms';
import { Observable, Subject, takeUntil, switchMap, shareReplay } from 'rxjs'; import { Observable, Subject, takeUntil, switchMap, shareReplay } from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service'; import { StatisticsService } from 'src/app/_services/statistics.service';
import { TopUserRead } from '../../_models/top-reads'; import { TopUserRead } from '../../_models/top-reads';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
export const TimePeriods: Array<{title: string, value: number}> = [{title: 'This Week', value: new Date().getDay() || 1}, {title: 'Last 7 Days', value: 7}, {title: 'Last 30 Days', value: 30}, {title: 'Last 90 Days', value: 90}, {title: 'Last Year', value: 365}, {title: 'All Time', value: 0}]; export const TimePeriods: Array<{title: string, value: number}> = [{title: 'This Week', value: new Date().getDay() || 1}, {title: 'Last 7 Days', value: 7}, {title: 'Last 30 Days', value: 30}, {title: 'Last 90 Days', value: 90}, {title: 'Last Year', value: 365}, {title: 'All Time', value: 0}];
@ -12,22 +21,22 @@ export const TimePeriods: Array<{title: string, value: number}> = [{title: 'This
styleUrls: ['./top-readers.component.scss'], styleUrls: ['./top-readers.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TopReadersComponent implements OnInit, OnDestroy { export class TopReadersComponent implements OnInit {
formGroup: FormGroup; formGroup: FormGroup;
timePeriods = TimePeriods; timePeriods = TimePeriods;
users$: Observable<TopUserRead[]>; users$: Observable<TopUserRead[]>;
private readonly onDestroy = new Subject<void>(); private readonly destroyRef = inject(DestroyRef);
constructor(private statsService: StatisticsService, private readonly cdRef: ChangeDetectorRef) { constructor(private statsService: StatisticsService, private readonly cdRef: ChangeDetectorRef) {
this.formGroup = new FormGroup({ this.formGroup = new FormGroup({
'days': new FormControl(this.timePeriods[0].value, []), 'days': new FormControl(this.timePeriods[0].value, []),
}); });
this.users$ = this.formGroup.valueChanges.pipe( this.users$ = this.formGroup.valueChanges.pipe(
switchMap(_ => this.statsService.getTopUsers(this.formGroup.get('days')?.value as number)), switchMap(_ => this.statsService.getTopUsers(this.formGroup.get('days')?.value as number)),
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
shareReplay(), shareReplay(),
); );
} }
@ -38,9 +47,4 @@ export class TopReadersComponent implements OnInit, OnDestroy {
this.formGroup.get('days')?.setValue(this.timePeriods[0].value, {emitEvent: true}); this.formGroup.get('days')?.setValue(this.timePeriods[0].value, {emitEvent: true});
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
} }

View File

@ -14,6 +14,6 @@
</div> </div>
<div class="row g-0 pt-4 pb-2 " style="height: 242px"> <div class="row g-0 pt-4 pb-2 " style="height: 242px">
<app-stat-list [data$]="precentageRead$" label="% Read" title="Library Read Progress"></app-stat-list> <app-stat-list [data$]="percentageRead$" label="% Read" title="Library Read Progress"></app-stat-list>
</div> </div>
</div> </div>

View File

@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { map, Observable, shareReplay, Subject, takeUntil } from 'rxjs'; import { map, Observable, shareReplay, Subject, takeUntil } from 'rxjs';
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service'; import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
import { UserReadStatistics } from 'src/app/statistics/_models/user-read-statistics'; import { UserReadStatistics } from 'src/app/statistics/_models/user-read-statistics';
@ -9,6 +17,7 @@ import { AccountService } from 'src/app/_services/account.service';
import { PieDataItem } from '../../_models/pie-data-item'; import { PieDataItem } from '../../_models/pie-data-item';
import { LibraryService } from 'src/app/_services/library.service'; import { LibraryService } from 'src/app/_services/library.service';
import { PercentPipe } from '@angular/common'; import { PercentPipe } from '@angular/common';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-user-stats', selector: 'app-user-stats',
@ -16,20 +25,19 @@ import { PercentPipe } from '@angular/common';
styleUrls: ['./user-stats.component.scss'], styleUrls: ['./user-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class UserStatsComponent implements OnInit, OnDestroy { export class UserStatsComponent implements OnInit {
userId: number | undefined = undefined; userId: number | undefined = undefined;
userStats$!: Observable<UserReadStatistics>; userStats$!: Observable<UserReadStatistics>;
readSeries$!: Observable<ReadHistoryEvent[]>; readSeries$!: Observable<ReadHistoryEvent[]>;
isAdmin$: Observable<boolean>; isAdmin$: Observable<boolean>;
precentageRead$!: Observable<PieDataItem[]>; percentageRead$!: Observable<PieDataItem[]>;
private readonly destroyRef = inject(DestroyRef);
private readonly onDestroy = new Subject<void>(); constructor(private readonly cdRef: ChangeDetectorRef, private statService: StatisticsService,
constructor(private readonly cdRef: ChangeDetectorRef, private statService: StatisticsService,
private filterService: FilterUtilitiesService, private accountService: AccountService, private memberService: MemberService, private filterService: FilterUtilitiesService, private accountService: AccountService, private memberService: MemberService,
private libraryService: LibraryService) { private libraryService: LibraryService) {
this.isAdmin$ = this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), map(u => { this.isAdmin$ = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), map(u => {
if (!u) return false; if (!u) return false;
return this.accountService.hasAdminRole(u); return this.accountService.hasAdminRole(u);
})); }));
@ -42,26 +50,20 @@ export class UserStatsComponent implements OnInit, OnDestroy {
this.memberService.getMember().subscribe(me => { this.memberService.getMember().subscribe(me => {
this.userId = me.id; this.userId = me.id;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.userStats$ = this.statService.getUserStatistics(this.userId).pipe(takeUntil(this.onDestroy), shareReplay()); this.userStats$ = this.statService.getUserStatistics(this.userId).pipe(takeUntilDestroyed(this.destroyRef), shareReplay());
this.readSeries$ = this.statService.getReadingHistory(this.userId).pipe( this.readSeries$ = this.statService.getReadingHistory(this.userId).pipe(
takeUntil(this.onDestroy), takeUntilDestroyed(this.destroyRef),
); );
const pipe = new PercentPipe('en-US'); const pipe = new PercentPipe('en-US');
this.libraryService.getLibraryNames().subscribe(names => { this.libraryService.getLibraryNames().subscribe(names => {
this.precentageRead$ = this.userStats$.pipe(takeUntil(this.onDestroy), map(d => d.percentReadPerLibrary.map(l => { this.percentageRead$ = this.userStats$.pipe(takeUntilDestroyed(this.destroyRef), map(d => d.percentReadPerLibrary.map(l => {
return {name: names[l.count], value: parseFloat((pipe.transform(l.value, '1.1-1') || '0').replace('%', ''))}; return {name: names[l.count], value: parseFloat((pipe.transform(l.value, '1.1-1') || '0').replace('%', ''))};
}).sort((a: PieDataItem, b: PieDataItem) => b.value - a.value))); }).sort((a: PieDataItem, b: PieDataItem) => b.value - a.value)));
}) })
});
}
ngOnDestroy(): void { });
this.onDestroy.next();
this.onDestroy.complete();
} }
} }

View File

@ -1,11 +1,30 @@
import { trigger, state, style, transition, animate } from '@angular/animations'; import { trigger, state, style, transition, animate } from '@angular/animations';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild, DestroyRef,
ElementRef,
EventEmitter,
HostListener,
inject,
Inject,
Input,
OnDestroy,
OnInit,
Output,
Renderer2,
RendererStyleFlags2,
TemplateRef,
ViewChild
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { Observable, ReplaySubject, Subject } from 'rxjs'; import { Observable, ReplaySubject, Subject } from 'rxjs';
import { auditTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators'; import { auditTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { SelectionCompareFn, TypeaheadSettings } from '../_models/typeahead-settings'; import { SelectionCompareFn, TypeaheadSettings } from '../_models/typeahead-settings';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
/** /**
@ -148,11 +167,11 @@ const ANIMATION_SPEED = 200;
]) ])
] ]
}) })
export class TypeaheadComponent implements OnInit, OnDestroy { export class TypeaheadComponent implements OnInit {
/** /**
* Settings for the typeahead * Settings for the typeahead
*/ */
@Input() settings!: TypeaheadSettings<any>; @Input({required: true}) settings!: TypeaheadSettings<any>;
/** /**
* When true, will reset field to no selections. When false, will reset to saved data * When true, will reset field to no selections. When false, will reset to saved data
*/ */
@ -173,6 +192,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
@Output() newItemAdded = new EventEmitter<any[] | any>(); @Output() newItemAdded = new EventEmitter<any[] | any>();
@Output() onUnlock = new EventEmitter<void>(); @Output() onUnlock = new EventEmitter<void>();
@Output() lockedChange = new EventEmitter<boolean>(); @Output() lockedChange = new EventEmitter<boolean>();
private readonly destroyRef = inject(DestroyRef);
@ViewChild('input') inputElem!: ElementRef<HTMLInputElement>; @ViewChild('input') inputElem!: ElementRef<HTMLInputElement>;
@ -189,23 +209,16 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
typeaheadControl!: FormControl; typeaheadControl!: FormControl;
typeaheadForm!: FormGroup; typeaheadForm!: FormGroup;
private readonly onDestroy = new Subject<void>();
constructor(private renderer2: Renderer2, @Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef) { } constructor(private renderer2: Renderer2, @Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef) { }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
ngOnInit() { ngOnInit() {
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((resetToEmpty: boolean) => { this.reset.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((resetToEmpty: boolean) => {
this.clearSelections(resetToEmpty); this.clearSelections(resetToEmpty);
this.init(); this.init();
}); });
if (this.focus) { if (this.focus) {
this.focus.pipe(takeUntil(this.onDestroy)).subscribe((id: string) => { this.focus.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((id: string) => {
if (this.settings.id !== id) return; if (this.settings.id !== id) return;
this.onInputFocus(); this.onInputFocus();
}); });
@ -258,7 +271,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
switchMap((val: string) => { switchMap((val: string) => {
this.isLoadingOptions = true; this.isLoadingOptions = true;
return this.settings.fetchFn(val.trim()).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item)))); return this.settings.fetchFn(val.trim()).pipe(takeUntilDestroyed(this.destroyRef), map((items: any[]) => items.filter(item => this.filterSelected(item))));
}), }),
tap((filteredOptions: any[]) => { tap((filteredOptions: any[]) => {
this.isLoadingOptions = false; this.isLoadingOptions = false;
@ -272,7 +285,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}), }),
shareReplay(), shareReplay(),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
@ -398,7 +411,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
} }
this.toggleSelection(opt); this.toggleSelection(opt);
console.log('Selected ', opt);
this.resetField(); this.resetField();
this.onInputFocus(); this.onInputFocus();
@ -411,7 +423,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
const newItem = this.settings.addTransformFn(title); const newItem = this.settings.addTransformFn(title);
this.newItemAdded.emit(newItem); this.newItemAdded.emit(newItem);
this.toggleSelection(newItem); this.toggleSelection(newItem);
console.log('Selected ', newItem);
this.resetField(); this.resetField();
this.onInputFocus(); this.onInputFocus();
@ -493,7 +504,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
if (!this.typeaheadControl.dirty) return; // Do we need this? if (!this.typeaheadControl.dirty) return; // Do we need this?
// Check if this new option will interfere with any existing ones not shown // Check if this new option will interfere with any existing ones not shown
if (typeof this.settings.compareFnForAdd == 'function') { if (typeof this.settings.compareFnForAdd == 'function') {
console.log('filtered options: ', this.optionSelection.selected()); console.log('filtered options: ', this.optionSelection.selected());
const willDuplicateExist = this.settings.compareFnForAdd(this.optionSelection.selected(), inputText); const willDuplicateExist = this.settings.compareFnForAdd(this.optionSelection.selected(), inputText);
@ -514,7 +525,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
return; return;
} }
} }
this.showAddItem = true; this.showAddItem = true;
if (this.showAddItem) { if (this.showAddItem) {

View File

@ -1,26 +0,0 @@
import { ThemeProvider } from '../../_models/preferences/site-theme';
import { SiteThemeProviderPipe } from './site-theme-provider.pipe';
describe('SiteThemeProviderPipe', () => {
let siteThemeProviderPipe: SiteThemeProviderPipe;
beforeEach(() => {
siteThemeProviderPipe = new SiteThemeProviderPipe();
})
it('translates system to System', () => {
expect(siteThemeProviderPipe.transform(ThemeProvider.System)).toBe('System');
});
it('translates user to User', () => {
expect(siteThemeProviderPipe.transform(ThemeProvider.User)).toBe('User');
});
it('translates null to empty string', () => {
expect(siteThemeProviderPipe.transform(null)).toBe('');
});
it('translates undefined to empty string', () => {
expect(siteThemeProviderPipe.transform(undefined)).toBe('');
});
});

View File

@ -1,17 +1,25 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import {
import { ToastrService } from 'ngx-toastr'; ChangeDetectionStrategy,
import { Subject } from 'rxjs'; ChangeDetectorRef,
import { take, takeUntil } from 'rxjs/operators'; Component, DestroyRef,
import { ConfirmService } from 'src/app/shared/confirm.service'; ElementRef, inject,
import { AccountService } from 'src/app/_services/account.service'; Input,
import { Clipboard } from '@angular/cdk/clipboard'; OnInit,
ViewChild
} from '@angular/core';
import {ToastrService} from 'ngx-toastr';
import {ConfirmService} from 'src/app/shared/confirm.service';
import {AccountService} from 'src/app/_services/account.service';
import {Clipboard} from '@angular/cdk/clipboard';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-api-key', selector: 'app-api-key',
templateUrl: './api-key.component.html', templateUrl: './api-key.component.html',
styleUrls: ['./api-key.component.scss'] styleUrls: ['./api-key.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ApiKeyComponent implements OnInit, OnDestroy { export class ApiKeyComponent implements OnInit {
@Input() title: string = 'API Key'; @Input() title: string = 'API Key';
@Input() showRefresh: boolean = true; @Input() showRefresh: boolean = true;
@ -19,13 +27,14 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
@Input() tooltipText: string = ''; @Input() tooltipText: string = '';
@ViewChild('apiKey') inputElem!: ElementRef; @ViewChild('apiKey') inputElem!: ElementRef;
key: string = ''; key: string = '';
private readonly onDestroy = new Subject<void>(); private readonly destroyRef = inject(DestroyRef);
constructor(private confirmService: ConfirmService, private accountService: AccountService, private toastr: ToastrService, private clipboard: Clipboard) { }
constructor(private confirmService: ConfirmService, private accountService: AccountService, private toastr: ToastrService, private clipboard: Clipboard,
private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
let key = ''; let key = '';
if (user) { if (user) {
key = user.apiKey; key = user.apiKey;
@ -35,19 +44,16 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
if (this.transform != undefined) { if (this.transform != undefined) {
this.key = this.transform(key); this.key = this.transform(key);
this.cdRef.markForCheck();
} }
}); });
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
async copy() { async copy() {
this.inputElem.nativeElement.select(); this.inputElem.nativeElement.select();
this.clipboard.copy(this.inputElem.nativeElement.value); this.clipboard.copy(this.inputElem.nativeElement.value);
this.inputElem.nativeElement.setSelectionRange(0, 0); this.inputElem.nativeElement.setSelectionRange(0, 0);
this.cdRef.markForCheck();
} }
async refresh() { async refresh() {
@ -56,6 +62,7 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
} }
this.accountService.resetApiKey().subscribe(newKey => { this.accountService.resetApiKey().subscribe(newKey => {
this.key = newKey; this.key = newKey;
this.cdRef.markForCheck();
this.toastr.success('API Key reset'); this.toastr.success('API Key reset');
}); });
} }
@ -63,6 +70,7 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
selectAll() { selectAll() {
if (this.inputElem) { if (this.inputElem) {
this.inputElem.nativeElement.setSelectionRange(0, this.key.length); this.inputElem.nativeElement.setSelectionRange(0, this.key.length);
this.cdRef.markForCheck();
} }
} }

View File

@ -1,10 +1,19 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { Observable, of, Subject, takeUntil, shareReplay, map, take } from 'rxjs'; import { Observable, of, Subject, takeUntil, shareReplay, map, take } from 'rxjs';
import { AgeRestriction } from 'src/app/_models/metadata/age-restriction'; import { AgeRestriction } from 'src/app/_models/metadata/age-restriction';
import { AgeRating } from 'src/app/_models/metadata/age-rating'; import { AgeRating } from 'src/app/_models/metadata/age-rating';
import { User } from 'src/app/_models/user'; import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service'; import { AccountService } from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-change-age-restriction', selector: 'app-change-age-restriction',
@ -12,7 +21,7 @@ import { AccountService } from 'src/app/_services/account.service';
styleUrls: ['./change-age-restriction.component.scss'], styleUrls: ['./change-age-restriction.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ChangeAgeRestrictionComponent implements OnInit, OnDestroy { export class ChangeAgeRestrictionComponent implements OnInit {
user: User | undefined = undefined; user: User | undefined = undefined;
hasChangeAgeRestrictionAbility: Observable<boolean> = of(false); hasChangeAgeRestrictionAbility: Observable<boolean> = of(false);
@ -20,22 +29,21 @@ export class ChangeAgeRestrictionComponent implements OnInit, OnDestroy {
selectedRestriction!: AgeRestriction; selectedRestriction!: AgeRestriction;
originalRestriction!: AgeRestriction; originalRestriction!: AgeRestriction;
reset: EventEmitter<AgeRestriction> = new EventEmitter(); reset: EventEmitter<AgeRestriction> = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
get AgeRating() { return AgeRating; } get AgeRating() { return AgeRating; }
private onDestroy = new Subject<void>();
constructor(private accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { } constructor(private accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), shareReplay(), take(1)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), take(1)).subscribe(user => {
if (!user) return; if (!user) return;
this.user = user; this.user = user;
this.originalRestriction = this.user.ageRestriction; this.originalRestriction = this.user.ageRestriction;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.hasChangeAgeRestrictionAbility = this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), shareReplay(), map(user => { this.hasChangeAgeRestrictionAbility = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), map(user => {
return user !== undefined && (!this.accountService.hasAdminRole(user) && this.accountService.hasChangeAgeRestrictionRole(user)); return user !== undefined && (!this.accountService.hasAdminRole(user) && this.accountService.hasChangeAgeRestrictionRole(user));
})); }));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -45,11 +53,6 @@ export class ChangeAgeRestrictionComponent implements OnInit, OnDestroy {
this.selectedRestriction = restriction; this.selectedRestriction = restriction;
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
resetForm() { resetForm() {
if (!this.user) return; if (!this.user) return;
this.reset.emit(this.originalRestriction); this.reset.emit(this.originalRestriction);

View File

@ -1,17 +1,19 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms'; import {FormControl, FormGroup, Validators} from '@angular/forms';
import { ToastrService } from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import { Observable, of, Subject, takeUntil, shareReplay, map, tap, take } from 'rxjs'; import {Observable, of, shareReplay, take} from 'rxjs';
import { UpdateEmailResponse } from 'src/app/_models/auth/update-email-response'; import {UpdateEmailResponse} from 'src/app/_models/auth/update-email-response';
import { User } from 'src/app/_models/user'; import {User} from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service'; import {AccountService} from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-change-email', selector: 'app-change-email',
templateUrl: './change-email.component.html', templateUrl: './change-email.component.html',
styleUrls: ['./change-email.component.scss'] styleUrls: ['./change-email.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ChangeEmailComponent implements OnInit, OnDestroy { export class ChangeEmailComponent implements OnInit {
form: FormGroup = new FormGroup({}); form: FormGroup = new FormGroup({});
user: User | undefined = undefined; user: User | undefined = undefined;
@ -21,17 +23,16 @@ export class ChangeEmailComponent implements OnInit, OnDestroy {
isViewMode: boolean = true; isViewMode: boolean = true;
emailLink: string = ''; emailLink: string = '';
emailConfirmed: boolean = true; emailConfirmed: boolean = true;
private readonly destroyRef = inject(DestroyRef);
public get email() { return this.form.get('email'); } public get email() { return this.form.get('email'); }
private onDestroy = new Subject<void>();
makeLink: (val: string) => string = (val: string) => {return this.emailLink}; makeLink: (val: string) => string = (val: string) => {return this.emailLink};
constructor(public accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { } constructor(public accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), shareReplay(), take(1)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), take(1)).subscribe(user => {
this.user = user; this.user = user;
this.form.addControl('email', new FormControl(user?.email, [Validators.required, Validators.email])); this.form.addControl('email', new FormControl(user?.email, [Validators.required, Validators.email]));
this.form.addControl('password', new FormControl('', [Validators.required])); this.form.addControl('password', new FormControl('', [Validators.required]));
@ -41,13 +42,6 @@ export class ChangeEmailComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
}); });
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
} }
resetForm() { resetForm() {
@ -71,9 +65,9 @@ export class ChangeEmailComponent implements OnInit, OnDestroy {
} else { } else {
this.toastr.success('The server is not publicly accessible. Ask the admin to fetch your confirmation link from the logs'); this.toastr.success('The server is not publicly accessible. Ask the admin to fetch your confirmation link from the logs');
} }
this.resetForm();
this.isViewMode = true; this.isViewMode = true;
this.resetForm();
}, err => { }, err => {
this.errors = err; this.errors = err;
}) })

View File

@ -1,9 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { map, Observable, of, shareReplay, Subject, take, takeUntil } from 'rxjs'; import { map, Observable, of, shareReplay, Subject, take, takeUntil } from 'rxjs';
import { User } from 'src/app/_models/user'; import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service'; import { AccountService } from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-change-password', selector: 'app-change-password',
@ -19,23 +28,23 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
observableHandles: Array<any> = []; observableHandles: Array<any> = [];
passwordsMatch = false; passwordsMatch = false;
resetPasswordErrors: string[] = []; resetPasswordErrors: string[] = [];
isViewMode: boolean = true; isViewMode: boolean = true;
private readonly destroyRef = inject(DestroyRef);
public get password() { return this.passwordChangeForm.get('password'); } public get password() { return this.passwordChangeForm.get('password'); }
public get confirmPassword() { return this.passwordChangeForm.get('confirmPassword'); } public get confirmPassword() { return this.passwordChangeForm.get('confirmPassword'); }
private onDestroy = new Subject<void>();
constructor(private accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { } constructor(private accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), shareReplay(), take(1)).subscribe(user => { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), take(1)).subscribe(user => {
this.user = user; this.user = user;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.hasChangePasswordAbility = this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), shareReplay(), map(user => { this.hasChangePasswordAbility = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), shareReplay(), map(user => {
return user !== undefined && (this.accountService.hasAdminRole(user) || this.accountService.hasChangePasswordRole(user)); return user !== undefined && (this.accountService.hasAdminRole(user) || this.accountService.hasChangePasswordRole(user));
})); }));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -53,8 +62,6 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
this.observableHandles.forEach(o => o.unsubscribe()); this.observableHandles.forEach(o => o.unsubscribe());
this.onDestroy.next();
this.onDestroy.complete();
} }
resetPasswordForm() { resetPasswordForm() {

View File

@ -1,10 +1,23 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter,
inject,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { Device } from 'src/app/_models/device/device'; import { Device } from 'src/app/_models/device/device';
import { DevicePlatform, devicePlatforms } from 'src/app/_models/device/device-platform'; import { DevicePlatform, devicePlatforms } from 'src/app/_models/device/device-platform';
import { DeviceService } from 'src/app/_services/device.service'; import { DeviceService } from 'src/app/_services/device.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-edit-device', selector: 'app-edit-device',
@ -12,29 +25,29 @@ import { DeviceService } from 'src/app/_services/device.service';
styleUrls: ['./edit-device.component.scss'], styleUrls: ['./edit-device.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EditDeviceComponent implements OnInit, OnChanges, OnDestroy { export class EditDeviceComponent implements OnInit, OnChanges {
@Input() device: Device | undefined; @Input() device: Device | undefined;
@Output() deviceAdded: EventEmitter<void> = new EventEmitter(); @Output() deviceAdded: EventEmitter<void> = new EventEmitter();
@Output() deviceUpdated: EventEmitter<Device> = new EventEmitter(); @Output() deviceUpdated: EventEmitter<Device> = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
settingsForm: FormGroup = new FormGroup({}); settingsForm: FormGroup = new FormGroup({});
devicePlatforms = devicePlatforms; devicePlatforms = devicePlatforms;
private readonly onDestroy = new Subject<void>();
constructor(public deviceService: DeviceService, private toastr: ToastrService, constructor(public deviceService: DeviceService, private toastr: ToastrService,
private readonly cdRef: ChangeDetectorRef) { } private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.settingsForm.addControl('name', new FormControl(this.device?.name || '', [Validators.required])); this.settingsForm.addControl('name', new FormControl(this.device?.name || '', [Validators.required]));
this.settingsForm.addControl('email', new FormControl(this.device?.emailAddress || '', [Validators.required, Validators.email])); this.settingsForm.addControl('email', new FormControl(this.device?.emailAddress || '', [Validators.required, Validators.email]));
this.settingsForm.addControl('platform', new FormControl(this.device?.platform || DevicePlatform.Custom, [Validators.required])); this.settingsForm.addControl('platform', new FormControl(this.device?.platform || DevicePlatform.Custom, [Validators.required]));
// If user has filled in email and the platform hasn't been explicitly updated, try to update it for them // If user has filled in email and the platform hasn't been explicitly updated, try to update it for them
this.settingsForm.get('email')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(email => { this.settingsForm.get('email')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(email => {
if (this.settingsForm.get('platform')?.dirty) return; if (this.settingsForm.get('platform')?.dirty) return;
if (email === null || email === undefined || email === '') return; if (email === null || email === undefined || email === '') return;
if (email.endsWith('@kindle.com')) this.settingsForm.get('platform')?.setValue(DevicePlatform.Kindle); if (email.endsWith('@kindle.com')) this.settingsForm.get('platform')?.setValue(DevicePlatform.Kindle);
@ -54,11 +67,6 @@ export class EditDeviceComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
addDevice() { addDevice() {
if (this.device !== undefined) { if (this.device !== undefined) {
this.deviceService.updateDevice(this.device.id, this.settingsForm.value.name, parseInt(this.settingsForm.value.platform, 10), this.settingsForm.value.email).subscribe(() => { this.deviceService.updateDevice(this.device.id, this.settingsForm.value.name, parseInt(this.settingsForm.value.platform, 10), this.settingsForm.value.email).subscribe(() => {

View File

@ -1,10 +1,19 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs'; import { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
import { ThemeService } from 'src/app/_services/theme.service'; import { ThemeService } from 'src/app/_services/theme.service';
import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme'; import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme';
import { User } from 'src/app/_models/user'; import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service'; import { AccountService } from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-theme-manager', selector: 'app-theme-manager',
@ -12,23 +21,23 @@ import { AccountService } from 'src/app/_services/account.service';
styleUrls: ['./theme-manager.component.scss'], styleUrls: ['./theme-manager.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ThemeManagerComponent implements OnDestroy { export class ThemeManagerComponent {
currentTheme: SiteTheme | undefined; currentTheme: SiteTheme | undefined;
isAdmin: boolean = false; isAdmin: boolean = false;
user: User | undefined; user: User | undefined;
private readonly destroyRef = inject(DestroyRef);
private readonly onDestroy = new Subject<void>();
get ThemeProvider() { get ThemeProvider() {
return ThemeProvider; return ThemeProvider;
} }
constructor(public themeService: ThemeService, private accountService: AccountService, constructor(public themeService: ThemeService, private accountService: AccountService,
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) {
themeService.currentTheme$.pipe(takeUntil(this.onDestroy), distinctUntilChanged()).subscribe(theme => { themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef), distinctUntilChanged()).subscribe(theme => {
this.currentTheme = theme; this.currentTheme = theme;
this.cdRef.markForCheck();
}); });
accountService.currentUser$.pipe(take(1)).subscribe(user => { accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -40,11 +49,6 @@ export class ThemeManagerComponent implements OnDestroy {
}); });
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
applyTheme(theme: SiteTheme) { applyTheme(theme: SiteTheme) {
if (this.user) { if (this.user) {

View File

@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
OnDestroy,
OnInit
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { take, takeUntil } from 'rxjs/operators'; import { take, takeUntil } from 'rxjs/operators';
@ -23,6 +31,7 @@ import { forkJoin, Subject } from 'rxjs';
import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component'; import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
import { BookService } from 'src/app/book-reader/_services/book.service'; import { BookService } from 'src/app/book-reader/_services/book.service';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum AccordionPanelID { enum AccordionPanelID {
ImageReader = 'image-reader', ImageReader = 'image-reader',
@ -79,8 +88,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
opdsEnabled: boolean = false; opdsEnabled: boolean = false;
baseUrl: string = ''; baseUrl: string = '';
makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)}; makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)};
private readonly destroyRef = inject(DestroyRef);
private onDestroy = new Subject<void>();
get AccordionPanelID() { get AccordionPanelID() {
return AccordionPanelID; return AccordionPanelID;
@ -165,7 +173,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(mode => { this.settingsForm.get('bookReaderImmersiveMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(mode => {
if (mode) { if (mode) {
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true); this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -176,8 +184,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
this.observableHandles.forEach(o => o.unsubscribe()); this.observableHandles.forEach(o => o.unsubscribe());
this.onDestroy.next();
this.onDestroy.complete();
} }

View File

@ -1,8 +1,20 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { AfterContentChecked, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; import {
AfterContentChecked,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef,
EventEmitter,
HostListener,
inject,
Inject,
OnInit,
ViewChild
} from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Subject, take, debounceTime, takeUntil } from 'rxjs'; import { take, debounceTime } from 'rxjs';
import { BulkSelectionService } from 'src/app/cards/bulk-selection.service'; import { BulkSelectionService } from 'src/app/cards/bulk-selection.service';
import { FilterSettings } from 'src/app/metadata-filter/filter-settings'; import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service'; import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
@ -19,6 +31,7 @@ import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service'; import { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service';
import { ScrollService } from 'src/app/_services/scroll.service'; import { ScrollService } from 'src/app/_services/scroll.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
@ -27,10 +40,11 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./want-to-read.component.scss'], styleUrls: ['./want-to-read.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class WantToReadComponent implements OnInit, OnDestroy, AfterContentChecked { export class WantToReadComponent implements OnInit, AfterContentChecked {
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined; @ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined; @ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
private readonly destroyRef = inject(DestroyRef);
isLoading: boolean = true; isLoading: boolean = true;
series: Array<Series> = []; series: Array<Series> = [];
@ -46,12 +60,11 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
filterOpen: EventEmitter<boolean> = new EventEmitter(); filterOpen: EventEmitter<boolean> = new EventEmitter();
private onDestroy: Subject<void> = new Subject<void>();
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`; trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
bulkActionCallback = (action: ActionItem<any>, data: any) => { bulkActionCallback = (action: ActionItem<any>, data: any) => {
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series'); const selectedSeriesIndices = this.bulkSelectionService.getSelectedCardsForSource('series');
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + '')); const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndices.includes(index + ''));
switch (action.action) { switch (action.action) {
case Action.RemoveFromWantToReadList: case Action.RemoveFromWantToReadList:
@ -62,7 +75,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
break; break;
} }
} }
collectionTag: any; collectionTag: any;
tagImage: any; tagImage: any;
@ -77,9 +90,9 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)'; return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
} }
constructor(public imageService: ImageService, private router: Router, private route: ActivatedRoute, constructor(public imageService: ImageService, private router: Router, private route: ActivatedRoute,
private seriesService: SeriesService, private titleService: Title, private seriesService: SeriesService, private titleService: Title,
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService, public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private hubService: MessageHubService, private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private hubService: MessageHubService,
private jumpbarService: JumpbarService) { private jumpbarService: JumpbarService) {
@ -91,25 +104,25 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
this.filterActiveCheck = this.filterUtilityService.createSeriesFilter(); this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => { this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
if (event.event === EVENTS.SeriesRemoved) { if (event.event === EVENTS.SeriesRemoved) {
const seriesRemoved = event.payload as SeriesRemovedEvent; const seriesRemoved = event.payload as SeriesRemovedEvent;
if (!this.utilityService.deepEqual(this.filter, this.filterActiveCheck)) { if (!this.utilityService.deepEqual(this.filter, this.filterActiveCheck)) {
this.loadPage(); this.loadPage();
return; return;
} }
this.series = this.series.filter(s => s.id != seriesRemoved.seriesId); this.series = this.series.filter(s => s.id != seriesRemoved.seriesId);
this.seriesPagination.totalItems--; this.seriesPagination.totalItems--;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.refresh.emit(); this.refresh.emit();
} }
}); });
} }
ngOnInit(): void { ngOnInit(): void {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy), debounceTime(2000)).subscribe(event => { this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(2000)).subscribe(event => {
if (event.event === EVENTS.SeriesRemoved) { if (event.event === EVENTS.SeriesRemoved) {
this.loadPage(); this.loadPage();
} }
@ -120,11 +133,6 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
this.scrollService.setScrollContainer(this.scrollingBlock); this.scrollService.setScrollContainer(this.scrollingBlock);
} }
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event']) @HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) { handleKeypress(event: KeyboardEvent) {
if (event.key === KEY_CODES.SHIFT) { if (event.key === KEY_CODES.SHIFT) {
@ -150,7 +158,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck); this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.isLoading = true; this.isLoading = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.seriesService.getWantToRead(undefined, undefined, this.filter).pipe(take(1)).subscribe(paginatedList => { this.seriesService.getWantToRead(undefined, undefined, this.filter).pipe(take(1)).subscribe(paginatedList => {
this.series = paginatedList.result; this.series = paginatedList.result;
this.seriesPagination = paginatedList.pagination; this.seriesPagination = paginatedList.pagination;
@ -163,7 +171,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
updateFilter(data: FilterEvent) { updateFilter(data: FilterEvent) {
this.filter = data.filter; this.filter = data.filter;
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, this.filter); if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, this.filter);
this.loadPage(); this.loadPage();
} }

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.2.13" "version": "0.7.2.14"
}, },
"servers": [ "servers": [
{ {