Change Detection: On Push aka UI Smoothness (#1369)
* Updated Series Info Cards to use OnPush and hooked in progress events when we do a mark as read/unread on entities. These events update progress bars but also will now trigger a re-calculation on Read Time Left. * Removed Library Card Component * Refactored manga reader title and subtitle calculation to the backend. * Coverted card actionables to onPush * Series Card on push cleanup * Updated edit collection tags for on push * Update cover image chooser for on push * Cleaned up carsouel reel * Updated cover image to allow for uploading gif and webp files * Bulk add to collection on push * Updated bulk operation to use on push. Updated bulk operation to have mark as unread and read buttons explicitly. Updated so add to collection is visible and delete. Fixed a bug where manage library component wasn't invoking the trackBy function * Updating entity title for on push * Removed file info component * Updated Mange Library for on push * Entity info cards on push * List item on push * Updated icon and title for on push and fixed some missing change detection on series detail * Restricted the typeahead interface to simplify the design * Edit Series Relation now shows a value in the dropdown for Parent relationships and disables the field. * Updated edit series relation to focus on new typeahead when adding a new relationship * Added some documentation and when Scanning a library, don't allow the user to enqueue the same job multiple times. * Applied the No-enqueue if already enqueued logic to other tasks * Library detail on push * Updated events widget to onpush * Card detail drawer on push. Card detail cover chooser now will show all chapter's covers for selection in cover chooser. * Chapter metadata detail on push * Removed Card Detail modal * All collections on push * Removed some comments * Updated bulk selection to use an observable rather than function calls so new on push strategy works * collection detail now uses on push and scroller is placed on correct element * Updated library recommended to on push. Ensure that when mark as read occurs, the appropriate streams are refreshed. * Updated library detail to on push * Update metadata fiter to onpush. Bugs found and reported to Project * person badge on push * Read more on push * Updated tag badge to on push * User login on push * When initing side nav, don't call an authenticated api until we are sure a user is logged in * Updated splash container to on push * Dashboard on push * Side nav slight refactor around some api calls * Cleaned up series card on push to use same cdRef naming convention * Updated Static Files to use caching * Added width and height to logo image * shortcuts modal on push * reading lists on push * Reading list detail on push * draggable ordered list on push * Refactored reading-list-detail to use a new item which drastically reduces renders on operations * series format on push * circular loader on push * Badge Expander on push * update notification modal on push * drawer on push * Edit Series Modal on push * reset password on push * review series modal on push * series metadata detail on push * theme manager on push * confirm reset password on push * register on push * confirm migration email on push * confirm email on push * add email to account migration on push * user preferences on push. Made global settings default open * edit series relation on push * Fixed an edge case bug for next chapter where if the current volume had a single chapter of 1 and the next volume had a chapter number of 0, it would say there are no more chapters. * Updated infinite scroller with on push support * Moved some animations over to typeahead, not integrated yet. * Manga reader is now on push * Reader settings on push * refactored how we close the book * Updated table of contents for on push * Updated book reader for on push. Fixed a bug where table of contents wasn't showing current page anchor due to a scroll calulation bug * Small code tweak * Icon and title on push * nav header on push * grouped typeahead on push * typeahead on push and added a new trackby identity function to allow even faster rendering of big lists * pdf reader on push * code cleanup
This commit is contained in:
parent
f5be0fac58
commit
4e49aa47ce
126 changed files with 1658 additions and 1674 deletions
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item clickable" tabindex="0" role="button" *ngFor="let collectionTag of lists | filter: filterList; let i = index" (click)="addToCollection(collectionTag)">
|
||||
<li class="list-group-item clickable" tabindex="0" role="button" *ngFor="let collectionTag of lists | filter: filterList; let i = index; trackBy: collectionTitleTrackby" (click)="addToCollection(collectionTag)">
|
||||
{{collectionTag.title}} <i class="fa fa-angle-double-up" *ngIf="collectionTag.promoted" title="Promoted"></i>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, ElementRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { FormGroup, FormControl } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
|
@ -10,7 +10,8 @@ import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
|||
selector: 'app-bulk-add-to-collection',
|
||||
templateUrl: './bulk-add-to-collection.component.html',
|
||||
encapsulation: ViewEncapsulation.None, // This is needed as per the bootstrap modal documentation to get styles to work.
|
||||
styleUrls: ['./bulk-add-to-collection.component.scss']
|
||||
styleUrls: ['./bulk-add-to-collection.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BulkAddToCollectionComponent implements OnInit {
|
||||
|
||||
|
|
@ -27,10 +28,13 @@ export class BulkAddToCollectionComponent implements OnInit {
|
|||
loading: boolean = false;
|
||||
listForm: FormGroup = new FormGroup({});
|
||||
|
||||
collectionTitleTrackby = (index: number, item: CollectionTag) => `${item.title}`;
|
||||
|
||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||
|
||||
|
||||
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService, private toastr: ToastrService) { }
|
||||
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
|
|
@ -38,9 +42,11 @@ export class BulkAddToCollectionComponent implements OnInit {
|
|||
this.listForm.addControl('filterQuery', new FormControl('', []));
|
||||
|
||||
this.loading = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.lists = tags;
|
||||
this.loading = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +54,7 @@ export class BulkAddToCollectionComponent implements OnInit {
|
|||
// Shift focus to input
|
||||
if (this.inputElem) {
|
||||
this.inputElem.nativeElement.select();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
<div *ngIf="data !== undefined">
|
||||
<div class="modal-header">
|
||||
<h4 *ngIf="libraryType !== LibraryType.Comic else comicHeader" class="modal-title" id="modal-basic-title">
|
||||
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Chapter ' : 'Volume ') + data.number : 'Special'}} Details</h4>
|
||||
<ng-template #comicHeader><h4 class="modal-title" id="modal-basic-title">
|
||||
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Issue #' : 'Volume ') + data.number : 'Special'}} Details</h4>
|
||||
</ng-template>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[0]" *ngIf="!tabs[0].disabled">
|
||||
<a ngbNavLink>{{tabs[0].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="container-fluid row g-0">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||
ID: {{data.id}}
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="row g-0">
|
||||
<h4>
|
||||
{{chapter?.titleName}}
|
||||
</h4>
|
||||
<span>
|
||||
<span *ngIf="chapterMetadata && chapterMetadata.releaseDate !== null">Release Date: {{chapterMetadata.releaseDate | date: 'shortDate' || '-'}}</span>
|
||||
</span>
|
||||
<span class="text-accent">{{data.pages}} pages</span>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
Age Rating: {{ageRating}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="chapterMetadata !== undefined">
|
||||
<div class="row g-0" *ngIf="chapterMetadata.tags && chapterMetadata.tags.length > 0">
|
||||
<h6>Tags</h6>
|
||||
<app-badge-expander [items]="chapterMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
<div class="row g-0" *ngIf="chapterMetadata.genres && chapterMetadata.genres.length > 0">
|
||||
<h6>Genres</h6>
|
||||
<app-badge-expander [items]="chapterMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[1]" *ngIf="!tabs[1].disabled">
|
||||
<a ngbNavLink>{{tabs[1].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-chapter-metadata-detail [chapter]="chapterMetadata"></app-chapter-metadata-detail>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[2]" *ngIf="!tabs[2].disabled">
|
||||
<a ngbNavLink>{{tabs[2].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="chapter.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
<div class="row g-0">
|
||||
<button class="btn btn-primary flex-end mb-2" [disabled]="coverImageSaveLoading" (click)="saveCoverImage()">
|
||||
<ng-container *ngIf="coverImageSaveLoading; else notSaving">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</ng-container>
|
||||
<ng-template #notSaving>
|
||||
Save
|
||||
</ng-template>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[3]" *ngIf="!tabs[3].disabled">
|
||||
<a ngbNavLink>{{tabs[3].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<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]="chapter.coverImage"></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">UNREAD</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>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">
|
||||
Pages: {{file.pages}}
|
||||
</div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
Added: {{(data.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.scrollable-modal {
|
||||
max-height: 90vh; // 600px
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import { LibraryType } from '../../../_models/library';
|
||||
import { LibraryService } from '../../../_services/library.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { PersonRole } from 'src/app/_models/person';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
|
||||
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-details-modal',
|
||||
templateUrl: './card-details-modal.component.html',
|
||||
styleUrls: ['./card-details-modal.component.scss']
|
||||
})
|
||||
export class CardDetailsModalComponent implements OnInit {
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@Input() libraryId: number = 0;
|
||||
@Input() data!: Volume | Chapter; // Volume | Chapter
|
||||
|
||||
/**
|
||||
* If this is a volume, this will be first chapter for said volume.
|
||||
*/
|
||||
chapter!: Chapter;
|
||||
isChapter = false;
|
||||
chapters: Chapter[] = [];
|
||||
|
||||
|
||||
/**
|
||||
* If a cover image update occured.
|
||||
*/
|
||||
coverImageUpdate: boolean = false;
|
||||
coverImageIndex: number = 0;
|
||||
/**
|
||||
* Url of the selected cover
|
||||
*/
|
||||
selectedCover: string = '';
|
||||
coverImageLocked: boolean = false;
|
||||
/**
|
||||
* When the API is doing work
|
||||
*/
|
||||
coverImageSaveLoading: boolean = false;
|
||||
imageUrls: Array<string> = [];
|
||||
|
||||
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
|
||||
|
||||
tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Info', disabled: false}];
|
||||
active = this.tabs[0];
|
||||
|
||||
chapterMetadata!: ChapterMetadata;
|
||||
ageRating!: string;
|
||||
|
||||
|
||||
get Breakpoint(): typeof Breakpoint {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
get PersonRole() {
|
||||
return PersonRole;
|
||||
}
|
||||
|
||||
get LibraryType(): typeof LibraryType {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, 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) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
|
||||
this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0];
|
||||
|
||||
this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id));
|
||||
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
|
||||
this.metadataService.getAgeRating(this.chapterMetadata.ageRating).subscribe(ageRating => this.ageRating = ageRating);
|
||||
});
|
||||
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
if (!this.accountService.hasAdminRole(user)) {
|
||||
this.tabs.find(s => s.title === 'Cover')!.disabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
});
|
||||
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit);
|
||||
|
||||
if (this.isChapter) {
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else if (!this.isChapter) {
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
// TODO: Move this into the backend
|
||||
this.chapters.sort(this.utilityService.sortChapters);
|
||||
this.chapters.forEach(c => c.coverImage = this.imageService.getChapterCoverImage(c.id));
|
||||
// Try to show an approximation of the reading order for files
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close({coverImageUpdate: this.coverImageUpdate});
|
||||
}
|
||||
|
||||
formatChapterNumber(chapter: Chapter) {
|
||||
if (chapter.number === '0') {
|
||||
return '1';
|
||||
}
|
||||
return chapter.number;
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, chapter);
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.coverImageIndex = index;
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageLocked = false;
|
||||
}
|
||||
|
||||
saveCoverImage() {
|
||||
this.coverImageSaveLoading = true;
|
||||
const selectedIndex = this.coverImageIndex || 0;
|
||||
if (selectedIndex > 0) {
|
||||
this.uploadService.updateChapterCoverImage(this.chapter.id, this.selectedCover).subscribe(() => {
|
||||
if (this.coverImageIndex > 0) {
|
||||
this.chapter.coverImageLocked = true;
|
||||
this.coverImageUpdate = true;
|
||||
}
|
||||
this.coverImageSaveLoading = false;
|
||||
}, err => this.coverImageSaveLoading = false);
|
||||
} else if (this.coverImageLocked === false) {
|
||||
this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => {
|
||||
this.toastr.info('Cover image reset');
|
||||
this.coverImageSaveLoading = false;
|
||||
this.coverImageUpdate = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
markChapterAsRead(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { /* No Action */ });
|
||||
}
|
||||
|
||||
markChapterAsUnread(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { /* No Action */ });
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: Action, chapter: Chapter) {
|
||||
switch (action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markChapterAsRead(chapter);
|
||||
break;
|
||||
case(Action.MarkAsUnread):
|
||||
this.markChapterAsUnread(chapter);
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addChapterToReadingList(chapter, this.seriesId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readChapter(chapter: Chapter) {
|
||||
if (chapter.pages === 0) {
|
||||
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, this.chapter.id, chapter.files[0].format));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Edit {{tag?.title}} Collection</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[TabID.General].id">
|
||||
<a ngbNavLink>{{tabs[TabID.General].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>
|
||||
<p class="alert alert-secondary" role="alert">
|
||||
This tag is currently {{tag?.promoted ? 'promoted' : 'not promoted'}} (<i class="fa fa-angle-double-up" aria-hidden="true"></i>).
|
||||
Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.
|
||||
</p>
|
||||
|
|
@ -52,7 +50,8 @@
|
|||
<li [ngbNavItem]="tabs[TabID.CoverImage].id">
|
||||
<a ngbNavLink>{{tabs[TabID.CoverImage].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-primary" role="alert">
|
||||
<p class="alert alert-secondary" role="alert">
|
||||
<!-- TODO: I don't think we need this anymore, it's a bit intuitive -->
|
||||
Upload and choose a new cover image. Press Save to upload and override the cover.
|
||||
</p>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="tag.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
|
@ -24,7 +24,8 @@ enum TabID {
|
|||
@Component({
|
||||
selector: 'app-edit-collection-tags',
|
||||
templateUrl: './edit-collection-tags.component.html',
|
||||
styleUrls: ['./edit-collection-tags.component.scss']
|
||||
styleUrls: ['./edit-collection-tags.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditCollectionTagsComponent implements OnInit {
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||
private imageService: ImageService, private uploadService: UploadService,
|
||||
public utilityService: UtilityService) { }
|
||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.pagination == undefined) {
|
||||
|
|
@ -97,6 +98,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
this.isLoading = false;
|
||||
|
||||
this.libraryNames = results[1];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -108,15 +110,18 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
} else if (numberOfSelected == this.series.length) {
|
||||
this.selectAll = true;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
togglePromotion() {
|
||||
const originalPromotion = this.tag.promoted;
|
||||
this.tag.promoted = !this.tag.promoted;
|
||||
this.cdRef.markForCheck();
|
||||
this.collectionService.updateTag(this.tag).subscribe(res => {
|
||||
this.toastr.success('Tag updated successfully');
|
||||
}, err => {
|
||||
this.tag.promoted = originalPromotion;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +149,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
];
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover))
|
||||
apis.push(this.uploadService.updateCollectionCoverImage(this.tag.id, this.selectedCover));
|
||||
}
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
|
|
@ -161,12 +166,14 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.collectionTagForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { forkJoin, Observable, of, Subject } from 'rxjs';
|
||||
|
|
@ -34,7 +34,8 @@ enum TabID {
|
|||
@Component({
|
||||
selector: 'app-edit-series-modal',
|
||||
templateUrl: './edit-series-modal.component.html',
|
||||
styleUrls: ['./edit-series-modal.component.scss']
|
||||
styleUrls: ['./edit-series-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -109,7 +110,8 @@ 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));
|
||||
|
|
@ -136,19 +138,23 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
publicationStatus: new FormControl('', []),
|
||||
language: new FormControl('', []),
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
this.ageRatings = ratings;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.metadataService.getAllPublicationStatus().subscribe(statuses => {
|
||||
this.publicationStatuses = statuses;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.metadataService.getAllValidLanguages().subscribe(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
})
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.seriesService.getMetadata(this.series.id).subscribe(metadata => {
|
||||
if (metadata) {
|
||||
|
|
@ -159,38 +165,46 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.editSeriesForm.get('ageRating')?.patchValue(this.metadata.ageRating);
|
||||
this.editSeriesForm.get('publicationStatus')?.patchValue(this.metadata.publicationStatus);
|
||||
this.editSeriesForm.get('language')?.patchValue(this.metadata.language);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.editSeriesForm.get('name')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.series.nameLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('sortName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.series.sortNameLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('localizedName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.series.localizedNameLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('summary')?.valueChanges.pipe(takeUntil(this.onDestroy)).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.metadata.ageRating = parseInt(val + '', 10);
|
||||
this.metadata.ageRatingLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.metadata.publicationStatus = parseInt(val + '', 10);
|
||||
this.metadata.publicationStatusLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.isLoadingVolumes = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.seriesService.getVolumes(this.series.id).subscribe(volumes => {
|
||||
this.seriesVolumes = volumes;
|
||||
this.isLoadingVolumes = false;
|
||||
|
|
@ -204,6 +218,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
return f;
|
||||
})).flat();
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -221,6 +236,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.setupLanguageTypeahead()
|
||||
]).subscribe(results => {
|
||||
this.collectionTags = this.metadata.collectionTags;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -441,8 +457,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.saveNestedComponents.emit();
|
||||
|
||||
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
this.modal.close({success: true, series: model, coverImageUpdate: selectedIndex > 0});
|
||||
});
|
||||
|
|
@ -450,16 +464,19 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
|
||||
updateCollections(tags: CollectionTag[]) {
|
||||
this.collectionTags = tags;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateTags(tags: Tag[]) {
|
||||
this.tags = tags;
|
||||
this.metadata.tags = tags;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateGenres(genres: Genre[]) {
|
||||
this.genres = genres;
|
||||
this.metadata.genres = genres;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateLanguage(language: Array<Language>) {
|
||||
|
|
@ -468,6 +485,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
this.metadata.language = language[0].isoCode;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updatePerson(persons: Person[], role: PersonRole) {
|
||||
|
|
@ -501,18 +519,20 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
break;
|
||||
case PersonRole.Translator:
|
||||
this.metadata.translators = persons;
|
||||
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.editSeriesForm.patchValue({
|
||||
coverImageIndex: index
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
|
|
@ -520,12 +540,14 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.editSeriesForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
unlock(b: any, field: string) {
|
||||
if (b) {
|
||||
b[field] = !b[field];
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,27 @@
|
|||
<div *ngIf="bulkSelectionService.hasSelections()" class="bulk-select mb-3 fixed-top" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
<span class="highlight"><i class="fa fa-check" aria-hidden="true"></i> {{bulkSelectionService.totalSelections()}} selected</span>
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
||||
<ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount">
|
||||
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 fixed-top" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
|
||||
<span class="highlight">
|
||||
<i class="fa fa-check me-1" aria-hidden="true"></i>
|
||||
{{selectionCount}} items selected
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<button *ngIf="hasMarkAsUnread" class="btn btn-icon" (click)="executeAction(Action.MarkAsUnread)" ngbTooltip="Mark as Unread" placement="bottom">
|
||||
<i class="fa-regular fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Mark as Unread</span>
|
||||
</button>
|
||||
<button *ngIf="hasMarkAsRead" class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" ngbTooltip="Mark as Read" placement="bottom">
|
||||
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Mark as Read</span>
|
||||
</button>
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -1,28 +1,44 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { BulkSelectionService } from '../bulk-selection.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-operations',
|
||||
templateUrl: './bulk-operations.component.html',
|
||||
styleUrls: ['./bulk-operations.component.scss']
|
||||
styleUrls: ['./bulk-operations.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BulkOperationsComponent implements OnInit {
|
||||
export class BulkOperationsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() actionCallback!: (action: Action, data: any) => void;
|
||||
topOffset: number = 0;
|
||||
|
||||
get actions() {
|
||||
return this.bulkSelectionService.getActions(this.actionCallback.bind(this));
|
||||
topOffset: number = 56;
|
||||
hasMarkAsRead: boolean = false;
|
||||
hasMarkAsUnread: boolean = false;
|
||||
actions: Array<ActionItem<any>> = [];
|
||||
|
||||
private onDestory: Subject<void> = new Subject();
|
||||
|
||||
get Action() {
|
||||
return Action;
|
||||
}
|
||||
|
||||
constructor(public bulkSelectionService: BulkSelectionService) { }
|
||||
constructor(public bulkSelectionService: BulkSelectionService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const navBar = document.querySelector('.navbar');
|
||||
if (navBar) {
|
||||
this.topOffset = Math.ceil(navBar.getBoundingClientRect().height); // TODO: We can make this fixed 63px
|
||||
}
|
||||
this.bulkSelectionService.actions$.pipe(takeUntil(this.onDestory)).subscribe(actions => {
|
||||
actions.forEach(a => a.callback = this.actionCallback.bind(this));
|
||||
this.actions = actions;
|
||||
this.hasMarkAsRead = this.actions.filter(act => act.action === Action.MarkAsRead).length > 0;
|
||||
this.hasMarkAsUnread = this.actions.filter(act => act.action === Action.MarkAsUnread).length > 0;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestory.next();
|
||||
this.onDestory.complete();
|
||||
}
|
||||
|
||||
handleActionCallback(action: Action, data: any) {
|
||||
|
|
@ -35,5 +51,10 @@ export class BulkOperationsComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
executeAction(action: Action) {
|
||||
const foundActions = this.actions.filter(act => act.action === action);
|
||||
if (foundActions.length > 0) {
|
||||
this.performAction(foundActions[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ChangeDetectorRef, Injectable } from '@angular/core';
|
||||
import { NavigationStart, Router } from '@angular/router';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { Action, ActionFactoryService } from '../_services/action-factory.service';
|
||||
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
|
||||
|
||||
type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark';
|
||||
|
||||
|
|
@ -23,6 +24,15 @@ export class BulkSelectionService {
|
|||
private dataSourceMax: { [key: string]: number} = {};
|
||||
public isShiftDown: boolean = false;
|
||||
|
||||
private actionsSource = new ReplaySubject<ActionItem<any>[]>(1);
|
||||
public actions$ = this.actionsSource.asObservable();
|
||||
|
||||
private selectionsSource = new ReplaySubject<number>(1);
|
||||
/**
|
||||
* Number of active selections
|
||||
*/
|
||||
public selections$ = this.selectionsSource.asObservable();
|
||||
|
||||
constructor(private router: Router, private actionFactory: ActionFactoryService) {
|
||||
router.events
|
||||
.pipe(filter(event => event instanceof NavigationStart))
|
||||
|
|
@ -61,6 +71,7 @@ export class BulkSelectionService {
|
|||
this.prevIndex = index;
|
||||
this.prevDataSource = dataSource;
|
||||
this.dataSourceMax[dataSource] = maxIndex;
|
||||
this.actionsSource.next(this.getActions(() => {}));
|
||||
}
|
||||
|
||||
isCardSelected(dataSource: DataSource, index: number) {
|
||||
|
|
@ -77,6 +88,7 @@ export class BulkSelectionService {
|
|||
|
||||
if (from === to) {
|
||||
this.selectedCards[dataSource][to] = value;
|
||||
this.selectionsSource.next(this.totalSelections());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -89,10 +101,12 @@ export class BulkSelectionService {
|
|||
for (let i = from; i <= to; i++) {
|
||||
this.selectedCards[dataSource][i] = value;
|
||||
}
|
||||
this.selectionsSource.next(this.totalSelections());
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedCards = {};
|
||||
this.selectionsSource.next(0);
|
||||
}
|
||||
|
||||
hasSelections() {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]">
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="!(isAdmin$ | async)">
|
||||
<a ngbNavLink>{{tabs[TabID.Cover].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<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]="chapter.coverImage"></app-image>
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { finalize, Observable, take, takeWhile } from 'rxjs';
|
||||
import { finalize, Observable, of, Subject, take, takeWhile, takeUntil, map, shareReplay } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
|
|
@ -33,9 +33,10 @@ enum TabID {
|
|||
@Component({
|
||||
selector: 'app-card-detail-drawer',
|
||||
templateUrl: './card-detail-drawer.component.html',
|
||||
styleUrls: ['./card-detail-drawer.component.scss']
|
||||
styleUrls: ['./card-detail-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardDetailDrawerComponent implements OnInit {
|
||||
export class CardDetailDrawerComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
|
|
@ -55,6 +56,8 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
*/
|
||||
coverImageUrl!: string;
|
||||
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
|
|
@ -70,7 +73,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
download$: Observable<Download> | null = null;
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
|
|
@ -97,59 +100,56 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
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) { }
|
||||
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),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
|
||||
this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0];
|
||||
if (this.isChapter) {
|
||||
this.coverImageUrl = this.imageService.getChapterCoverImage(this.data.id);
|
||||
} else {
|
||||
this.coverImageUrl = this.imageService.getVolumeCoverImage(this.data.id);
|
||||
}
|
||||
|
||||
this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id));
|
||||
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.coverImageUrl = this.imageService.getChapterCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asChapter(this.data).summary || '';
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else {
|
||||
this.coverImageUrl = this.imageService.getVolumeCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asVolume(this.data).chapters[0].summary || '';
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
if (!this.accountService.hasAdminRole(user)) {
|
||||
this.tabs.find(s => s.title === 'Cover')!.disabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
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});
|
||||
|
||||
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
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});
|
||||
|
||||
if (this.isChapter) {
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else if (!this.isChapter) {
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
// TODO: Move this into the backend
|
||||
this.chapters.sort(this.utilityService.sortChapters);
|
||||
this.chapters.forEach(c => c.coverImage = this.imageService.getChapterCoverImage(c.id));
|
||||
// Try to show an approximation of the reading order for files
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
this.imageUrls = this.chapters.map(c => this.imageService.getChapterCoverImage(c.id));
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
close() {
|
||||
|
|
@ -184,7 +184,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { /* No Action */ });
|
||||
this.actionService.markChapterAsRead(this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
markChapterAsUnread(chapter: Chapter) {
|
||||
|
|
@ -192,7 +192,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { /* No Action */ });
|
||||
this.actionService.markChapterAsUnread(this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: Action, chapter: Chapter) {
|
||||
|
|
@ -249,8 +249,10 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
this.download$.subscribe(() => {});
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-actionables',
|
||||
templateUrl: './card-actionables.component.html',
|
||||
styleUrls: ['./card-actionables.component.scss']
|
||||
styleUrls: ['./card-actionables.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardActionablesComponent implements OnInit {
|
||||
|
||||
|
|
@ -19,11 +20,12 @@ export class CardActionablesComponent implements OnInit {
|
|||
nonAdminActions: ActionItem<any>[] = [];
|
||||
|
||||
|
||||
constructor() { }
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.nonAdminActions = this.actions.filter(item => !item.requiresAdmin);
|
||||
this.adminActions = this.actions.filter(item => item.requiresAdmin);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
preventClick(event: any) {
|
||||
|
|
|
|||
|
|
@ -132,7 +132,8 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
||||
public utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, public bulkSelectionService: BulkSelectionService,
|
||||
private messageHub: MessageHubService, private accountService: AccountService, private scrollService: ScrollService, private changeDetectionRef: ChangeDetectorRef) {}
|
||||
private messageHub: MessageHubService, private accountService: AccountService,
|
||||
private scrollService: ScrollService, private readonly changeDetectionRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||
|
|
@ -143,6 +144,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
if (this.suppressLibraryLink === false) {
|
||||
if (this.entity !== undefined && this.entity.hasOwnProperty('libraryId')) {
|
||||
this.libraryId = (this.entity as Series).libraryId;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
if (this.libraryId !== undefined && this.libraryId > 0) {
|
||||
|
|
@ -175,7 +177,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||
|
||||
this.read = updateEvent.pagesRead;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.changeDetectionRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +191,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
if (!this.allowSelection) return;
|
||||
|
||||
this.selectionInProgress = false;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('touchstart', ['$event'])
|
||||
|
|
@ -304,5 +307,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
event.stopPropagation();
|
||||
}
|
||||
this.selection.emit(this.selected);
|
||||
this.changeDetectionRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SeriesCardComponent } from './series-card/series-card.component';
|
||||
import { LibraryCardComponent } from './library-card/library-card.component';
|
||||
import { CoverImageChooserComponent } from './cover-image-chooser/cover-image-chooser.component';
|
||||
import { EditSeriesModalComponent } from './_modals/edit-series-modal/edit-series-modal.component';
|
||||
import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.component';
|
||||
|
|
@ -14,12 +13,10 @@ import { SharedModule } from '../shared/shared.module';
|
|||
import { RouterModule } from '@angular/router';
|
||||
import { TypeaheadModule } from '../typeahead/typeahead.module';
|
||||
import { CardDetailLayoutComponent } from './card-detail-layout/card-detail-layout.component';
|
||||
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
|
||||
import { BulkOperationsComponent } from './bulk-operations/bulk-operations.component';
|
||||
import { BulkAddToCollectionComponent } from './_modals/bulk-add-to-collection/bulk-add-to-collection.component';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { ChapterMetadataDetailComponent } from './chapter-metadata-detail/chapter-metadata-detail.component';
|
||||
import { FileInfoComponent } from './file-info/file-info.component';
|
||||
import { MetadataFilterModule } from '../metadata-filter/metadata-filter.module';
|
||||
import { EditSeriesRelationComponent } from './edit-series-relation/edit-series-relation.component';
|
||||
import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-drawer.component';
|
||||
|
|
@ -36,17 +33,14 @@ import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.
|
|||
declarations: [
|
||||
CardItemComponent,
|
||||
SeriesCardComponent,
|
||||
LibraryCardComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditSeriesModalComponent,
|
||||
EditCollectionTagsComponent,
|
||||
CardActionablesComponent,
|
||||
CardDetailLayoutComponent,
|
||||
CardDetailsModalComponent,
|
||||
BulkOperationsComponent,
|
||||
BulkAddToCollectionComponent,
|
||||
ChapterMetadataDetailComponent,
|
||||
FileInfoComponent,
|
||||
EditSeriesRelationComponent,
|
||||
CardDetailDrawerComponent,
|
||||
EntityTitleComponent,
|
||||
|
|
@ -89,15 +83,12 @@ import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.
|
|||
exports: [
|
||||
CardItemComponent,
|
||||
SeriesCardComponent,
|
||||
LibraryCardComponent,
|
||||
SeriesCardComponent,
|
||||
LibraryCardComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditSeriesModalComponent,
|
||||
EditCollectionTagsComponent,
|
||||
CardActionablesComponent,
|
||||
CardDetailLayoutComponent,
|
||||
CardDetailsModalComponent,
|
||||
BulkOperationsComponent,
|
||||
ChapterMetadataDetailComponent,
|
||||
EditSeriesRelationComponent,
|
||||
|
|
|
|||
|
|
@ -1,43 +1,16 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { PersonRole } from 'src/app/_models/person';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chapter-metadata-detail',
|
||||
templateUrl: './chapter-metadata-detail.component.html',
|
||||
styleUrls: ['./chapter-metadata-detail.component.scss']
|
||||
styleUrls: ['./chapter-metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ChapterMetadataDetailComponent implements OnInit {
|
||||
@Input() chapter: ChapterMetadata | undefined;
|
||||
|
||||
@Input() chapter!: ChapterMetadata;
|
||||
@Input() libraryType: LibraryType = LibraryType.Manga;
|
||||
constructor() { }
|
||||
|
||||
roles: string[] = [];
|
||||
|
||||
get LibraryType(): typeof LibraryType {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.roles = Object.keys(PersonRole).filter(role => /[0-9]/.test(role) === false);
|
||||
}
|
||||
|
||||
getPeople(role: string) {
|
||||
if (this.chapter) {
|
||||
return (this.chapter as any)[role.toLowerCase()];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, chapter);
|
||||
}
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
|
||||
<form [formGroup]="form">
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" accept=".png,.jpg,.jpeg" [directory]="false" dropZoneClassName="file-upload" contentClassName="file-upload-zone" [directory]="false">
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false" dropZoneClassName="file-upload" contentClassName="file-upload-zone" [directory]="false">
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
|
||||
<div class="mx-auto">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgxFileDropEntry, FileSystemFileEntry } from 'ngx-file-drop';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
|
|
@ -14,7 +14,8 @@ export type SelectCoverFunction = (selectedCover: string) => void;
|
|||
@Component({
|
||||
selector: 'app-cover-image-chooser',
|
||||
templateUrl: './cover-image-chooser.component.html',
|
||||
styleUrls: ['./cover-image-chooser.component.scss']
|
||||
styleUrls: ['./cover-image-chooser.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -58,17 +59,19 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
appliedIndex: number = 0;
|
||||
form!: FormGroup;
|
||||
files: NgxFileDropEntry[] = [];
|
||||
acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].join(',');
|
||||
|
||||
mode: 'file' | 'url' | 'all' = 'all';
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
constructor(public imageService: ImageService, private fb: FormBuilder, private toastr: ToastrService, private uploadService: UploadService,
|
||||
@Inject(DOCUMENT) private document: Document) { }
|
||||
@Inject(DOCUMENT) private document: Document, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
coverImageUrl: new FormControl('', [])
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
@ -86,13 +89,14 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0);
|
||||
var dataURL = canvas.toDataURL("image/png");
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
selectImage(index: number) {
|
||||
if (this.selectedIndex === index) { return; }
|
||||
this.selectedIndex = index;
|
||||
this.cdRef.markForCheck();
|
||||
this.imageSelected.emit(this.selectedIndex);
|
||||
this.selectedBase64Url.emit(this.imageUrls[this.selectedIndex]);
|
||||
}
|
||||
|
|
@ -101,6 +105,7 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
if (this.showApplyButton) {
|
||||
this.applyCover.emit(this.imageUrls[index]);
|
||||
this.appliedIndex = index;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,25 +117,27 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
|
||||
loadImage() {
|
||||
const url = this.form.get('coverImageUrl')?.value.trim();
|
||||
if (url && url != '') {
|
||||
if (!url && url === '') return;
|
||||
|
||||
this.uploadService.uploadByUrl(url).subscribe(filename => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = this.imageService.getCoverUploadImage(filename);
|
||||
img.onload = (e) => this.handleUrlImageAdd(img);
|
||||
img.onerror = (e) => {
|
||||
this.toastr.error('The image could not be fetched due to server refusing request. Please download and upload from file instead.');
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
};
|
||||
this.uploadService.uploadByUrl(url).subscribe(filename => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = this.imageService.getCoverUploadImage(filename);
|
||||
img.onload = (e) => this.handleUrlImageAdd(img);
|
||||
img.onerror = (e) => {
|
||||
this.toastr.error('The image could not be fetched due to server refusing request. Please download and upload from file instead.');
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
});
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
};
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
changeMode(mode: 'url') {
|
||||
this.mode = mode;
|
||||
this.setupEnterHandler();
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
setTimeout(() => (this.document.querySelector('#load-image') as HTMLInputElement)?.focus(), 10);
|
||||
}
|
||||
|
|
@ -159,12 +166,14 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
this.selectedIndex += 1;
|
||||
this.imageSelected.emit(this.selectedIndex); // Auto select newly uploaded image
|
||||
this.selectedBase64Url.emit(e.target.result);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleUrlImageAdd(img: HTMLImageElement) {
|
||||
const url = this.getBase64Image(img);
|
||||
this.imageUrls.push(url);
|
||||
this.imageUrlsChange.emit(this.imageUrls);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
setTimeout(() => {
|
||||
// Auto select newly uploaded image and tell parent of new base64 url
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<form>
|
||||
<div class="row g-0 mb-3" *ngFor="let relation of relations; let idx = index; let isLast = last;">
|
||||
<div class="col-sm-12 col-md-7">
|
||||
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings">
|
||||
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
</div>
|
||||
<div class="col-sm-auto col-md-3">
|
||||
<select class="form-select" [formControl]="relation.formControl">
|
||||
<option [value]="RelationKind.Parent" disabled>Parent</option>
|
||||
<option *ngFor="let opt of relationOptions" [value]="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, 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';
|
||||
|
|
@ -19,7 +19,8 @@ interface RelationControl {
|
|||
@Component({
|
||||
selector: 'app-edit-series-relation',
|
||||
templateUrl: './edit-series-relation.component.html',
|
||||
styleUrls: ['./edit-series-relation.component.scss']
|
||||
styleUrls: ['./edit-series-relation.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -30,18 +31,22 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
|||
@Input() save: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
@Output() saveApi = new ReplaySubject(1);
|
||||
relationOptions = RelationKinds;
|
||||
|
||||
relationOptions = RelationKinds;
|
||||
relations: Array<RelationControl> = [];
|
||||
seriesSettings: TypeaheadSettings<SearchResult> = new TypeaheadSettings();
|
||||
libraryNames: {[key:number]: string} = {};
|
||||
|
||||
get RelationKind() {
|
||||
return RelationKind;
|
||||
}
|
||||
|
||||
|
||||
private onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(private seriesService: SeriesService, private utilityService: UtilityService,
|
||||
public imageService: ImageService, private libraryService: LibraryService) { }
|
||||
public imageService: ImageService, private libraryService: LibraryService,
|
||||
private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seriesService.getRelatedForSeries(this.series.id).subscribe(async relations => {
|
||||
|
|
@ -57,10 +62,12 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
|||
this.setupRelationRows(relations.doujinshis, RelationKind.Doujinshi);
|
||||
this.setupRelationRows(relations.contains, RelationKind.Contains);
|
||||
this.setupRelationRows(relations.parent, RelationKind.Parent);
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
|
||||
this.libraryService.getLibraryNames().subscribe(names => {
|
||||
this.libraryNames = names;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.save.pipe(takeUntil(this.onDestroy)).subscribe(() => this.saveState());
|
||||
|
|
@ -74,27 +81,43 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
|||
setupRelationRows(relations: Array<Series>, kind: RelationKind) {
|
||||
relations.map(async item => {
|
||||
const settings = await firstValueFrom(this.createSeriesTypeahead(item, kind));
|
||||
return {series: item, typeaheadSettings: settings, formControl: new FormControl(kind, [])}
|
||||
const form = new FormControl(kind, []);
|
||||
if (kind === RelationKind.Parent) {
|
||||
form.disable();
|
||||
}
|
||||
return {series: item, typeaheadSettings: settings, formControl: form};
|
||||
}).forEach(async p => {
|
||||
this.relations.push(await p);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
async addNewRelation() {
|
||||
this.relations.push({series: undefined, formControl: new FormControl(RelationKind.Adaptation, []), typeaheadSettings: await firstValueFrom(this.createSeriesTypeahead(undefined, RelationKind.Adaptation))});
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
// Focus on the new typeahead
|
||||
setTimeout(() => {
|
||||
const typeahead = document.querySelector(`#relation--${this.relations.length - 1} .typeahead-input input`) as HTMLInputElement;
|
||||
if (typeahead) typeahead.focus();
|
||||
}, 10);
|
||||
|
||||
}
|
||||
|
||||
removeRelation(index: number) {
|
||||
this.relations.splice(index, 1);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
updateSeries(event: Array<SearchResult | undefined>, relation: RelationControl) {
|
||||
if (event[0] === undefined) {
|
||||
relation.series = undefined;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
relation.series = {id: event[0].seriesId, name: event[0].name};
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
createSeriesTypeahead(series: Series | undefined, relationship: RelationKind): Observable<TypeaheadSettings<SearchResult>> {
|
||||
|
|
@ -143,8 +166,6 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
|
|||
const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id);
|
||||
|
||||
// TODO: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
|
||||
|
||||
//this.saveApi.next(this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis));
|
||||
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis).subscribe(() => {});
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
|
|
@ -13,7 +13,8 @@ import { SeriesService } from 'src/app/_services/series.service';
|
|||
@Component({
|
||||
selector: 'app-entity-info-cards',
|
||||
templateUrl: './entity-info-cards.component.html',
|
||||
styleUrls: ['./entity-info-cards.component.scss']
|
||||
styleUrls: ['./entity-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
|||
return AgeRating;
|
||||
}
|
||||
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService) {}
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
|
|
@ -61,6 +62,7 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
|||
if (this.includeMetadata) {
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -86,11 +88,11 @@ export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
|||
this.readingTime.maxHours = vol.maxHoursToRead;
|
||||
this.readingTime.avgHours = vol.avgHoursToRead;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
|
|
@ -7,7 +7,8 @@ import { Volume } from 'src/app/_models/volume';
|
|||
@Component({
|
||||
selector: 'app-entity-title',
|
||||
templateUrl: './entity-title.component.html',
|
||||
styleUrls: ['./entity-title.component.scss']
|
||||
styleUrls: ['./entity-title.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EntityTitleComponent implements OnInit {
|
||||
|
||||
|
|
@ -27,7 +28,6 @@ export class EntityTitleComponent implements OnInit {
|
|||
@Input() prioritizeTitleName: boolean = true;
|
||||
|
||||
isChapter = false;
|
||||
chapter!: Chapter;
|
||||
titleName: string = '';
|
||||
volumeTitle: string = '';
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export class EntityTitleComponent implements OnInit {
|
|||
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService) {
|
||||
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -53,5 +53,6 @@ export class EntityTitleComponent implements OnInit {
|
|||
this.volumeTitle = v.name || '';
|
||||
this.titleName = v.chapters[0].titleName || '';
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
<li class="list-group-item">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Pages: {{file.pages}}
|
||||
</div>
|
||||
<div class="col" *ngIf="created != undefined">
|
||||
Added: {{(created | date: 'short') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-info',
|
||||
templateUrl: './file-info.component.html',
|
||||
styleUrls: ['./file-info.component.scss']
|
||||
})
|
||||
export class FileInfoComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* MangaFile to display
|
||||
*/
|
||||
@Input() file!: MangaFile;
|
||||
/**
|
||||
* DateTime the entity this file belongs to was created
|
||||
*/
|
||||
@Input() created: string | undefined = undefined;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<ng-container *ngIf="data !== undefined">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<div class="overlay" (click)="handleClick()">
|
||||
<i class="fa {{icon}} card-img-top text-center" aria-hidden="true"></i>
|
||||
<div class="card-actions">
|
||||
<app-card-actionables [actions]="actions" [labelBy]="data.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body text-center" *ngIf="data.name.length > 0 || actions.length > 0">
|
||||
<span class="card-data.name" (click)="handleClick()">
|
||||
{{data.name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
|
||||
.card {
|
||||
margin: 10px;
|
||||
max-width: 160px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-top: 5px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
max-width: 150px;
|
||||
height: 52px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
height: 160px;
|
||||
margin-top: 40% !important;
|
||||
margin: auto;
|
||||
font-size: 52px;
|
||||
|
||||
}
|
||||
|
||||
.overlay {
|
||||
height: 160px;
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
|
||||
.overlay-item {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-item {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
position: absolute;
|
||||
top: 125px;
|
||||
right: -5px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
.dropdown-toggle:after {
|
||||
content: none !important;
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
|
||||
// Represents a library type card.
|
||||
@Component({
|
||||
selector: 'app-library-card',
|
||||
templateUrl: './library-card.component.html',
|
||||
styleUrls: ['./library-card.component.scss']
|
||||
})
|
||||
export class LibraryCardComponent implements OnInit, OnChanges {
|
||||
@Input() data!: Library;
|
||||
@Output() clicked = new EventEmitter<Library>();
|
||||
|
||||
isAdmin = false;
|
||||
actions: ActionItem<Library>[] = [];
|
||||
icon = 'fa-book-open';
|
||||
|
||||
constructor(private accountService: AccountService, private router: Router,
|
||||
private actionFactoryService: ActionFactoryService, private actionService: ActionService) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: any) {
|
||||
if (this.data) {
|
||||
if (this.data.type === 0 || this.data.type === 1) {
|
||||
this.icon = 'fa-book-open';
|
||||
} else {
|
||||
this.icon = 'fa-book';
|
||||
}
|
||||
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
handleAction(action: Action, library: Library) {
|
||||
switch (action) {
|
||||
case(Action.ScanLibrary):
|
||||
this.actionService.scanLibrary(library);
|
||||
break;
|
||||
case(Action.RefreshMetadata):
|
||||
this.actionService.refreshMetadata(library);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
performAction(action: ActionItem<Library>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, this.data);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.clicked.emit(this.data);
|
||||
this.router.navigate(['library', this.data?.id]);
|
||||
}
|
||||
|
||||
preventClick(event: any) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ $image-height: 230px;
|
|||
$image-width: 160px;
|
||||
$triangle-size: 30px;
|
||||
|
||||
// with summary and cards, we have a height of 220px, we might want to default to 220px and let it grow from there to help with virtualization
|
||||
|
||||
.download {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { finalize, Observable, of, take, takeWhile } from 'rxjs';
|
||||
import { finalize, Observable, take, takeWhile } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
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';
|
||||
|
|
@ -14,7 +13,8 @@ import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
|||
@Component({
|
||||
selector: 'app-list-item',
|
||||
templateUrl: './list-item.component.html',
|
||||
styleUrls: ['./list-item.component.scss']
|
||||
styleUrls: ['./list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ListItemComponent implements OnInit {
|
||||
|
||||
|
|
@ -70,7 +70,6 @@ export class ListItemComponent implements OnInit {
|
|||
@Output() read: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
summary$: Observable<string> = of('');
|
||||
summary: string = '';
|
||||
isChapter: boolean = false;
|
||||
|
||||
|
|
@ -84,16 +83,17 @@ export class ListItemComponent implements OnInit {
|
|||
}
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService, private toastr: ToastrService) { }
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
if (this.isChapter) {
|
||||
this.summary = this.utilityService.asChapter(this.entity).summary || '';
|
||||
} else {
|
||||
this.summary = this.utilityService.asVolume(this.entity).chapters[0].summary || '';
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
|
|
@ -109,6 +109,7 @@ export class ListItemComponent implements OnInit {
|
|||
const wantToDownload = await this.downloadService.confirmSize(size, 'volume');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
|
|
@ -116,7 +117,9 @@ export class ListItemComponent implements OnInit {
|
|||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else if (this.utilityService.isChapter(this.entity)) {
|
||||
const chapter = this.utilityService.asChapter(this.entity);
|
||||
|
|
@ -124,6 +127,7 @@ export class ListItemComponent implements OnInit {
|
|||
const wantToDownload = await this.downloadService.confirmSize(size, 'chapter');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
|
|
@ -131,7 +135,9 @@ export class ListItemComponent implements OnInit {
|
|||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
}));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
return; // Don't propagate the download from a card
|
||||
|
|
|
|||
|
|
@ -44,33 +44,28 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
*/
|
||||
@Output() selection = new EventEmitter<boolean>();
|
||||
|
||||
isAdmin = false;
|
||||
actions: ActionItem<Series>[] = [];
|
||||
imageUrl: string = '';
|
||||
onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(private accountService: AccountService, private router: Router,
|
||||
constructor(private router: Router, private cdRef: ChangeDetectorRef,
|
||||
private seriesService: SeriesService, private toastr: ToastrService,
|
||||
private modalService: NgbModal, private imageService: ImageService,
|
||||
private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, private changeDetectionRef: ChangeDetectorRef) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
private actionService: ActionService) {}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.imageUrl = this.imageService.getSeriesCoverImage(this.data.id);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: any) {
|
||||
if (this.data) {
|
||||
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +115,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
if (closeResult.success) {
|
||||
this.seriesService.getSeries(data.id).subscribe(series => {
|
||||
this.data = series;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.cdRef.markForCheck();
|
||||
this.reload.emit(true);
|
||||
this.dataChanged.emit(series);
|
||||
});
|
||||
|
|
@ -150,7 +145,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.actionService.markSeriesAsUnread(series, () => {
|
||||
if (this.data) {
|
||||
this.data.pagesRead = 0;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
this.dataChanged.emit(series);
|
||||
|
|
@ -161,7 +156,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.actionService.markSeriesAsRead(series, () => {
|
||||
if (this.data) {
|
||||
this.data.pagesRead = series.pages;
|
||||
this.changeDetectionRef.markForCheck();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
this.dataChanged.emit(series);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, 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';
|
||||
import { UserProgressUpdateEvent } from 'src/app/_models/events/user-progress-update-event';
|
||||
import { HourEstimateRange } from 'src/app/_models/hour-estimate-range';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesMetadata } from 'src/app/_models/series-metadata';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-info-cards',
|
||||
templateUrl: './series-info-cards.component.html',
|
||||
styleUrls: ['./series-info-cards.component.scss']
|
||||
styleUrls: ['./series-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SeriesInfoCardsComponent implements OnInit {
|
||||
export class SeriesInfoCardsComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input() series!: Series;
|
||||
@Input() seriesMetadata!: SeriesMetadata;
|
||||
|
|
@ -27,6 +32,8 @@ export class SeriesInfoCardsComponent implements OnInit {
|
|||
|
||||
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
|
@ -35,21 +42,50 @@ export class SeriesInfoCardsComponent implements OnInit {
|
|||
return FilterQueryParam;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private readerService: ReaderService) { }
|
||||
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))
|
||||
.subscribe(updateEvent => {
|
||||
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
|
||||
if (user === undefined || user.username !== updateEvent.username) return;
|
||||
if (updateEvent.seriesId !== this.series.id) return;
|
||||
this.getReadingTimeLeft();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.series !== null) {
|
||||
if (this.showReadingTimeLeft) this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => this.readingTimeLeft = timeLeft);
|
||||
this.getReadingTimeLeft();
|
||||
this.readingTime.minHours = this.series.minHoursToRead;
|
||||
this.readingTime.maxHours = this.series.maxHoursToRead;
|
||||
this.readingTime.avgHours = this.series.avgHoursToRead;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
handleGoTo(queryParamName: FilterQueryParam, filter: any) {
|
||||
this.goTo.emit({queryParamName, filter});
|
||||
}
|
||||
|
||||
|
||||
|
||||
private getReadingTimeLeft() {
|
||||
if (this.showReadingTimeLeft) this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => {
|
||||
this.readingTimeLeft = timeLeft;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue