mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
9bc8361381
commit
9c06cccd35
21753
UI/Web/package-lock.json
generated
21753
UI/Web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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,20 +14,22 @@ 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',
|
||||
ChangePassword = 'Change Password',
|
||||
Bookmark = 'Bookmark',
|
||||
Download = 'Download',
|
||||
ChangeRestriction = 'Change Restriction'
|
||||
ChangeRestriction = 'Change Restriction'
|
||||
}
|
||||
|
||||
@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,21 +44,14 @@ export class AccountService implements OnDestroy {
|
||||
*/
|
||||
private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(private httpClient: HttpClient, private router: Router,
|
||||
constructor(private httpClient: HttpClient, private router: Router,
|
||||
private messageHub: MessageHubService, private themeService: ThemeService) {
|
||||
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
|
||||
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
|
||||
map(evt => evt.payload as UserUpdateEvent),
|
||||
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
|
||||
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
|
||||
switchMap(() => this.refreshToken()))
|
||||
.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)
|
||||
);
|
||||
}
|
||||
|
||||
@ -114,7 +109,7 @@ export class AccountService implements OnDestroy {
|
||||
|
||||
this.currentUser = user;
|
||||
this.currentUserSource.next(user);
|
||||
|
||||
|
||||
this.stopRefreshTokenTimer();
|
||||
|
||||
if (this.currentUser !== undefined) {
|
||||
@ -135,15 +130,15 @@ export class AccountService implements OnDestroy {
|
||||
|
||||
/**
|
||||
* Registers the first admin on the account. Only used for that. All other registrations must occur through invite
|
||||
* @param model
|
||||
* @returns
|
||||
* @param model
|
||||
* @returns
|
||||
*/
|
||||
register(model: {username: string, password: string, email: string}) {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/register', model).pipe(
|
||||
map((user: User) => {
|
||||
return user;
|
||||
}),
|
||||
takeUntil(this.onDestroy)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
}
|
||||
|
||||
@ -177,8 +172,8 @@ export class AccountService implements OnDestroy {
|
||||
|
||||
/**
|
||||
* Given a user id, returns a full url for setting up the user account
|
||||
* @param userId
|
||||
* @returns
|
||||
* @param userId
|
||||
* @returns
|
||||
*/
|
||||
getInviteUrl(userId: number, withBaseUrl: boolean = true) {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'account/invite-url?userId=' + userId + '&withBaseUrl=' + withBaseUrl, TextResonse);
|
||||
@ -214,7 +209,7 @@ export class AccountService implements OnDestroy {
|
||||
|
||||
/**
|
||||
* This will get latest preferences for a user and cache them into user store
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
getPreferences() {
|
||||
return this.httpClient.get<Preferences>(this.baseUrl + 'users/get-preferences').pipe(map(pref => {
|
||||
@ -223,7 +218,7 @@ export class AccountService implements OnDestroy {
|
||||
this.setCurrentUser(this.currentUser);
|
||||
}
|
||||
return pref;
|
||||
}), takeUntil(this.onDestroy));
|
||||
}), takeUntilDestroyed(this.destroyRef));
|
||||
}
|
||||
|
||||
updatePreferences(userPreferences: Preferences) {
|
||||
@ -233,13 +228,13 @@ export class AccountService implements OnDestroy {
|
||||
this.setCurrentUser(this.currentUser);
|
||||
}
|
||||
return settings;
|
||||
}), takeUntil(this.onDestroy));
|
||||
}), takeUntilDestroyed(this.destroyRef));
|
||||
}
|
||||
|
||||
getUserFromLocalStorage(): User | undefined {
|
||||
|
||||
const userString = localStorage.getItem(this.userKey);
|
||||
|
||||
|
||||
if (userString) {
|
||||
return JSON.parse(userString)
|
||||
};
|
||||
@ -254,7 +249,7 @@ export class AccountService implements OnDestroy {
|
||||
user.apiKey = key;
|
||||
|
||||
localStorage.setItem(this.userKey, JSON.stringify(user));
|
||||
|
||||
|
||||
this.currentUserSource.next(user);
|
||||
this.currentUser = user;
|
||||
}
|
||||
@ -270,7 +265,7 @@ export class AccountService implements OnDestroy {
|
||||
this.currentUser.token = user.token;
|
||||
this.currentUser.refreshToken = user.refreshToken;
|
||||
}
|
||||
|
||||
|
||||
this.setCurrentUser(this.currentUser);
|
||||
return user;
|
||||
}));
|
||||
|
@ -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);
|
||||
@ -56,8 +50,8 @@ export class ImageService implements OnDestroy {
|
||||
|
||||
/**
|
||||
* Returns the entity type from a cover image url. Undefied if not applicable
|
||||
* @param url
|
||||
* @returns
|
||||
* @param url
|
||||
* @returns
|
||||
*/
|
||||
getEntityTypeFromUrl(url: string) {
|
||||
if (url.indexOf('?') < 0) return undefined;
|
||||
|
@ -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,
|
||||
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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
@ -25,13 +36,12 @@ export class ThemeService implements OnDestroy {
|
||||
|
||||
private themesSource = new ReplaySubject<SiteTheme[]>(1);
|
||||
public themes$ = this.themesSource.asObservable();
|
||||
|
||||
|
||||
/**
|
||||
* Maintain a cache of themes. SignalR will inform us if we need to refresh cache
|
||||
*/
|
||||
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,31 +66,26 @@ export class ThemeService implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
getColorScheme() {
|
||||
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* --theme-color from theme. Updates the meta tag
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
getThemeColor() {
|
||||
return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* --msapplication-TileColor from theme. Updates the meta tag
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
getTileColor() {
|
||||
return getComputedStyle(this.document.body).getPropertyValue('--title-color').trim();
|
||||
}
|
||||
|
||||
|
||||
getCssVariable(variable: string) {
|
||||
return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
|
||||
}
|
||||
|
@ -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]),
|
||||
|
@ -13,8 +13,8 @@ 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> = [];
|
||||
selectedRestriction!: AgeRestriction;
|
||||
|
@ -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',
|
||||
@ -16,12 +28,12 @@ export class ManageAlertsComponent implements OnInit {
|
||||
|
||||
@Output() alertCount = new EventEmitter<number>();
|
||||
@ViewChildren(SortableHeader<KavitaMediaError>) headers!: QueryList<SortableHeader<KavitaMediaError>>;
|
||||
private readonly serverService = inject(ServerService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
private readonly serverService = inject(ServerService);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
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();
|
||||
|
||||
@ -30,7 +42,7 @@ export class ManageAlertsComponent implements OnInit {
|
||||
formGroup = new FormGroup({
|
||||
filter: new FormControl('', [])
|
||||
});
|
||||
|
||||
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
@ -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,10 +35,9 @@ 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 destroyRef = inject(DestroyRef);
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(private modalService: NgbModal, private libraryService: LibraryService,
|
||||
constructor(private modalService: NgbModal, private libraryService: LibraryService,
|
||||
private toastr: ToastrService, private confirmService: ConfirmService,
|
||||
private hubService: MessageHubService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
@ -37,10 +45,10 @@ 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),
|
||||
filter(event => event.event === EVENTS.ScanSeries || event.event === EVENTS.NotificationProgress),
|
||||
distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) =>
|
||||
this.hasMessageChanged(prev, curr)))
|
||||
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)))
|
||||
.subscribe((event: Message<ScanSeriesEvent | NotificationProgressEvent>) => {
|
||||
let libId = 0;
|
||||
if (event.event === EVENTS.ScanSeries) {
|
||||
@ -62,11 +70,6 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
hasMessageChanged(prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) {
|
||||
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();
|
||||
}
|
||||
|
@ -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');
|
||||
@ -69,7 +79,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
|
||||
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||
@ -87,13 +97,13 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private router: Router, private seriesService: SeriesService,
|
||||
private titleService: Title, private actionService: ActionService,
|
||||
constructor(private router: Router, private seriesService: SeriesService,
|
||||
private titleService: Title, private actionService: ActionService,
|
||||
public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService, private route: ActivatedRoute,
|
||||
private utilityService: UtilityService, private route: ActivatedRoute,
|
||||
private filterUtilityService: FilterUtilitiesService, private jumpbarService: JumpbarService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
|
||||
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
|
||||
this.title = this.route.snapshot.queryParamMap.get('title') || 'All 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) {
|
||||
@ -130,11 +135,11 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
||||
this.bulkSelectionService.isShiftDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
|
||||
|
||||
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
|
||||
this.loadPage();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 + '%');
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
*/
|
||||
@ -33,14 +33,14 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||
|
||||
|
||||
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService,
|
||||
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.listForm.addControl('title', new FormControl(this.title, []));
|
||||
this.listForm.addControl('filterQuery', new FormControl('', []));
|
||||
|
||||
|
||||
this.loading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
@ -77,7 +77,7 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
||||
this.toastr.success('Series added to ' + tag.title + ' collection');
|
||||
this.modal.close();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
filterList = (listItem: ReadingList) => {
|
||||
|
@ -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();
|
||||
@ -57,7 +66,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
|
||||
return TabID;
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||
private imageService: ImageService, private uploadService: UploadService,
|
||||
@ -76,7 +85,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.collectionTagForm.get('title')?.valueChanges.pipe(
|
||||
debounceTime(100),
|
||||
debounceTime(100),
|
||||
distinctUntilChanged(),
|
||||
switchMap(name => this.collectionService.tagNameExists(name)),
|
||||
tap(exists => {
|
||||
@ -84,22 +93,17 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
|
||||
if (!exists || isExistingName) {
|
||||
this.collectionTagForm.get('title')?.setErrors(null);
|
||||
} else {
|
||||
this.collectionTagForm.get('title')?.setErrors({duplicateName: true})
|
||||
this.collectionTagForm.get('title')?.setErrors({duplicateName: true})
|
||||
}
|
||||
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();
|
||||
@ -153,7 +157,7 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
|
||||
const unselectedIds = this.selections.unselected().map(s => s.id);
|
||||
const tag = this.collectionTagForm.value;
|
||||
tag.id = this.tag.id;
|
||||
|
||||
|
||||
if (unselectedIds.length == this.series.length && !await this.confirmSerivce.confirm('Warning! No series are selected, saving will delete the tag. Are you sure you want to continue?')) {
|
||||
return;
|
||||
}
|
||||
@ -162,11 +166,11 @@ export class EditCollectionTagsComponent implements OnInit, OnDestroy {
|
||||
this.collectionService.updateTag(tag),
|
||||
this.collectionService.updateSeriesForTag(tag, this.selections.unselected().map(s => s.id))
|
||||
];
|
||||
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover));
|
||||
}
|
||||
|
||||
|
||||
forkJoin(apis).subscribe(() => {
|
||||
this.modal.close({success: true, coverImageUpdated: selectedIndex > 0});
|
||||
this.toastr.success('Tag updated');
|
||||
|
@ -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();
|
||||
@ -117,16 +125,16 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
private libraryService: LibraryService,
|
||||
private collectionService: CollectionTagService,
|
||||
private uploadService: UploadService,
|
||||
private metadataService: MetadataService,
|
||||
private metadataService: MetadataService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
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];
|
||||
});
|
||||
|
||||
|
||||
this.initSeries = Object.assign({}, this.series);
|
||||
|
||||
this.editSeriesForm = this.fb.group({
|
||||
@ -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([
|
||||
@ -490,7 +494,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
.filter(v => v !== null && v !== '')
|
||||
.join(',');
|
||||
|
||||
|
||||
|
||||
|
||||
const apis = [
|
||||
this.seriesService.updateMetadata(this.metadata, this.collectionTags)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,9 +33,10 @@ 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,
|
||||
General = 0,
|
||||
Metadata = 1,
|
||||
Cover = 2,
|
||||
Files = 3
|
||||
@ -38,13 +48,14 @@ 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.
|
||||
*/
|
||||
@ -63,7 +74,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
|
||||
|
||||
tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Info', disabled: false}];
|
||||
@ -74,8 +85,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;
|
||||
@ -97,16 +106,15 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
return TabID;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService,
|
||||
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
|
||||
constructor(public utilityService: UtilityService,
|
||||
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),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
@ -133,7 +141,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this))
|
||||
.filter(item => item.action !== Action.Edit);
|
||||
this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []});
|
||||
this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []});
|
||||
if (this.isChapter) {
|
||||
const chapter = this.utilityService.asChapter(this.data);
|
||||
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, chapter);
|
||||
@ -146,7 +154,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
|
||||
var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
this.chapters.forEach((c: Chapter) => {
|
||||
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
|
||||
@ -157,10 +165,6 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.activeOffcanvas.close();
|
||||
@ -193,7 +197,7 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.actionService.markChapterAsRead(this.libraryId, this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
||||
ngOnChanges(): void {
|
||||
this.jumpBarKeysToRender = [...this.jumpBarKeys];
|
||||
this.resizeJumpBar();
|
||||
|
||||
|
||||
// Don't resume jump key when there is a custom sort order, as it won't work
|
||||
if (!this.hasCustomSort()) {
|
||||
if (!this.hasResumedJumpKey && this.jumpBarKeysToRender.length > 0) {
|
||||
@ -124,7 +124,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
||||
if (resumeKey === '') return;
|
||||
const keys = this.jumpBarKeysToRender.filter(k => k.key === resumeKey);
|
||||
if (keys.length < 1) return;
|
||||
|
||||
|
||||
this.hasResumedJumpKey = true;
|
||||
setTimeout(() => this.scrollTo(keys[0]), 100);
|
||||
}
|
||||
|
@ -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,17 +127,17 @@ 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,
|
||||
public bulkSelectionService: BulkSelectionService,
|
||||
private messageHub: MessageHubService, private accountService: AccountService,
|
||||
private messageHub: MessageHubService, private accountService: AccountService,
|
||||
private scrollService: ScrollService, private readonly cdRef: ChangeDetectorRef,
|
||||
private actionFactoryService: ActionFactoryService) {}
|
||||
|
||||
@ -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,18 +188,18 @@ 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;
|
||||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||
|
||||
// For volume or Series, we can't just take the event
|
||||
// For volume or Series, we can't just take the event
|
||||
if (this.utilityService.isChapter(this.entity)) {
|
||||
const c = this.utilityService.asChapter(this.entity);
|
||||
c.pagesRead = updateEvent.pagesRead;
|
||||
@ -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) {
|
||||
|
@ -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() { }
|
||||
}
|
||||
|
@ -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,15 +50,14 @@ 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,
|
||||
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) {
|
||||
|
@ -19,9 +19,9 @@ 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
|
||||
* This will pull extra information
|
||||
*/
|
||||
@Input() includeMetadata: boolean = false;
|
||||
|
||||
@ -84,19 +84,19 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.totalPages = this.chapter.pages;
|
||||
if (!this.isChapter) {
|
||||
this.totalPages = this.utilityService.asVolume(this.entity).pages;
|
||||
}
|
||||
|
||||
|
||||
this.totalWordCount = this.chapter.wordCount;
|
||||
if (!this.isChapter) {
|
||||
this.totalWordCount = this.utilityService.asVolume(this.entity).chapters.map(c => c.wordCount).reduce((sum, d) => sum + d);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.readingTime.minHours = this.chapter.minHoursToRead;
|
||||
this.readingTime.maxHours = this.chapter.maxHoursToRead;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,24 +79,23 @@ 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 = '';
|
||||
isChapter: boolean = false;
|
||||
|
||||
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService,
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -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;
|
||||
}
|
||||
|
@ -18,11 +18,11 @@ 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;
|
||||
/**
|
||||
* If the entity is selected or not.
|
||||
* If the entity is selected or not.
|
||||
*/
|
||||
@Input() selected: boolean = false;
|
||||
/**
|
||||
@ -51,7 +51,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
constructor(private router: Router, private cdRef: ChangeDetectorRef,
|
||||
private seriesService: SeriesService, private toastr: ToastrService,
|
||||
private modalService: NgbModal, private imageService: ImageService,
|
||||
private modalService: NgbModal, private imageService: ImageService,
|
||||
private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService) {}
|
||||
|
||||
@ -157,7 +157,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.data.pagesRead = 0;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
this.dataChanged.emit(series);
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
@ -42,16 +53,16 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return FilterQueryParam;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService, public metadataService: MetadataService,
|
||||
private readerService: ReaderService, private readonly cdRef: ChangeDetectorRef,
|
||||
constructor(public utilityService: UtilityService, public metadataService: MetadataService,
|
||||
private readerService: ReaderService, private readonly cdRef: ChangeDetectorRef,
|
||||
private messageHub: MessageHubService, private accountService: AccountService) {
|
||||
// Listen for progress events and re-calculate getTimeLeft
|
||||
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});
|
||||
|
@ -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,13 +38,14 @@ 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,
|
||||
private titleService: Title, private jumpbarService: JumpbarService,
|
||||
private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
|
||||
private titleService: Title, private jumpbarService: JumpbarService,
|
||||
private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService,
|
||||
public accountService: AccountService) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
@ -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();
|
||||
|
@ -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
|
||||
@ -32,26 +42,25 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
|
||||
libraries$: Observable<Library[]> = of([]);
|
||||
isLoading = true;
|
||||
|
||||
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
recentlyUpdatedSeries: SeriesGroup[] = [];
|
||||
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,
|
||||
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;
|
||||
|
||||
@ -61,7 +70,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
} else if (res.event === EVENTS.SeriesRemoved) {
|
||||
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
|
||||
|
||||
|
||||
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
|
||||
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
|
||||
@ -73,12 +82,12 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
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;
|
||||
@ -183,7 +187,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
params['title'] = 'Newly Added';
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeFromArray(arr: Array<any>, element: any) {
|
||||
|
@ -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:
|
||||
@ -86,7 +95,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
this.loadPage();
|
||||
});
|
||||
|
||||
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||
@ -104,8 +113,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
private utilityService: UtilityService, public navService: NavService, private filterUtilityService: FilterUtilitiesService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
@ -130,7 +139,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
|
||||
|
||||
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
|
||||
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
|
||||
@ -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;
|
||||
@ -161,8 +170,8 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
this.cdRef.markForCheck();
|
||||
this.refresh.emit();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
} else if (event.event === EVENTS.SeriesRemoved) {
|
||||
const seriesRemoved = event.payload as SeriesRemovedEvent;
|
||||
if (seriesRemoved.libraryId !== this.libraryId) return;
|
||||
@ -179,10 +188,6 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.shift', ['$event'])
|
||||
handleKeypress(event: KeyboardEvent) {
|
||||
@ -242,9 +247,9 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
||||
this.loadingSeries = true;
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.seriesService.getSeriesForLibrary(0, undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -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,
|
||||
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),
|
||||
map(genres => genres[Math.floor(Math.random() * genres.length)]),
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +79,7 @@ export class LibraryRecommendedComponent implements OnInit, OnDestroy {
|
||||
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.quickReads$ = this.quickReads$.pipe(filter(series => !series.includes(seriesObj)));
|
||||
this.quickCatchups$ = this.quickCatchups$.pipe(filter(series => !series.includes(seriesObj)));
|
||||
}
|
||||
|
@ -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() imageFit$!: Observable<FITTING_OPTION>;
|
||||
@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;
|
||||
@ -47,13 +61,13 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
*/
|
||||
imageFitClass$!: Observable<string>;
|
||||
renderWithCanvas: boolean = false;
|
||||
|
||||
|
||||
|
||||
|
||||
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;
|
||||
@ -65,19 +79,19 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
})).subscribe(() => {});
|
||||
|
||||
this.darkenss$ = this.readerSettings$.pipe(
|
||||
map(values => 'brightness(' + values.darkness + '%)'),
|
||||
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)
|
||||
if (this.canvasImage === null) return fit;
|
||||
|
||||
// Would this ever execute given that we perform splitting only in this renderer?
|
||||
// Would this ever execute given that we perform splitting only in this renderer?
|
||||
if (
|
||||
this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src)) &&
|
||||
this.mangaReaderService.shouldRenderAsFitSplit(this.pageSplit)
|
||||
@ -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;
|
||||
@ -103,8 +117,8 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
).subscribe(() => {});
|
||||
|
||||
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
|
||||
map(showOverlay => showOverlay ? 'blur' : ''),
|
||||
takeUntil(this.onDestroy)
|
||||
map(showOverlay => showOverlay ? 'blur' : ''),
|
||||
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;
|
||||
@ -126,7 +136,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
updateSplitPage() {
|
||||
if (this.canvasImage == null) return;
|
||||
const needsSplitting = this.mangaReaderService.isWidePage(this.readerService.imageUrlToPageNum(this.canvasImage.src));
|
||||
|
||||
|
||||
if (!needsSplitting || this.mangaReaderService.isNoSplit(this.pageSplit)) {
|
||||
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
|
||||
return needsSplitting;
|
||||
@ -171,8 +181,8 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
|
||||
/**
|
||||
* This renderer does not render when splitting is not needed
|
||||
* @param img
|
||||
* @returns
|
||||
* @param img
|
||||
* @returns
|
||||
*/
|
||||
renderPage(img: Array<HTMLImageElement | null>) {
|
||||
this.renderWithCanvas = false;
|
||||
@ -184,7 +194,7 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
if (this.layoutMode !== LayoutMode.Single || !ValidSplits.includes(this.pageSplit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const needsSplitting = this.updateSplitPage();
|
||||
if (!needsSplitting) return;
|
||||
|
||||
@ -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',
|
||||
|
@ -1,18 +1,18 @@
|
||||
<ng-container *ngIf="isValid()">
|
||||
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
|
||||
[style.filter]="(darkenss$ | async) ?? '' | safeStyle"
|
||||
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
|
||||
[style.filter]="(darkness$ | async) ?? '' | safeStyle"
|
||||
[ngClass]="{'center-double': (shouldRenderDouble$ | async)}">
|
||||
<ng-container *ngIf="currentImage">
|
||||
<img alt=" "
|
||||
#image [src]="currentImage.src"
|
||||
id="image-1"
|
||||
<img alt=" "
|
||||
#image [src]="currentImage.src"
|
||||
id="image-1"
|
||||
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
|
||||
>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="shouldRenderDouble$ | async">
|
||||
<img alt=" " [src]="currentImage2.src"
|
||||
id="image-2"
|
||||
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}">
|
||||
<img alt=" " [src]="currentImage2.src"
|
||||
id="image-2"
|
||||
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}">
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -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 {
|
||||
|
||||
@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;
|
||||
export class DoubleNoCoverRendererComponent implements OnInit {
|
||||
|
||||
@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;
|
||||
@ -47,60 +60,58 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
currentImage = new Image();
|
||||
/**
|
||||
* Used solely for LayoutMode.Double rendering.
|
||||
* Used solely for LayoutMode.Double rendering.
|
||||
* @remarks Used for rendering to screen.
|
||||
*/
|
||||
currentImage2 = new Image();
|
||||
|
||||
/**
|
||||
* Determines if we should render a double page.
|
||||
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
|
||||
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
|
||||
* and the next page is not a wide image (as only non-wides should be shown next to each other).
|
||||
* @remarks This will always fail if the window's width is greater than the height
|
||||
*/
|
||||
shouldRenderDouble$!: Observable<boolean>;
|
||||
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
get ReaderMode() {return ReaderMode;}
|
||||
get FITTING_OPTION() {return FITTING_OPTION;}
|
||||
get LayoutMode() {return LayoutMode;}
|
||||
|
||||
get ReaderMode() {return ReaderMode;}
|
||||
get FITTING_OPTION() {return FITTING_OPTION;}
|
||||
get LayoutMode() {return LayoutMode;}
|
||||
|
||||
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
@Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.readerModeClass$ = this.readerSettings$.pipe(
|
||||
map(values => values.readerMode),
|
||||
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(
|
||||
map(values => 'brightness(' + values.darkness + '%)'),
|
||||
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' : ''),
|
||||
map(enabled => enabled ? 'book-shadow' : ''),
|
||||
filter(_ => this.isValid()),
|
||||
takeUntil(this.onDestroy)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
|
||||
map(showOverlay => showOverlay ? 'blur' : ''),
|
||||
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');
|
||||
@ -156,18 +167,13 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
|
||||
|
||||
const image2 = this.document.querySelector('#image-2');
|
||||
if (image2 != null) elements.push(image2);
|
||||
|
||||
|
||||
this.mangaReaderService.applyBookmarkEffect(elements);
|
||||
}),
|
||||
filter(_ => this.isValid()),
|
||||
).subscribe(() => {});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
shouldRenderDouble() {
|
||||
if (!this.isValid()) return false;
|
||||
|
||||
@ -202,11 +208,11 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
|
||||
isValid() {
|
||||
return this.layoutMode === LayoutMode.DoubleNoCover;
|
||||
}
|
||||
|
||||
|
||||
renderPage(img: Array<HTMLImageElement | null>): void {
|
||||
if (img === null || img.length === 0 || img[0] === null) return;
|
||||
if (!this.isValid()) return;
|
||||
|
||||
|
||||
// First load, switching from double manga -> double, this is 0 and thus not rendering
|
||||
if (!this.shouldRenderDouble() && (this.currentImage.height || img[0].height) > 0) {
|
||||
this.imageHeight.emit(this.currentImage.height || img[0].height);
|
||||
@ -297,7 +303,7 @@ export class DoubleNoCoverRendererComponent implements OnInit, OnDestroy {
|
||||
if (!(this.debugMode & DEBUG_MODES.Logs)) return;
|
||||
|
||||
if (extraData !== undefined) {
|
||||
console.log(message, extraData);
|
||||
console.log(message, extraData);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
<ng-container *ngIf="isValid()">
|
||||
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
|
||||
[style.filter]="(darkenss$ | async) ?? '' | safeStyle"
|
||||
<div class="image-container {{imageFitClass$ | async}} {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
|
||||
[style.filter]="(darkness$ | async) ?? '' | safeStyle"
|
||||
[ngClass]="{'center-double': (shouldRenderDouble$ | async)}">
|
||||
<ng-container *ngIf="currentImage">
|
||||
<img alt=" "
|
||||
#image [src]="currentImage.src"
|
||||
id="image-1"
|
||||
<img alt=" "
|
||||
#image [src]="currentImage.src"
|
||||
id="image-1"
|
||||
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
|
||||
>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="shouldRenderDouble$ | async">
|
||||
<img alt=" " [src]="currentImage2.src"
|
||||
id="image-2"
|
||||
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}">
|
||||
<img alt=" " [src]="currentImage2.src"
|
||||
id="image-2"
|
||||
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}">
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -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;
|
||||
@ -47,60 +60,58 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
|
||||
*/
|
||||
currentImage = new Image();
|
||||
/**
|
||||
* Used solely for LayoutMode.Double rendering.
|
||||
* Used solely for LayoutMode.Double rendering.
|
||||
* @remarks Used for rendering to screen.
|
||||
*/
|
||||
currentImage2 = new Image();
|
||||
|
||||
/**
|
||||
* Determines if we should render a double page.
|
||||
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
|
||||
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
|
||||
* and the next page is not a wide image (as only non-wides should be shown next to each other).
|
||||
* @remarks This will always fail if the window's width is greater than the height
|
||||
*/
|
||||
shouldRenderDouble$!: Observable<boolean>;
|
||||
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
get ReaderMode() {return ReaderMode;}
|
||||
get FITTING_OPTION() {return FITTING_OPTION;}
|
||||
get LayoutMode() {return LayoutMode;}
|
||||
|
||||
get ReaderMode() {return ReaderMode;}
|
||||
get FITTING_OPTION() {return FITTING_OPTION;}
|
||||
get LayoutMode() {return LayoutMode;}
|
||||
|
||||
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
@Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.readerModeClass$ = this.readerSettings$.pipe(
|
||||
map(values => values.readerMode),
|
||||
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(
|
||||
map(values => 'brightness(' + values.darkness + '%)'),
|
||||
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' : ''),
|
||||
map(enabled => enabled ? 'book-shadow' : ''),
|
||||
filter(_ => this.isValid()),
|
||||
takeUntil(this.onDestroy)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
|
||||
map(showOverlay => showOverlay ? 'blur' : ''),
|
||||
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');
|
||||
@ -157,17 +168,13 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
|
||||
|
||||
const image2 = this.document.querySelector('#image-2');
|
||||
if (image2 != null) elements.push(image2);
|
||||
|
||||
|
||||
this.mangaReaderService.applyBookmarkEffect(elements);
|
||||
}),
|
||||
filter(_ => this.isValid()),
|
||||
).subscribe(() => {});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
shouldRenderDouble() {
|
||||
if (!this.isValid()) return false;
|
||||
@ -203,11 +210,11 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
|
||||
isValid() {
|
||||
return this.layoutMode === LayoutMode.Double;
|
||||
}
|
||||
|
||||
|
||||
renderPage(img: Array<HTMLImageElement | null>): void {
|
||||
if (img === null || img.length === 0 || img[0] === null) return;
|
||||
if (!this.isValid()) return;
|
||||
|
||||
|
||||
// First load, switching from double manga -> double, this is 0 and thus not rendering
|
||||
if (!this.shouldRenderDouble() && (this.currentImage.height || img[0].height) > 0) {
|
||||
this.imageHeight.emit(this.currentImage.height || img[0].height);
|
||||
@ -291,7 +298,7 @@ export class DoubleRendererComponent implements OnInit, OnDestroy, ImageRenderer
|
||||
if (!(this.debugMode & DEBUG_MODES.Logs)) return;
|
||||
|
||||
if (extraData !== undefined) {
|
||||
console.log(message, extraData);
|
||||
console.log(message, extraData);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
<ng-container *ngIf="isValid()">
|
||||
<div class="image-container {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
|
||||
[style.filter]="(darkenss$ | async) ?? '' | safeStyle"
|
||||
<div class="image-container {{layoutClass$ | async}} {{emulateBookClass$ | async}}"
|
||||
[style.filter]="(darkness$ | async) ?? '' | safeStyle"
|
||||
[ngClass]="{'center-double': (shouldRenderDouble$ | async), 'reverse': (shouldRenderDouble$ | async)}">
|
||||
<ng-container *ngIf="leftImage">
|
||||
<img alt=" "
|
||||
#image [src]="leftImage.src"
|
||||
id="image-1"
|
||||
<img alt=" "
|
||||
#image [src]="leftImage.src"
|
||||
id="image-1"
|
||||
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
|
||||
>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="shouldRenderDouble$ | async">
|
||||
<img alt=" " [src]="rightImage.src"
|
||||
id="image-2"
|
||||
<img alt=" " [src]="rightImage.src"
|
||||
id="image-2"
|
||||
class="image-2 {{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"> <!--reverse-->
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -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,10 +20,11 @@ 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
|
||||
* page 11 page 10.
|
||||
* page 11 page 10.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-double-reverse-renderer',
|
||||
@ -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;
|
||||
@ -57,52 +70,50 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
|
||||
|
||||
/**
|
||||
* Determines if we should render a double page.
|
||||
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
|
||||
* The general gist is if we are on double layout mode, the current page (first page) is not a cover image or a wide image
|
||||
* and the next page is not a wide image (as only non-wides should be shown next to each other).
|
||||
* @remarks This will always fail if the window's width is greater than the height
|
||||
*/
|
||||
shouldRenderDouble$!: Observable<boolean>;
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
get ReaderMode() {return ReaderMode;}
|
||||
get FITTING_OPTION() {return FITTING_OPTION;}
|
||||
get LayoutMode() {return LayoutMode;}
|
||||
|
||||
get ReaderMode() {return ReaderMode;}
|
||||
get FITTING_OPTION() {return FITTING_OPTION;}
|
||||
get LayoutMode() {return LayoutMode;}
|
||||
|
||||
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
@Inject(DOCUMENT) private document: Document, public readerService: ReaderService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.readerModeClass$ = this.readerSettings$.pipe(
|
||||
filter(_ => this.isValid()),
|
||||
map(values => values.readerMode),
|
||||
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(
|
||||
map(values => 'brightness(' + values.darkness + '%)'),
|
||||
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' : ''),
|
||||
map(enabled => enabled ? 'book-shadow' : ''),
|
||||
filter(_ => this.isValid()),
|
||||
takeUntil(this.onDestroy)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
this.showClickOverlayClass$ = this.showClickOverlay$.pipe(
|
||||
map(showOverlay => showOverlay ? 'blur' : ''),
|
||||
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;
|
||||
|
||||
@ -204,7 +210,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
|
||||
isValid() {
|
||||
return this.layoutMode === LayoutMode.DoubleReversed;
|
||||
}
|
||||
|
||||
|
||||
renderPage(img: Array<HTMLImageElement | null>): void {
|
||||
if (img === null || img.length === 0 || img[0] === null) return;
|
||||
if (!this.isValid()) return;
|
||||
@ -305,7 +311,7 @@ export class DoubleReverseRendererComponent implements OnInit, OnDestroy, ImageR
|
||||
if (!(this.debugMode & DEBUG_MODES.Logs)) return;
|
||||
|
||||
if (extraData !== undefined) {
|
||||
console.log(message, extraData);
|
||||
console.log(message, extraData);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
|
@ -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>;
|
||||
|
||||
@ -110,7 +127,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/**
|
||||
* If the user has scrolled all the way to the bottom. This is used solely for continuous reading
|
||||
*/
|
||||
atBottom: boolean = false;
|
||||
atBottom: boolean = false;
|
||||
/**
|
||||
* If the user has scrolled all the way to the top. This is used solely for continuous reading
|
||||
*/
|
||||
@ -149,10 +166,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return this.webtoonImageWidth > (innerWidth || document.body.clientWidth);
|
||||
}
|
||||
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(private readerService: ReaderService, private renderer: Renderer2,
|
||||
constructor(private readerService: ReaderService, private renderer: Renderer2,
|
||||
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
|
||||
private readonly cdRef: ChangeDetectorRef, private mangaReaderService: ManagaReaderService) {
|
||||
// This will always exist at this point in time since this is used within manga reader
|
||||
@ -172,18 +186,16 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.intersectionObserver.disconnect();
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for binding the scroll handler to the correct event. On non-fullscreen, body is correct. However, on fullscreen, we must use the reader as that is what
|
||||
* Responsible for binding the scroll handler to the correct event. On non-fullscreen, body is correct. However, on fullscreen, we must use the reader as that is what
|
||||
* gets promoted to fullscreen.
|
||||
*/
|
||||
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,11 +221,11 @@ 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');
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
this.renderer.removeClass(image, 'bookmark-effect');
|
||||
}, 1000);
|
||||
@ -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();
|
||||
@ -251,13 +263,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
return (offset
|
||||
|| document.body.scrollTop
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop
|
||||
|| document.documentElement.scrollTop
|
||||
|| 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* On scroll in document, calculate if the user/javascript has scrolled to the current image element (and it's visible), update that scrolling has ended completely,
|
||||
* On scroll in document, calculate if the user/javascript has scrolled to the current image element (and it's visible), update that scrolling has ended completely,
|
||||
* and calculate the direction the scrolling is occuring. This is not used for prefetching.
|
||||
* @param event Scroll Event
|
||||
*/
|
||||
@ -279,7 +291,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
if (!this.isScrolling) {
|
||||
// Use offset of the image against the scroll container to test if the most of the image is visible on the screen. We can use this
|
||||
// to mark the current page and separate the prefetching code.
|
||||
// to mark the current page and separate the prefetching code.
|
||||
const midlineImages = Array.from(document.querySelectorAll('img[id^="page-"]'))
|
||||
.filter(entry => this.shouldElementCountAsCurrentPage(entry));
|
||||
|
||||
@ -336,7 +348,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
|
||||
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
|
||||
// This if statement will fire once we scroll into the spacer at all
|
||||
this.loadNextChapter.emit();
|
||||
this.cdRef.markForCheck();
|
||||
@ -345,12 +357,12 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
// < 5 because debug mode and FF (mobile) can report non 0, despite being at 0
|
||||
if (this.getScrollTop() < 5 && this.pageNum === 0 && !this.atTop) {
|
||||
this.atBottom = false;
|
||||
this.atTop = true;
|
||||
this.atTop = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Scroll user back to original location
|
||||
this.previousScrollHeightMinusTop = document.body.scrollHeight - document.body.scrollTop;
|
||||
|
||||
|
||||
const reader = this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body;
|
||||
requestAnimationFrame(() => this.scrollService.scrollTo((SPACER_SCROLL_INTO_PX / 2), reader));
|
||||
} else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) {
|
||||
@ -362,7 +374,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @returns Height, Width
|
||||
*/
|
||||
getInnerDimensions() {
|
||||
@ -377,10 +389,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Is any part of the element visible in the scrollport. Does not take into account
|
||||
* style properites, just scroll port visibility.
|
||||
* @param elem
|
||||
* @returns
|
||||
* Is any part of the element visible in the scrollport. Does not take into account
|
||||
* style properites, just scroll port visibility.
|
||||
* @param elem
|
||||
* @returns
|
||||
*/
|
||||
isElementVisible(elem: Element) {
|
||||
if (elem === null || elem === undefined) { return false; }
|
||||
@ -391,8 +403,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
const [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||
|
||||
return (rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
return (rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= (innerHeight || document.body.clientHeight) &&
|
||||
rect.left <= (innerWidth || document.body.clientWidth)
|
||||
);
|
||||
@ -400,7 +412,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
/**
|
||||
* Is any part of the element visible in the scrollport and is it above the midline trigger.
|
||||
* The midline trigger does not mean it is half of the screen. It may be top 25%.
|
||||
* The midline trigger does not mean it is half of the screen. It may be top 25%.
|
||||
* @param elem HTML Element
|
||||
* @returns If above midline
|
||||
*/
|
||||
@ -412,8 +424,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||
|
||||
|
||||
if (rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
if (rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= (innerHeight || document.body.clientHeight) &&
|
||||
rect.left <= (innerWidth || document.body.clientWidth)
|
||||
) {
|
||||
@ -444,7 +456,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/**
|
||||
* Callback for an image onLoad. At this point the image is already rendered in DOM (may not be visible)
|
||||
* This will be used to scroll to current page for intial load
|
||||
* @param event
|
||||
* @param event
|
||||
*/
|
||||
onImageLoad(event: any) {
|
||||
const imagePage = this.readerService.imageUrlToPageNum(event.target.src);
|
||||
@ -468,13 +480,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.debugLog('[Image Load] ! Loaded current page !', this.pageNum);
|
||||
this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum);
|
||||
// There needs to be a bit of time before we scroll
|
||||
if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) {
|
||||
if (this.currentPageElem && !this.isElementVisible(this.currentPageElem)) {
|
||||
this.scrollToCurrentPage();
|
||||
} else {
|
||||
this.initFinished = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
this.allImagesLoaded = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
@ -530,7 +542,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.currentPageElem = document.querySelector('img#page-' + this.pageNum);
|
||||
if (!this.currentPageElem) { return; }
|
||||
this.debugLog('[GoToPage] Scrolling to page', this.pageNum);
|
||||
|
||||
|
||||
// Update prevScrollPosition, so the next scroll event properly calculates direction
|
||||
this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
|
||||
this.isScrolling = true;
|
||||
@ -553,7 +565,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
this.debugLog('\t[PREFETCH] Prefetching ', page);
|
||||
|
||||
|
||||
const data = this.webtoonImages.value.concat({src: this.urlProvider(page), page});
|
||||
|
||||
data.sort((a: WebtoonImage, b: WebtoonImage) => {
|
||||
@ -582,9 +594,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
/**
|
||||
* Finds the ranges of indecies to load from backend. totalPages - 1 is due to backend will automatically return last page for any page number
|
||||
* above totalPages. Webtoon reader might ask for that which results in duplicate last pages.
|
||||
* @param pageNum
|
||||
* @returns
|
||||
* above totalPages. Webtoon reader might ask for that which results in duplicate last pages.
|
||||
* @param pageNum
|
||||
* @returns
|
||||
*/
|
||||
calculatePrefetchIndecies(pageNum: number = -1) {
|
||||
if (pageNum == -1) {
|
||||
@ -646,7 +658,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
if (this.debugLogFilter.filter(str => message.replace('\t', '').startsWith(str)).length > 0) return;
|
||||
if (extraData !== undefined) {
|
||||
console.log(message, extraData);
|
||||
console.log(message, extraData);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
|
@ -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;
|
||||
@ -53,7 +68,7 @@ enum ChapterInfoPosition {
|
||||
enum KeyDirection {
|
||||
Right = 0,
|
||||
Left = 1,
|
||||
Up = 2,
|
||||
Up = 2,
|
||||
Down = 3
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -155,7 +170,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
isLoading = true;
|
||||
hasBookmarkRights: boolean = false; // TODO: This can be an observable
|
||||
|
||||
|
||||
|
||||
getPageFn!: (pageNum: number) => HTMLImageElement;
|
||||
|
||||
@ -165,9 +180,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
* @remarks Used for rendering to screen.
|
||||
*/
|
||||
canvasImage = new Image();
|
||||
|
||||
|
||||
/**
|
||||
* Dictates if we use render with canvas or with image.
|
||||
* Dictates if we use render with canvas or with image.
|
||||
* @remarks This is only for Splitting.
|
||||
*/
|
||||
//renderWithCanvas: boolean = false;
|
||||
@ -314,7 +329,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
prevIsVerticalScrollLeft = true;
|
||||
|
||||
/**
|
||||
* Has the user scrolled to the far right side. This is used for swipe to next page and must ensure user is at end of scroll then on next swipe, will move pages.
|
||||
* Has the user scrolled to the far right side. This is used for swipe to next page and must ensure user is at end of scroll then on next swipe, will move pages.
|
||||
*/
|
||||
hasHitRightScroll = false;
|
||||
/**
|
||||
@ -345,7 +360,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
private pageNumSubject: Subject<{pageNum: number, maxPages: number}> = new ReplaySubject();
|
||||
pageNum$: Observable<{pageNum: number, maxPages: number}> = this.pageNumSubject.asObservable();
|
||||
|
||||
|
||||
|
||||
bookmarkPageHandler = this.bookmarkPage.bind(this);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -373,7 +386,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.FittingOption !== FITTING_OPTION.HEIGHT) {
|
||||
return this.mangaReaderService.getPageDimensions(this.pageNum)?.height + 'px';
|
||||
}
|
||||
|
||||
|
||||
return this.readingArea?.nativeElement?.clientHeight + 'px';
|
||||
}
|
||||
|
||||
@ -428,8 +441,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||
public readerService: ReaderService, private formBuilder: FormBuilder, private navService: NavService,
|
||||
private toastr: ToastrService, private memberService: MemberService,
|
||||
public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
|
||||
private modalService: NgbModal, private readonly cdRef: ChangeDetectorRef,
|
||||
public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
|
||||
private modalService: NgbModal, private readonly cdRef: ChangeDetectorRef,
|
||||
public mangaReaderService: ManagaReaderService) {
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
@ -494,18 +507,18 @@ 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();
|
||||
|
||||
|
||||
this.pagingDirection$.pipe(
|
||||
distinctUntilChanged(),
|
||||
tap(dir => {
|
||||
this.pagingDirection = dir;
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
takeUntil(this.onDestroy)
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(() => {});
|
||||
|
||||
this.readerMode$.pipe(
|
||||
@ -513,12 +526,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
tap(mode => {
|
||||
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();
|
||||
}
|
||||
@ -607,7 +618,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@HostListener('window:orientationchange', ['$event'])
|
||||
onResize() {
|
||||
onResize() {
|
||||
this.disableDoubleRendererIfScreenTooSmall();
|
||||
}
|
||||
|
||||
@ -692,7 +703,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return;
|
||||
};
|
||||
if (this.layoutMode === LayoutMode.Single || this.readerMode === ReaderMode.Webtoon) return;
|
||||
|
||||
|
||||
this.generalSettingsForm.get('layoutMode')?.setValue(LayoutMode.Single);
|
||||
this.generalSettingsForm.get('layoutMode')?.disable();
|
||||
this.toastr.info('Layout mode switched to Single due to insufficient space to render double layout');
|
||||
@ -704,13 +715,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
* @param pageNum Page Number to load
|
||||
* @param forceNew Forces to fetch a new image
|
||||
* @param chapterId ChapterId to fetch page from. Defaults to current chapterId. Not used when in bookmark mode
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) {
|
||||
|
||||
let img = undefined;
|
||||
if (this.bookmarkMode) img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
|
||||
else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
|
||||
else img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum
|
||||
&& (this.readerService.imageUrlToChapterId(img.src) == chapterId || this.readerService.imageUrlToChapterId(img.src) === -1)
|
||||
);
|
||||
if (!img || forceNew) {
|
||||
@ -721,7 +732,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return img;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
isHorizontalScrollLeft() {
|
||||
const scrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0;
|
||||
@ -736,11 +747,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const scrollTop = this.readingArea?.nativeElement?.scrollTop || 0;
|
||||
return scrollTop < this.ReadingAreaHeight;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is there any room to scroll in the direction we are giving? If so, return false. Otherwise return true.
|
||||
* @param direction
|
||||
* @returns
|
||||
* @param direction
|
||||
* @returns
|
||||
*/
|
||||
checkIfPaginationAllowed(direction: KeyDirection) {
|
||||
if (this.readingArea === undefined || this.readingArea.nativeElement === undefined) return true;
|
||||
@ -782,7 +793,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
this.nextChapterId = CHAPTER_ID_NOT_FETCHED;
|
||||
@ -887,7 +898,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
// Fetch the first page of next chapter
|
||||
this.getPage(0, this.nextChapterId);
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
|
||||
@ -976,11 +987,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.hasHitBottomTopScroll = false;
|
||||
this.hasHitZeroTopScroll = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This executes BEFORE fromEvent('scroll')
|
||||
* @param event
|
||||
* @returns
|
||||
* @param event
|
||||
* @returns
|
||||
*/
|
||||
onSwipeMove(_: SwipeEvent) {
|
||||
this.prevScrollLeft = this.readingArea?.nativeElement?.scrollLeft || 0;
|
||||
@ -989,7 +1000,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
triggerSwipePagination(direction: KeyDirection) {
|
||||
if (!this.generalSettingsForm.get('swipeToPaginate')?.value) return;
|
||||
|
||||
|
||||
switch(direction) {
|
||||
case KeyDirection.Down:
|
||||
this.nextPage();
|
||||
@ -1004,7 +1015,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.readingDirection === ReadingDirection.LeftToRight ? this.prevPage() : this.nextPage();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
onSwipeEnd(event: SwipeEvent) {
|
||||
@ -1117,17 +1128,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD);
|
||||
|
||||
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.singleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
|
||||
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.singleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
|
||||
this.doubleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
|
||||
this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
|
||||
this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.FORWARD)
|
||||
);
|
||||
const notInSplit = this.canvasRenderer.shouldMovePrev();
|
||||
|
||||
if ((this.pageNum + pageAmount >= this.maxPages && notInSplit)) {
|
||||
if ((this.pageNum + pageAmount >= this.maxPages && notInSplit)) {
|
||||
// Move to next volume/chapter automatically
|
||||
this.loadNextChapter();
|
||||
return;
|
||||
@ -1142,7 +1153,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
this.resetSwipeModifiers();
|
||||
|
||||
this.isLoading = true;
|
||||
@ -1151,8 +1162,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
|
||||
|
||||
|
||||
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
|
||||
this.singleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
|
||||
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
|
||||
this.singleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
|
||||
this.doubleRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
|
||||
this.doubleNoCoverRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS),
|
||||
this.doubleReverseRenderer.getPageAmount(PAGING_DIRECTION.BACKWARDS)
|
||||
@ -1165,7 +1176,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.loadPrevChapter();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.setPageNum(this.pageNum - pageAmount);
|
||||
this.loadPage();
|
||||
}
|
||||
@ -1179,13 +1190,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.canvasImage.addEventListener('load', () => {
|
||||
this.currentImage.next(this.canvasImage);
|
||||
}, false);
|
||||
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
loadNextChapter() {
|
||||
if (this.nextPageDisabled || this.nextChapterDisabled || this.bookmarkMode) {
|
||||
if (this.nextPageDisabled || this.nextChapterDisabled || this.bookmarkMode) {
|
||||
this.toastr.info('No Next Chapter');
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
@ -1203,11 +1214,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
loadPrevChapter() {
|
||||
if (this.prevPageDisabled || this.prevChapterDisabled || this.bookmarkMode) {
|
||||
if (this.prevPageDisabled || this.prevChapterDisabled || this.bookmarkMode) {
|
||||
this.toastr.info('No Previous Chapter');
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
return;
|
||||
}
|
||||
this.continuousChaptersStack.pop();
|
||||
const prevChapter = this.continuousChaptersStack.peek();
|
||||
@ -1254,12 +1265,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderPage() {
|
||||
const page = [this.canvasImage];
|
||||
|
||||
this.canvasRenderer?.renderPage(page);
|
||||
this.canvasRenderer?.renderPage(page);
|
||||
this.singleRenderer?.renderPage(page);
|
||||
this.doubleRenderer?.renderPage(page);
|
||||
this.doubleNoCoverRenderer?.renderPage(page);
|
||||
@ -1331,7 +1342,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
loadPage() {
|
||||
if (this.readerMode === ReaderMode.Webtoon) return;
|
||||
|
||||
|
||||
this.isLoading = true;
|
||||
this.setCanvasImage();
|
||||
this.cdRef.markForCheck();
|
||||
@ -1339,7 +1350,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.renderPage();
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.prefetch();
|
||||
}
|
||||
|
||||
@ -1423,12 +1434,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Loads the first 5 images (throwaway cache) from the given chapterId
|
||||
* @param chapterId
|
||||
* @param chapterId
|
||||
* @param direction Used to indicate if the chapter is behind or ahead of curent chapter
|
||||
*/
|
||||
prefetchStartOfChapter(chapterId: number, direction: PAGING_DIRECTION) {
|
||||
let pages = [];
|
||||
|
||||
|
||||
if (direction === PAGING_DIRECTION.BACKWARDS) {
|
||||
if (this.continuousChapterInfos[ChapterInfoPosition.Previous] === undefined) return;
|
||||
const n = this.continuousChapterInfos[ChapterInfoPosition.Previous]!.pages;
|
||||
@ -1436,7 +1447,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
pages = [0, 1, 2, 3, 4];
|
||||
}
|
||||
|
||||
|
||||
let images = [];
|
||||
pages.forEach((_, i: number) => {
|
||||
let img = new Image();
|
||||
@ -1564,7 +1575,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.bookmarkMode) return;
|
||||
|
||||
const pageNum = this.pageNum;
|
||||
const isDouble = Math.max(this.canvasRenderer.getBookmarkPageCount(), this.singleRenderer.getBookmarkPageCount(),
|
||||
const isDouble = Math.max(this.canvasRenderer.getBookmarkPageCount(), this.singleRenderer.getBookmarkPageCount(),
|
||||
this.doubleRenderer.getBookmarkPageCount(), this.doubleReverseRenderer.getBookmarkPageCount(), this.doubleNoCoverRenderer.getBookmarkPageCount()) > 1;
|
||||
|
||||
if (this.CurrentPageBookmarked) {
|
||||
@ -1640,5 +1651,5 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
<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"
|
||||
id="image-1"
|
||||
<img alt=" "
|
||||
#image [src]="currentImage.src"
|
||||
id="image-1"
|
||||
class="{{imageFitClass$ | async}} {{readerModeClass$ | async}} {{showClickOverlayClass$ | async}}"
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -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,31 +52,29 @@ 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;}
|
||||
|
||||
get ReaderMode() {return ReaderMode;}
|
||||
get LayoutMode() {return LayoutMode;}
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, public mangaReaderService: ManagaReaderService,
|
||||
@Inject(DOCUMENT) private document: Document) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.readerModeClass$ = this.readerSettings$.pipe(
|
||||
map(values => values.readerMode),
|
||||
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' : ''),
|
||||
map(enabled => enabled ? 'book-shadow' : ''),
|
||||
filter(_ => this.isValid()),
|
||||
takeUntil(this.onDestroy)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
this.imageContainerHeight$ = this.readerSettings$.pipe(
|
||||
map(values => values.fitting),
|
||||
map(values => values.fitting),
|
||||
map(mode => {
|
||||
if ( mode !== FITTING_OPTION.HEIGHT) return '';
|
||||
|
||||
@ -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(
|
||||
map(values => 'brightness(' + values.darkness + '%)'),
|
||||
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),
|
||||
map(showOverlay => showOverlay ? 'blur' : ''),
|
||||
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,15 +153,10 @@ 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;
|
||||
|
||||
|
||||
this.currentImage = img[0];
|
||||
this.cdRef.markForCheck();
|
||||
this.imageHeight.emit(this.currentImage.height);
|
||||
|
@ -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;
|
||||
}
|
||||
@ -87,7 +97,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
return SortField;
|
||||
}
|
||||
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private utilityService: UtilityService,
|
||||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private utilityService: UtilityService,
|
||||
private collectionTagService: CollectionTagService, public toggleService: ToggleService,
|
||||
private readonly cdRef: ChangeDetectorRef, private filterUtilitySerivce: FilterUtilitiesService) {
|
||||
}
|
||||
@ -99,13 +109,13 @@ 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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.filter = this.filterUtilitySerivce.createSeriesFilter();
|
||||
this.readProgressGroup = new FormGroup({
|
||||
read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []),
|
||||
@ -126,7 +136,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
max: new FormControl({value: undefined, disabled: this.filterSettings.releaseYearDisabled}, [Validators.min(1000), Validators.max(9999)])
|
||||
});
|
||||
|
||||
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,
|
||||
@ -161,8 +171,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
|
||||
map(val => (val || '').trim()),
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.onDestroy)
|
||||
distinctUntilChanged(),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(changes => {
|
||||
this.filter.seriesNameQuery = changes; // TODO: See if we can make this into observable
|
||||
@ -170,8 +180,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.releaseYearRange.valueChanges.pipe(
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.onDestroy)
|
||||
distinctUntilChanged(),
|
||||
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];
|
||||
}
|
||||
@ -203,7 +208,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read);
|
||||
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead);
|
||||
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets.readStatus.inProgress);
|
||||
|
||||
|
||||
if (this.filterSettings.presets.sortOptions) {
|
||||
this.sortGroup.get('sortField')?.setValue(this.filterSettings.presets.sortOptions.sortField);
|
||||
this.isAscendingSort = this.filterSettings.presets.sortOptions.isAscending;
|
||||
@ -248,7 +253,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.formatSettings.id = 'format';
|
||||
this.formatSettings.unique = true;
|
||||
this.formatSettings.addIfNonExisting = false;
|
||||
this.formatSettings.fetchFn = (filter: string) => of(mangaFormatFilters).pipe(map(items => this.formatSettings.compareFn(items, filter)));
|
||||
this.formatSettings.fetchFn = (filter: string) => of(mangaFormatFilters).pipe(map(items => this.formatSettings.compareFn(items, filter)));
|
||||
this.formatSettings.compareFn = (options: FilterItem<MangaFormat>[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
@ -271,7 +276,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.librarySettings.addIfNonExisting = false;
|
||||
this.librarySettings.fetchFn = (filter: string) => {
|
||||
return this.libraryService.getLibraries()
|
||||
.pipe(map(items => this.librarySettings.compareFn(items, filter)));
|
||||
.pipe(map(items => this.librarySettings.compareFn(items, filter)));
|
||||
};
|
||||
this.librarySettings.compareFn = (options: Library[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
@ -298,7 +303,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.genreSettings.addIfNonExisting = false;
|
||||
this.genreSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllGenres(this.filter.libraries)
|
||||
.pipe(map(items => this.genreSettings.compareFn(items, filter)));
|
||||
.pipe(map(items => this.genreSettings.compareFn(items, filter)));
|
||||
};
|
||||
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
@ -324,12 +329,12 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.ageRatingSettings.unique = true;
|
||||
this.ageRatingSettings.addIfNonExisting = false;
|
||||
this.ageRatingSettings.fetchFn = (filter: string) => this.metadataService.getAllAgeRatings(this.filter.libraries)
|
||||
.pipe(map(items => this.ageRatingSettings.compareFn(items, filter)));
|
||||
|
||||
.pipe(map(items => this.ageRatingSettings.compareFn(items, filter)));
|
||||
|
||||
this.ageRatingSettings.compareFn = (options: AgeRatingDto[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.ageRatingSettings.selectionCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
|
||||
return a.title == b.title;
|
||||
@ -352,8 +357,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
this.publicationStatusSettings.unique = true;
|
||||
this.publicationStatusSettings.addIfNonExisting = false;
|
||||
this.publicationStatusSettings.fetchFn = (filter: string) => this.metadataService.getAllPublicationStatus(this.filter.libraries)
|
||||
.pipe(map(items => this.publicationStatusSettings.compareFn(items, filter)));
|
||||
|
||||
.pipe(map(items => this.publicationStatusSettings.compareFn(items, filter)));
|
||||
|
||||
this.publicationStatusSettings.compareFn = (options: PublicationStatusDto[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
@ -382,8 +387,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags(this.filter.libraries)
|
||||
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
|
||||
|
||||
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
|
||||
|
||||
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
@ -408,7 +413,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => this.metadataService.getAllLanguages(this.filter.libraries)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
this.languageSettings.selectionCompareFn = (a: Language, b: Language) => {
|
||||
return a.isoCode == b.isoCode;
|
||||
@ -461,7 +466,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
this.peopleSettings[role] = personSettings;
|
||||
return of(true);
|
||||
|
||||
@ -472,7 +477,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
|
||||
return forkJoin([
|
||||
this.updateFromPreset('writers', this.filter.writers, this.filterSettings.presets?.writers, PersonRole.Writer),
|
||||
this.updateFromPreset('character', this.filter.character, this.filterSettings.presets?.character, PersonRole.Character),
|
||||
this.updateFromPreset('character', this.filter.character, this.filterSettings.presets?.character, PersonRole.Character),
|
||||
this.updateFromPreset('colorist', this.filter.colorist, this.filterSettings.presets?.colorist, PersonRole.Colorist),
|
||||
this.updateFromPreset('cover-artist', this.filter.coverArtist, this.filterSettings.presets?.coverArtist, PersonRole.CoverArtist),
|
||||
this.updateFromPreset('editor', this.filter.editor, this.filterSettings.presets?.editor, PersonRole.Editor),
|
||||
@ -486,7 +491,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
||||
}));
|
||||
}
|
||||
|
||||
fetchPeople(role: PersonRole, filter: string) {
|
||||
fetchPeople(role: PersonRole, filter: string) {
|
||||
return this.metadataService.getAllPeople(this.filter.libraries).pipe(map(people => {
|
||||
return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter));
|
||||
}));
|
||||
|
@ -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)
|
||||
*/
|
||||
@ -53,21 +62,19 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
return EVENTS;
|
||||
}
|
||||
|
||||
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
|
||||
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
|
||||
private accountService: AccountService, private confirmService: ConfirmService,
|
||||
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) {
|
||||
}
|
||||
|
||||
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,8 +93,8 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntil(this.onDestroy),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
@ -187,11 +194,11 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
let data = [];
|
||||
if (messageEvent.name === EVENTS.Info) {
|
||||
data = this.infoSource.getValue();
|
||||
data = data.filter(m => m !== messageEvent);
|
||||
data = data.filter(m => m !== messageEvent);
|
||||
this.infoSource.next(data);
|
||||
} else {
|
||||
data = this.errorSource.getValue();
|
||||
data = data.filter(m => m !== messageEvent);
|
||||
data = data.filter(m => m !== messageEvent);
|
||||
this.errorSource.next(data);
|
||||
}
|
||||
this.activeEvents = Math.max(this.activeEvents - 1, 0);
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="search">
|
||||
<input #input [id]="id" type="text" inputmode="search" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
|
||||
aria-haspopup="listbox" aria-owns="dropdown" aria-expanded="hasFocus && (grouppedData.persons.length || grouppedData.collections.length || grouppedData.series.length || grouppedData.persons.length || grouppedData.tags.length || grouppedData.genres.length)"
|
||||
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search"
|
||||
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)" role="search"
|
||||
>
|
||||
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
@ -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>
|
||||
|
@ -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,7 +78,8 @@ 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;
|
||||
isLoading: 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);
|
||||
}
|
||||
|
||||
|
||||
@ -92,7 +107,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
handleKeyPress(event: KeyboardEvent) {
|
||||
handleKeyPress(event: KeyboardEvent) {
|
||||
if (!this.hasFocus) { return; }
|
||||
|
||||
switch(event.key) {
|
||||
@ -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,17 +142,12 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
onInputFocus(event: any) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
this.openDropdown();
|
||||
return this.hasFocus;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
[minQueryLength]="2"
|
||||
initialValue=""
|
||||
placeholder="Search…"
|
||||
[grouppedData]="searchResults"
|
||||
[groupedData]="searchResults"
|
||||
(inputChanged)="onChangeSearch($event)"
|
||||
(clearField)="clearSearch()"
|
||||
(focusChanged)="focusUpdate($event)"
|
||||
|
@ -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();
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { RelationshipPipe } from './relationship.pipe';
|
||||
|
||||
describe('RelationshipPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new RelationshipPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
@ -54,7 +54,7 @@ export class DraggableOrderedListComponent {
|
||||
}
|
||||
|
||||
updateIndex(previousIndex: number, item: any) {
|
||||
// get the new value of the input
|
||||
// get the new value of the input
|
||||
var inputElem = <HTMLInputElement>document.querySelector('#reorder-' + previousIndex);
|
||||
const newIndex = parseInt(inputElem.value, 10);
|
||||
if (previousIndex === newIndex) return;
|
||||
|
@ -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} = {};
|
||||
/**
|
||||
|
@ -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
|
||||
@ -71,14 +71,14 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
||||
|
||||
this.listForm.addControl('title', new FormControl(this.title, []));
|
||||
this.listForm.addControl('filterQuery', new FormControl('', []));
|
||||
|
||||
|
||||
this.loading = true;
|
||||
this.readingListService.getReadingLists(false, true).subscribe(lists => {
|
||||
this.lists = lists.result;
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@ -130,6 +130,6 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
||||
this.modal.close();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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,13 +46,11 @@ 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; }
|
||||
|
||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
||||
public utilityService: UtilityService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
constructor(private ngModal: NgbActiveModal, private readingListService: ReadingListService,
|
||||
public utilityService: UtilityService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
private imageService: ImageService, private readonly cdRef: ChangeDetectorRef, public accountService: AccountService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -58,7 +67,7 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
|
||||
this.coverImageLocked = this.readingList.coverImageLocked;
|
||||
|
||||
this.reviewGroup.get('title')?.valueChanges.pipe(
|
||||
debounceTime(100),
|
||||
debounceTime(100),
|
||||
distinctUntilChanged(),
|
||||
switchMap(name => this.readingListService.nameExists(name)),
|
||||
tap(exists => {
|
||||
@ -66,11 +75,11 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
|
||||
if (!exists || isExistingName) {
|
||||
this.reviewGroup.get('title')?.setErrors(null);
|
||||
} else {
|
||||
this.reviewGroup.get('title')?.setErrors({duplicateName: true})
|
||||
this.reviewGroup.get('title')?.setErrors({duplicateName: true})
|
||||
}
|
||||
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);
|
||||
}
|
||||
@ -101,11 +105,11 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
|
||||
model.endingMonth = model.endingMonth || 0;
|
||||
model.endingYear = model.endingYear || 0;
|
||||
const apis = [this.readingListService.update(model)];
|
||||
|
||||
|
||||
if (this.selectedCover !== '') {
|
||||
apis.push(this.uploadService.updateReadingListCoverImage(this.readingList.id, this.selectedCover))
|
||||
}
|
||||
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
this.readingList.title = model.title;
|
||||
this.readingList.summary = model.summary;
|
||||
|
@ -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({});
|
||||
@ -22,7 +22,7 @@ export class AddEmailToAccountMigrationModalComponent implements OnInit {
|
||||
emailLinkUrl: SafeUrl | undefined;
|
||||
error: string = '';
|
||||
|
||||
constructor(private accountService: AccountService, private modal: NgbActiveModal,
|
||||
constructor(private accountService: AccountService, private modal: NgbActiveModal,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -117,7 +132,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
itemSize: number = 10; // when 10 done, 16 loads
|
||||
|
||||
|
||||
/**
|
||||
* Track by function for Volume to tell when to refresh card data
|
||||
*/
|
||||
@ -207,9 +222,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
||||
}
|
||||
}
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
|
||||
get LibraryType() {
|
||||
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) {
|
||||
@ -543,7 +550,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
||||
if (detail == null) return;
|
||||
this.unreadCount = detail.unreadCount;
|
||||
this.totalCount = detail.totalCount;
|
||||
|
||||
|
||||
this.hasSpecials = detail.specials.length > 0;
|
||||
this.specials = detail.specials;
|
||||
|
||||
@ -792,7 +799,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
||||
} else {
|
||||
this.actionService.addMultipleSeriesToWantToReadList([this.series.id]);
|
||||
}
|
||||
|
||||
|
||||
this.isWantToRead = !this.isWantToRead;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { Person } from '../../_models/metadata/person';
|
||||
})
|
||||
export class PersonBadgeComponent {
|
||||
|
||||
@Input() person!: Person;
|
||||
@Input({required: true}) person!: Person;
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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) { }
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
@ -34,7 +45,7 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Input() filterActive: boolean = false;
|
||||
|
||||
@Input() extraDrawer!: TemplateRef<any>;
|
||||
@Input() extraDrawer!: TemplateRef<any>;
|
||||
|
||||
|
||||
@Output() filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
@ -42,9 +53,9 @@ 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,
|
||||
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);
|
||||
|
@ -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),
|
||||
.pipe(filter(event => event instanceof NavigationEnd),
|
||||
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();
|
||||
|
@ -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,18 +42,16 @@ 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,
|
||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService,
|
||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService,
|
||||
public navService: NavService, private router: Router, private readonly cdRef: ChangeDetectorRef,
|
||||
private ngbModal: NgbModal, private imageService: ImageService) {
|
||||
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
takeUntil(this.onDestroy),
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
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):
|
||||
@ -129,4 +132,4 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
||||
this.navService.toggleSideNav();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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> = [];
|
||||
@ -54,17 +65,16 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
|
||||
selectedFolders: string[] = [];
|
||||
madeChanges = false;
|
||||
libraryTypes: string[] = []
|
||||
|
||||
|
||||
isAddLibrary = false;
|
||||
setupStep = StepID.General;
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
get TabID() { return TabID; }
|
||||
get StepID() { return StepID; }
|
||||
|
||||
constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal,
|
||||
private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService,
|
||||
private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService,
|
||||
private libraryService: LibraryService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef,
|
||||
private imageService: ImageService) { }
|
||||
|
||||
@ -87,7 +97,7 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.libraryForm.get('name')?.valueChanges.pipe(
|
||||
debounceTime(100),
|
||||
debounceTime(100),
|
||||
distinctUntilChanged(),
|
||||
switchMap(name => this.libraryService.libraryNameExists(name)),
|
||||
tap(exists => {
|
||||
@ -95,23 +105,17 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
|
||||
if (!exists || isExistingName) {
|
||||
this.libraryForm.get('name')?.setErrors(null);
|
||||
} else {
|
||||
this.libraryForm.get('name')?.setErrors({duplicateName: true})
|
||||
this.libraryForm.get('name')?.setErrors({duplicateName: true})
|
||||
}
|
||||
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);
|
||||
@ -159,7 +163,7 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
|
||||
model.type = parseInt(model.type, 10);
|
||||
|
||||
if (model.type !== this.library.type) {
|
||||
if (!await this.confirmService.confirm(`Changing library type will trigger a new scan with different parsing rules and may lead to
|
||||
if (!await this.confirmService.confirm(`Changing library type will trigger a new scan with different parsing rules and may lead to
|
||||
series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?`)) return;
|
||||
}
|
||||
|
||||
@ -221,7 +225,7 @@ export class LibrarySettingsModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
isNextDisabled() {
|
||||
switch (this.setupStep) {
|
||||
case StepID.General:
|
||||
case StepID.General:
|
||||
return this.libraryForm.get('name')?.invalid || this.libraryForm.get('type')?.invalid;
|
||||
case StepID.Folder:
|
||||
return this.selectedFolders.length === 0;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,13 +22,13 @@ 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);
|
||||
|
||||
|
@ -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,13 +21,12 @@ 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);
|
||||
|
||||
|
@ -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,10 +34,10 @@ 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(
|
||||
this.data$ = this.formGroup.valueChanges.pipe(
|
||||
switchMap(_ => this.statService.getReadCountByDay(this.formGroup.get('users')!.value, this.formGroup.get('days')!.value)),
|
||||
map(data => {
|
||||
const gList = data.reduce((formats, entry) => {
|
||||
@ -56,26 +57,20 @@ export class ReadingActivityComponent implements OnInit, OnDestroy {
|
||||
return {name: format, value: 0, series: gList[format].series}
|
||||
});
|
||||
}),
|
||||
takeUntil(this.onDestroy),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
|
||||
this.data$.subscribe();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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,41 +28,42 @@ 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() {
|
||||
onResize() {
|
||||
this.breakpointSubject.next(this.utilityService.getActiveBreakpoint());
|
||||
}
|
||||
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
|
||||
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
|
||||
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
|
||||
private metadataService: MetadataService, private modalService: NgbModal, private utilityService: UtilityService) {
|
||||
this.seriesImage = (data: PieDataItem) => {
|
||||
if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id);
|
||||
return '';
|
||||
return '';
|
||||
}
|
||||
|
||||
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 });
|
||||
@ -130,6 +127,6 @@ export class ServerStatsComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
@ -31,7 +31,7 @@ export class StatListComponent {
|
||||
@Input() handleClick: ((data: PieDataItem) => void) | undefined = undefined;
|
||||
|
||||
doClick(item: PieDataItem) {
|
||||
if (!this.handleClick) return;
|
||||
if (!this.handleClick) return;
|
||||
this.handleClick(item);
|
||||
}
|
||||
|
||||
|
@ -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,22 +21,22 @@ 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>();
|
||||
|
||||
constructor(private statsService: StatisticsService, private readonly cdRef: ChangeDetectorRef) {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(private statsService: StatisticsService, private readonly cdRef: ChangeDetectorRef) {
|
||||
this.formGroup = new FormGroup({
|
||||
'days': new FormControl(this.timePeriods[0].value, []),
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
</div>
|
||||
|
@ -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[]>;
|
||||
percentageRead$!: Observable<PieDataItem[]>;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, private statService: StatisticsService,
|
||||
constructor(private readonly cdRef: ChangeDetectorRef, private statService: StatisticsService,
|
||||
private filterService: FilterUtilitiesService, private accountService: AccountService, private memberService: MemberService,
|
||||
private libraryService: LibraryService) {
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(takeUntil(this.onDestroy), map(u => {
|
||||
private libraryService: LibraryService) {
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef), map(u => {
|
||||
if (!u) return false;
|
||||
return this.accountService.hasAdminRole(u);
|
||||
}));
|
||||
@ -42,26 +50,20 @@ export class UserStatsComponent implements OnInit, OnDestroy {
|
||||
this.memberService.getMember().subscribe(me => {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
@ -493,7 +504,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
if (!this.typeaheadControl.dirty) return; // Do we need this?
|
||||
|
||||
// Check if this new option will interfere with any existing ones not shown
|
||||
|
||||
|
||||
if (typeof this.settings.compareFnForAdd == 'function') {
|
||||
console.log('filtered options: ', this.optionSelection.selected());
|
||||
const willDuplicateExist = this.settings.compareFnForAdd(this.optionSelection.selected(), inputText);
|
||||
@ -514,7 +525,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.showAddItem = true;
|
||||
|
||||
if (this.showAddItem) {
|
||||
|
@ -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('');
|
||||
});
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>();
|
||||
get AgeRating() { return AgeRating; }
|
||||
|
||||
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);
|
||||
|
@ -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() {
|
||||
@ -71,9 +65,9 @@ export class ChangeEmailComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
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;
|
||||
})
|
||||
|
@ -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',
|
||||
@ -19,23 +28,23 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
observableHandles: Array<any> = [];
|
||||
passwordsMatch = false;
|
||||
resetPasswordErrors: string[] = [];
|
||||
isViewMode: boolean = true;
|
||||
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() {
|
||||
|
@ -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,29 +25,29 @@ 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,
|
||||
constructor(public deviceService: DeviceService, private toastr: ToastrService,
|
||||
private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
|
||||
this.settingsForm.addControl('name', new FormControl(this.device?.name || '', [Validators.required]));
|
||||
this.settingsForm.addControl('email', new FormControl(this.device?.emailAddress || '', [Validators.required, Validators.email]));
|
||||
this.settingsForm.addControl('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(() => {
|
||||
|
@ -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,23 +21,23 @@ 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;
|
||||
}
|
||||
|
||||
constructor(public themeService: ThemeService, private accountService: AccountService,
|
||||
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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
@ -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:
|
||||
@ -62,7 +75,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
collectionTag: any;
|
||||
tagImage: any;
|
||||
|
||||
@ -77,9 +90,9 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
|
||||
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
|
||||
}
|
||||
|
||||
constructor(public imageService: ImageService, private router: Router, private route: ActivatedRoute,
|
||||
private seriesService: SeriesService, private titleService: Title,
|
||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||
constructor(public imageService: ImageService, private router: Router, private route: ActivatedRoute,
|
||||
private seriesService: SeriesService, private titleService: Title,
|
||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
|
||||
private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private hubService: MessageHubService,
|
||||
private jumpbarService: JumpbarService) {
|
||||
@ -91,25 +104,25 @@ 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)) {
|
||||
this.loadPage();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.series = this.series.filter(s => s.id != seriesRemoved.seriesId);
|
||||
this.seriesPagination.totalItems--;
|
||||
this.cdRef.markForCheck();
|
||||
this.refresh.emit();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -150,7 +158,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.seriesService.getWantToRead(undefined, undefined, this.filter).pipe(take(1)).subscribe(paginatedList => {
|
||||
this.series = paginatedList.result;
|
||||
this.seriesPagination = paginatedList.pagination;
|
||||
@ -163,7 +171,7 @@ export class WantToReadComponent implements OnInit, OnDestroy, AfterContentCheck
|
||||
|
||||
updateFilter(data: FilterEvent) {
|
||||
this.filter = data.filter;
|
||||
|
||||
|
||||
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.seriesPagination, this.filter);
|
||||
this.loadPage();
|
||||
}
|
||||
|
@ -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": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user