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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue