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:
Joseph Milazzo 2022-07-11 11:57:07 -04:00 committed by GitHub
parent f5be0fac58
commit 4e49aa47ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
126 changed files with 1658 additions and 1674 deletions

View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
.scrollable-modal {
max-height: 90vh; // 600px
overflow: auto;
}

View file

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

View file

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

View file

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

View file

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