Kavita+ Enhancements (#2616)
This commit is contained in:
parent
625c56b265
commit
dd44f55747
43 changed files with 1056 additions and 468 deletions
|
|
@ -0,0 +1,9 @@
|
|||
import {Recommendation} from "./recommendation";
|
||||
import {UserReview} from "../../_single-module/review-card/user-review";
|
||||
import {Rating} from "../rating";
|
||||
|
||||
export interface SeriesDetailPlus {
|
||||
recommendations: Recommendation;
|
||||
reviews: Array<UserReview>;
|
||||
ratings: Array<Rating>;
|
||||
}
|
||||
|
|
@ -237,6 +237,13 @@ export class ActionFactoryService {
|
|||
requiresAdmin: true,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Delete,
|
||||
title: 'delete',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: true,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -135,6 +135,26 @@ export class ActionService implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
async deleteLibrary(library: Partial<Library>, callback?: LibraryActionCallback) {
|
||||
if (!library.hasOwnProperty('id') || library.id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.confirmService.alert(translate('toasts.confirm-library-delete'))) {
|
||||
if (callback) {
|
||||
callback(library);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.libraryService.delete(library?.id).pipe(take(1)).subscribe((res: any) => {
|
||||
this.toastr.info(translate('toasts.library-deleted', {name: library.name}));
|
||||
if (callback) {
|
||||
callback(library);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a series as read; updates the series pagesRead
|
||||
* @param series Series, must have id and name populated
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {SortField} from "../_models/metadata/series-filter";
|
|||
import {FilterCombination} from "../_models/metadata/v2/filter-combination";
|
||||
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
|
||||
import {FilterStatement} from "../_models/metadata/v2/filter-statement";
|
||||
import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -25,7 +26,11 @@ export class MetadataService {
|
|||
baseUrl = environment.apiUrl;
|
||||
private validLanguages: Array<Language> = [];
|
||||
|
||||
constructor(private httpClient: HttpClient, private router: Router) { }
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getSeriesMetadataFromPlus(seriesId: number) {
|
||||
return this.httpClient.get<SeriesDetailPlus | null>(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
getAllAgeRatings(libraries?: Array<number>) {
|
||||
let method = 'metadata/age-ratings'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
import {ScrobblingService} from "../../_services/scrobbling.service";
|
||||
import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {ScrobbleEvent, ScrobbleEventType} from "../../_models/scrobbling/scrobble-event";
|
||||
import {ScrobbleEventTypePipe} from "../scrobble-event-type.pipe";
|
||||
|
|
@ -11,10 +11,11 @@ import {debounceTime, take} from "rxjs/operators";
|
|||
import {PaginatedResult, Pagination} from "../../_models/pagination";
|
||||
import {SortableHeader, SortEvent} from "../table/_directives/sortable-header.directive";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-scrobble-history',
|
||||
|
|
@ -26,9 +27,11 @@ import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
|||
})
|
||||
export class UserScrobbleHistoryComponent implements OnInit {
|
||||
|
||||
private readonly scrobbleService = inject(ScrobblingService);
|
||||
private readonly scrobblingService = inject(ScrobblingService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
protected readonly ScrobbleEventType = ScrobbleEventType;
|
||||
|
||||
pagination: Pagination | undefined;
|
||||
events: Array<ScrobbleEvent> = [];
|
||||
|
|
@ -36,11 +39,16 @@ export class UserScrobbleHistoryComponent implements OnInit {
|
|||
'filter': new FormControl('', [])
|
||||
});
|
||||
|
||||
get ScrobbleEventType() { return ScrobbleEventType; }
|
||||
|
||||
ngOnInit() {
|
||||
this.loadPage({column: 'createdUtc', direction: 'desc'});
|
||||
|
||||
this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => {
|
||||
if (hasExpired) {
|
||||
this.toastr.error(translate('toasts.anilist-token-expired'));
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.formGroup.get('filter')?.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe(query => {
|
||||
this.loadPage();
|
||||
})
|
||||
|
|
@ -73,7 +81,7 @@ export class UserScrobbleHistoryComponent implements OnInit {
|
|||
const field = this.mapSortColumnField(sortEvent?.column);
|
||||
const query = this.formGroup.get('filter')?.value;
|
||||
|
||||
this.scrobbleService.getScrobbleEvents({query, field, isDescending}, page, pageSize)
|
||||
this.scrobblingService.getScrobbleEvents({query, field, isDescending}, page, pageSize)
|
||||
.pipe(take(1))
|
||||
.subscribe((result: PaginatedResult<ScrobbleEvent[]>) => {
|
||||
this.events = result.result;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ export interface DirectoryPickerResult {
|
|||
folderPath: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-directory-picker',
|
||||
templateUrl: './directory-picker.component.html',
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { RouterLink } from '@angular/router';
|
|||
import { NgFor, NgIf } from '@angular/common';
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-library',
|
||||
|
|
@ -35,6 +36,15 @@ import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
|||
})
|
||||
export class ManageLibraryComponent implements OnInit {
|
||||
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
private readonly modalService = inject(NgbModal);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly hubService = inject(MessageHubService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
libraries: Library[] = [];
|
||||
loading = false;
|
||||
/**
|
||||
|
|
@ -42,11 +52,8 @@ export class ManageLibraryComponent implements OnInit {
|
|||
*/
|
||||
deletionInProgress: boolean = false;
|
||||
libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(private modalService: NgbModal, private libraryService: LibraryService,
|
||||
private toastr: ToastrService, private confirmService: ConfirmService,
|
||||
private hubService: MessageHubService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getLibraries();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
<app-card-item [entity]="item" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
|
||||
[suppressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
|
||||
[actions]="actions"
|
||||
[selected]="bulkSelectionService.isCardSelected('bookmark', position)" (selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('bookmark', position)"
|
||||
(selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
|
||||
></app-card-item>
|
||||
</ng-template>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import {
|
|||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs';
|
||||
import {take} from 'rxjs';
|
||||
import { BulkSelectionService } from 'src/app/cards/bulk-selection.service';
|
||||
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import {DownloadService} from 'src/app/shared/_services/download.service';
|
||||
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||
|
|
@ -25,7 +25,6 @@ import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/acti
|
|||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { JumpbarService } from 'src/app/_services/jumpbar.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import {DecimalPipe, NgIf} from '@angular/common';
|
||||
import { CardItemComponent } from '../../../cards/card-item/card-item.component';
|
||||
import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component';
|
||||
|
|
@ -45,11 +44,25 @@ import {Title} from "@angular/platform-browser";
|
|||
})
|
||||
export class BookmarksComponent implements OnInit {
|
||||
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
private readonly downloadService = inject(DownloadService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly jumpbarService = inject(JumpbarService);
|
||||
private readonly titleService = inject(Title);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
public readonly imageService = inject(ImageService);
|
||||
|
||||
bookmarks: Array<PageBookmark> = [];
|
||||
series: Array<Series> = [];
|
||||
loadingBookmarks: boolean = false;
|
||||
seriesIds: {[id: number]: number} = {};
|
||||
downloadingSeries: {[id: number]: boolean} = {};
|
||||
clearingSeries: {[id: number]: boolean} = {};
|
||||
actions: ActionItem<Series>[] = [];
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
|
|
@ -64,16 +77,7 @@ export class BookmarksComponent implements OnInit {
|
|||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
refresh: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
|
||||
constructor(private readerService: ReaderService,
|
||||
private downloadService: DownloadService, private toastr: ToastrService,
|
||||
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
|
||||
public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||
private router: Router, private readonly cdRef: ChangeDetectorRef,
|
||||
private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute,
|
||||
private jumpbarService: JumpbarService, private titleService: Title) {
|
||||
|
||||
constructor() {
|
||||
this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => {
|
||||
this.filter = filter;
|
||||
|
||||
|
|
@ -160,8 +164,11 @@ export class BookmarksComponent implements OnInit {
|
|||
this.readerService.getAllBookmarks(this.filter).pipe(take(1)).subscribe(bookmarks => {
|
||||
this.bookmarks = bookmarks;
|
||||
this.bookmarks.forEach(bmk => {
|
||||
this.downloadingSeries[bmk.seriesId] = false;
|
||||
this.clearingSeries[bmk.seriesId] = false;
|
||||
if (!this.seriesIds.hasOwnProperty(bmk.seriesId)) {
|
||||
this.seriesIds[bmk.seriesId] = 0;
|
||||
}
|
||||
this.seriesIds[bmk.seriesId] += 1;
|
||||
});
|
||||
|
||||
const distinctSeriesMap = new Map();
|
||||
|
|
@ -199,14 +206,7 @@ export class BookmarksComponent implements OnInit {
|
|||
}
|
||||
|
||||
downloadBookmarks(series: Series) {
|
||||
this.downloadingSeries[series.id] = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.downloadService.download('bookmark', this.bookmarks.filter(bmk => bmk.seriesId === series.id), (d) => {
|
||||
if (!d) {
|
||||
this.downloadingSeries[series.id] = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
this.downloadService.download('bookmark', this.bookmarks.filter(bmk => bmk.seriesId === series.id));
|
||||
}
|
||||
|
||||
updateFilter(data: FilterEvent) {
|
||||
|
|
|
|||
|
|
@ -108,45 +108,52 @@
|
|||
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="(isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Files].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
@if (!utilityService.isChapter(data)) {
|
||||
<h4>{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
}
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<!-- TODO: Localize title -->
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span>
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">{{t('unread') | uppercase}}</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">{{t('read') | uppercase}}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">{{t('unread') | uppercase}}</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">{{t('read') | uppercase}}</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>{{t('files')}}</ng-template>
|
||||
</h5>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('pages')}} {{file.pages | number:''}}
|
||||
@for (file of chapter.files; track file.id) {
|
||||
<li class="list-group-item no-hover">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('pages')}} {{file.pages | number:''}}
|
||||
</div>
|
||||
@if (data.hasOwnProperty('created')) {
|
||||
<div class="col">
|
||||
{{t('added')}} {{file.created | date: 'short' | defaultDate}}
|
||||
</div>
|
||||
}
|
||||
<div class="col">
|
||||
{{t('size')}} {{file.bytes | bytes}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
{{t('added')}} {{file.created | date: 'short' | defaultDate}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{t('size')}} {{file.bytes | bytes}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -68,12 +68,17 @@ enum TabID {
|
|||
})
|
||||
export class CardDetailDrawerComponent implements OnInit {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly TabID = TabID;
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@Input() libraryId: number = 0;
|
||||
@Input({required: true}) data!: Volume | Chapter;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
/**
|
||||
* If this is a volume, this will be first chapter for said volume.
|
||||
|
|
@ -104,26 +109,13 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
chapterMetadata!: ChapterMetadata;
|
||||
chapterMetadata: ChapterMetadata | undefined;
|
||||
summary: string = '';
|
||||
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
||||
get Breakpoint() {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
get LibraryType() {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
get TabID() {
|
||||
return TabID;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService,
|
||||
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
|
||||
|
|
@ -160,7 +152,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -277,26 +277,9 @@ export class CardItemComponent implements OnInit {
|
|||
});
|
||||
|
||||
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.id == this.entity.id
|
||||
&& 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.id == this.entity.id
|
||||
&& 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.id == this.entity.id
|
||||
&& e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null;
|
||||
}
|
||||
// Is PageBookmark[]
|
||||
if(this.entity.hasOwnProperty('length')) {
|
||||
return events.find(e => e.entityType === 'bookmark'
|
||||
&& e.subTitle === this.downloadService.downloadSubtitle('bookmark', [(this.entity as PageBookmark)])) || null;
|
||||
}
|
||||
return null;
|
||||
console.log('Card Item download obv called for entity: ', this.entity);
|
||||
return this.downloadService.mapToEntityType(events, this.entity);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,86 +1,98 @@
|
|||
<app-side-nav-companion-bar></app-side-nav-companion-bar>
|
||||
|
||||
<ng-container *transloco="let t; read: 'dashboard'">
|
||||
<ng-container *ngIf="libraries$ | async as libraries">
|
||||
<ng-container *ngIf="libraries.length === 0">
|
||||
<div class="mt-3" *ngIf="isAdmin$ | async as isAdmin">
|
||||
<div *ngIf="isAdmin" class="d-flex justify-content-center">
|
||||
<p>{{t('no-libraries')}} <a routerLink="/admin/dashboard" fragment="libraries">{{t('server-settings-link')}}</a></p>
|
||||
</div>
|
||||
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
|
||||
<p>{{t('not-granted')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@if (libraries$ | async; as libraries) {
|
||||
@if (libraries.length === 0) {
|
||||
@if (isAdmin$ | async; as isAdmin) {
|
||||
<div class="mt-3">
|
||||
@if (isAdmin) {
|
||||
<div class="d-flex justify-content-center">
|
||||
<p>{{t('no-libraries')}} <a routerLink="/admin/dashboard" fragment="libraries">{{t('server-settings-link')}}</a></p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="d-flex justify-content-center">
|
||||
<p>{{t('not-granted')}}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<ng-container *ngFor="let stream of streams">
|
||||
<ng-container [ngSwitch]="stream.streamType">
|
||||
<ng-container *ngSwitchCase="StreamType.OnDeck" [ngTemplateOutlet]="onDeck" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
@for(stream of streams; track stream.id) {
|
||||
@switch (stream.streamType) {
|
||||
@case (StreamType.OnDeck) {
|
||||
<ng-container [ngTemplateOutlet]="onDeck" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
}
|
||||
@case (StreamType.RecentlyUpdated) {
|
||||
<ng-container [ngTemplateOutlet]="recentlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
}
|
||||
@case (StreamType.NewlyAdded) {
|
||||
<ng-container [ngTemplateOutlet]="newlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
}
|
||||
@case (StreamType.SmartFilter) {
|
||||
<ng-container [ngTemplateOutlet]="smartFilter" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
}
|
||||
@case (StreamType.MoreInGenre) {
|
||||
<ng-container [ngTemplateOutlet]="moreInGenre" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
}
|
||||
}
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.RecentlyUpdated" [ngTemplateOutlet]="recentlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.NewlyAdded" [ngTemplateOutlet]="newlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.SmartFilter" [ngTemplateOutlet]="smartFilter" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="StreamType.MoreInGenre" [ngTemplateOutlet]="moreInGenre" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #smartFilter let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
@if(stream.api | async; as data) {
|
||||
<app-carousel-reel [items]="data" [title]="stream.name" (sectionClick)="handleFilterSectionClick(stream)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId"
|
||||
(reload)="reloadStream(item.id)" (dataChanged)="reloadStream(item.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #onDeck let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick('on deck')">
|
||||
@if(stream.api | async; as data) {
|
||||
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick(StreamId.OnDeck)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" [isOnDeck]="true"
|
||||
(reload)="reloadStream(stream.id)" (dataChanged)="reloadStream(stream.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #recentlyUpdated let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('recently-updated-title')" (sectionClick)="handleSectionClick('recently updated series')">
|
||||
@if(stream.api | async; as data) {
|
||||
<app-carousel-reel [items]="data" [title]="t('recently-updated-title')" (sectionClick)="handleSectionClick(StreamId.RecentlyUpdatedSeries)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||
[suppressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #newlyUpdated let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('recently-added-title')" (sectionClick)="handleSectionClick('newly added series')">
|
||||
@if(stream.api | async; as data) {
|
||||
<app-carousel-reel [items]="data" [title]="t('recently-added-title')" (sectionClick)="handleSectionClick(StreamId.NewlyAddedSeries)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="reloadStream(stream.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #moreInGenre let-stream: DashboardStream>
|
||||
<ng-container *ngIf="(stream.api | async) as data">
|
||||
<app-carousel-reel [items]="data" [title]="t('more-in-genre-title', {genre: genre?.title})" (sectionClick)="handleSectionClick('more in genre')">
|
||||
@if(stream.api | async; as data) {
|
||||
<app-carousel-reel [items]="data" [title]="t('more-in-genre-title', {genre: genre?.title})" (sectionClick)="handleSectionClick(StreamId.MoreInGenre)">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="reloadStream(stream.id)"></app-series-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<app-loading [loading]="isLoadingDashboard || (streamCount !== streamsLoaded)"></app-loading>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs';
|
||||
|
|
@ -16,7 +16,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import {CardItemComponent} from '../../cards/card-item/card-item.component';
|
||||
import {SeriesCardComponent} from '../../cards/series-card/series-card.component';
|
||||
import {CarouselReelComponent} from '../../carousel/_components/carousel-reel/carousel-reel.component';
|
||||
import {AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet} from '@angular/common';
|
||||
import {AsyncPipe, NgForOf, NgTemplateOutlet} from '@angular/common';
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
|
|
@ -30,6 +30,16 @@ import {Genre} from "../../_models/metadata/genre";
|
|||
import {DashboardStream} from "../../_models/dashboard/dashboard-stream";
|
||||
import {StreamType} from "../../_models/dashboard/stream-type.enum";
|
||||
import {LoadingComponent} from "../../shared/loading/loading.component";
|
||||
import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
|
||||
enum StreamId {
|
||||
OnDeck,
|
||||
RecentlyUpdatedSeries,
|
||||
NewlyAddedSeries,
|
||||
MoreInGenre,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
|
|
@ -37,8 +47,8 @@ import {LoadingComponent} from "../../shared/loading/loading.component";
|
|||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [SideNavCompanionBarComponent, NgIf, RouterLink, CarouselReelComponent, SeriesCardComponent,
|
||||
CardItemComponent, AsyncPipe, TranslocoDirective, NgSwitchCase, NgSwitch, NgForOf, NgTemplateOutlet, LoadingComponent],
|
||||
imports: [SideNavCompanionBarComponent, RouterLink, CarouselReelComponent, SeriesCardComponent,
|
||||
CardItemComponent, AsyncPipe, TranslocoDirective, NgForOf, NgTemplateOutlet, LoadingComponent],
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
|
|
@ -55,6 +65,8 @@ export class DashboardComponent implements OnInit {
|
|||
private readonly messageHub = inject(MessageHubService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly dashboardService = inject(DashboardService);
|
||||
private readonly scrobblingService = inject(ScrobblingService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
|
||||
libraries$: Observable<Library[]> = this.libraryService.getLibraries().pipe(take(1), takeUntilDestroyed(this.destroyRef))
|
||||
isLoadingDashboard = true;
|
||||
|
|
@ -73,6 +85,7 @@ export class DashboardComponent implements OnInit {
|
|||
*/
|
||||
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
|
||||
protected readonly StreamType = StreamType;
|
||||
protected readonly StreamId = StreamId;
|
||||
|
||||
constructor() {
|
||||
this.loadDashboard();
|
||||
|
|
@ -105,6 +118,14 @@ export class DashboardComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
|
||||
this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => {
|
||||
if (hasExpired) {
|
||||
this.toastr.error(translate('toasts.anilist-token-expired'));
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
||||
|
|
@ -186,18 +207,18 @@ export class DashboardComponent implements OnInit {
|
|||
await this.router.navigateByUrl('all-series?' + stream.smartFilterEncoded);
|
||||
}
|
||||
|
||||
handleSectionClick(sectionTitle: string) {
|
||||
if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||
handleSectionClick(streamId: StreamId) {
|
||||
if (streamId === StreamId.RecentlyUpdatedSeries) {
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = 'Recently Updated';
|
||||
params['title'] = translate('dashboard.recently-updated-title');
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
if (filter.sortOptions) {
|
||||
filter.sortOptions.sortField = SortField.LastChapterAdded;
|
||||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||
} else if (streamId === StreamId.OnDeck) {
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = translate('dashboard.on-deck-title');
|
||||
|
|
@ -210,7 +231,7 @@ export class DashboardComponent implements OnInit {
|
|||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
||||
} else if (sectionTitle.toLowerCase() === 'newly added series') {
|
||||
} else if (streamId === StreamId.NewlyAddedSeries) {
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = translate('dashboard.recently-added-title');
|
||||
|
|
@ -220,10 +241,10 @@ export class DashboardComponent implements OnInit {
|
|||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
||||
} else if (sectionTitle.toLowerCase() === 'more in genre') {
|
||||
} else if (streamId === StreamId.MoreInGenre) {
|
||||
const params: any = {};
|
||||
params['page'] = 1;
|
||||
params['title'] = translate('more-in-genre-title', {genre: this.genre?.title});
|
||||
params['title'] = translate('dashboard.more-in-genre-title', {genre: this.genre?.title});
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.statements.push({field: FilterField.Genres, value: this.genre?.id + '', comparison: FilterComparison.MustContains});
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
||||
|
|
|
|||
|
|
@ -132,6 +132,9 @@
|
|||
</div>
|
||||
</li>
|
||||
</ng-container>
|
||||
@if(activeDownloads.length > 1) {
|
||||
<li class="list-group-item dark-menu-item">{{activeDownloads.length}} downloads in Queue</li>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -61,16 +61,15 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||
|
||||
activeEvents: number = 0;
|
||||
|
||||
debugMode: boolean = false;
|
||||
debugMode: boolean = true;
|
||||
|
||||
protected readonly EVENTS = EVENTS;
|
||||
|
||||
get EVENTS() {
|
||||
return EVENTS;
|
||||
}
|
||||
public readonly downloadService = inject(DownloadService);
|
||||
|
||||
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
|
||||
private accountService: AccountService, private confirmService: ConfirmService,
|
||||
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) {
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Component, DestroyRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
|
|
@ -20,6 +20,7 @@ import {NgxStarsModule} from "ngx-stars";
|
|||
import {ThemeService} from "../../../_services/theme.service";
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {ImageComponent} from "../../../shared/image/image.component";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
||||
@Component({
|
||||
selector: 'app-external-rating',
|
||||
|
|
@ -31,28 +32,31 @@ import {ImageComponent} from "../../../shared/image/image.component";
|
|||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ExternalRatingComponent implements OnInit {
|
||||
@Input({required: true}) seriesId!: number;
|
||||
@Input({required: true}) userRating!: number;
|
||||
@Input({required: true}) hasUserRated!: boolean;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
public readonly destroyRef = inject(DestroyRef);
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
@Input({required: true}) seriesId!: number;
|
||||
@Input({required: true}) userRating!: number;
|
||||
@Input({required: true}) hasUserRated!: boolean;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
|
||||
|
||||
ratings: Array<Rating> = [];
|
||||
isLoading: boolean = false;
|
||||
overallRating: number = -1;
|
||||
|
||||
starColor = this.themeService.getCssVariable('--rating-star-color');
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
this.seriesService.getOverallRating(this.seriesId).subscribe(r => this.overallRating = r.averageScore);
|
||||
|
||||
this.accountService.hasValidLicense$.subscribe((res) => {
|
||||
this.accountService.hasValidLicense$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((res) => {
|
||||
if (!res) return;
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
@ -74,6 +78,4 @@ export class ExternalRatingComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,22 +105,32 @@
|
|||
</div>
|
||||
|
||||
<div class="col-auto ms-2 d-none d-md-block" *ngIf="isAdmin || hasDownloadingRole">
|
||||
<button class="btn btn-secondary" (click)="downloadSeries()" [title]="t('download-series--tooltip')" [disabled]="downloadInProgress">
|
||||
<ng-container *ngIf="downloadInProgress; else notDownloading">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">{{t('downloading-status')}}</span>
|
||||
</ng-container>
|
||||
<ng-template #notDownloading>
|
||||
@if (download$ | async; as download) {
|
||||
<button class="btn btn-secondary" (click)="downloadSeries()" [title]="t('download-series--tooltip')" [disabled]="download !== null">
|
||||
<ng-container *ngIf="download !== null; else notDownloading">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">{{t('downloading-status')}}</span>
|
||||
</ng-container>
|
||||
<ng-template #notDownloading>
|
||||
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
</button>
|
||||
} @else {
|
||||
<button class="btn btn-secondary" (click)="downloadSeries()" [title]="t('download-series--tooltip')">
|
||||
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
</button>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="seriesMetadata" class="mt-2">
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"
|
||||
[libraryType]="libraryType"
|
||||
[hasReadingProgress]="hasReadingProgress"></app-series-metadata-detail>
|
||||
</div>
|
||||
|
||||
@if (seriesMetadata) {
|
||||
<div class="mt-2">
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"
|
||||
[libraryType]="libraryType"
|
||||
[hasReadingProgress]="hasReadingProgress"></app-series-metadata-detail>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" [ngClass]="{'pt-3': !seriesMetadata || seriesMetadata?.summary?.length === 0}">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AsyncPipe,
|
||||
DecimalPipe,
|
||||
DOCUMENT,
|
||||
NgClass,
|
||||
|
|
@ -42,13 +43,13 @@ import {
|
|||
NgbTooltip
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {catchError, forkJoin, of} from 'rxjs';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {catchError, forkJoin, Observable, of} from 'rxjs';
|
||||
import {filter, map, take} from 'rxjs/operators';
|
||||
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
|
||||
import {CardDetailDrawerComponent} from 'src/app/cards/card-detail-drawer/card-detail-drawer.component';
|
||||
import {EditSeriesModalComponent} from 'src/app/cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||
import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component';
|
||||
import {DownloadService} from 'src/app/shared/_services/download.service';
|
||||
import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.service';
|
||||
import {KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Chapter} from 'src/app/_models/chapter';
|
||||
import {Device} from 'src/app/_models/device/device';
|
||||
|
|
@ -105,6 +106,7 @@ import {PublicationStatus} from "../../../_models/metadata/publication-status";
|
|||
import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter";
|
||||
import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component";
|
||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||
import {MetadataService} from "../../../_services/metadata.service";
|
||||
|
||||
interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
|
|
@ -126,19 +128,22 @@ interface StoryLineItem {
|
|||
isChapter: boolean;
|
||||
}
|
||||
|
||||
const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.Book];
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-detail',
|
||||
templateUrl: './series-detail.component.html',
|
||||
styleUrls: ['./series-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent, NgClass, NgOptimizedImage, ProviderImagePipe]
|
||||
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent, NgClass, NgOptimizedImage, ProviderImagePipe, AsyncPipe]
|
||||
})
|
||||
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly metadataService = inject(MetadataService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly modalService = inject(NgbModal);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
|
|
@ -261,6 +266,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
|
||||
user: User | undefined;
|
||||
|
||||
/**
|
||||
* This is the download we get from download service.
|
||||
*/
|
||||
download$: Observable<DownloadEvent | null> | null = null;
|
||||
|
||||
bulkActionCallback = (action: ActionItem<any>, data: any) => {
|
||||
if (this.series === undefined) {
|
||||
return;
|
||||
|
|
@ -368,6 +378,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
return;
|
||||
}
|
||||
|
||||
// Setup the download in progress
|
||||
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
|
||||
return this.downloadService.mapToEntityType(events, this.series);
|
||||
}));
|
||||
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
|
||||
if (event.event === EVENTS.SeriesRemoved) {
|
||||
const seriesRemovedEvent = event.payload as SeriesRemovedEvent;
|
||||
|
|
@ -545,7 +560,14 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
|
||||
if (![PublicationStatus.Ended, PublicationStatus.OnGoing].includes(this.seriesMetadata.publicationStatus)) return;
|
||||
this.seriesService.getNextExpectedChapterDate(seriesId).subscribe(date => {
|
||||
if (date == null || date.expectedDate === null) return;
|
||||
if (date == null || date.expectedDate === null) {
|
||||
if (this.nextExpectedChapter !== undefined) {
|
||||
// Clear out the data so the card removes
|
||||
this.nextExpectedChapter = undefined;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.nextExpectedChapter = date;
|
||||
this.cdRef.markForCheck();
|
||||
|
|
@ -563,6 +585,10 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
});
|
||||
this.setContinuePoint();
|
||||
|
||||
if (KavitaPlusSupportedLibraryTypes.includes(this.libraryType) && loadExternal) {
|
||||
this.loadPlusMetadata(this.seriesId);
|
||||
}
|
||||
|
||||
forkJoin({
|
||||
libType: this.libraryService.getLibraryType(this.libraryId),
|
||||
series: this.seriesService.getSeries(seriesId)
|
||||
|
|
@ -570,10 +596,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
this.libraryType = results.libType;
|
||||
this.series = results.series;
|
||||
|
||||
if (this.libraryType !== LibraryType.Comic && loadExternal) {
|
||||
this.loadReviews(true);
|
||||
}
|
||||
|
||||
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
|
||||
|
||||
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
|
||||
|
|
@ -670,23 +692,37 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
}
|
||||
|
||||
loadRecommendations() {
|
||||
this.seriesService.getRecommendationsForSeries(this.seriesId).subscribe(rec => {
|
||||
rec.ownedSeries.map(r => {
|
||||
// loadRecommendations() {
|
||||
// this.seriesService.getRecommendationsForSeries(this.seriesId).subscribe(rec => {
|
||||
// rec.ownedSeries.map(r => {
|
||||
// this.seriesService.getMetadata(r.id).subscribe(m => r.summary = m.summary);
|
||||
// });
|
||||
// this.combinedRecs = [...rec.ownedSeries, ...rec.externalSeries];
|
||||
// this.hasRecommendations = this.combinedRecs.length > 0;
|
||||
// this.cdRef.markForCheck();
|
||||
// });
|
||||
// }
|
||||
|
||||
loadPlusMetadata(seriesId: number) {
|
||||
this.metadataService.getSeriesMetadataFromPlus(seriesId).subscribe(data => {
|
||||
if (data === null) return;
|
||||
|
||||
// Reviews
|
||||
this.reviews = [...data.reviews];
|
||||
|
||||
// Recommendations
|
||||
data.recommendations.ownedSeries.map(r => {
|
||||
this.seriesService.getMetadata(r.id).subscribe(m => r.summary = m.summary);
|
||||
});
|
||||
this.combinedRecs = [...rec.ownedSeries, ...rec.externalSeries];
|
||||
this.combinedRecs = [...data.recommendations.ownedSeries, ...data.recommendations.externalSeries];
|
||||
this.hasRecommendations = this.combinedRecs.length > 0;
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
loadReviews(loadRecs: boolean = false) {
|
||||
loadReviews() {
|
||||
this.seriesService.getReviews(this.seriesId).subscribe(reviews => {
|
||||
this.reviews = [...reviews];
|
||||
if (loadRecs) {
|
||||
this.loadRecommendations(); // We do this as first load will spam 3 calls on API layer
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
|
@ -829,7 +865,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
modalRef.componentInstance.series = this.series;
|
||||
modalRef.closed.subscribe((closeResult: {success: boolean}) => {
|
||||
if (closeResult.success) {
|
||||
this.loadReviews();
|
||||
this.loadReviews(); // TODO: Ensure reviews get updated here
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
|
||||
|
||||
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" heading="Ratings">
|
||||
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" [heading]="t('rating-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,18 @@ import {ImageComponent} from "../../../shared/image/image.component";
|
|||
})
|
||||
export class SeriesMetadataDetailComponent implements OnChanges {
|
||||
|
||||
protected readonly imageService = inject(ImageService);
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
|
||||
protected readonly FilterField = FilterField;
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
@Input({required: true}) seriesMetadata!: SeriesMetadata;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
@Input() hasReadingProgress: boolean = false;
|
||||
|
|
@ -60,23 +72,11 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||
isCollapsed: boolean = true;
|
||||
hasExtendedProperties: boolean = false;
|
||||
|
||||
protected readonly imageService = inject(ImageService);
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
|
||||
/**
|
||||
* Html representation of Series Summary
|
||||
*/
|
||||
seriesSummary: string = '';
|
||||
|
||||
protected FilterField = FilterField;
|
||||
protected LibraryType = LibraryType;
|
||||
protected MangaFormat = MangaFormat;
|
||||
protected TagBadgeCursor = TagBadgeCursor;
|
||||
|
||||
get WebLinks() {
|
||||
if (this.seriesMetadata?.webLinks === '') return [];
|
||||
return this.seriesMetadata?.webLinks.split(',') || [];
|
||||
|
|
@ -121,6 +121,4 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||
navigate(basePage: string, id: number) {
|
||||
this.router.navigate([basePage, id]);
|
||||
}
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
tap,
|
||||
finalize,
|
||||
of,
|
||||
filter,
|
||||
filter, Subject,
|
||||
} from 'rxjs';
|
||||
import { download, Download } from '../_models/download';
|
||||
import { PageBookmark } from 'src/app/_models/readers/page-bookmark';
|
||||
|
|
@ -22,6 +22,10 @@ import { BytesPipe } from 'src/app/_pipes/bytes.pipe';
|
|||
import {translate} from "@ngneat/transloco";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {SAVER, Saver} from "../../_providers/saver.provider";
|
||||
import {UtilityService} from "./utility.service";
|
||||
import {CollectionTag} from "../../_models/collection-tag";
|
||||
import {RecentlyAddedItem} from "../../_models/recently-added-item";
|
||||
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||
|
||||
export const DEBOUNCE_TIME = 100;
|
||||
|
||||
|
|
@ -55,6 +59,7 @@ export type DownloadEntityType = 'volume' | 'chapter' | 'series' | 'bookmark' |
|
|||
*/
|
||||
export type DownloadEntity = Series | Volume | Chapter | PageBookmark[] | undefined;
|
||||
|
||||
export type QueueableDownloadType = Chapter | Volume;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -68,14 +73,33 @@ export class DownloadService {
|
|||
public SIZE_WARNING = 104_857_600;
|
||||
|
||||
private downloadsSource: BehaviorSubject<DownloadEvent[]> = new BehaviorSubject<DownloadEvent[]>([]);
|
||||
/**
|
||||
* Active Downloads
|
||||
*/
|
||||
public activeDownloads$ = this.downloadsSource.asObservable();
|
||||
|
||||
private downloadQueue: BehaviorSubject<QueueableDownloadType[]> = new BehaviorSubject<QueueableDownloadType[]>([]);
|
||||
/**
|
||||
* Queued Downloads
|
||||
*/
|
||||
public queuedDownloads$ = this.downloadQueue.asObservable();
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly httpClient = inject(HttpClient);
|
||||
private readonly utilityService = inject(UtilityService);
|
||||
|
||||
constructor(@Inject(SAVER) private save: Saver) { }
|
||||
constructor(@Inject(SAVER) private save: Saver) {
|
||||
this.downloadQueue.subscribe((queue) => {
|
||||
if (queue.length > 0) {
|
||||
const entity = queue.shift();
|
||||
console.log('Download Queue shifting entity: ', entity);
|
||||
if (entity === undefined) return;
|
||||
this.processDownload(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -84,7 +108,7 @@ export class DownloadService {
|
|||
* @param downloadEntity
|
||||
* @returns
|
||||
*/
|
||||
downloadSubtitle(downloadEntityType: DownloadEntityType, downloadEntity: DownloadEntity | undefined) {
|
||||
downloadSubtitle(downloadEntityType: DownloadEntityType | undefined, downloadEntity: DownloadEntity | undefined) {
|
||||
switch (downloadEntityType) {
|
||||
case 'series':
|
||||
return (downloadEntity as Series).name;
|
||||
|
|
@ -97,6 +121,7 @@ export class DownloadService {
|
|||
case 'logs':
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,10 +142,12 @@ export class DownloadService {
|
|||
case 'volume':
|
||||
sizeCheckCall = this.downloadVolumeSize((entity as Volume).id);
|
||||
downloadCall = this.downloadVolume(entity as Volume);
|
||||
//this.enqueueDownload(entity as Volume);
|
||||
break;
|
||||
case 'chapter':
|
||||
sizeCheckCall = this.downloadChapterSize((entity as Chapter).id);
|
||||
downloadCall = this.downloadChapter(entity as Chapter);
|
||||
//this.enqueueDownload(entity as Chapter);
|
||||
break;
|
||||
case 'bookmark':
|
||||
sizeCheckCall = of(0);
|
||||
|
|
@ -145,8 +172,10 @@ export class DownloadService {
|
|||
})
|
||||
).pipe(filter(wantsToDownload => {
|
||||
return wantsToDownload;
|
||||
}), switchMap(() => {
|
||||
return downloadCall.pipe(
|
||||
}),
|
||||
filter(_ => downloadCall !== undefined),
|
||||
switchMap(() => {
|
||||
return (downloadCall || of(undefined)).pipe(
|
||||
tap((d) => {
|
||||
if (callback) callback(d);
|
||||
}),
|
||||
|
|
@ -187,7 +216,40 @@ export class DownloadService {
|
|||
);
|
||||
}
|
||||
|
||||
private getIdKey(entity: Chapter | Volume) {
|
||||
if (this.utilityService.isVolume(entity)) return 'volumeId';
|
||||
if (this.utilityService.isChapter(entity)) return 'chapterId';
|
||||
if (this.utilityService.isSeries(entity)) return 'seriesId';
|
||||
return 'id';
|
||||
}
|
||||
|
||||
private getDownloadEntityType(entity: Chapter | Volume): DownloadEntityType {
|
||||
if (this.utilityService.isVolume(entity)) return 'volume';
|
||||
if (this.utilityService.isChapter(entity)) return 'chapter';
|
||||
if (this.utilityService.isSeries(entity)) return 'series';
|
||||
return 'logs'; // This is a hack but it will never occur
|
||||
}
|
||||
|
||||
private downloadEntity<T>(entity: Chapter | Volume): Observable<any> {
|
||||
const downloadEntityType = this.getDownloadEntityType(entity);
|
||||
const subtitle = this.downloadSubtitle(downloadEntityType, entity);
|
||||
const idKey = this.getIdKey(entity);
|
||||
const url = `${this.baseUrl}download/${downloadEntityType}?${idKey}=${entity.id}`;
|
||||
|
||||
return this.httpClient.get(url, { observe: 'events', responseType: 'blob', reportProgress: true }).pipe(
|
||||
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
download((blob, filename) => {
|
||||
this.save(blob, decodeURIComponent(filename));
|
||||
}),
|
||||
tap((d) => this.updateDownloadState(d, downloadEntityType, subtitle, entity.id)),
|
||||
finalize(() => this.finalizeDownloadState(downloadEntityType, subtitle))
|
||||
);
|
||||
}
|
||||
|
||||
private downloadSeries(series: Series) {
|
||||
|
||||
// TODO: Call backend for all the volumes and loose leaf chapters then enqueque them all
|
||||
|
||||
const downloadType = 'series';
|
||||
const subtitle = this.downloadSubtitle(downloadType, series);
|
||||
return this.httpClient.get(this.baseUrl + 'download/series?seriesId=' + series.id,
|
||||
|
|
@ -227,38 +289,42 @@ export class DownloadService {
|
|||
}
|
||||
|
||||
private downloadChapter(chapter: Chapter) {
|
||||
const downloadType = 'chapter';
|
||||
const subtitle = this.downloadSubtitle(downloadType, chapter);
|
||||
return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
|
||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
).pipe(
|
||||
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
download((blob, filename) => {
|
||||
this.save(blob, decodeURIComponent(filename));
|
||||
}),
|
||||
tap((d) => this.updateDownloadState(d, downloadType, subtitle, chapter.id)),
|
||||
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
);
|
||||
return this.downloadEntity(chapter);
|
||||
|
||||
// const downloadType = 'chapter';
|
||||
// const subtitle = this.downloadSubtitle(downloadType, chapter);
|
||||
// return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
|
||||
// {observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
// ).pipe(
|
||||
// throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
// download((blob, filename) => {
|
||||
// this.save(blob, decodeURIComponent(filename));
|
||||
// }),
|
||||
// tap((d) => this.updateDownloadState(d, downloadType, subtitle, chapter.id)),
|
||||
// finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
// );
|
||||
}
|
||||
|
||||
private downloadVolume(volume: Volume): Observable<Download> {
|
||||
const downloadType = 'volume';
|
||||
const subtitle = this.downloadSubtitle(downloadType, volume);
|
||||
return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
|
||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
).pipe(
|
||||
throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
download((blob, filename) => {
|
||||
this.save(blob, decodeURIComponent(filename));
|
||||
}),
|
||||
tap((d) => this.updateDownloadState(d, downloadType, subtitle, volume.id)),
|
||||
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
);
|
||||
private downloadVolume(volume: Volume) {
|
||||
return this.downloadEntity(volume);
|
||||
// const downloadType = 'volume';
|
||||
// const subtitle = this.downloadSubtitle(downloadType, volume);
|
||||
// return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
|
||||
// {observe: 'events', responseType: 'blob', reportProgress: true}
|
||||
// ).pipe(
|
||||
// throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }),
|
||||
// download((blob, filename) => {
|
||||
// this.save(blob, decodeURIComponent(filename));
|
||||
// }),
|
||||
// tap((d) => this.updateDownloadState(d, downloadType, subtitle, volume.id)),
|
||||
// finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
// );
|
||||
}
|
||||
|
||||
private async confirmSize(size: number, entityType: DownloadEntityType) {
|
||||
return (size < this.SIZE_WARNING ||
|
||||
await this.confirmService.confirm(translate('toasts.confirm-download-size', {entityType: translate('entity-type.' + entityType), size: bytesPipe.transform(size)})));
|
||||
await this.confirmService.confirm(translate('toasts.confirm-download-size',
|
||||
{entityType: translate('entity-type.' + entityType), size: bytesPipe.transform(size)})));
|
||||
}
|
||||
|
||||
private downloadBookmarks(bookmarks: PageBookmark[]) {
|
||||
|
|
@ -276,4 +342,60 @@ export class DownloadService {
|
|||
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private processDownload(entity: QueueableDownloadType): void {
|
||||
const downloadObservable = this.downloadEntity(entity);
|
||||
console.log('Process Download called for entity: ', entity);
|
||||
|
||||
// When we consume one, we need to take it off the queue
|
||||
|
||||
downloadObservable.subscribe((downloadEvent) => {
|
||||
// Download completed, process the next item in the queue
|
||||
if (downloadEvent.state === 'DONE') {
|
||||
this.processNextDownload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private processNextDownload(): void {
|
||||
const currentQueue = this.downloadQueue.value;
|
||||
if (currentQueue.length > 0) {
|
||||
const nextEntity = currentQueue[0];
|
||||
this.processDownload(nextEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private enqueueDownload(entity: QueueableDownloadType): void {
|
||||
const currentQueue = this.downloadQueue.value;
|
||||
const newQueue = [...currentQueue, entity];
|
||||
this.downloadQueue.next(newQueue);
|
||||
|
||||
// If the queue was empty, start processing the download
|
||||
if (currentQueue.length === 0) {
|
||||
this.processNextDownload();
|
||||
}
|
||||
}
|
||||
|
||||
mapToEntityType(events: DownloadEvent[], entity: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem | NextExpectedChapter) {
|
||||
if(this.utilityService.isSeries(entity)) {
|
||||
return events.find(e => e.entityType === 'series' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('series', (entity as Series))) || null;
|
||||
}
|
||||
if(this.utilityService.isVolume(entity)) {
|
||||
return events.find(e => e.entityType === 'volume' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('volume', (entity as Volume))) || null;
|
||||
}
|
||||
if(this.utilityService.isChapter(entity)) {
|
||||
return events.find(e => e.entityType === 'chapter' && e.id == entity.id
|
||||
&& e.subTitle === this.downloadSubtitle('chapter', (entity as Chapter))) || null;
|
||||
}
|
||||
// Is PageBookmark[]
|
||||
if(entity.hasOwnProperty('length')) {
|
||||
return events.find(e => e.entityType === 'bookmark'
|
||||
&& e.subTitle === this.downloadSubtitle('bookmark', [(entity as PageBookmark)])) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,9 @@ export class SideNavComponent implements OnInit {
|
|||
case (Action.AnalyzeFiles):
|
||||
await this.actionService.analyzeFiles(library);
|
||||
break;
|
||||
case (Action.Delete):
|
||||
await this.actionService.deleteLibrary(library);
|
||||
break;
|
||||
case (Action.Edit):
|
||||
this.actionService.editLibrary(library, () => window.scrollTo(0, 0));
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,18 @@
|
|||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11"><h4 id="anilist-token-header">{{t('title')}}</h4></div>
|
||||
<div class="col-10 col-sm-11">
|
||||
<h4 id="anilist-token-header">{{t('title')}}
|
||||
@if(!tokenExpired) {
|
||||
<i class="fa-solid fa-circle-check ms-1 confirm-icon" aria-hidden="true" [ngbTooltip]="t('token-valid')"></i>
|
||||
<span class="visually-hidden">{{t('token-valid')}}</span>
|
||||
} @else {
|
||||
<i class="fa-solid fa-circle ms-1 confirm-icon error" aria-hidden="true" [ngbTooltip]="t('token-not-valid')"></i>
|
||||
<span class="visually-hidden">{{t('token-not-valid')}}</span>
|
||||
}
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button class="btn btn-primary btn-sm" [disabled]="!hasValidLicense" (click)="toggleViewMode()">{{isViewMode ? t('edit') : t('cancel')}}</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
color: var(--primary-color);
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@
|
|||
<div class="container-fluid row mb-2">
|
||||
<div class="col-10 col-sm-11">
|
||||
<h4 id="email-card">{{t('email-label')}}
|
||||
<ng-container *ngIf="!emailConfirmed">
|
||||
<i class="fa-solid fa-circle ms-1 confirm-icon" aria-hidden="true" [ngbTooltip]="t('email-not-confirmed')"></i>
|
||||
@if(emailConfirmed) {
|
||||
<i class="fa-solid fa-circle-check ms-1 confirm-icon" aria-hidden="true" [ngbTooltip]="t('email-confirmed')"></i>
|
||||
<span class="visually-hidden">{{t('email-confirmed')}}</span>
|
||||
} @else {
|
||||
<i class="fa-solid fa-circle ms-1 confirm-icon error" aria-hidden="true" [ngbTooltip]="t('email-not-confirmed')"></i>
|
||||
<span class="visually-hidden">{{t('email-not-confirmed')}}</span>
|
||||
</ng-container>
|
||||
}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
|
|
|
|||
|
|
@ -3,3 +3,7 @@
|
|||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue