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

View File

@ -1,5 +1,5 @@
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 { filter, map, switchMap, takeUntil } from 'rxjs/operators';
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 { AgeRestriction } from '../_models/metadata/age-restriction';
import { TextResonse } from '../_types/text-response';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
export enum Role {
Admin = 'Admin',
@ -26,8 +27,9 @@ export enum Role {
@Injectable({
providedIn: 'root'
})
export class AccountService implements OnDestroy {
export class AccountService {
private readonly destroyRef = inject(DestroyRef);
baseUrl = environment.apiUrl;
userKey = 'kavita-user';
public lastLoginKey = 'kavita-lastlogin';
@ -42,8 +44,6 @@ export class AccountService implements OnDestroy {
*/
private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined;
private readonly onDestroy = new Subject<void>();
constructor(private httpClient: HttpClient, private router: Router,
private messageHub: MessageHubService, private themeService: ThemeService) {
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
@ -53,11 +53,6 @@ export class AccountService implements OnDestroy {
.subscribe(() => {});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
hasAdminRole(user: User) {
return user && user.roles.includes(Role.Admin);
}
@ -91,7 +86,7 @@ export class AccountService implements OnDestroy {
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) => {
return user;
}),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
}
@ -223,7 +218,7 @@ export class AccountService implements OnDestroy {
this.setCurrentUser(this.currentUser);
}
return pref;
}), takeUntil(this.onDestroy));
}), takeUntilDestroyed(this.destroyRef));
}
updatePreferences(userPreferences: Preferences) {
@ -233,7 +228,7 @@ export class AccountService implements OnDestroy {
this.setCurrentUser(this.currentUser);
}
return settings;
}), takeUntil(this.onDestroy));
}), takeUntilDestroyed(this.destroyRef));
}
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 { takeUntil } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { ThemeService } from './theme.service';
import { RecentlyAddedItem } from '../_models/recently-added-item';
import { AccountService } from './account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Injectable({
providedIn: 'root'
})
export class ImageService implements OnDestroy {
export class ImageService {
private readonly destroyRef = inject(DestroyRef);
baseUrl = environment.apiUrl;
apiKey: string = '';
encodedKey: string = '';
@ -19,10 +20,8 @@ export class ImageService implements OnDestroy {
public resetCoverImage = 'assets/images/image-reset-cover-min.png';
public errorWebLinkImage = 'assets/images/broken-white-32x32.png';
private onDestroy: Subject<void> = new Subject();
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()) {
this.placeholderImage = 'assets/images/image-placeholder.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) {
this.apiKey = user.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) {
if (item.chapterId === 0) {
return this.getVolumeCoverImage(item.volumeId);

View File

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

View File

@ -1,6 +1,15 @@
import { DOCUMENT } from '@angular/common';
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 { ToastrService } from 'ngx-toastr';
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 { TextResonse } from '../_types/text-response';
import { EVENTS, MessageHubService } from './message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Injectable({
providedIn: 'root'
})
export class ThemeService implements OnDestroy {
export class ThemeService {
private readonly destroyRef = inject(DestroyRef);
public defaultTheme: string = 'dark';
public defaultBookTheme: string = 'Dark';
@ -31,7 +42,6 @@ export class ThemeService implements OnDestroy {
*/
private themeCache: Array<SiteTheme> = [];
private readonly onDestroy = new Subject<void>();
private renderer: Renderer2;
private baseUrl = environment.apiUrl;
@ -42,7 +52,7 @@ export class ThemeService implements OnDestroy {
this.getThemes();
messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(message => {
messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => {
if (message.event !== EVENTS.NotificationProgress) return;
const notificationEvent = (message.payload as NotificationProgressEvent);
@ -56,11 +66,6 @@ export class ThemeService implements OnDestroy {
});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
getColorScheme() {
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 {
@Input() member!: Member;
@Input({required: true}) member!: Member;
errorMessage = '';
resetPasswordForm: FormGroup = new FormGroup({
password: new FormControl('', [Validators.required]),

View File

@ -13,7 +13,7 @@ import { AccountService } from 'src/app/_services/account.service';
})
export class EditUserComponent implements OnInit {
@Input() member!: Member;
@Input({required: true}) member!: Member;
selectedRoles: Array<string> = [];
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 { SortEvent, SortableHeader, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { KavitaMediaError } from '../_models/media-error';
import { ServerService } from 'src/app/_services/server.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { FormControl, FormGroup } from '@angular/forms';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-manage-alerts',
@ -19,9 +31,9 @@ export class ManageAlertsComponent implements OnInit {
private readonly serverService = inject(ServerService);
private readonly messageHub = inject(MessageHubService);
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$: 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 { ToastrService } from 'ngx-toastr';
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 { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-manage-library',
@ -17,7 +26,7 @@ import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hu
styleUrls: ['./manage-library.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManageLibraryComponent implements OnInit, OnDestroy {
export class ManageLibraryComponent implements OnInit {
libraries: Library[] = [];
loading = false;
@ -26,8 +35,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
*/
deletionInProgress: boolean = false;
libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`;
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
constructor(private modalService: NgbModal, private libraryService: LibraryService,
private toastr: ToastrService, private confirmService: ConfirmService,
@ -37,7 +45,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
this.getLibraries();
// 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),
distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) =>
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>) {
if (curr.event !== prev.event) return true;
if (curr.event === EVENTS.ScanSeries) {
@ -91,7 +94,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
editLibrary(library: Library) {
const modalRef = this.modalService.open(LibrarySettingsModalComponent, { size: 'xl' });
modalRef.componentInstance.library = library;
modalRef.closed.pipe(takeUntil(this.onDestroy)).subscribe(refresh => {
modalRef.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(refresh => {
if (refresh) {
this.getLibraries();
}
@ -100,7 +103,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
addLibrary() {
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) {
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 { ActivatedRoute, Router } from '@angular/router';
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 { MessageHubService, Message, EVENTS } from 'src/app/_services/message-hub.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'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AllSeriesComponent implements OnInit, OnDestroy {
export class AllSeriesComponent implements OnInit {
title: string = 'All Series';
series: Series[] = [];
loadingSeries = false;
pagination!: Pagination;
filter: SeriesFilter | undefined = undefined;
onDestroy: Subject<void> = new Subject<void>();
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActiveCheck!: SeriesFilter;
filterActive: boolean = false;
jumpbarKeys: Array<JumpKey> = [];
private readonly destroyRef = inject(DestroyRef);
bulkActionCallback = (action: ActionItem<any>, data: any) => {
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
@ -106,17 +116,12 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
}
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;
this.loadPage();
});
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) {
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 { ActivatedRoute, Router } from '@angular/router';
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 { ScrollService } from 'src/app/_services/scroll.service';
import { PAGING_DIRECTION } from 'src/app/manga-reader/_models/reader-enums';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID {
@ -260,7 +275,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
writingStyle: WritingStyle = WritingStyle.Horizontal;
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
@ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef<HTMLDivElement>;
/**
@ -453,7 +468,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
fromEvent(this.reader.nativeElement, 'scroll')
.pipe(
debounceTime(200),
takeUntil(this.onDestroy))
takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
if (this.isLoading) return;
@ -509,9 +524,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.navService.showNavBar();
this.navService.showSideNav();
this.onDestroy.next();
this.onDestroy.complete();
}
ngOnInit(): void {

View File

@ -1,5 +1,16 @@
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 { Subject, take, takeUntil } from 'rxjs';
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 { BookWhiteTheme } from '../../_models/book-white-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
@ -74,7 +86,7 @@ const mobileBreakpointMarginOverride = 700;
styleUrls: ['./reader-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReaderSettingsComponent implements OnInit, OnDestroy {
export class ReaderSettingsComponent implements OnInit {
/**
* Outputs when clickToPaginate is changed
*/
@ -134,9 +146,7 @@ export class ReaderSettingsComponent implements OnInit, OnDestroy {
* System provided themes
*/
themes: Array<BookTheme> = bookColorThemes;
private onDestroy: Subject<void> = new Subject();
private readonly destroyRef = inject(DestroyRef);
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.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;
if (familyName === 'default') {
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.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.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.clickToPaginateChanged.emit(value);
});
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
this.pageStyles['line-height'] = value + '%';
this.styleUpdate.emit(this.pageStyles);
});
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(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-right'] = value + 'vw';
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.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.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) {
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() {
if (this.user) {
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 {
@Input() chapterId!: number;
@Input() pageNum!: number;
@Input() currentPageAnchor!: string;
@Input({required: true}) chapterId!: number;
@Input({required: true}) pageNum!: number;
@Input({required: true}) currentPageAnchor!: string;
@Input() chapters:Array<BookChapterItem> = [];
@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 {
@Input() title!: string;
@Input({required: true}) title!: string;
/**
* 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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
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 { SeriesService } from 'src/app/_services/series.service';
import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID {
@ -28,9 +38,9 @@ enum TabID {
styleUrls: ['./edit-collection-tags.component.scss'],
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> = [];
selections!: SelectionModel<Series>;
isLoading: boolean = true;
@ -42,8 +52,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
active = TabID.General;
imageUrls: Array<string> = [];
selectedCover: string = '';
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
get hasSomeSelected() {
return this.selections != null && this.selections.hasSomeSelected();
@ -88,18 +97,13 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
}
this.cdRef.markForCheck();
}),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
).subscribe();
this.imageUrls.push(this.imageService.randomize(this.imageService.getCollectionCoverImage(this.tag.id)));
this.loadSeries();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
onPageChange(pageNum: number) {
this.pagination.currentPage = pageNum;
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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
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 { SeriesService } from 'src/app/_services/series.service';
import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID {
General = 0,
@ -38,9 +48,9 @@ enum TabID {
styleUrls: ['./edit-series-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EditSeriesModalComponent implements OnInit, OnDestroy {
export class EditSeriesModalComponent implements OnInit {
@Input() series!: Series;
@Input({required: true}) series!: Series;
seriesVolumes: any[] = [];
isLoadingVolumes = false;
/**
@ -56,9 +66,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
editSeriesForm!: FormGroup;
libraryName: string | undefined = undefined;
size: number = 0;
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
// Typeaheads
ageRatingSettings: TypeaheadSettings<AgeRatingDto> = new TypeaheadSettings();
@ -123,7 +131,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
ngOnInit(): void {
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];
});
@ -180,41 +188,41 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
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.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.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.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.summary = val;
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.ageRatingLocked = true;
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.publicationStatusLocked = true;
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.releaseYearLocked = true;
this.cdRef.markForCheck();
@ -257,10 +265,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
});
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
setupTypeaheads() {
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 { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { BulkSelectionService } from '../bulk-selection.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-bulk-operations',
@ -9,16 +19,15 @@ import { BulkSelectionService } from '../bulk-selection.service';
styleUrls: ['./bulk-operations.component.scss'],
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;
hasMarkAsRead: boolean = false;
hasMarkAsUnread: boolean = false;
actions: Array<ActionItem<any>> = [];
private onDestory: Subject<void> = new Subject();
private readonly destroyRef = inject(DestroyRef);
get Action() {
return Action;
@ -28,7 +37,7 @@ export class BulkOperationsComponent implements OnInit, OnDestroy {
private actionFactoryService: ActionFactoryService) { }
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
this.actions = this.actionFactoryService.applyCallbackToList(actions, this.actionCallback.bind(this));
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) {
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 { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap';
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 { SeriesService } from 'src/app/_services/series.service';
import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID {
General = 0,
@ -38,12 +48,13 @@ enum TabID {
styleUrls: ['./card-detail-drawer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CardDetailDrawerComponent implements OnInit, OnDestroy {
export class CardDetailDrawerComponent implements OnInit {
@Input() parentName = '';
@Input() seriesId: 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.
@ -75,8 +86,6 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
download$: Observable<Download> | null = null;
downloadInProgress: boolean = false;
private readonly onDestroy = new Subject<void>();
get MangaFormat() {
return MangaFormat;
}
@ -101,11 +110,10 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, private router: Router, private libraryService: LibraryService,
private seriesService: SeriesService, private readerService: ReaderService, public metadataService: MetadataService,
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef,
private deviceSerivce: DeviceService) {
private seriesService: SeriesService, private readerService: ReaderService,
public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService, private readonly cdRef: ChangeDetectorRef) {
this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay()
);
@ -157,10 +165,6 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
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 { filter, map, takeUntil } from 'rxjs/operators';
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 { ScrollService } from 'src/app/_services/scroll.service';
import { BulkSelectionService } from '../bulk-selection.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-card-item',
@ -26,7 +38,7 @@ import { BulkSelectionService } from '../bulk-selection.service';
styleUrls: ['./card-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CardItemComponent implements OnInit, OnDestroy {
export class CardItemComponent implements OnInit {
/**
* 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.
*/
@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.
*/
@ -115,12 +127,12 @@ export class CardItemComponent implements OnInit, OnDestroy {
selectionInProgress: boolean = false;
private user: User | undefined;
private readonly destroyRef = inject(DestroyRef);
get MangaFormat(): typeof MangaFormat {
return MangaFormat;
}
private readonly onDestroy = new Subject<void>();
constructor(public imageService: ImageService, private libraryService: LibraryService,
public utilityService: UtilityService, private downloadService: DownloadService,
@ -136,14 +148,14 @@ export class CardItemComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
}
if (this.suppressLibraryLink === false) {
if (!this.suppressLibraryLink) {
if (this.entity !== undefined && this.entity.hasOwnProperty('libraryId')) {
this.libraryId = (this.entity as Series).libraryId;
this.cdRef.markForCheck();
}
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.cdRef.markForCheck();
});
@ -176,12 +188,12 @@ export class CardItemComponent implements OnInit, OnDestroy {
}
this.filterSendTo();
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
this.user = user;
});
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.utilityService.isChapter(this.entity) && updateEvent.chapterId !== 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.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.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;
@ -223,10 +235,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('touchmove', ['$event'])
onTouchMove(event: TouchEvent) {

View File

@ -14,7 +14,7 @@ export class DownloadIndicatorComponent {
/**
* Observable that represents when the download completes
*/
@Input() download$!: Observable<Download | DownloadEvent | null> | null;
@Input({required: true}) download$!: Observable<Download | DownloadEvent | null> | null;
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 { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs';
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 { SearchService } from 'src/app/_services/search.service';
import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
interface RelationControl {
series: {id: number, name: string} | undefined; // Will add type as well
@ -23,11 +34,11 @@ interface RelationControl {
styleUrls: ['./edit-series-relation.component.scss'],
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();
@ -39,14 +50,13 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
libraryNames: {[key:number]: string} = {};
focusTypeahead = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
get RelationKind() {
return RelationKind;
}
private onDestroy: Subject<void> = new Subject<void>();
constructor(private seriesService: SeriesService, private utilityService: UtilityService,
public imageService: ImageService, private libraryService: LibraryService, private searchService: SearchService,
private readonly cdRef: ChangeDetectorRef) {}
@ -74,12 +84,7 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
});
this.save.pipe(takeUntil(this.onDestroy)).subscribe(() => this.saveState());
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.save.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.saveState());
}
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 {
@Input() entity!: Volume | Chapter;
@Input({required: true}) entity!: Volume | Chapter;
/**
* This will pull extra information
*/

View File

@ -17,13 +17,13 @@ export class EntityTitleComponent implements OnInit {
*/
@Input() libraryType: LibraryType = LibraryType.Manga;
@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
*/
@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;

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

View File

@ -18,7 +18,7 @@ import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
@Input() data!: Series;
@Input({required: true}) data!: Series;
@Input() libraryId = 0;
@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 { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.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 { MetadataService } from 'src/app/_services/metadata.service';
import { ReaderService } from 'src/app/_services/reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-series-info-cards',
@ -18,10 +30,10 @@ import { ReaderService } from 'src/app/_services/reader.service';
styleUrls: ['./series-info-cards.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
export class SeriesInfoCardsComponent implements OnInit, OnChanges {
@Input() series!: Series;
@Input() seriesMetadata!: SeriesMetadata;
@Input({required: true}) series!: Series;
@Input({required: true}) seriesMetadata!: SeriesMetadata;
@Input() hasReadingProgress: boolean = false;
@Input() readingTimeLeft: HourEstimateRange | undefined;
/**
@ -31,8 +43,7 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
@Output() goTo: EventEmitter<{queryParamName: FilterQueryParam, filter: any}> = new EventEmitter();
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
get MangaFormat() {
return MangaFormat;
@ -49,9 +60,9 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
map(evt => evt.payload as UserProgressUpdateEvent),
debounceTime(500),
takeUntil(this.onDestroy))
takeUntilDestroyed(this.destroyRef))
.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 (updateEvent.seriesId !== this.series.id) return;
this.getReadingTimeLeft();
@ -73,10 +84,6 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
this.cdRef.markForCheck();
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
handleGoTo(queryParamName: FilterQueryParam, filter: any) {
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 { Router } from '@angular/router';
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 { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
@ -21,7 +30,7 @@ import { JumpbarService } from 'src/app/_services/jumpbar.service';
styleUrls: ['./all-collections.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AllCollectionsComponent implements OnInit, OnDestroy {
export class AllCollectionsComponent implements OnInit {
isLoading: boolean = true;
collections: CollectionTag[] = [];
@ -29,9 +38,10 @@ export class AllCollectionsComponent implements OnInit, OnDestroy {
jumpbarKeys: Array<JumpKey> = [];
trackByIdentity = (index: number, item: CollectionTag) => `${item.id}_${item.title}`;
isAdmin$: Observable<boolean> = of(false);
private readonly onDestroy = new Subject<void>();
filterOpen: EventEmitter<boolean> = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
constructor(private collectionService: CollectionTagService, private router: Router,
private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
@ -46,18 +56,12 @@ export class AllCollectionsComponent implements OnInit, OnDestroy {
this.loadPage();
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
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;
return this.accountService.hasAdminRole(user);
}));
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
loadCollection(item: CollectionTag) {
this.router.navigate(['collections', item.id]);
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 { Router } from '@angular/router';
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 { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service';
import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-dashboard',
@ -23,7 +33,7 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./dashboard.component.scss'],
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
@ -39,19 +49,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
inProgress: Series[] = [];
recentlyAddedSeries: Series[] = [];
private readonly onDestroy = new Subject<void>();
/**
* We use this Replay subject to slow the amount of times we reload the UI
*/
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
private readonly destroyRef = inject(DestroyRef);
constructor(public accountService: AccountService, private libraryService: LibraryService,
private seriesService: SeriesService, private router: Router,
private titleService: Title, public imageService: ImageService,
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) {
const seriesAddedEvent = res.payload as SeriesAddedEvent;
@ -73,12 +82,12 @@ export class DashboardComponent implements OnInit, OnDestroy {
});
this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay()
);
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntil(this.onDestroy)).subscribe(() => {
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.loadRecentlyUpdated();
this.loadRecentlyAddedSeries();
this.cdRef.markForCheck();
@ -98,11 +107,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.reloadSeries();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
reloadSeries() {
this.loadOnDeck();
this.loadRecentlyUpdated();
@ -127,7 +131,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
if (this.libraryId > 0) {
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.cdRef.markForCheck();
});
@ -138,7 +142,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
if (this.libraryId > 0) {
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.cdRef.markForCheck();
});
@ -150,7 +154,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
if (this.libraryId > 0) {
api = this.seriesService.getRecentlyUpdatedSeries();
}
api.pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
api.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(updatedSeries => {
this.recentlyUpdatedSeries = updatedSeries.filter(group => {
if (this.libraryId === 0) return true;
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 { ActivatedRoute, Router } from '@angular/router';
import { Subject } from 'rxjs';
@ -20,6 +29,7 @@ import { FilterUtilitiesService } from '../shared/_services/filter-utilities.ser
import { FilterSettings } from '../metadata-filter/filter-settings';
import { JumpKey } from '../_models/jumpbar/jump-key';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-library-detail',
@ -27,7 +37,7 @@ import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
styleUrls: ['./library-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LibraryDetailComponent implements OnInit, OnDestroy {
export class LibraryDetailComponent implements OnInit {
libraryId!: number;
libraryName = '';
@ -36,7 +46,6 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
pagination!: Pagination;
actions: ActionItem<Library>[] = [];
filter: SeriesFilter | undefined = undefined;
onDestroy: Subject<void> = new Subject<void>();
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false;
@ -47,14 +56,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
tabs: Array<{title: string, fragment: string, icon: string}> = [
{title: 'Library', fragment: '', icon: 'fa-landmark'},
{title: 'Recommended', fragment: 'recomended', icon: 'fa-award'},
{title: 'Recommended', fragment: 'recommended', icon: 'fa-award'},
];
active = this.tabs[0];
private readonly destroyRef = inject(DestroyRef);
bulkActionCallback = (action: ActionItem<any>, data: any) => {
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
const selectedSeriesIndices = this.bulkSelectionService.getSelectedCardsForSource('series');
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndices.includes(index + ''));
switch (action.action) {
case Action.AddToReadingList:
@ -143,7 +152,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
}
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) {
const seriesAdded = event.payload as SeriesAddedEvent;
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'])
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 { Genre } from 'src/app/_models/metadata/genre';
import { Series } from 'src/app/_models/series';
import { MetadataService } from 'src/app/_services/metadata.service';
import { RecommendationService } from 'src/app/_services/recommendation.service';
import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-library-recommended',
@ -12,9 +22,10 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./library-recommended.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LibraryRecommendedComponent implements OnInit, OnDestroy {
export class LibraryRecommendedComponent implements OnInit {
@Input() libraryId: number = 0;
private readonly destroyRef = inject(DestroyRef);
quickReads$!: Observable<Series[]>;
quickCatchups$!: Observable<Series[]>;
@ -26,43 +37,36 @@ export class LibraryRecommendedComponent implements OnInit, OnDestroy {
all$!: Observable<any>;
private onDestroy: Subject<void> = new Subject();
constructor(private recommendationService: RecommendationService, private seriesService: SeriesService,
private metadataService: MetadataService) { }
ngOnInit(): void {
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)
.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)
.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)
.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)
.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(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(genres => genres[Math.floor(Math.random() * genres.length)]),
shareReplay()
);
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));
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.all$ = merge(this.quickReads$, this.quickCatchups$, this.highlyRated$, this.rediscover$, this.onDeck$, this.genre$).pipe(takeUntilDestroyed(this.destroyRef));
}

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 { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
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 { ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRightToLeft];
@ -16,18 +30,18 @@ const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRigh
styleUrls: ['./canvas-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy, ImageRenderer {
export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input({required: true}) bookmark$!: Observable<number>;
@Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input() imageFit$!: Observable<FITTING_OPTION>;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
@ViewChild('content') canvas: ElementRef | undefined;
private ctx!: CanvasRenderingContext2D;
private readonly onDestroy = new Subject<void>();
currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT;
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) { }
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.pageSplit = value.pageSplit;
this.layoutMode = value.layoutMode;
@ -67,11 +81,11 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
this.darkenss$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map((values: ReaderSetting) => values.fitting),
map(fit => {
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(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(_ => {
if (this.currentImageSplitPart === SPLIT_PAGE_PART.NO_SPLIT) return;
if (!this.canvas) return;
@ -104,7 +118,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
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() {
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
@ -236,8 +246,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
setCanvasSize() {
if (this.canvasImage == null) return;
if (!this.ctx || !this.canvas) { return; }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const isSafari = [
'iPad Simulator',
'iPhone Simulator',

View File

@ -1,6 +1,6 @@
<ng-container *ngIf="isValid()">
<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)}">
<ng-container *ngIf="currentImage">
<img alt=" "

View File

@ -1,5 +1,16 @@
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 { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
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 { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
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
@ -19,22 +31,23 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./double-no-cover-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
export class DoubleNoCoverRendererComponent implements OnInit {
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement;
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input({required: true}) bookmark$!: Observable<number>;
@Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input({required: true}) getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
debugMode: DEBUG_MODES = DEBUG_MODES.Logs;
imageFitClass$!: Observable<string>;
showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)');
darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit;
@ -61,8 +74,6 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
@ -77,30 +88,30 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.darkenss$ = this.readerSettings$.pipe(
this.darkness$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.pageNum$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(pageInfo => {
this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages;
@ -114,20 +125,20 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map((_) => this.shouldRenderDouble()),
filter(_ => this.isValid()),
);
this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(values => values.fitting),
filter(_ => this.isValid()),
shareReplay()
);
this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
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.HEIGHT) return 'fit-to-height-double-offset';
@ -139,7 +150,7 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(values => {
this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit;
@ -148,7 +159,7 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
).subscribe(() => {});
this.bookmark$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(_ => {
const elements = [];
const image1 = this.document.querySelector('#image-1');
@ -163,11 +174,6 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
).subscribe(() => {});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() {
if (!this.isValid()) return false;

View File

@ -1,6 +1,6 @@
<ng-container *ngIf="isValid()">
<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)}">
<ng-container *ngIf="currentImage">
<img alt=" "

View File

@ -1,5 +1,16 @@
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 { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
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 { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
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
@ -19,22 +31,23 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./double-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer {
export class DoubleRendererComponent implements OnInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement;
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input({required: true}) bookmark$!: Observable<number>;
@Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input({required: true}) getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
debugMode: DEBUG_MODES = DEBUG_MODES.None;
imageFitClass$!: Observable<string>;
showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)');
darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit;
@ -61,8 +74,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
@ -77,30 +88,30 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.darkenss$ = this.readerSettings$.pipe(
this.darkness$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.pageNum$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(pageInfo => {
this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages;
@ -114,20 +125,20 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map((_) => this.shouldRenderDouble()),
filter(_ => this.isValid()),
);
this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(values => values.fitting),
filter(_ => this.isValid()),
shareReplay()
);
this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
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.HEIGHT) return 'fit-to-height-double-offset';
@ -140,7 +151,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(values => {
this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit;
@ -149,7 +160,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {});
this.bookmark$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(_ => {
const elements = [];
const image1 = this.document.querySelector('#image-1');
@ -164,10 +175,6 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() {
if (!this.isValid()) return false;

View File

@ -1,6 +1,6 @@
<ng-container *ngIf="isValid()">
<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)}">
<ng-container *ngIf="leftImage">
<img alt=" "

View File

@ -1,5 +1,16 @@
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 { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
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 { DEBUG_MODES, ImageRenderer } from '../../_models/renderer';
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
@ -20,16 +32,17 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./double-reverse-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageRenderer {
export class DoubleReverseRendererComponent implements OnInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input() getPage!: (pageNum: number) => HTMLImageElement;
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input({required: true}) bookmark$!: Observable<number>;
@Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input({required: true}) getPage!: (pageNum: number) => HTMLImageElement;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
debugMode: DEBUG_MODES = DEBUG_MODES.None;
@ -37,7 +50,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>;
layoutClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)');
darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$: Observable<string> = of('');
layoutMode: LayoutMode = LayoutMode.Single;
pageSplit: PageSplitOption = PageSplitOption.FitSplit;
@ -63,8 +76,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
*/
shouldRenderDouble$!: Observable<boolean>;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;}
get FITTING_OPTION() {return FITTING_OPTION;}
get LayoutMode() {return LayoutMode;}
@ -79,30 +90,30 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
filter(_ => this.isValid()),
map(values => values.readerMode),
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 + '%)'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.pageNum$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(pageInfo => {
this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages;
@ -114,21 +125,21 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
).subscribe(() => {});
this.shouldRenderDouble$ = this.pageNum$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(() => this.shouldRenderDouble()),
filter(() => this.isValid()),
shareReplay()
);
this.imageFitClass$ = this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(values => values.fitting),
filter(_ => this.isValid()),
shareReplay()
);
this.layoutClass$ = combineLatest([this.shouldRenderDouble$, this.readerSettings$]).pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
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.HEIGHT) return 'fit-to-height-double-offset';
@ -141,7 +152,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(values => {
this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit;
@ -150,7 +161,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
).subscribe(() => {});
this.bookmark$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(_ => {
const elements = [];
const image1 = this.document.querySelector('#image-1');
@ -165,11 +176,6 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
).subscribe(() => {});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
shouldRenderDouble() {
if (!this.isValid()) return false;

View File

@ -1,5 +1,20 @@
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 { debounceTime, takeUntil } from 'rxjs/operators';
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 { WebtoonImage } from '../../_models/webtoon-image';
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
@ -58,7 +74,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
/**
* 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() loadNextChapter: 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() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>();
@Input() fullscreenToggled: ReplaySubject<boolean> = new ReplaySubject<boolean>();
private readonly destroyRef = inject(DestroyRef);
readerElemRef!: ElementRef<HTMLDivElement>;
@ -149,9 +166,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return this.webtoonImageWidth > (innerWidth || document.body.clientWidth);
}
private readonly onDestroy = new Subject<void>();
constructor(private readerService: ReaderService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) {
@ -172,8 +186,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
ngOnDestroy(): void {
this.intersectionObserver.disconnect();
this.onDestroy.next();
this.onDestroy.complete();
}
/**
@ -183,7 +195,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
initScrollHandler() {
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')
.pipe(debounceTime(20), takeUntil(this.onDestroy))
.pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.handleScrollEvent(event));
}
@ -193,7 +205,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.recalculateImageWidth();
if (this.goToPage) {
this.goToPage.pipe(takeUntil(this.onDestroy)).subscribe(page => {
this.goToPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {
const isSamePage = this.pageNum === page;
if (isSamePage) { return; }
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) {
this.bookmarkPage.pipe(takeUntil(this.onDestroy)).subscribe(page => {
this.bookmarkPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {
const image = document.querySelector('img[id^="page-' + page + '"]');
if (image) {
this.renderer.addClass(image, 'bookmark-effect');
@ -222,7 +234,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
}
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.isFullscreenMode = isFullscreen;
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 { ActivatedRoute, Router } from '@angular/router';
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 { DoubleNoCoverRendererComponent } from '../double-renderer-no-cover/double-no-cover-renderer.component';
import { SwipeEvent } from 'src/app/ng-swipe/ag-swipe.core';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
const PREFETCH_PAGES = 10;
@ -98,7 +113,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(DoubleRendererComponent, { static: false }) doubleRenderer!: DoubleRendererComponent;
@ViewChild(DoubleReverseRendererComponent, { static: false }) doubleReverseRenderer!: DoubleReverseRendererComponent;
@ViewChild(DoubleNoCoverRendererComponent, { static: false }) doubleNoCoverRenderer!: DoubleNoCoverRendererComponent;
private readonly destroyRef = inject(DestroyRef);
libraryId!: number;
seriesId!: number;
@ -354,8 +369,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return this.readerService.getPageUrl(chapterId, pageNum);
}
private readonly onDestroy = new Subject<void>();
get PageNumber() {
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
this.readerSettings$ = merge(this.generalSettingsForm.valueChanges, this.pagingDirection$, this.readerMode$).pipe(
map(_ => this.createReaderSettingsUpdate()),
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
);
this.updateForm();
@ -505,7 +518,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.pagingDirection = dir;
this.cdRef.markForCheck();
}),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
this.readerMode$.pipe(
@ -514,11 +527,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readerMode = mode;
this.cdRef.markForCheck();
}),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
).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;
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.pageSplitOption = parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10);
@ -570,7 +583,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
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.LeftRight && this.FittingOption === FITTING_OPTION.HEIGHT) {
this.rightPaginationOffset = (this.readingArea.nativeElement.scrollLeft) * -1;
@ -581,12 +594,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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;
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.prevScrollTop = this.readingArea?.nativeElement?.scrollTop || 0;
this.hasScrolledX = true;
@ -598,8 +611,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readerService.resetOverrideStyles();
this.navService.showNavBar();
this.navService.showSideNav();
this.onDestroy.next();
this.onDestroy.complete();
this.showBookmarkEffectEvent.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)">
<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">
<img alt=" "
#image [src]="currentImage.src"

View File

@ -1,5 +1,16 @@
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 { PageSplitOption } from 'src/app/_models/preferences/page-split-option';
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 { ImageRenderer } from '../../_models/renderer';
import { ManagaReaderService } from '../../_series/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-single-renderer',
@ -16,21 +28,22 @@ import { ManagaReaderService } from '../../_series/managa-reader.service';
styleUrls: ['./single-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer {
export class SingleRendererComponent implements OnInit, ImageRenderer {
@Input() readerSettings$!: Observable<ReaderSetting>;
@Input() image$!: Observable<HTMLImageElement | null>;
@Input() bookmark$!: Observable<number>;
@Input() showClickOverlay$!: Observable<boolean>;
@Input() pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Input({required: true}) readerSettings$!: Observable<ReaderSetting>;
@Input({required: true}) image$!: Observable<HTMLImageElement | null>;
@Input({required: true}) bookmark$!: Observable<number>;
@Input({required: true}) showClickOverlay$!: Observable<boolean>;
@Input({required: true}) pageNum$!: Observable<{pageNum: number, maxPages: number}>;
@Output() imageHeight: EventEmitter<number> = new EventEmitter<number>();
private readonly destroyRef = inject(DestroyRef);
imageFitClass$!: Observable<string>;
imageContainerHeight$!: Observable<string>;
showClickOverlayClass$!: Observable<string>;
readerModeClass$!: Observable<string>;
darkenss$: Observable<string> = of('brightness(100%)');
darkness$: Observable<string> = of('brightness(100%)');
emulateBookClass$!: Observable<string>;
currentImage!: HTMLImageElement;
layoutMode: LayoutMode = LayoutMode.Single;
@ -39,8 +52,6 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
pageNum: number = 0;
maxPages: number = 1;
private readonly onDestroy = new Subject<void>();
get ReaderMode() {return ReaderMode;}
get LayoutMode() {return LayoutMode;}
@ -52,14 +63,14 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
map(values => values.readerMode),
map(mode => mode === ReaderMode.LeftRight || mode === ReaderMode.UpDown ? '' : 'd-none'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.imageContainerHeight$ = this.readerSettings$.pipe(
@ -76,31 +87,31 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
return 'calc(100vh)'
}),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.pageNum$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(pageInfo => {
this.pageNum = pageInfo.pageNum;
this.maxPages = pageInfo.maxPages;
}),
).subscribe(() => {});
this.darkenss$ = this.readerSettings$.pipe(
this.darkness$ = this.readerSettings$.pipe(
map(values => 'brightness(' + values.darkness + '%)'),
filter(_ => this.isValid()),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
map(showOverlay => showOverlay ? 'blur' : ''),
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
filter(_ => this.isValid()),
);
this.readerSettings$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(values => {
this.layoutMode = values.layoutMode;
this.pageSplit = values.pageSplit;
@ -109,7 +120,7 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
).subscribe(() => {});
this.bookmark$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
tap(_ => {
const elements = [];
const image1 = this.document.querySelector('#image-1');
@ -134,7 +145,7 @@ export class SingleRendererComponent implements OnInit, OnDestroy, ImageRenderer
}),
shareReplay(),
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;
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
renderPage(img: Array<HTMLImageElement | null>): void {
if (img === null || img.length === 0 || img[0] === null) 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 { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
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 { ToggleService } from '../_services/toggle.service';
import { FilterSettings } from './filter-settings';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-metadata-filter',
@ -27,7 +39,7 @@ import { FilterSettings } from './filter-settings';
styleUrls: ['./metadata-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MetadataFilterComponent implements OnInit, OnDestroy {
export class MetadataFilterComponent implements OnInit {
/**
* 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() filterSettings!: FilterSettings;
@Input({required: true}) filterSettings!: FilterSettings;
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
private readonly destroyRef = inject(DestroyRef);
formatSettings: TypeaheadSettings<FilterItem<MangaFormat>> = new TypeaheadSettings();
@ -58,7 +71,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
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;
@ -76,9 +89,6 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
fullyLoaded: boolean = false;
private onDestroy: Subject<void> = new Subject();
get PersonRole(): typeof PersonRole {
return PersonRole;
}
@ -99,7 +109,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
if (this.filterOpen) {
this.filterOpen.pipe(takeUntil(this.onDestroy)).subscribe(openState => {
this.filterOpen.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(openState => {
this.filteringCollapsed = !openState;
this.toggleService.set(!this.filteringCollapsed);
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)])
});
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.inProgress = this.readProgressGroup.get('inProgress')?.value;
this.filter.readStatus.notRead = this.readProgressGroup.get('notRead')?.value;
@ -148,7 +158,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
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) {
this.filter.sortOptions = {
isAscending: this.isAscendingSort,
@ -162,7 +172,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
map(val => (val || '').trim()),
distinctUntilChanged(),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
)
.subscribe(changes => {
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(
distinctUntilChanged(),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
)
.subscribe(changes => {
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();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
getPersonsSettings(role: PersonRole) {
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 { BehaviorSubject, Observable, of, Subject } from 'rxjs';
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 { AccountService } from 'src/app/_services/account.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-nav-events-toggle',
@ -21,12 +31,11 @@ import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hu
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EventsWidgetComponent implements OnInit, OnDestroy {
@Input() user!: User;
@Input({required: true}) user!: User;
private readonly destroyRef = inject(DestroyRef);
isAdmin$: Observable<boolean> = of(false);
private readonly onDestroy = new Subject<void>();
/**
* Progress events (Event Type: 'started', 'ended', 'updated' that have progress property)
*/
@ -59,15 +68,13 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.progressEventsSource.complete();
this.singleUpdateSource.complete();
this.errorSource.complete();
}
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) {
this.processNotificationProgressEvent(event);
} else if (event.event === EVENTS.Error) {
@ -86,7 +93,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
});
this.isAdmin$ = this.accountService.currentUser$.pipe(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(user => (user && this.accountService.hasAdminRole(user)) || false),
shareReplay()
);

View File

@ -13,90 +13,90 @@
</div>
<div class="dropdown" *ngIf="hasFocus">
<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>
<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">
<ng-container [ngTemplateOutlet]="seriesTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="collectionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="readingListTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="libraryTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="genreTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="tagTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="personTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="chapterTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</li>
</ul>
</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>
<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">
<ng-container [ngTemplateOutlet]="fileTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
</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 { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-grouped-typeahead',
@ -11,7 +27,7 @@ import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
styleUrls: ['./grouped-typeahead.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
export class GroupedTypeaheadComponent implements OnInit {
/**
* Unique id to tie with a label element
*/
@ -24,7 +40,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
* Initial value of the search model
*/
@Input() initialValue: string = '';
@Input() grouppedData: SearchResultGroup = new SearchResultGroup();
@Input() groupedData: SearchResultGroup = new SearchResultGroup();
/**
* Placeholder for the input
*/
@ -62,6 +78,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
@ContentChild('readingListTemplate') readingListTemplate!: TemplateRef<any>;
@ContentChild('fileTemplate') fileTemplate!: TemplateRef<any>;
@ContentChild('chapterTemplate') chapterTemplate!: TemplateRef<any>;
private readonly destroyRef = inject(DestroyRef);
hasFocus: boolean = false;
@ -70,16 +87,14 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
prevSearchTerm: string = '';
private onDestroy: Subject<void> = new Subject();
get searchTerm() {
return this.typeaheadForm.get('typeahead')?.value || '';
}
get hasData() {
return !(this.noResultsTemplate != undefined && !this.grouppedData.persons.length && !this.grouppedData.collections.length
&& !this.grouppedData.series.length && !this.grouppedData.persons.length && !this.grouppedData.tags.length && !this.grouppedData.genres.length && !this.grouppedData.libraries.length
&& !this.grouppedData.files.length && !this.grouppedData.chapters.length);
return !(this.noResultsTemplate != undefined && !this.groupedData.persons.length && !this.groupedData.collections.length
&& !this.groupedData.series.length && !this.groupedData.persons.length && !this.groupedData.tags.length && !this.groupedData.genres.length && !this.groupedData.libraries.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.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;
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) {
if (event) {
event.stopPropagation();

View File

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

View File

@ -1,5 +1,15 @@
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 { fromEvent, Subject } from 'rxjs';
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 { ScrollService } from 'src/app/_services/scroll.service';
import { SearchService } from 'src/app/_services/search.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-nav-header',
@ -24,9 +35,10 @@ import { SearchService } from 'src/app/_services/search.service';
styleUrls: ['./nav-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NavHeaderComponent implements OnInit, OnDestroy {
export class NavHeaderComponent implements OnInit {
@ViewChild('search') searchViewRef!: any;
private readonly destroyRef = inject(DestroyRef);
isLoading = false;
debounceTime = 300;
@ -48,7 +60,6 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
backToTopNeeded = false;
searchFocused: boolean = false;
scrollElem: HTMLElement;
private readonly onDestroy = new Subject<void>();
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
@ -57,7 +68,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
}
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) {
this.scrollElem = 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();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
logout() {
this.accountService.logout();
this.navService.hideNavBar();
@ -109,7 +115,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
this.searchTerm = val.trim();
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.isLoading = false;
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 {
@Input() item!: ReadingListItem;
@Input({required: true}) item!: ReadingListItem;
@Input() position: number = 0;
@Input() libraryTypes: {[key: number]: LibraryType} = {};
/**

View File

@ -20,7 +20,7 @@ export enum ADD_FLOW {
})
export class AddToListModalComponent implements OnInit, AfterViewInit {
@Input() title!: string;
@Input({required: true}) title!: string;
/**
* 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
*/
@Input() type!: ADD_FLOW;
@Input({required: true}) type!: ADD_FLOW;
/**
* 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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
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 { ReadingListService } from 'src/app/_services/reading-list.service';
import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID {
General = 'General',
@ -21,9 +31,10 @@ enum TabID {
styleUrls: ['./edit-reading-list-modal.component.scss'],
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;
coverImageIndex: number = 0;
@ -35,8 +46,6 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
imageUrls: Array<string> = [];
active = TabID.General;
private readonly onDestroy = new Subject<void>();
get Breakpoint() { return Breakpoint; }
get TabID() { return TabID; }
@ -70,7 +79,7 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
}
this.cdRef.markForCheck();
}),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
).subscribe();
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() {
this.ngModal.dismiss(undefined);
}

View File

@ -13,8 +13,8 @@ import { AccountService } from 'src/app/_services/account.service';
})
export class AddEmailToAccountMigrationModalComponent implements OnInit {
@Input() username!: string;
@Input() password!: string;
@Input({required: true}) username!: string;
@Input({required: true}) password!: string;
isSaving: boolean = false;
registerForm: FormGroup = new FormGroup({});

View File

@ -1,5 +1,18 @@
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 { Title } from '@angular/platform-browser';
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 { ReviewSeriesModalComponent } from '../../_modals/review-series-modal/review-series-modal.component';
import { PageLayoutMode } from 'src/app/_models/page-layout-mode';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
interface RelatedSeris {
series: Series;
@ -68,10 +82,11 @@ interface StoryLineItem {
styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChecked {
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
private readonly destroyRef = inject(DestroyRef);
/**
* 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() {
return LibraryType;
}
@ -301,7 +313,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
return;
}
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
if (event.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = event.payload as SeriesRemovedEvent;
if (seriesRemovedEvent.seriesId === this.seriesId) {
@ -322,18 +334,13 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
this.changeDetectionRef.markForCheck();
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;
this.renderMode = val;
this.changeDetectionRef.markForCheck();
});
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) {
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 {
@Input() seriesMetadata!: SeriesMetadata;
@Input({required: true}) seriesMetadata!: SeriesMetadata;
@Input() hasReadingProgress: boolean = false;
/**
* Reading lists with a connection to the Series
*/
@Input() readingLists: Array<ReadingList> = [];
@Input() series!: Series;
@Input({required: true}) series!: Series;
isCollapsed: boolean = true;
hasExtendedProperties: boolean = false;

View File

@ -12,7 +12,7 @@ import { SeriesService } from 'src/app/_services/series.service';
})
export class ReviewSeriesModalComponent implements OnInit {
@Input() series!: Series;
@Input({required: true}) series!: Series;
reviewGroup!: FormGroup;
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 { takeUntil } from 'rxjs/operators';
import { CoverUpdateEvent } from 'src/app/_models/events/cover-update-event';
import { ImageService } from 'src/app/_services/image.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.
@ -14,12 +26,12 @@ import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service
styleUrls: ['./image.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImageComponent implements OnChanges, OnDestroy {
export class ImageComponent implements OnChanges {
/**
* Source url to load image
*/
@Input() imageUrl!: string;
@Input({required: true}) imageUrl!: string;
/**
* 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;
@ViewChild('img', {static: true}) imgElem!: ElementRef<HTMLImageElement>;
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
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 (res.event === EVENTS.CoverUpdate) {
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 {
@Input() person!: Person;
@Input({required: true}) person!: Person;
constructor() { }
}

View File

@ -10,7 +10,7 @@ export class ReadMoreComponent implements OnChanges {
/**
* String to apply readmore on
*/
@Input() text!: string;
@Input({required: true}) text!: string;
/**
* 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 {
@Input() updateData!: UpdateVersionEvent;
@Input({required: true}) updateData!: UpdateVersionEvent;
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 { Subject, takeUntil } from 'rxjs';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { NavService } from 'src/app/_services/nav.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.
@ -14,7 +25,7 @@ import { ToggleService } from 'src/app/_services/toggle.service';
templateUrl: './side-nav-companion-bar.component.html',
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
*/
@ -42,7 +53,7 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
isFilterOpen = false;
isExtrasOpen = false;
private onDestroy: Subject<void> = new Subject();
private readonly destroyRef = inject(DestroyRef);
constructor(private navService: NavService, private utilityService: UtilityService, public toggleService: ToggleService,
private offcanvasService: NgbOffcanvas) {
@ -52,7 +63,7 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
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
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) {
this.isFilterOpen = false;
this.filterOpen.emit(this.isFilterOpen);
@ -60,11 +71,6 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
});
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
toggleFilter() {
this.isFilterOpen = !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 { filter, map, Subject, takeUntil } from 'rxjs';
import { NavService } from 'src/app/_services/nav.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
@ -10,7 +20,7 @@ import { NavService } from 'src/app/_services/nav.service';
styleUrls: ['./side-nav-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SideNavItemComponent implements OnInit, OnDestroy {
export class SideNavItemComponent implements OnInit {
/**
* Icon to display next to item. ie) 'fa-home'
*/
@ -31,35 +41,30 @@ export class SideNavItemComponent implements OnInit, OnDestroy {
@Input() external: boolean = false;
@Input() comparisonMethod: 'startsWith' | 'equals' = 'equals';
private readonly destroyRef = inject(DestroyRef);
highlighted = false;
private onDestroy: Subject<void> = new Subject();
constructor(public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef) {
router.events
.pipe(filter(event => event instanceof NavigationEnd),
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd))
.subscribe((evt: NavigationEnd) => {
this.updateHightlight(evt.url.split('?')[0]);
this.updateHighlight(evt.url.split('?')[0]);
});
}
ngOnInit(): void {
setTimeout(() => {
this.updateHightlight(this.router.url.split('?')[0]);
this.updateHighlight(this.router.url.split('?')[0]);
}, 100);
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
updateHightlight(page: string) {
updateHighlight(page: string) {
if (this.link === undefined) {
this.highlighted = false;
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 { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subject } from 'rxjs';
@ -14,6 +22,7 @@ import { Action, ActionFactoryService, ActionItem } from '../../../_services/act
import { ActionService } from '../../../_services/action.service';
import { LibraryService } from '../../../_services/library.service';
import { NavService } from '../../../_services/nav.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-side-nav',
@ -21,8 +30,9 @@ import { NavService } from '../../../_services/nav.service';
styleUrls: ['./side-nav.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SideNavComponent implements OnInit, OnDestroy {
export class SideNavComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
libraries: Library[] = [];
actions: ActionItem<Library>[] = [];
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;
}
private onDestroy: Subject<void> = new Subject();
constructor(public accountService: AccountService, private libraryService: LibraryService,
public utilityService: UtilityService, private messageHub: MessageHubService,
@ -43,7 +51,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd),
filter(() => this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet))
.subscribe((evt: NavigationEnd) => {
@ -68,7 +76,7 @@ export class SideNavComponent implements OnInit, OnDestroy {
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.libraries = [...libraries];
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) {
switch (action.action) {
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 { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
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 { LibraryService } from 'src/app/_services/library.service';
import { UploadService } from 'src/app/_services/upload.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum TabID {
General = 'General',
@ -32,9 +42,10 @@ enum StepID {
styleUrls: ['./library-settings-modal.component.scss'],
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;
imageUrls: Array<string> = [];
@ -57,7 +68,6 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
isAddLibrary = false;
setupStep = StepID.General;
private readonly onDestroy = new Subject<void>();
get Breakpoint() { return Breakpoint; }
get TabID() { return TabID; }
@ -99,19 +109,13 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
}
this.cdRef.markForCheck();
}),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
).subscribe();
this.setValues();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
setValues() {
if (this.library !== undefined) {
this.libraryForm.get('name')?.setValue(this.library.name);

View File

@ -1,20 +1,20 @@
import { Component, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import { LegendPosition } from '@swimlane/ngx-charts';
import { Subject, map, takeUntil, Observable } from 'rxjs';
import { DayOfWeek, StatisticsService } from 'src/app/_services/statistics.service';
import { PieDataItem } from '../../_models/pie-data-item';
import { StatCount } from '../../_models/stat-count';
import { DayOfWeekPipe } from '../../_pipes/day-of-week.pipe';
import {ChangeDetectionStrategy, Component, DestroyRef, inject} from '@angular/core';
import {FormControl} from '@angular/forms';
import {LegendPosition} from '@swimlane/ngx-charts';
import {map, Observable} from 'rxjs';
import {DayOfWeek, StatisticsService} from 'src/app/_services/statistics.service';
import {PieDataItem} from '../../_models/pie-data-item';
import {StatCount} from '../../_models/stat-count';
import {DayOfWeekPipe} from '../../_pipes/day-of-week.pipe';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-day-breakdown',
templateUrl: './day-breakdown.component.html',
styleUrls: ['./day-breakdown.component.scss']
styleUrls: ['./day-breakdown.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DayBreakdownComponent implements OnDestroy {
private readonly onDestroy = new Subject<void>();
export class DayBreakdownComponent {
view: [number, number] = [0,0];
gradient: boolean = true;
@ -28,6 +28,7 @@ export class DayBreakdownComponent implements OnDestroy {
formControl: FormControl = new FormControl(true, []);
dayBreakdown$!: Observable<Array<PieDataItem>>;
private readonly destroyRef = inject(DestroyRef);
constructor(private statService: StatisticsService) {
const dayOfWeekPipe = new DayOfWeekPipe();
@ -37,13 +38,8 @@ export class DayBreakdownComponent implements OnDestroy {
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 { LegendPosition } from '@swimlane/ngx-charts';
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 { SortableHeader, SortEvent, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { FileExtension, FileExtensionBreakdown } from '../../_models/file-breakdown';
import { PieDataItem } from '../../_models/pie-data-item';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
export interface StackedBarChartDataItem {
name: string,
series: Array<PieDataItem>;
}
const mangaFormatPipe = new MangaFormatPipe();
@Component({
selector: 'app-file-breakdown-stats',
templateUrl: './file-breakdown-stats.component.html',
styleUrls: ['./file-breakdown-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileBreakdownStatsComponent implements OnDestroy {
export class FileBreakdownStatsComponent {
@ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>;
@ -29,11 +35,12 @@ export class FileBreakdownStatsComponent implements OnDestroy {
files$!: Observable<Array<FileExtension>>;
vizData$!: Observable<Array<StackedBarChartDataItem>>;
vizData2$!: Observable<Array<PieDataItem>>;
private readonly onDestroy = new Subject<void>();
currentSort = new BehaviorSubject<SortEvent<FileExtension>>({column: 'extension', direction: 'asc'});
currentSort$: Observable<SortEvent<FileExtension>> = this.currentSort.asObservable();
private readonly destroyRef = inject(DestroyRef);
view: [number, number] = [700, 400];
gradient: boolean = true;
showLegend: boolean = true;
@ -48,7 +55,7 @@ export class FileBreakdownStatsComponent implements OnDestroy {
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(
map(([sortConfig, data]) => {
@ -61,20 +68,15 @@ export class FileBreakdownStatsComponent implements OnDestroy {
return sortConfig.direction === 'asc' ? res : -res;
}) : 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};
})));
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
onSort(evt: SortEvent<FileExtension>) {
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 { LegendPosition } from '@swimlane/ngx-charts';
import { Observable, Subject, BehaviorSubject, combineLatest, map, takeUntil } from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service';
import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { PieDataItem } from '../../_models/pie-data-item';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-manga-format-stats',
@ -12,12 +22,12 @@ import { PieDataItem } from '../../_models/pie-data-item';
styleUrls: ['./manga-format-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MangaFormatStatsComponent implements OnInit, OnDestroy {
export class MangaFormatStatsComponent {
@ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>;
private readonly destroyRef = inject(DestroyRef);
formats$!: Observable<Array<PieDataItem>>;
private readonly onDestroy = new Subject<void>();
currentSort = new BehaviorSubject<SortEvent<PieDataItem>>({column: 'value', direction: 'asc'});
currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable();
@ -44,20 +54,10 @@ export class MangaFormatStatsComponent implements OnInit, OnDestroy {
return sortConfig.direction === 'asc' ? res : -res;
}) : 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>) {
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 { LegendPosition } from '@swimlane/ngx-charts';
import { Observable, Subject, map, takeUntil, combineLatest, BehaviorSubject } from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service';
import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { PieDataItem } from '../../_models/pie-data-item';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-publication-status-stats',
@ -12,12 +21,11 @@ import { PieDataItem } from '../../_models/pie-data-item';
styleUrls: ['./publication-status-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PublicationStatusStatsComponent implements OnDestroy {
export class PublicationStatusStatsComponent {
@ViewChildren(SortableHeader<PieDataItem>) headers!: QueryList<SortableHeader<PieDataItem>>;
publicationStatues$!: Observable<Array<PieDataItem>>;
private readonly onDestroy = new Subject<void>();
currentSort = new BehaviorSubject<SortEvent<PieDataItem>>({column: 'value', direction: 'asc'});
currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable();
@ -32,6 +40,8 @@ export class PublicationStatusStatsComponent implements OnDestroy {
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
};
private readonly destroyRef = inject(DestroyRef);
formControl: FormControl = new FormControl(true, []);
@ -44,15 +54,10 @@ export class PublicationStatusStatsComponent implements OnDestroy {
return sortConfig.direction === 'asc' ? res : -res;
}) : data;
}),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
onSort(evt: SortEvent<PieDataItem>) {
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 { filter, map, Observable, of, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
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 { PieDataItem } from '../../_models/pie-data-item';
import { TimePeriods } from '../top-readers/top-readers.component';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
const mangaFormatPipe = new MangaFormatPipe();
@ -17,7 +18,7 @@ const mangaFormatPipe = new MangaFormatPipe();
styleUrls: ['./reading-activity.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReadingActivityComponent implements OnInit, OnDestroy {
export class ReadingActivityComponent implements OnInit {
/**
* Only show for one user
*/
@ -33,7 +34,7 @@ export class ReadingActivityComponent implements OnInit, OnDestroy {
users$: Observable<Member[]> | undefined;
data$: Observable<Array<PieDataItem>>;
timePeriods = TimePeriods;
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
constructor(private statService: StatisticsService, private memberService: MemberService) {
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}
});
}),
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
shareReplay(),
);
@ -64,18 +65,12 @@ export class ReadingActivityComponent implements OnInit, OnDestroy {
}
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});
if (!this.isAdmin) {
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 { 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 { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
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 { ServerStatistics } from '../../_models/server-statistics';
import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-server-stats',
@ -18,7 +19,7 @@ import { GenericListModalComponent } from '../_modals/generic-list-modal/generic
styleUrls: ['./server-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ServerStatsComponent implements OnDestroy {
export class ServerStatsComponent {
releaseYears$!: Observable<Array<PieDataItem>>;
mostActiveUsers$!: Observable<Array<PieDataItem>>;
@ -27,15 +28,16 @@ export class ServerStatsComponent implements OnDestroy {
recentlyRead$!: Observable<Array<PieDataItem>>;
stats$!: Observable<ServerStatistics>;
seriesImage: (data: PieDataItem) => string;
private readonly onDestroy = new Subject<void>();
openSeries = (data: PieDataItem) => {
const series = data.extra as Series;
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();
private readonly destroyRef = inject(DestroyRef);
@HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event'])
onResize() {
@ -54,14 +56,14 @@ export class ServerStatsComponent implements OnDestroy {
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
this.stats$ = this.statService.getServerStatistics().pipe(takeUntil(this.onDestroy), shareReplay());
this.releaseYears$ = this.statService.getTopYears().pipe(takeUntil(this.onDestroy));
this.stats$ = this.statService.getServerStatistics().pipe(takeUntilDestroyed(this.destroyRef), shareReplay());
this.releaseYears$ = this.statService.getTopYears().pipe(takeUntilDestroyed(this.destroyRef));
this.mostActiveUsers$ = this.stats$.pipe(
map(d => d.mostActiveUsers),
map(userCounts => userCounts.map(count => {
return {name: count.value.username, value: count.count};
})),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.mostActiveLibrary$ = this.stats$.pipe(
@ -69,7 +71,7 @@ export class ServerStatsComponent implements OnDestroy {
map(counts => counts.map(count => {
return {name: count.value.name, value: count.count};
})),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.mostActiveSeries$ = this.stats$.pipe(
@ -77,7 +79,7 @@ export class ServerStatsComponent implements OnDestroy {
map(counts => counts.map(count => {
return {name: count.value.name, value: count.count, extra: count.value};
})),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
this.recentlyRead$ = this.stats$.pipe(
@ -85,15 +87,10 @@ export class ServerStatsComponent implements OnDestroy {
map(counts => counts.map(count => {
return {name: count.name, value: -1, extra: count};
})),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
openGenreList() {
this.metadataService.getAllGenres().subscribe(genres => {
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });

View File

@ -23,7 +23,7 @@ export class StatListComponent {
* Optional data to put in tooltip
*/
@Input() description: string = '';
@Input() data$!: Observable<PieDataItem[]>;
@Input({required: true}) data$!: Observable<PieDataItem[]>;
@Input() image: ((data: PieDataItem) => string) | undefined = undefined;
/**
* 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 { Observable, Subject, takeUntil, switchMap, shareReplay } from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service';
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}];
@ -12,13 +21,13 @@ export const TimePeriods: Array<{title: string, value: number}> = [{title: 'This
styleUrls: ['./top-readers.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TopReadersComponent implements OnInit, OnDestroy {
export class TopReadersComponent implements OnInit {
formGroup: FormGroup;
timePeriods = TimePeriods;
users$: Observable<TopUserRead[]>;
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
constructor(private statsService: StatisticsService, private readonly cdRef: ChangeDetectorRef) {
this.formGroup = new FormGroup({
@ -27,7 +36,7 @@ export class TopReadersComponent implements OnInit, OnDestroy {
this.users$ = this.formGroup.valueChanges.pipe(
switchMap(_ => this.statsService.getTopUsers(this.formGroup.get('days')?.value as number)),
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
shareReplay(),
);
}
@ -38,9 +47,4 @@ export class TopReadersComponent implements OnInit, OnDestroy {
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 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>

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 { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
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 { LibraryService } from 'src/app/_services/library.service';
import { PercentPipe } from '@angular/common';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-user-stats',
@ -16,20 +25,19 @@ import { PercentPipe } from '@angular/common';
styleUrls: ['./user-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserStatsComponent implements OnInit, OnDestroy {
export class UserStatsComponent implements OnInit {
userId: number | undefined = undefined;
userStats$!: Observable<UserReadStatistics>;
readSeries$!: Observable<ReadHistoryEvent[]>;
isAdmin$: Observable<boolean>;
precentageRead$!: Observable<PieDataItem[]>;
private readonly onDestroy = new Subject<void>();
percentageRead$!: Observable<PieDataItem[]>;
private readonly destroyRef = inject(DestroyRef);
constructor(private readonly cdRef: ChangeDetectorRef, private statService: StatisticsService,
private filterService: FilterUtilitiesService, private accountService: AccountService, private memberService: MemberService,
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;
return this.accountService.hasAdminRole(u);
}));
@ -43,25 +51,19 @@ export class UserStatsComponent implements OnInit, OnDestroy {
this.userId = me.id;
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(
takeUntil(this.onDestroy),
takeUntilDestroyed(this.destroyRef),
);
const pipe = new PercentPipe('en-US');
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('%', ''))};
}).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 { 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 { Observable, ReplaySubject, Subject } from 'rxjs';
import { auditTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
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
*/
@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
*/
@ -173,6 +192,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
@Output() newItemAdded = new EventEmitter<any[] | any>();
@Output() onUnlock = new EventEmitter<void>();
@Output() lockedChange = new EventEmitter<boolean>();
private readonly destroyRef = inject(DestroyRef);
@ViewChild('input') inputElem!: ElementRef<HTMLInputElement>;
@ -189,23 +209,16 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
typeaheadControl!: FormControl;
typeaheadForm!: FormGroup;
private readonly onDestroy = new Subject<void>();
constructor(private renderer2: Renderer2, @Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef) { }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
ngOnInit() {
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((resetToEmpty: boolean) => {
this.reset.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((resetToEmpty: boolean) => {
this.clearSelections(resetToEmpty);
this.init();
});
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;
this.onInputFocus();
});
@ -258,7 +271,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
switchMap((val: string) => {
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[]) => {
this.isLoadingOptions = false;
@ -272,7 +285,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}),
shareReplay(),
takeUntil(this.onDestroy)
takeUntilDestroyed(this.destroyRef)
);
@ -398,7 +411,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}
this.toggleSelection(opt);
console.log('Selected ', opt);
this.resetField();
this.onInputFocus();
@ -411,7 +423,6 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
const newItem = this.settings.addTransformFn(title);
this.newItemAdded.emit(newItem);
this.toggleSelection(newItem);
console.log('Selected ', newItem);
this.resetField();
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 { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { AccountService } from 'src/app/_services/account.service';
import { Clipboard } from '@angular/cdk/clipboard';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
ElementRef, inject,
Input,
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({
selector: 'app-api-key',
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() showRefresh: boolean = true;
@ -19,13 +27,14 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
@Input() tooltipText: string = '';
@ViewChild('apiKey') inputElem!: ElementRef;
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 {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
let key = '';
if (user) {
key = user.apiKey;
@ -35,19 +44,16 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
if (this.transform != undefined) {
this.key = this.transform(key);
this.cdRef.markForCheck();
}
});
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
async copy() {
this.inputElem.nativeElement.select();
this.clipboard.copy(this.inputElem.nativeElement.value);
this.inputElem.nativeElement.setSelectionRange(0, 0);
this.cdRef.markForCheck();
}
async refresh() {
@ -56,6 +62,7 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
}
this.accountService.resetApiKey().subscribe(newKey => {
this.key = newKey;
this.cdRef.markForCheck();
this.toastr.success('API Key reset');
});
}
@ -63,6 +70,7 @@ export class ApiKeyComponent implements OnInit, OnDestroy {
selectAll() {
if (this.inputElem) {
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 { Observable, of, Subject, takeUntil, shareReplay, map, take } from 'rxjs';
import { AgeRestriction } from 'src/app/_models/metadata/age-restriction';
import { AgeRating } from 'src/app/_models/metadata/age-rating';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-change-age-restriction',
@ -12,7 +21,7 @@ import { AccountService } from 'src/app/_services/account.service';
styleUrls: ['./change-age-restriction.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChangeAgeRestrictionComponent implements OnInit, OnDestroy {
export class ChangeAgeRestrictionComponent implements OnInit {
user: User | undefined = undefined;
hasChangeAgeRestrictionAbility: Observable<boolean> = of(false);
@ -20,22 +29,21 @@ export class ChangeAgeRestrictionComponent implements OnInit, OnDestroy {
selectedRestriction!: AgeRestriction;
originalRestriction!: AgeRestriction;
reset: EventEmitter<AgeRestriction> = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
get AgeRating() { return AgeRating; }
private onDestroy = new Subject<void>();
constructor(private accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
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;
this.user = user;
this.originalRestriction = this.user.ageRestriction;
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));
}));
this.cdRef.markForCheck();
@ -45,11 +53,6 @@ export class ChangeAgeRestrictionComponent implements OnInit, OnDestroy {
this.selectedRestriction = restriction;
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
resetForm() {
if (!this.user) return;
this.reset.emit(this.originalRestriction);

View File

@ -1,17 +1,19 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { Observable, of, Subject, takeUntil, shareReplay, map, tap, take } from 'rxjs';
import { UpdateEmailResponse } from 'src/app/_models/auth/update-email-response';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {ToastrService} from 'ngx-toastr';
import {Observable, of, shareReplay, take} from 'rxjs';
import {UpdateEmailResponse} from 'src/app/_models/auth/update-email-response';
import {User} from 'src/app/_models/user';
import {AccountService} from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-change-email',
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({});
user: User | undefined = undefined;
@ -21,17 +23,16 @@ export class ChangeEmailComponent implements OnInit, OnDestroy {
isViewMode: boolean = true;
emailLink: string = '';
emailConfirmed: boolean = true;
private readonly destroyRef = inject(DestroyRef);
public get email() { return this.form.get('email'); }
private onDestroy = new Subject<void>();
makeLink: (val: string) => string = (val: string) => {return this.emailLink};
constructor(public accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
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.form.addControl('email', new FormControl(user?.email, [Validators.required, Validators.email]));
this.form.addControl('password', new FormControl('', [Validators.required]));
@ -41,13 +42,6 @@ export class ChangeEmailComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
});
});
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
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.resetForm();
this.isViewMode = true;
this.resetForm();
}, 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 { ToastrService } from 'ngx-toastr';
import { map, Observable, of, shareReplay, Subject, take, takeUntil } from 'rxjs';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-change-password',
@ -20,22 +29,22 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
passwordsMatch = false;
resetPasswordErrors: string[] = [];
isViewMode: boolean = true;
private readonly destroyRef = inject(DestroyRef);
public get password() { return this.passwordChangeForm.get('password'); }
public get confirmPassword() { return this.passwordChangeForm.get('confirmPassword'); }
private onDestroy = new Subject<void>();
constructor(private accountService: AccountService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
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.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));
}));
this.cdRef.markForCheck();
@ -53,8 +62,6 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.observableHandles.forEach(o => o.unsubscribe());
this.onDestroy.next();
this.onDestroy.complete();
}
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 { ToastrService } from 'ngx-toastr';
import { Subject, takeUntil } from 'rxjs';
import { Device } from 'src/app/_models/device/device';
import { DevicePlatform, devicePlatforms } from 'src/app/_models/device/device-platform';
import { DeviceService } from 'src/app/_services/device.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-edit-device',
@ -12,17 +25,17 @@ import { DeviceService } from 'src/app/_services/device.service';
styleUrls: ['./edit-device.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EditDeviceComponent implements OnInit, OnChanges, OnDestroy {
export class EditDeviceComponent implements OnInit, OnChanges {
@Input() device: Device | undefined;
@Output() deviceAdded: EventEmitter<void> = new EventEmitter();
@Output() deviceUpdated: EventEmitter<Device> = new EventEmitter();
private readonly destroyRef = inject(DestroyRef);
settingsForm: FormGroup = new FormGroup({});
devicePlatforms = devicePlatforms;
private readonly onDestroy = new Subject<void>();
constructor(public deviceService: DeviceService, private toastr: ToastrService,
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]));
// 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 (email === null || email === undefined || email === '') return;
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() {
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(() => {

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 { distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
import { ThemeService } from 'src/app/_services/theme.service';
import { SiteTheme, ThemeProvider } from 'src/app/_models/preferences/site-theme';
import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-theme-manager',
@ -12,13 +21,12 @@ import { AccountService } from 'src/app/_services/account.service';
styleUrls: ['./theme-manager.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ThemeManagerComponent implements OnDestroy {
export class ThemeManagerComponent {
currentTheme: SiteTheme | undefined;
isAdmin: boolean = false;
user: User | undefined;
private readonly onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
get ThemeProvider() {
return ThemeProvider;
@ -27,8 +35,9 @@ export class ThemeManagerComponent implements OnDestroy {
constructor(public themeService: ThemeService, private accountService: AccountService,
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.cdRef.markForCheck();
});
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) {
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 { ToastrService } from 'ngx-toastr';
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 { BookService } from 'src/app/book-reader/_services/book.service';
import { environment } from 'src/environments/environment';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
enum AccordionPanelID {
ImageReader = 'image-reader',
@ -79,8 +88,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
opdsEnabled: boolean = false;
baseUrl: string = '';
makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)};
private onDestroy = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
get AccordionPanelID() {
return AccordionPanelID;
@ -165,7 +173,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
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) {
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(true);
this.cdRef.markForCheck();
@ -176,8 +184,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.observableHandles.forEach(o => o.unsubscribe());
this.onDestroy.next();
this.onDestroy.complete();
}

View File

@ -1,8 +1,20 @@
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 { 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 { FilterSettings } from 'src/app/metadata-filter/filter-settings';
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 { ScrollService } from 'src/app/_services/scroll.service';
import { SeriesService } from 'src/app/_services/series.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
@ -27,10 +40,11 @@ import { SeriesService } from 'src/app/_services/series.service';
styleUrls: ['./want-to-read.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WantToReadComponent implements OnInit, OnDestroy, AfterContentChecked {
export class WantToReadComponent implements OnInit, AfterContentChecked {
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
private readonly destroyRef = inject(DestroyRef);
isLoading: boolean = true;
series: Array<Series> = [];
@ -46,12 +60,11 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
filterOpen: EventEmitter<boolean> = new EventEmitter();
private onDestroy: Subject<void> = new Subject<void>();
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
bulkActionCallback = (action: ActionItem<any>, data: any) => {
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
const selectedSeriesIndices = this.bulkSelectionService.getSelectedCardsForSource('series');
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndices.includes(index + ''));
switch (action.action) {
case Action.RemoveFromWantToReadList:
@ -91,7 +104,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
this.filterActiveCheck = this.filterUtilityService.createSeriesFilter();
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) {
const seriesRemoved = event.payload as SeriesRemovedEvent;
if (!this.utilityService.deepEqual(this.filter, this.filterActiveCheck)) {
@ -109,7 +122,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
}
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) {
this.loadPage();
}
@ -120,11 +133,6 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
this.scrollService.setScrollContainer(this.scrollingBlock);
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
@HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) {
if (event.key === KEY_CODES.SHIFT) {

View File

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