Metadata Editing from the UI! (#1135)
* 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>
This commit is contained in:
parent
47a92a2e01
commit
ba77954d5c
60 changed files with 3605 additions and 723 deletions
|
@ -1,16 +1,37 @@
|
|||
import { Genre } from "./genre";
|
||||
import { AgeRating } from "./metadata/age-rating";
|
||||
import { PublicationStatus } from "./metadata/publication-status";
|
||||
import { Person } from "./person";
|
||||
import { Tag } from "./tag";
|
||||
|
||||
export interface ChapterMetadata {
|
||||
id: number;
|
||||
chapterId: number;
|
||||
title: string;
|
||||
year: string;
|
||||
|
||||
ageRating: AgeRating;
|
||||
releaseDate: string;
|
||||
language: string;
|
||||
publicationStatus: PublicationStatus;
|
||||
summary: string;
|
||||
count: number;
|
||||
totalCount: number;
|
||||
|
||||
|
||||
genres: Array<Genre>;
|
||||
tags: Array<Tag>;
|
||||
writers: Array<Person>;
|
||||
penciller: Array<Person>;
|
||||
inker: Array<Person>;
|
||||
colorist: Array<Person>;
|
||||
letterer: Array<Person>;
|
||||
coverArtist: Array<Person>;
|
||||
editor: Array<Person>;
|
||||
coverArtists: Array<Person>;
|
||||
publishers: Array<Person>;
|
||||
characters: Array<Person>;
|
||||
pencillers: Array<Person>;
|
||||
inkers: Array<Person>;
|
||||
colorists: Array<Person>;
|
||||
letterers: Array<Person>;
|
||||
editors: Array<Person>;
|
||||
translators: Array<Person>;
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { MangaFile } from './manga-file';
|
||||
import { Person } from './person';
|
||||
import { Tag } from './tag';
|
||||
|
||||
/**
|
||||
* Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields.
|
||||
*/
|
||||
export interface Chapter {
|
||||
id: number;
|
||||
range: string;
|
||||
|
@ -18,19 +19,8 @@ export interface Chapter {
|
|||
isSpecial: boolean;
|
||||
title: string;
|
||||
created: string;
|
||||
|
||||
titleName: string;
|
||||
/**
|
||||
* This is only Year and Month, Day is not supported from underlying sources
|
||||
* Actual name of the Chapter if populated in underlying metadata
|
||||
*/
|
||||
releaseDate: string;
|
||||
writers: Array<Person>;
|
||||
penciller: Array<Person>;
|
||||
inker: Array<Person>;
|
||||
colorist: Array<Person>;
|
||||
letterer: Array<Person>;
|
||||
coverArtist: Array<Person>;
|
||||
editor: Array<Person>;
|
||||
publisher: Array<Person>;
|
||||
tags: Array<Tag>;
|
||||
titleName: string;
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@ import { Person } from "./person";
|
|||
import { Tag } from "./tag";
|
||||
|
||||
export interface SeriesMetadata {
|
||||
publisher: string;
|
||||
seriesId: number;
|
||||
summary: string;
|
||||
collectionTags: Array<CollectionTag>;
|
||||
|
||||
genres: Array<Genre>;
|
||||
tags: Array<Tag>;
|
||||
collectionTags: Array<CollectionTag>;
|
||||
writers: Array<Person>;
|
||||
coverArtists: Array<Person>;
|
||||
publishers: Array<Person>;
|
||||
|
@ -24,6 +25,23 @@ export interface SeriesMetadata {
|
|||
ageRating: AgeRating;
|
||||
releaseYear: number;
|
||||
language: string;
|
||||
seriesId: number;
|
||||
publicationStatus: PublicationStatus;
|
||||
|
||||
summaryLocked: boolean;
|
||||
genresLocked: boolean;
|
||||
tagsLocked: boolean;
|
||||
writersLocked: boolean;
|
||||
coverArtistsLocked: boolean;
|
||||
publishersLocked: boolean;
|
||||
charactersLocked: boolean;
|
||||
pencillersLocked: boolean;
|
||||
inkersLocked: boolean;
|
||||
coloristsLocked: boolean;
|
||||
letterersLocked: boolean;
|
||||
editorsLocked: boolean;
|
||||
translatorsLocked: boolean;
|
||||
ageRatingLocked: boolean;
|
||||
releaseYearLocked: boolean;
|
||||
languageLocked: boolean;
|
||||
publicationStatusLocked: boolean;
|
||||
}
|
|
@ -11,6 +11,9 @@ export interface Series {
|
|||
localizedName: string;
|
||||
sortName: string;
|
||||
coverImageLocked: boolean;
|
||||
sortNameLocked: boolean;
|
||||
localizedNameLocked: boolean;
|
||||
nameLocked: boolean;
|
||||
volumes: Volume[];
|
||||
/**
|
||||
* Total pages in series
|
||||
|
|
|
@ -8,5 +8,5 @@ export interface Volume {
|
|||
lastModified: string;
|
||||
pages: number;
|
||||
pagesRead: number;
|
||||
chapters?: Array<Chapter>;
|
||||
chapters: Array<Chapter>; // TODO: Validate any cases where this is undefined
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ export class ActionFactoryService {
|
|||
|
||||
this.chapterActions.push({
|
||||
action: Action.Edit,
|
||||
title: 'Info',
|
||||
title: 'Details',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false
|
||||
});
|
||||
|
@ -247,7 +247,7 @@ export class ActionFactoryService {
|
|||
},
|
||||
{
|
||||
action: Action.Edit,
|
||||
title: 'Info',
|
||||
title: 'Details',
|
||||
callback: this.dummyCallback,
|
||||
requiresAdmin: false
|
||||
}
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { of } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from '../typeahead/typeahead-settings';
|
||||
import { ChapterMetadata } from '../_models/chapter-metadata';
|
||||
import { Genre } from '../_models/genre';
|
||||
import { AgeRating } from '../_models/metadata/age-rating';
|
||||
import { AgeRatingDto } from '../_models/metadata/age-rating-dto';
|
||||
import { Language } from '../_models/metadata/language';
|
||||
import { PublicationStatusDto } from '../_models/metadata/publication-status-dto';
|
||||
import { Person } from '../_models/person';
|
||||
import { Person, PersonRole } from '../_models/person';
|
||||
import { Tag } from '../_models/tag';
|
||||
|
||||
@Injectable({
|
||||
|
@ -21,7 +23,7 @@ export class MetadataService {
|
|||
|
||||
private ageRatingTypes: {[key: number]: string} | undefined = undefined;
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
||||
|
||||
getAgeRating(ageRating: AgeRating) {
|
||||
if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
|
||||
|
@ -77,6 +79,13 @@ export class MetadataService {
|
|||
return this.httpClient.get<Array<Language>>(this.baseUrl + method);
|
||||
}
|
||||
|
||||
/**
|
||||
* All the potential language tags there can be
|
||||
*/
|
||||
getAllValidLanguages() {
|
||||
return this.httpClient.get<Array<Language>>(this.baseUrl + 'metadata/all-languages');
|
||||
}
|
||||
|
||||
getAllPeople(libraries?: Array<number>) {
|
||||
let method = 'metadata/people'
|
||||
if (libraries != undefined && libraries.length > 0) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { of } from 'rxjs';
|
|||
import { map } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Chapter } from '../_models/chapter';
|
||||
import { ChapterMetadata } from '../_models/chapter-metadata';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { PaginatedResult } from '../_models/pagination';
|
||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||
|
@ -85,6 +86,10 @@ export class SeriesService {
|
|||
return this.httpClient.get<Chapter>(this.baseUrl + 'series/chapter?chapterId=' + chapterId);
|
||||
}
|
||||
|
||||
getChapterMetadata(chapterId: number) {
|
||||
return this.httpClient.get<ChapterMetadata>(this.baseUrl + 'series/chapter-metadata?chapterId=' + chapterId);
|
||||
}
|
||||
|
||||
getData(id: number) {
|
||||
return of(id);
|
||||
}
|
||||
|
@ -161,10 +166,10 @@ export class SeriesService {
|
|||
}));
|
||||
}
|
||||
|
||||
updateMetadata(seriesMetadata: SeriesMetadata, tags: CollectionTag[]) {
|
||||
updateMetadata(seriesMetadata: SeriesMetadata, collectionTags: CollectionTag[]) {
|
||||
const data = {
|
||||
seriesMetadata,
|
||||
tags
|
||||
collectionTags,
|
||||
};
|
||||
return this.httpClient.post(this.baseUrl + 'series/metadata', data, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
@ -173,11 +178,6 @@ export class SeriesService {
|
|||
let params = new HttpParams();
|
||||
|
||||
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
|
||||
// NOTE: I'm not sure the paginated result is doing anything
|
||||
// if (this.paginatedSeriesForTagsResults?.pagination !== undefined && this.paginatedSeriesForTagsResults?.pagination?.currentPage === pageNum) {
|
||||
// return of(this.paginatedSeriesForTagsResults);
|
||||
// }
|
||||
|
||||
return this.httpClient.get<PaginatedResult<Series[]>>(this.baseUrl + 'series/series-by-collection?collectionId=' + collectionTagId, {observe: 'response', params}).pipe(
|
||||
map((response: any) => {
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
<h4>Email Services (SMTP)</h4>
|
||||
<p class="accent">Kavita comes out of the box with an email service to power flows like invite user, forgot password, etc. Emails sent via our service are deleted immediately. You can use your own
|
||||
email service. Set the url of the email service and use the Test button to ensure it works. At any time you can reset to ours. There is no way to disable emails althought confirmation links will always
|
||||
email service. Set the url of the email service and use the Test button to ensure it works. At any time you can reset to ours. There is no way to disable emails although confirmation links will always
|
||||
be saved to logs.
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
|
|
|
@ -29,13 +29,12 @@ import { CollectionsModule } from './collections/collections.module';
|
|||
import { ReadingListModule } from './reading-list/reading-list.module';
|
||||
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
||||
import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.component';
|
||||
import { PersonRolePipe } from './_pipes/person-role.pipe';
|
||||
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
|
||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
||||
import { RegistrationModule } from './registration/registration.module';
|
||||
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
||||
import { PublicationStatusPipe } from './_pipes/publication-status.pipe';
|
||||
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||
import { PipeModule } from './pipe/pipe.module';
|
||||
|
||||
|
||||
@NgModule({
|
||||
|
@ -51,8 +50,6 @@ import { ThemeTestComponent } from './theme-test/theme-test.component';
|
|||
OnDeckComponent,
|
||||
DashboardComponent,
|
||||
NavEventsToggleComponent,
|
||||
PersonRolePipe,
|
||||
PublicationStatusPipe,
|
||||
SeriesMetadataDetailComponent,
|
||||
AllSeriesComponent,
|
||||
GroupedTypeaheadComponent,
|
||||
|
@ -83,6 +80,7 @@ import { ThemeTestComponent } from './theme-test/theme-test.component';
|
|||
RegistrationModule,
|
||||
|
||||
NgbAccordionModule, // ThemeTest Component only
|
||||
PipeModule,
|
||||
|
||||
|
||||
ToastrModule.forRoot({
|
||||
|
|
|
@ -9,76 +9,145 @@
|
|||
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal" *ngIf="utilityService.isChapter(data)">
|
||||
<ng-container *ngIf="utilityService.isChapter(data)">
|
||||
<app-chapter-metadata-detail [chapter]="data"></app-chapter-metadata-detail>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal" *ngIf="utilityService.isVolume(data)">
|
||||
<h4 *ngIf="utilityService.isVolume(data)">Information</h4>
|
||||
|
||||
<ng-container *ngIf="utilityService.isVolume(data) || utilityService.isChapter(data)">
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Id: {{data.id}}
|
||||
</div>
|
||||
<div class="col" *ngIf="series !== undefined">
|
||||
Format: <span class="badge bg-secondary">{{utilityService.mangaFormat(series.format) | sentenceCase}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
Added: {{(data.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{data.pages}}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<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 {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
||||
<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 *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
<span >
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<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>File(s)</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="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">
|
||||
<div class="col">
|
||||
Pages: {{file.pages}}
|
||||
<h4>
|
||||
{{chapter?.titleName}}
|
||||
</h4>
|
||||
<span>
|
||||
<span *ngIf="chapterMetadata && chapterMetadata.releaseDate !== null">Release Date: {{chapterMetadata.releaseDate | date: 'shortDate' || '-'}}</span>
|
||||
</span>
|
||||
<span class="text-accent">{{chapter.pages}} pages</span>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
Added: {{(data.created | date: 'short') || '-'}}
|
||||
</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>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngFor="let bookmark of bookmarks; let idx = index">
|
||||
<app-bookmark [bookmark]="bookmark" class="col-auto" (bookmarkRemoved)="removeBookmark(bookmark, idx)"></app-bookmark>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[4]" *ngIf="!tabs[4].disabled">
|
||||
<a ngbNavLink>{{tabs[4].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 {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
||||
<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 *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<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>File(s)</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>
|
||||
</div>
|
||||
</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="button" class="btn btn-secondary" [disabled]="!isAdmin" (click)="updateCover()">Update Cover</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from '@angular/router';
|
|||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
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';
|
||||
|
@ -12,11 +12,16 @@ import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/acti
|
|||
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 { ChangeCoverImageModalComponent } from '../change-cover-image/change-cover-image-modal.component';
|
||||
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';
|
||||
|
||||
|
||||
|
||||
|
@ -30,38 +35,95 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@Input() libraryId: number = 0;
|
||||
@Input() data!: any; // Volume | Chapter
|
||||
@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[] = [];
|
||||
seriesVolumes: any[] = [];
|
||||
isLoadingVolumes = false;
|
||||
formatKeys = Object.keys(MangaFormat);
|
||||
|
||||
|
||||
/**
|
||||
* If a cover image update occured.
|
||||
*/
|
||||
coverImageUpdate: boolean = false;
|
||||
isAdmin: 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;
|
||||
series: Series | undefined = undefined;
|
||||
|
||||
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(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
|
||||
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 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) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
if (!this.accountService.hasAdminRole(user)) {
|
||||
this.tabs.find(s => s.title === 'Cover')!.disabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -72,10 +134,11 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit);
|
||||
|
||||
if (this.isChapter) {
|
||||
this.chapters.push(this.data);
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else if (!this.isChapter) {
|
||||
this.chapters.push(...this.data?.chapters);
|
||||
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
|
||||
|
@ -83,10 +146,6 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
this.chapters.forEach((c: Chapter) => {
|
||||
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
|
||||
});
|
||||
|
||||
this.seriesService.getSeries(this.seriesId).subscribe(series => {
|
||||
this.series = series;
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -106,34 +165,36 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
updateCover() {
|
||||
const modalRef = this.modalService.open(ChangeCoverImageModalComponent, { size: 'lg' }); // scrollable: true, size: 'lg', windowClass: 'scrollable-modal' (these don't work well on mobile)
|
||||
if (this.utilityService.isChapter(this.data)) {
|
||||
const chapter = this.utilityService.asChapter(this.data)
|
||||
chapter.coverImage = this.imageService.getChapterCoverImage(chapter.id);
|
||||
modalRef.componentInstance.chapter = chapter;
|
||||
modalRef.componentInstance.title = 'Select ' + (chapter.isSpecial ? '' : this.utilityService.formatChapterName(this.libraryType, false, true)) + chapter.range + '\'s Cover';
|
||||
} else {
|
||||
const volume = this.utilityService.asVolume(this.data);
|
||||
const chapters = volume.chapters;
|
||||
if (chapters && chapters.length > 0) {
|
||||
modalRef.componentInstance.chapter = chapters[0];
|
||||
modalRef.componentInstance.title = 'Select Volume ' + volume.number + '\'s Cover';
|
||||
}
|
||||
}
|
||||
|
||||
modalRef.closed.subscribe((closeResult: {success: boolean, chapter: Chapter, coverImageUpdate: boolean}) => {
|
||||
if (closeResult.success) {
|
||||
this.coverImageUpdate = closeResult.coverImageUpdate;
|
||||
if (!this.coverImageUpdate) {
|
||||
this.uploadService.resetChapterCoverLock(closeResult.chapter.id).subscribe(() => {
|
||||
this.toastr.info('Please refresh in a bit for the cover image to be reflected.');
|
||||
});
|
||||
} else {
|
||||
closeResult.chapter.coverImage = this.imageService.randomize(this.imageService.getChapterCoverImage(closeResult.chapter.id));
|
||||
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) {
|
||||
|
@ -180,4 +241,10 @@ export class CardDetailsModalComponent implements OnInit {
|
|||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<div class="modal-header">{{title}}</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
<p class="alert alert-primary" role="alert">
|
||||
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]="chapter.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="cancel()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="loading">Save</button>
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
.scrollable-modal {
|
||||
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-cover-image-modal',
|
||||
templateUrl: './change-cover-image-modal.component.html',
|
||||
styleUrls: ['./change-cover-image-modal.component.scss']
|
||||
})
|
||||
export class ChangeCoverImageModalComponent implements OnInit {
|
||||
|
||||
@Input() chapter!: Chapter;
|
||||
@Input() title: string = '';
|
||||
|
||||
selectedCover: string = '';
|
||||
imageUrls: Array<string> = [];
|
||||
coverImageIndex: number = 0;
|
||||
coverImageLocked: boolean = false;
|
||||
loading: boolean = false;
|
||||
|
||||
constructor(private imageService: ImageService, private uploadService: UploadService, public modal: NgbActiveModal) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// Randomization isn't needed as this is only the chooser
|
||||
this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id));
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.modal.close({success: false, coverImageUpdate: false})
|
||||
}
|
||||
save() {
|
||||
this.loading = true;
|
||||
if (this.coverImageIndex > 0) {
|
||||
this.chapter.coverImageLocked = true;
|
||||
this.uploadService.updateChapterCoverImage(this.chapter.id, this.selectedCover).subscribe(() => {
|
||||
if (this.coverImageIndex > 0) {
|
||||
this.chapter.coverImageLocked = true;
|
||||
}
|
||||
this.modal.close({success: true, chapter: this.chapter, coverImageUpdate: this.chapter.coverImageLocked});
|
||||
this.loading = false;
|
||||
}, err => this.loading = false);
|
||||
} else {
|
||||
this.modal.close({success: true, chapter: this.chapter, coverImageUpdate: this.chapter.coverImageLocked});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.coverImageIndex = index;
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageLocked = false;
|
||||
this.chapter.coverImageLocked = false;
|
||||
this.modal.close({success: true, chapter: this.chapter, coverImageUpdate: this.chapter.coverImageLocked});
|
||||
}
|
||||
|
||||
}
|
|
@ -7,82 +7,321 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
|
||||
<form [formGroup]="editSeriesForm">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[0]">
|
||||
<a ngbNavLink>{{tabs[0]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<form [formGroup]="editSeriesForm">
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input id="name" class="form-control" formControlName="name" type="text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="sort-name" class="form-label">Sort Name</label>
|
||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="localized-name" class="form-label">Localized Name</label>
|
||||
<input id="localized-name" class="form-control" formControlName="localizedName" type="text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0" *ngIf="metadata">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="author" class="form-label">Author</label>
|
||||
<input id="author" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="author" type="text">
|
||||
|
||||
<li [ngbNavItem]="tabs[0]">
|
||||
<a ngbNavLink>{{tabs[0]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<div class="input-group {{series.nameLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'nameLocked' }"></ng-container>
|
||||
<input id="name" class="form-control" formControlName="name" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="artist" class="form-label">Artist</label>
|
||||
<input id="artist" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="artist" type="text">
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="sort-name" class="form-label">Sort Name</label>
|
||||
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'sortNameLocked' }"></ng-container>
|
||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0" *ngIf="metadata">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="genres" class="form-label">Genres</label>
|
||||
<input id="genres" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="genres" type="text">
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="localized-name" class="form-label">Localized Name</label>
|
||||
<div class="input-group {{series.localizedNameLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'localizedNameLocked' }"></ng-container>
|
||||
<input id="localized-name" class="form-control" formControlName="localizedName" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="collections" class="form-label">Collections</label>
|
||||
<app-typeahead (selectedData)="updateCollections($event)" [settings]="settings">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
|
||||
<div class="row g-0" *ngIf="metadata">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="summary" class="form-label">Summary</label>
|
||||
<div class="input-group {{metadata?.summaryLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'summaryLocked' }"></ng-container>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="summary" class="form-label">Summary</label>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="tabs[1]" *ngIf="metadata">
|
||||
<a ngbNavLink>{{tabs[1]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="tabs[1]">
|
||||
<a ngbNavLink>{{tabs[1]}}</a>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="collections" class="form-label">Collections </label>
|
||||
<app-typeahead (selectedData)="updateCollections($event)" [settings]="collectionTagSettings" [locked]="true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="genres" class="form-label">Genres</label>
|
||||
<app-typeahead (selectedData)="updateGenres($event)" [settings]="genreSettings"
|
||||
[(locked)]="metadata.genresLocked" (onUnlock)="metadata.genresLocked = false"
|
||||
(newItemAdded)="metadata.genresLocked = true" (selectedData)="metadata.genresLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="tags" class="form-label">Tags</label>
|
||||
<app-typeahead (selectedData)="updateTags($event)" [settings]="tagsSettings"
|
||||
[(locked)]="metadata.tagsLocked" (onUnlock)="metadata.tagsLocked = false"
|
||||
(newItemAdded)="metadata.tagsLocked = true" (selectedData)="metadata.tagsLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="language" class="form-label">Language</label>
|
||||
<app-typeahead (selectedData)="updateLanguage($event)" [settings]="languageSettings"
|
||||
[(locked)]="metadata.languageLocked" (onUnlock)="metadata.languageLocked = false"
|
||||
(newItemAdded)="metadata.languageLocked = true" (selectedData)="metadata.languageLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.title}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.title}} ({{item.isoCode}})
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="age-rating" class="form-label">Age Rating</label>
|
||||
<div class="input-group {{metadata.ageRatingLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'ageRatingLocked' }"></ng-container>
|
||||
<select class="form-select"id="age-rating" formControlName="ageRating">
|
||||
<option *ngFor="let opt of ageRatings" [value]="opt.value">{{opt.title | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="publication-status" class="form-label">Publication Status</label>
|
||||
<div class="input-group {{metadata.publicationStatusLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'publicationStatusLocked' }"></ng-container>
|
||||
<select class="form-select"id="publication-status" formControlName="publicationStatus">
|
||||
<option *ngFor="let opt of publicationStatuses" [value]="opt.value">{{opt.title | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[2]">
|
||||
<a ngbNavLink>{{tabs[2]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="writer" class="form-label">Writer</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)"
|
||||
[(locked)]="metadata.writersLocked" (onUnlock)="metadata.writersLocked = false"
|
||||
(newItemAdded)="metadata.writersLocked = true" (selectedData)="metadata.writersLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="cover-artist" class="form-label">Cover Artist</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
|
||||
[(locked)]="metadata.coverArtistsLocked" (onUnlock)="metadata.coverArtistsLocked = false"
|
||||
(newItemAdded)="metadata.coverArtistsLocked = true" (selectedData)="metadata.coverArtistsLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="publisher" class="form-label">Publisher</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)"
|
||||
[(locked)]="metadata.publishersLocked" (onUnlock)="metadata.publishersLocked = false"
|
||||
(newItemAdded)="metadata.publishersLocked = true" (selectedData)="metadata.publishersLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="penciller" class="form-label">Penciller</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)"
|
||||
[(locked)]="metadata.pencillersLocked" (onUnlock)="metadata.pencillersLocked = false"
|
||||
(newItemAdded)="metadata.pencillersLocked = true" (selectedData)="metadata.pencillersLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="letterer" class="form-label">Letterer</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)"
|
||||
[(locked)]="metadata.letterersLocked" (onUnlock)="metadata.letterersLocked = false"
|
||||
(newItemAdded)="metadata.letterersLocked = true" (selectedData)="metadata.letterersLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="inker" class="form-label">Inker</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)"
|
||||
[(locked)]="metadata.inkersLocked" (onUnlock)="metadata.inkersLocked = false"
|
||||
(newItemAdded)="metadata.inkersLocked = true" (selectedData)="metadata.inkersLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="editor" class="form-label">Editor</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)"
|
||||
[(locked)]="metadata.editorsLocked" (onUnlock)="metadata.editorsLocked = false"
|
||||
(newItemAdded)="metadata.editorsLocked = true" (selectedData)="metadata.editorsLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="colorist" class="form-label">Colorist</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)"
|
||||
[(locked)]="metadata.coloristsLocked" (onUnlock)="metadata.coloristsLocked = false"
|
||||
(newItemAdded)="metadata.coloristsLocked = true" (selectedData)="metadata.coloristsLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="character" class="form-label">Character</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)"
|
||||
[(locked)]="metadata.charactersLocked" (onUnlock)="metadata.charactersLocked = false"
|
||||
(newItemAdded)="metadata.charactersLocked = true" (selectedData)="metadata.charactersLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="translator" class="form-label">Translators</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)"
|
||||
[(locked)]="metadata.translatorsLocked" (onUnlock)="metadata.translatorsLocked = false"
|
||||
(newItemAdded)="metadata.translatorsLocked = true" (selectedData)="metadata.translatorsLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[3]">
|
||||
<a ngbNavLink>{{tabs[3]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-primary" role="alert">
|
||||
Upload and choose a new cover image. Press Save to upload and override the cover.
|
||||
|
@ -90,8 +329,8 @@
|
|||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="series.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="tabs[2]">
|
||||
<a ngbNavLink>{{tabs[2]}}</a>
|
||||
<li [ngbNavItem]="tabs[4]">
|
||||
<a ngbNavLink>{{tabs[4]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4>Information</h4>
|
||||
<div class="row g-0 mb-2">
|
||||
|
@ -152,8 +391,9 @@
|
|||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||
|
@ -162,3 +402,10 @@
|
|||
</div>
|
||||
|
||||
|
||||
<ng-template #lock let-item="item" let-field="field">
|
||||
<span class="input-group-text clickable" (click)="unlock(item, field)">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Field is locked</span>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
|
|
|
@ -2,3 +2,10 @@
|
|||
max-height: 90vh; // 600px
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.lock-active {
|
||||
> .input-group-text {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
|
@ -1,17 +1,24 @@
|
|||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { forkJoin, Observable, of, Subject } from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { Genre } from 'src/app/_models/genre';
|
||||
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
|
||||
import { Language } from 'src/app/_models/metadata/language';
|
||||
import { PublicationStatusDto } from 'src/app/_models/metadata/publication-status-dto';
|
||||
import { Person, PersonRole } from 'src/app/_models/person';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesMetadata } from 'src/app/_models/series-metadata';
|
||||
import { Tag } from 'src/app/_models/tag';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
|
||||
|
@ -28,14 +35,27 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
|
||||
isCollapsed = true;
|
||||
volumeCollapsed: any = {};
|
||||
tabs = ['General', 'Cover Image', 'Info'];
|
||||
tabs = ['General', 'Metadata', 'People', 'Cover Image', 'Info'];
|
||||
active = this.tabs[0];
|
||||
editSeriesForm!: FormGroup;
|
||||
libraryName: string | undefined = undefined;
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
settings: TypeaheadSettings<CollectionTag> = new TypeaheadSettings();
|
||||
tags: CollectionTag[] = [];
|
||||
|
||||
// Typeaheads
|
||||
ageRatingSettings: TypeaheadSettings<AgeRatingDto> = new TypeaheadSettings();
|
||||
publicationStatusSettings: TypeaheadSettings<PublicationStatusDto> = new TypeaheadSettings();
|
||||
tagsSettings: TypeaheadSettings<Tag> = new TypeaheadSettings();
|
||||
languageSettings: TypeaheadSettings<Language> = new TypeaheadSettings();
|
||||
peopleSettings: {[PersonRole: string]: TypeaheadSettings<Person>} = {};
|
||||
collectionTagSettings: TypeaheadSettings<CollectionTag> = new TypeaheadSettings();
|
||||
genreSettings: TypeaheadSettings<Genre> = new TypeaheadSettings();
|
||||
|
||||
|
||||
collectionTags: CollectionTag[] = [];
|
||||
tags: Tag[] = [];
|
||||
genres: Genre[] = [];
|
||||
|
||||
metadata!: SeriesMetadata;
|
||||
imageUrls: Array<string> = [];
|
||||
/**
|
||||
|
@ -43,10 +63,22 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
selectedCover: string = '';
|
||||
|
||||
ageRatings: Array<AgeRatingDto> = [];
|
||||
publicationStatuses: Array<PublicationStatusDto> = [];
|
||||
validLanguages: Array<Language> = [];
|
||||
|
||||
get Breakpoint(): typeof Breakpoint {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
get PersonRole() {
|
||||
return PersonRole;
|
||||
}
|
||||
|
||||
getPersonsSettings(role: PersonRole) {
|
||||
return this.peopleSettings[role];
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal,
|
||||
private seriesService: SeriesService,
|
||||
public utilityService: UtilityService,
|
||||
|
@ -54,7 +86,8 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
public imageService: ImageService,
|
||||
private libraryService: LibraryService,
|
||||
private collectionService: CollectionTagService,
|
||||
private uploadService: UploadService) { }
|
||||
private uploadService: UploadService,
|
||||
private metadataService: MetadataService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
|
||||
|
@ -63,9 +96,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.libraryName = names[this.series.libraryId];
|
||||
});
|
||||
|
||||
this.setupTypeaheadSettings();
|
||||
|
||||
|
||||
|
||||
this.editSeriesForm = this.fb.group({
|
||||
id: new FormControl(this.series.id, []),
|
||||
|
@ -75,20 +105,70 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
sortName: new FormControl(this.series.sortName, []),
|
||||
rating: new FormControl(this.series.userRating, []),
|
||||
|
||||
genres: new FormControl('', []),
|
||||
author: new FormControl('', []),
|
||||
artist: new FormControl('', []),
|
||||
|
||||
coverImageIndex: new FormControl(0, []),
|
||||
coverImageLocked: new FormControl(this.series.coverImageLocked, [])
|
||||
coverImageLocked: new FormControl(this.series.coverImageLocked, []),
|
||||
|
||||
ageRating: new FormControl('', []),
|
||||
publicationStatus: new FormControl('', []),
|
||||
language: new FormControl('', []),
|
||||
});
|
||||
|
||||
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
this.ageRatings = ratings;
|
||||
});
|
||||
|
||||
this.metadataService.getAllPublicationStatus().subscribe(statuses => {
|
||||
this.publicationStatuses = statuses;
|
||||
});
|
||||
|
||||
this.metadataService.getAllValidLanguages().subscribe(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
})
|
||||
|
||||
this.seriesService.getMetadata(this.series.id).subscribe(metadata => {
|
||||
if (metadata) {
|
||||
this.metadata = metadata;
|
||||
this.settings.savedData = metadata.collectionTags;
|
||||
this.tags = metadata.collectionTags;
|
||||
|
||||
this.setupTypeaheads();
|
||||
this.editSeriesForm.get('summary')?.setValue(this.metadata.summary);
|
||||
this.editSeriesForm.get('ageRating')?.setValue(this.metadata.ageRating);
|
||||
this.editSeriesForm.get('publicationStatus')?.setValue(this.metadata.publicationStatus);
|
||||
this.editSeriesForm.get('language')?.setValue(this.metadata.language);
|
||||
|
||||
this.editSeriesForm.get('name')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
if (!this.editSeriesForm.get('name')?.touched) return;
|
||||
this.series.nameLocked = true;
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('sortName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
if (!this.editSeriesForm.get('sortName')?.touched) return;
|
||||
this.series.sortNameLocked = true;
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('localizedName')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
if (!this.editSeriesForm.get('localizedName')?.touched) return;
|
||||
this.series.localizedNameLocked = true;
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('summary')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
if (!this.editSeriesForm.get('summary')?.touched) return;
|
||||
this.metadata.summaryLocked = true;
|
||||
this.metadata.summary = val;
|
||||
});
|
||||
|
||||
|
||||
this.editSeriesForm.get('ageRating')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.metadata.ageRating = parseInt(val + '', 10);
|
||||
if (!this.editSeriesForm.get('ageRating')?.touched) return;
|
||||
this.metadata.ageRatingLocked = true;
|
||||
});
|
||||
|
||||
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
|
||||
this.metadata.publicationStatus = parseInt(val + '', 10);
|
||||
if (!this.editSeriesForm.get('publicationStatus')?.touched) return;
|
||||
this.metadata.publicationStatusLocked = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -114,22 +194,192 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
setupTypeaheadSettings() {
|
||||
this.settings.minCharacters = 0;
|
||||
this.settings.multiple = true;
|
||||
this.settings.id = 'collections';
|
||||
this.settings.unique = true;
|
||||
this.settings.addIfNonExisting = true;
|
||||
this.settings.fetchFn = (filter: string) => this.fetchCollectionTags(filter).pipe(map(items => this.settings.compareFn(items, filter)));
|
||||
this.settings.addTransformFn = ((title: string) => {
|
||||
setupTypeaheads() {
|
||||
forkJoin([
|
||||
this.setupCollectionTagsSettings(),
|
||||
this.setupTagSettings(),
|
||||
this.setupGenreTypeahead(),
|
||||
this.setupPersonTypeahead(),
|
||||
this.setupLanguageTypeahead()
|
||||
]).subscribe(results => {
|
||||
this.collectionTags = this.metadata.collectionTags;
|
||||
this.editSeriesForm.get('summary')?.setValue(this.metadata.summary);
|
||||
});
|
||||
}
|
||||
|
||||
setupCollectionTagsSettings() {
|
||||
this.collectionTagSettings.minCharacters = 0;
|
||||
this.collectionTagSettings.multiple = true;
|
||||
this.collectionTagSettings.id = 'collections';
|
||||
this.collectionTagSettings.unique = true;
|
||||
this.collectionTagSettings.addIfNonExisting = true;
|
||||
this.collectionTagSettings.fetchFn = (filter: string) => this.fetchCollectionTags(filter).pipe(map(items => this.collectionTagSettings.compareFn(items, filter)));
|
||||
this.collectionTagSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false };
|
||||
});
|
||||
this.settings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.settings.singleCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
this.collectionTagSettings.singleCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
|
||||
if (this.metadata.collectionTags) {
|
||||
this.collectionTagSettings.savedData = this.metadata.collectionTags;
|
||||
}
|
||||
|
||||
return of(true);
|
||||
}
|
||||
|
||||
setupTagSettings() {
|
||||
this.tagsSettings.minCharacters = 0;
|
||||
this.tagsSettings.multiple = true;
|
||||
this.tagsSettings.id = 'tags';
|
||||
this.tagsSettings.unique = true;
|
||||
this.tagsSettings.showLocked = true;
|
||||
this.tagsSettings.addIfNonExisting = true;
|
||||
|
||||
|
||||
this.tagsSettings.compareFn = (options: Tag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags()
|
||||
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
|
||||
|
||||
this.tagsSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, title: title };
|
||||
});
|
||||
this.tagsSettings.singleCompareFn = (a: Tag, b: Tag) => {
|
||||
return a.id == b.id;
|
||||
}
|
||||
|
||||
if (this.metadata.tags) {
|
||||
this.tagsSettings.savedData = this.metadata.tags;
|
||||
}
|
||||
return of(true);
|
||||
}
|
||||
|
||||
setupGenreTypeahead() {
|
||||
this.genreSettings.minCharacters = 0;
|
||||
this.genreSettings.multiple = true;
|
||||
this.genreSettings.id = 'genres';
|
||||
this.genreSettings.unique = true;
|
||||
this.genreSettings.showLocked = true;
|
||||
this.genreSettings.addIfNonExisting = true;
|
||||
this.genreSettings.fetchFn = (filter: string) => {
|
||||
return this.metadataService.getAllGenres()
|
||||
.pipe(map(items => this.genreSettings.compareFn(items, filter)));
|
||||
};
|
||||
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.genreSettings.singleCompareFn = (a: Genre, b: Genre) => {
|
||||
return a.title == b.title;
|
||||
}
|
||||
|
||||
this.genreSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, title: title };
|
||||
});
|
||||
|
||||
if (this.metadata.genres) {
|
||||
this.genreSettings.savedData = this.metadata.genres;
|
||||
}
|
||||
return of(true);
|
||||
}
|
||||
|
||||
updateFromPreset(id: string, presetField: Array<Person> | undefined, role: PersonRole) {
|
||||
const personSettings = this.createBlankPersonSettings(id, role)
|
||||
if (presetField && presetField.length > 0) {
|
||||
const fetch = personSettings.fetchFn as ((filter: string) => Observable<Person[]>);
|
||||
return fetch('').pipe(map(people => {
|
||||
const persetIds = presetField.map(p => p.id);
|
||||
personSettings.savedData = people.filter(person => persetIds.includes(person.id));
|
||||
this.peopleSettings[role] = personSettings;
|
||||
this.updatePerson(personSettings.savedData as Person[], role);
|
||||
return true;
|
||||
}));
|
||||
} else {
|
||||
this.peopleSettings[role] = personSettings;
|
||||
return of(true);
|
||||
}
|
||||
}
|
||||
|
||||
setupLanguageTypeahead() {
|
||||
this.languageSettings.minCharacters = 0;
|
||||
this.languageSettings.multiple = false;
|
||||
this.languageSettings.id = 'language';
|
||||
this.languageSettings.unique = true;
|
||||
this.languageSettings.showLocked = true;
|
||||
this.languageSettings.addIfNonExisting = false;
|
||||
this.languageSettings.compareFn = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
this.languageSettings.singleCompareFn = (a: Language, b: Language) => {
|
||||
return a.isoCode == b.isoCode;
|
||||
}
|
||||
|
||||
if (this.metadata.language) {
|
||||
const l = this.validLanguages.find(l => l.isoCode === this.metadata.language);
|
||||
if (l !== undefined) {
|
||||
this.languageSettings.savedData = l;
|
||||
}
|
||||
}
|
||||
return of(true);
|
||||
}
|
||||
|
||||
setupPersonTypeahead() {
|
||||
this.peopleSettings = {};
|
||||
|
||||
return forkJoin([
|
||||
this.updateFromPreset('writer', this.metadata.writers, PersonRole.Writer),
|
||||
this.updateFromPreset('character', this.metadata.characters, PersonRole.Character),
|
||||
this.updateFromPreset('colorist', this.metadata.colorists, PersonRole.Colorist),
|
||||
this.updateFromPreset('cover-artist', this.metadata.coverArtists, PersonRole.CoverArtist),
|
||||
this.updateFromPreset('editor', this.metadata.editors, PersonRole.Editor),
|
||||
this.updateFromPreset('inker', this.metadata.inkers, PersonRole.Inker),
|
||||
this.updateFromPreset('letterer', this.metadata.letterers, PersonRole.Letterer),
|
||||
this.updateFromPreset('penciller', this.metadata.pencillers, PersonRole.Penciller),
|
||||
this.updateFromPreset('publisher', this.metadata.publishers, PersonRole.Publisher),
|
||||
this.updateFromPreset('translator', this.metadata.translators, PersonRole.Translator)
|
||||
]).pipe(map(results => {
|
||||
//this.resetTypeaheads.next(true);
|
||||
return of(true);
|
||||
}));
|
||||
}
|
||||
|
||||
fetchPeople(role: PersonRole, filter: string) {
|
||||
return this.metadataService.getAllPeople().pipe(map(people => {
|
||||
return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter));
|
||||
}));
|
||||
}
|
||||
|
||||
createBlankPersonSettings(id: string, role: PersonRole) {
|
||||
var personSettings = new TypeaheadSettings<Person>();
|
||||
personSettings.minCharacters = 0;
|
||||
personSettings.multiple = true;
|
||||
personSettings.showLocked = true;
|
||||
personSettings.unique = true;
|
||||
personSettings.addIfNonExisting = true;
|
||||
personSettings.id = id;
|
||||
personSettings.compareFn = (options: Person[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
}
|
||||
|
||||
personSettings.singleCompareFn = (a: Person, b: Person) => {
|
||||
return a.name == b.name && a.role == b.role;
|
||||
}
|
||||
personSettings.fetchFn = (filter: string) => {
|
||||
return this.fetchPeople(role, filter).pipe(map(items => personSettings.compareFn(items, filter)));
|
||||
};
|
||||
|
||||
personSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, name: title, role: role };
|
||||
});
|
||||
|
||||
return personSettings;
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -150,11 +400,17 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
save() {
|
||||
const model = this.editSeriesForm.value;
|
||||
const selectedIndex = this.editSeriesForm.get('coverImageIndex')?.value || 0;
|
||||
|
||||
const apis = [
|
||||
this.seriesService.updateSeries(model),
|
||||
this.seriesService.updateMetadata(this.metadata, this.tags)
|
||||
this.seriesService.updateMetadata(this.metadata, this.collectionTags)
|
||||
];
|
||||
|
||||
// We only need to call updateSeries if we changed name, sort name, or localized name
|
||||
if (this.editSeriesForm.get('name')?.dirty || this.editSeriesForm.get('sortName')?.dirty || this.editSeriesForm.get('localizedName')?.dirty) {
|
||||
apis.push(this.seriesService.updateSeries(model));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
apis.push(this.uploadService.updateSeriesCoverImage(model.id, this.selectedCover));
|
||||
|
@ -165,8 +421,65 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
handleUnlock(field: string) {
|
||||
console.log('todo: unlock ', field);
|
||||
}
|
||||
|
||||
hello(val: boolean) {
|
||||
console.log('hello: ', val);
|
||||
}
|
||||
|
||||
updateCollections(tags: CollectionTag[]) {
|
||||
this.collectionTags = tags;
|
||||
}
|
||||
|
||||
updateTags(tags: Tag[]) {
|
||||
this.tags = tags;
|
||||
this.metadata.tags = tags;
|
||||
}
|
||||
|
||||
updateGenres(genres: Genre[]) {
|
||||
this.genres = genres;
|
||||
this.metadata.genres = genres;
|
||||
}
|
||||
|
||||
updateLanguage(language: Language) {
|
||||
this.metadata.language = language.isoCode;
|
||||
}
|
||||
|
||||
updatePerson(persons: Person[], role: PersonRole) {
|
||||
switch (role) {
|
||||
case PersonRole.CoverArtist:
|
||||
this.metadata.coverArtists = persons;
|
||||
break;
|
||||
case PersonRole.Character:
|
||||
this.metadata.characters = persons;
|
||||
break;
|
||||
case PersonRole.Colorist:
|
||||
this.metadata.colorists = persons;
|
||||
break;
|
||||
case PersonRole.Editor:
|
||||
this.metadata.editors = persons;
|
||||
break;
|
||||
case PersonRole.Inker:
|
||||
this.metadata.inkers = persons;
|
||||
break;
|
||||
case PersonRole.Letterer:
|
||||
this.metadata.letterers = persons;
|
||||
break;
|
||||
case PersonRole.Penciller:
|
||||
this.metadata.pencillers = persons;
|
||||
break;
|
||||
case PersonRole.Publisher:
|
||||
this.metadata.publishers = persons;
|
||||
break;
|
||||
case PersonRole.Writer:
|
||||
this.metadata.writers = persons;
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.metadata.translators = persons;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
|
@ -185,4 +498,10 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
unlock(b: any, field: string) {
|
||||
if (b) {
|
||||
b[field] = !b[field];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ 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';
|
||||
import { ChangeCoverImageModalComponent } from './_modals/change-cover-image/change-cover-image-modal.component';
|
||||
import { BookmarksModalComponent } from './_modals/bookmarks-modal/bookmarks-modal.component';
|
||||
import { NgbTooltipModule, NgbCollapseModule, NgbPaginationModule, NgbDropdownModule, NgbProgressbarModule, NgbNavModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component';
|
||||
|
@ -34,7 +33,6 @@ import { BookmarkComponent } from './bookmark/bookmark.component';
|
|||
CoverImageChooserComponent,
|
||||
EditSeriesModalComponent,
|
||||
EditCollectionTagsComponent,
|
||||
ChangeCoverImageModalComponent,
|
||||
BookmarksModalComponent,
|
||||
CardActionablesComponent,
|
||||
CardDetailLayoutComponent,
|
||||
|
@ -75,7 +73,6 @@ import { BookmarkComponent } from './bookmark/bookmark.component';
|
|||
CoverImageChooserComponent,
|
||||
EditSeriesModalComponent,
|
||||
EditCollectionTagsComponent,
|
||||
ChangeCoverImageModalComponent,
|
||||
BookmarksModalComponent,
|
||||
CardActionablesComponent,
|
||||
CardDetailLayoutComponent,
|
||||
|
|
|
@ -1,118 +1,102 @@
|
|||
<ng-container *ngIf="chapter !== undefined">
|
||||
<div class="container-fluid">
|
||||
<!-- <h4>{{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}} <span title="Id">({{chapter.id}})</span></h4> -->
|
||||
|
||||
|
||||
<!-- Arc Information -->
|
||||
<ng-container>
|
||||
<span *ngIf="chapter.writers.length === 0 && chapter.coverArtists.length === 0
|
||||
&& chapter.pencillers.length === 0 && chapter.inkers.length === 0
|
||||
&& chapter.colorists.length === 0 && chapter.letterers.length === 0
|
||||
&& chapter.editors.length === 0 && chapter.publishers.length === 0
|
||||
&& chapter.characters.length === 0 && chapter.translators.length === 0">
|
||||
No metadata available
|
||||
</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Id: {{chapter.id}}
|
||||
<div class="col-auto" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<h6>Writers</h6>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Title: {{chapter.titleName || '-'}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{chapter.pages}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
|
||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Release Date: {{(chapter.releaseDate | date: 'shortDate') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled" >
|
||||
<li class="d-flex my-4">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
||||
<app-image class="me-3" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
<!-- TODO: Add back in
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</span> -->
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<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 file-list">
|
||||
<app-file-info *ngFor="let file of chapter.files" [file]="file" [created]="chapter.created"></app-file-info>
|
||||
</ul>
|
||||
|
||||
|
||||
<ng-container>
|
||||
<div class="row g-0 mt-1" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Writers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of chapter.writers" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="chapter.coverArtist && chapter.coverArtist.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Artists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of chapter.coverArtist" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="chapter.publisher && chapter.publisher.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Publishers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of chapter.publisher" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="col-auto" *ngIf="chapter.coverArtists && chapter.coverArtists.length > 0">
|
||||
<h6>Cover Artists</h6>
|
||||
<app-badge-expander [items]="chapter.coverArtists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.pencillers && chapter.pencillers.length > 0">
|
||||
<h6>Pencillers</h6>
|
||||
<app-badge-expander [items]="chapter.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.inkers && chapter.inkers.length > 0">
|
||||
<h6>Inkers</h6>
|
||||
<app-badge-expander [items]="chapter.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.colorists && chapter.colorists.length > 0">
|
||||
<h6>Colorists</h6>
|
||||
<app-badge-expander [items]="chapter.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!--
|
||||
<div class="col-auto" *ngIf="chapter.letterers && chapter.letterers.length > 0">
|
||||
<h6>Letterers</h6>
|
||||
<app-badge-expander [items]="chapter.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid" *ngIf="metadata !== undefined">
|
||||
Chapter {{chapter.range}} {{metadata.title.length > 0 ? ' - ' + metadata.title : ''}}
|
||||
Title: {{metadata.title || '-'}}
|
||||
Year: {{metadata.year || '-'}}
|
||||
Arc Information
|
||||
|
||||
<div class="col-auto" *ngIf="chapter.editors && chapter.editors.length > 0">
|
||||
<h6>Editors</h6>
|
||||
<app-badge-expander [items]="chapter.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Id: {{chapter.id}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{chapter.pages}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto" *ngIf="chapter.publishers && chapter.publishers.length > 0">
|
||||
<h6>Publishers</h6>
|
||||
<app-badge-expander [items]="chapter.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
|
||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||
<div class="col-auto" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<h6>Characters</h6>
|
||||
<app-badge-expander [items]="chapter.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
<div class="col-auto" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<h6>Translators</h6>
|
||||
<app-badge-expander [items]="chapter.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{chapter.pages}}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</ng-container>
|
||||
</ng-container>
|
|
@ -5,6 +5,7 @@ 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',
|
||||
|
@ -13,9 +14,10 @@ import { ActionItem } from 'src/app/_services/action-factory.service';
|
|||
})
|
||||
export class ChapterMetadataDetailComponent implements OnInit {
|
||||
|
||||
@Input() chapter!: Chapter;
|
||||
@Input() chapter!: ChapterMetadata;
|
||||
@Input() libraryType: LibraryType = LibraryType.Manga;
|
||||
//metadata!: ChapterMetadata;
|
||||
|
||||
roles: string[] = [];
|
||||
|
||||
get LibraryType(): typeof LibraryType {
|
||||
return LibraryType;
|
||||
|
@ -24,10 +26,14 @@ export class ChapterMetadataDetailComponent implements OnInit {
|
|||
constructor(private metadataService: MetadataService, public utilityService: UtilityService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// this.metadataService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
// console.log('Chapter ', this.chapter.number, ' metadata: ', metadata);
|
||||
// this.metadata = metadata;
|
||||
// })
|
||||
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) {
|
||||
|
|
|
@ -24,12 +24,13 @@
|
|||
</ng-container>
|
||||
|
||||
<div (click)="toggleMenu()" class="reading-area">
|
||||
|
||||
<!-- TODO: Change this logic to only render for split pages and use image for all else-->
|
||||
<canvas style="display: none;" #content class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"
|
||||
ondragstart="return false;" onselectstart="return false;">
|
||||
</canvas>
|
||||
<div *ngIf="isCoverImage && shouldRenderAsFitSplit()" class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||
<img [src]="canvasImage.src" style="width: 100%">
|
||||
|
||||
<div *ngIf="isCoverImage && shouldRenderAsFitSplit()">
|
||||
<img [src]="canvasImage.src" class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||
</div>
|
||||
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading && !inSetup">
|
||||
<app-infinite-scroller [pageNum]="pageNum"
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FilterPipe } from './filter.pipe';
|
||||
import { PersonRolePipe } from './person-role.pipe';
|
||||
import { PublicationStatusPipe } from './publication-status.pipe';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
FilterPipe
|
||||
FilterPipe,
|
||||
PersonRolePipe,
|
||||
PublicationStatusPipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
],
|
||||
exports: [
|
||||
FilterPipe
|
||||
FilterPipe,
|
||||
PersonRolePipe,
|
||||
PublicationStatusPipe
|
||||
]
|
||||
})
|
||||
export class PipeModule { }
|
||||
|
|
|
@ -43,7 +43,10 @@ export class ThemeTestComponent implements OnInit {
|
|||
sortName: '',
|
||||
userRating: 0,
|
||||
userReview: '',
|
||||
volumes: []
|
||||
volumes: [],
|
||||
localizedNameLocked: false,
|
||||
nameLocked: false,
|
||||
sortNameLocked: false
|
||||
}
|
||||
|
||||
seriesWithProgress: Series = {
|
||||
|
@ -61,7 +64,10 @@ export class ThemeTestComponent implements OnInit {
|
|||
sortName: '',
|
||||
userRating: 0,
|
||||
userReview: '',
|
||||
volumes: []
|
||||
volumes: [],
|
||||
localizedNameLocked: false,
|
||||
nameLocked: false,
|
||||
sortNameLocked: false
|
||||
}
|
||||
|
||||
get TagBadgeCursor(): typeof TagBadgeCursor {
|
||||
|
|
|
@ -16,6 +16,10 @@ export class TypeaheadSettings<T> {
|
|||
* Id of the input element, for linking label elements (accessibility)
|
||||
*/
|
||||
id: string = '';
|
||||
/**
|
||||
* Show a locked icon next to input and provide functionality around locking/unlocking a field
|
||||
*/
|
||||
showLocked: boolean = false;
|
||||
/**
|
||||
* Data to preload the typeahead with on first load
|
||||
*/
|
||||
|
|
|
@ -1,36 +1,40 @@
|
|||
<form [formGroup]="typeaheadForm">
|
||||
<ng-container *ngIf="settings.multiple" >
|
||||
<div class="typeahead-input" (click)="onInputFocus($event)">
|
||||
<div>
|
||||
<app-tag-badge *ngFor="let option of optionSelection.selected(); let i = index">
|
||||
<ng-container [ngTemplateOutlet]="badgeTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }"></ng-container>
|
||||
<i class="fa fa-times" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
|
||||
</app-tag-badge>
|
||||
<div class="input-group {{hasFocus ? 'open': ''}} {{locked ? 'lock-active' : ''}}">
|
||||
<ng-container *ngIf="settings.showLocked">
|
||||
<span class="input-group-text clickable" (click)="unlock($event)"><i class="fa fa-lock" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Field is locked</span>
|
||||
</span>
|
||||
</ng-container>
|
||||
<div class="typeahead-input" [ngStyle]="{'width': (settings.showLocked ? '93%' : '100%')}" (click)="onInputFocus($event)">
|
||||
<app-tag-badge *ngFor="let option of optionSelection.selected(); let i = index">
|
||||
<ng-container [ngTemplateOutlet]="badgeTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }"></ng-container>
|
||||
<i class="fa fa-times" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
|
||||
</app-tag-badge>
|
||||
|
||||
<input #input [id]="settings.id" type="text" autocomplete="off" formControlName="typeahead">
|
||||
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoadingOptions">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<!-- TODO: Add a clear all button -->
|
||||
<input #input [id]="settings.id" type="text" autocomplete="off" formControlName="typeahead">
|
||||
<div class="spinner-border spinner-border-sm {{settings.multiple ? 'close-offset' : ''}}" role="status" *ngIf="isLoadingOptions">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown" *ngIf="hasFocus">
|
||||
<ul class="list-group results" #results>
|
||||
<li *ngIf="showAddItem"
|
||||
class="list-group-item add-item" role="option" (mouseenter)="focusedIndex = 0; updateHighlight();" (click)="addNewItem(typeaheadControl.value)">
|
||||
Add {{typeaheadControl.value}}...
|
||||
</li>
|
||||
<li *ngFor="let option of filteredOptions | async; let index = index;" (click)="handleOptionClick(option)"
|
||||
class="list-group-item" role="option"
|
||||
(mouseenter)="focusedIndex = index + (showAddItem ? 1 : 0); updateHighlight();">
|
||||
<ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
|
||||
</li>
|
||||
<li *ngIf="(filteredOptions | async)?.length === 0 && !showAddItem" class="list-group-item no-hover" role="status">
|
||||
No data{{this.settings.addIfNonExisting ? ', type to add a custom item.' : '.'}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ng-container *ngIf="settings.multiple">
|
||||
<button class="btn btn-close float-end mt-2" style="font-size: 0.8rem;" (click)="clearSelections($event)"></button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
</form>
|
||||
<div class="dropdown" *ngIf="hasFocus">
|
||||
<ul class="list-group results" #results>
|
||||
<li *ngIf="showAddItem"
|
||||
class="list-group-item add-item" role="option" (mouseenter)="focusedIndex = 0; updateHighlight();" (click)="addNewItem(typeaheadControl.value)">
|
||||
Add {{typeaheadControl.value}}...
|
||||
</li>
|
||||
<li *ngFor="let option of filteredOptions | async; let index = index;" (click)="handleOptionClick(option)"
|
||||
class="list-group-item" role="option"
|
||||
(mouseenter)="focusedIndex = index + (showAddItem ? 1 : 0); updateHighlight();">
|
||||
<ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
|
||||
</li>
|
||||
<li *ngIf="(filteredOptions | async)?.length === 0 && !showAddItem" class="list-group-item no-hover" role="status">
|
||||
No data{{this.settings.addIfNonExisting ? ', type to add a custom item.' : '.'}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
|
@ -2,6 +2,10 @@ form {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
flex-wrap: inherit;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 15px;
|
||||
opacity: 1px;
|
||||
|
@ -10,6 +14,18 @@ input {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.lock-active {
|
||||
> .input-group-text {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.close-offset {
|
||||
right: 29px !important;
|
||||
top: 29% !important;
|
||||
}
|
||||
|
||||
.typeahead-input {
|
||||
padding: 0px 6px;
|
||||
display: inline-block;
|
||||
|
@ -44,14 +60,22 @@ input {
|
|||
}
|
||||
}
|
||||
|
||||
.open .input-group-text {
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
|
||||
.open .typeahead-input {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: 100%;
|
||||
min-width: 10rem;
|
||||
background: var(--input-bg-color);
|
||||
z-index:1000;
|
||||
margin: 2px 0 0;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
margin-top: -7px;
|
||||
margin-top: -1px;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
position: absolute;
|
||||
|
@ -59,6 +83,11 @@ input {
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.list-group {
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
|
|
|
@ -137,30 +137,36 @@ export class SelectionModel<T> {
|
|||
styleUrls: ['./typeahead.component.scss']
|
||||
})
|
||||
export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
|
||||
filteredOptions!: Observable<string[]>;
|
||||
isLoadingOptions: boolean = false;
|
||||
typeaheadControl!: FormControl;
|
||||
typeaheadForm!: FormGroup;
|
||||
|
||||
|
||||
/**
|
||||
* Settings for the typeahead
|
||||
*/
|
||||
@Input() settings!: TypeaheadSettings<any>;
|
||||
/**
|
||||
* When true, component will re-init and set back to false.
|
||||
*/
|
||||
@Input() reset: Subject<boolean> = new ReplaySubject(1);
|
||||
/**
|
||||
* When a field is locked, we render custom css to indicate to the user. Does not affect functionality.
|
||||
*/
|
||||
@Input() locked: boolean = false;
|
||||
@Output() selectedData = new EventEmitter<any[] | any>();
|
||||
@Output() newItemAdded = new EventEmitter<any[] | any>();
|
||||
@Output() onUnlock = new EventEmitter<void>();
|
||||
@Output() lockedChange = new EventEmitter<boolean>();
|
||||
|
||||
@ViewChild('input') inputElem!: ElementRef<HTMLInputElement>;
|
||||
@ContentChild('optionItem') optionTemplate!: TemplateRef<any>;
|
||||
@ContentChild('badgeItem') badgeTemplate!: TemplateRef<any>;
|
||||
|
||||
optionSelection!: SelectionModel<any>;
|
||||
|
||||
hasFocus = false; // Whether input has active focus
|
||||
focusedIndex: number = 0;
|
||||
showAddItem: boolean = false;
|
||||
|
||||
@ViewChild('input') inputElem!: ElementRef<HTMLInputElement>;
|
||||
@ContentChild('optionItem') optionTemplate!: TemplateRef<any>;
|
||||
@ContentChild('badgeItem') badgeTemplate!: TemplateRef<any>;
|
||||
filteredOptions!: Observable<string[]>;
|
||||
isLoadingOptions: boolean = false;
|
||||
typeaheadControl!: FormControl;
|
||||
typeaheadForm!: FormGroup;
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
|
@ -245,10 +251,17 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
if (this.settings.multiple) {
|
||||
this.optionSelection = new SelectionModel<any>(true, this.settings.savedData);
|
||||
}
|
||||
// else {
|
||||
// this.optionSelection = new SelectionModel<any>(true, this.settings.savedData[0]);
|
||||
// this.typeaheadControl.setValue(this.settings.displayFn(this.settings.savedData))
|
||||
// }
|
||||
else {
|
||||
const isArray = this.settings.savedData.hasOwnProperty('length');
|
||||
if (isArray) {
|
||||
this.optionSelection = new SelectionModel<any>(true, this.settings.savedData);
|
||||
} else {
|
||||
this.optionSelection = new SelectionModel<any>(true, [this.settings.savedData]);
|
||||
}
|
||||
|
||||
|
||||
//this.typeaheadControl.setValue(this.settings.displayFn(this.settings.savedData))
|
||||
}
|
||||
} else {
|
||||
this.optionSelection = new SelectionModel<any>();
|
||||
}
|
||||
|
@ -336,7 +349,17 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
this.resetField();
|
||||
}
|
||||
|
||||
clearSelections(event: any) {
|
||||
this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false));
|
||||
this.selectedData.emit(this.optionSelection.selected());
|
||||
this.resetField();
|
||||
}
|
||||
|
||||
handleOptionClick(opt: any) {
|
||||
if (!this.settings.multiple && this.optionSelection.selected().length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleSelection(opt);
|
||||
|
||||
this.resetField();
|
||||
|
@ -375,12 +398,17 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (!this.settings.multiple && this.optionSelection.selected().length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inputElem) {
|
||||
// hack: To prevent multiple typeaheads from being open at once, click document then trigger the focus
|
||||
document.querySelector('body')?.click();
|
||||
this.inputElem.nativeElement.focus();
|
||||
this.hasFocus = true;
|
||||
}
|
||||
|
||||
|
||||
this.openDropdown();
|
||||
}
|
||||
|
@ -415,4 +443,10 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||
|
||||
}
|
||||
|
||||
unlock(event: any) {
|
||||
this.locked = !this.locked;
|
||||
this.onUnlock.emit();
|
||||
this.lockedChange.emit(this.locked);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,4 +13,8 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
z-index: 1055 !important; // ngb v12 bug: https://github.com/ng-bootstrap/ng-bootstrap/issues/2686
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue