
* Added the skeleton code for layout, hooked up Age Rating, Publication Status, and Tags * Tweaked message of Scan service to Finished scan of to better indicate the total scan time * Hooked in foundation for person typeaheads * Fixed people not populating typeaheads on load * For manga/comics, when parsing, set the SeriesSort from ComicInfo if it exists. * Implemented the ability to override and create new genre tags. Code is ready to flush out the rest. * Ability to update metadata from the UI is hooked up. Next is locking. * Updated typeahead to allow for non-multiple usage. Implemented ability to update Language tag in Series Metadata. * Fixed a bug in GetContinuePoint for a case where we have Volumes, Loose Leaf chapters and no read progress. * Added ETag headers on Images to allow for better caching (bookmarks and images in manga reader) * Built out UI code to show locked indication to user * Implemented Series locking and refactored a lot of styles in typeahead to make the lock setting work, plus misc cleanup. * Added locked properties to dtos. Updated typeahead loading indicator to not interfere with close button if present * Hooked up locking flags in UI * Integrated regular field locking/unlocking * Removed some old code * Prevent input group from wrapping * Implemented some basic layout for metadata on volume/chapter card modal. Refactored out all metadata from Chapter object in terms of UI and put into a separate call to ensure speedy delivery and simplicity of code. * Refactored code to hide covers section if not an admin * Implemented ability to modify a chapter/volume cover from the detail modal * Removed a few variables and change cover image modal * Added bookmark to single chapter view * Put a temp fix in for a ngb v12 z-index bug (reported). Bumped ngb to 12.0 stable and fixed some small rendering bugs * loading buttons ftw * Lots of cleanup, looks like the story is finished * Changed action name from Info to Details * Style tweaks * Fixed an issue where Summary would assume it's locked due to a subscription firing on setting the model * Fixed some misc bugs * Code smells Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
250 lines
8.3 KiB
TypeScript
250 lines
8.3 KiB
TypeScript
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;
|
|
|
|
bookmarks: PageBookmark[] = [];
|
|
|
|
tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Bookmarks', 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);
|
|
console.log('isChapter: ', this.isChapter);
|
|
|
|
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));
|
|
|
|
let bookmarkApi;
|
|
if (this.isChapter) {
|
|
bookmarkApi = this.readerService.getBookmarks(this.chapter.id);
|
|
} else {
|
|
bookmarkApi = this.readerService.getBookmarksForVolume(this.data.id);
|
|
}
|
|
|
|
bookmarkApi.pipe(take(1)).subscribe(bookmarks => {
|
|
this.bookmarks = bookmarks;
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) {
|
|
this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id]);
|
|
} else {
|
|
this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id]);
|
|
}
|
|
}
|
|
|
|
removeBookmark(bookmark: PageBookmark, index: number) {
|
|
this.readerService.unbookmark(bookmark.seriesId, bookmark.volumeId, bookmark.chapterId, bookmark.page).subscribe(() => {
|
|
this.bookmarks.splice(index, 1);
|
|
});
|
|
}
|
|
}
|