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

21749
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,6 +14,7 @@ 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',
@ -26,8 +27,9 @@ export enum Role {
@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,8 +44,6 @@ 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),
@ -53,11 +53,6 @@ export class AccountService implements OnDestroy {
.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)
); );
} }
@ -143,7 +138,7 @@ export class AccountService implements OnDestroy {
map((user: User) => { map((user: User) => {
return user; return user;
}), }),
takeUntil(this.onDestroy) takeUntilDestroyed(this.destroyRef)
); );
} }
@ -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,7 +228,7 @@ 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 {

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);

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';
@ -31,7 +42,6 @@ export class ThemeService implements OnDestroy {
*/ */
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,11 +66,6 @@ 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();
} }

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,7 +13,7 @@ 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> = [];

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',
@ -19,9 +31,9 @@ export class ManageAlertsComponent implements OnInit {
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();

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,8 +35,7 @@ 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,
@ -37,7 +45,7 @@ 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)))
@ -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');
@ -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) {

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
*/ */

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();
@ -88,18 +97,13 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
} }
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();

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();
@ -123,7 +131,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
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];
}); });
@ -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([

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,6 +33,7 @@ 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,
@ -38,12 +48,13 @@ 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.
@ -75,8 +86,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;
} }
@ -101,11 +110,10 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
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()
); );
@ -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();

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,12 +127,12 @@ 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,
@ -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,12 +188,12 @@ 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;
@ -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,14 +50,13 @@ 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,7 +19,7 @@ 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
*/ */

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,6 +79,7 @@ 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 = '';
@ -77,8 +89,6 @@ export class ListItemComponent implements OnInit, OnDestroy {
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 '';
@ -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,7 +18,7 @@ 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;
/** /**

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;
@ -49,9 +60,9 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
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,9 +38,10 @@ 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,
@ -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
@ -39,19 +49,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
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;
@ -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;

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:
@ -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;
@ -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) {

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();
} }

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;
@ -53,7 +67,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
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;
@ -67,11 +81,11 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
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)
@ -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;
@ -104,7 +118,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
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;
@ -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,6 +1,6 @@
<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=" "

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;
@ -61,8 +74,6 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
shouldRenderDouble$!: Observable<boolean>; shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;} get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;} get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;} get LayoutMode() {return LayoutMode;}
@ -77,30 +88,30 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
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');
@ -163,11 +174,6 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
).subscribe(() => {}); ).subscribe(() => {});
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() { shouldRenderDouble() {
if (!this.isValid()) return false; if (!this.isValid()) return false;

View File

@ -1,6 +1,6 @@
<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=" "

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;
@ -61,8 +74,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
shouldRenderDouble$!: Observable<boolean>; shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;} get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;} get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;} get LayoutMode() {return LayoutMode;}
@ -77,30 +88,30 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
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');
@ -164,10 +175,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {}); ).subscribe(() => {});
} }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() { shouldRenderDouble() {
if (!this.isValid()) return false; if (!this.isValid()) return false;

View File

@ -1,6 +1,6 @@
<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=" "

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";
/** /**
* 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
@ -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;
@ -63,8 +76,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
*/ */
shouldRenderDouble$!: Observable<boolean>; shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;} get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;} get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;} get LayoutMode() {return LayoutMode;}
@ -79,30 +90,30 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
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;

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>;
@ -149,9 +166,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return this.webtoonImageWidth > (innerWidth || document.body.clientWidth); return this.webtoonImageWidth > (innerWidth || document.body.clientWidth);
} }
private readonly onDestroy = new Subject<void>();
constructor(private readerService: ReaderService, private renderer: Renderer2, 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) {
@ -172,8 +186,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.intersectionObserver.disconnect(); this.intersectionObserver.disconnect();
this.onDestroy.next();
this.onDestroy.complete();
} }
/** /**
@ -183,7 +195,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
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,7 +221,7 @@ 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');
@ -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();

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;
@ -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;
@ -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);
} }
@ -494,7 +507,7 @@ 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();
@ -505,7 +518,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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(
@ -514,11 +527,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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();
} }

View File

@ -1,6 +1,6 @@
<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"

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,8 +52,6 @@ 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 ReaderMode() {return ReaderMode;}
get LayoutMode() {return LayoutMode;} get LayoutMode() {return LayoutMode;}
@ -52,14 +63,14 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
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(
@ -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,11 +153,6 @@ 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;

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;
} }
@ -99,7 +109,7 @@ 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();
@ -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,
@ -162,7 +172,7 @@ 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
@ -171,7 +181,7 @@ 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];
} }

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)
*/ */
@ -59,15 +68,13 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
} }
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,7 +93,7 @@ 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()
); );

View File

@ -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,6 +78,7 @@ 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;
@ -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);
} }
@ -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,11 +142,6 @@ 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();

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

@ -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

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,8 +46,6 @@ 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; }
@ -70,7 +79,7 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
} }
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);
} }

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({});

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
@ -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) {

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
*/ */
@ -42,7 +53,7 @@ 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,8 +42,6 @@ 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,
@ -43,7 +51,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
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):

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> = [];
@ -57,7 +68,6 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
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; }
@ -99,19 +109,13 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
} }
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);

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,12 +22,12 @@ 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,12 +21,11 @@ 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,7 +34,7 @@ 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(
@ -56,7 +57,7 @@ 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(),
); );
@ -64,18 +65,12 @@ export class ReadingActivityComponent implements OnInit, OnDestroy {
} }
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,15 +28,16 @@ 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() {
@ -54,14 +56,14 @@ export class ServerStatsComponent implements OnDestroy {
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 });

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

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,13 +21,13 @@ 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({
@ -27,7 +36,7 @@ export class TopReadersComponent implements OnInit, OnDestroy {
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);
})); }));
@ -43,25 +51,19 @@ export class UserStatsComponent implements OnInit, OnDestroy {
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();

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() {
@ -72,8 +66,8 @@ export class ChangeEmailComponent implements OnInit, OnDestroy {
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',
@ -20,22 +29,22 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
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,17 +25,17 @@ 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) { }
@ -34,7 +47,7 @@ export class EditDeviceComponent implements OnInit, OnChanges, OnDestroy {
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,13 +21,12 @@ 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;
@ -27,8 +35,9 @@ export class ThemeManagerComponent implements OnDestroy {
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:
@ -91,7 +104,7 @@ 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)) {
@ -109,7 +122,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
} }
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) {

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": [
{ {