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:
Joseph Milazzo 2022-03-04 15:04:15 -06:00 committed by GitHub
parent 47a92a2e01
commit ba77954d5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 3605 additions and 723 deletions

View file

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

View file

@ -2,3 +2,10 @@
max-height: 90vh; // 600px
overflow: auto;
}
.lock-active {
> .input-group-text {
background-color: var(--primary-color);
color: white;
}
}

View file

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