UX Overhaul Part 2 (#3112)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
0247bc5012
commit
3d8aa2ad24
192 changed files with 14808 additions and 1874 deletions
|
|
@ -0,0 +1,124 @@
|
|||
<ng-container *transloco="let t; read: 'details-tab'">
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="genres" [title]="t('genres-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-tag-badge (click)="openGeneric(FilterField.Genres, item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="tags" [title]="t('tags-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-tag-badge (click)="openGeneric(FilterField.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
@if (genres.length > 0 || tags.length > 0) {
|
||||
<div class="setting-section-break" aria-hidden="true"></div>
|
||||
}
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.writers" [title]="t('writers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Writers, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.colorists" [title]="t('colorists-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Colorist, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.editors" [title]="t('editors-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Editor, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.coverArtists" [title]="t('cover-artists-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.CoverArtist, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.inkers" [title]="t('inkers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Inker, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.letterers" [title]="t('letterers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Letterer, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.pencillers" [title]="t('pencillers-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Penciller, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.translators" [title]="t('translators-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Translators, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.characters" [title]="t('characters-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Characters, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.locations" [title]="t('locations-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Location, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.teams" [title]="t('teams-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Team, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="metadata.imprints" [title]="t('imprints-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-person-badge [person]="item" (click)="openPerson(FilterField.Imprint, item)"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
|
||||
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {IHasCast} from "../../_models/common/i-has-cast";
|
||||
import {Person, PersonRole} from "../../_models/metadata/person";
|
||||
import {Router} from "@angular/router";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
||||
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
|
||||
import {Genre} from "../../_models/metadata/genre";
|
||||
import {Tag} from "../../_models/tag";
|
||||
import {TagBadgeComponent, TagBadgeCursor} from "../../shared/tag-badge/tag-badge.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-tab',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CarouselReelComponent,
|
||||
PersonBadgeComponent,
|
||||
TranslocoDirective,
|
||||
TagBadgeComponent
|
||||
],
|
||||
templateUrl: './details-tab.component.html',
|
||||
styleUrl: './details-tab.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DetailsTabComponent {
|
||||
|
||||
private readonly router = inject(Router);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
protected readonly PersonRole = PersonRole;
|
||||
protected readonly FilterField = FilterField;
|
||||
|
||||
@Input({required: true}) metadata!: IHasCast;
|
||||
@Input() genres: Array<Genre> = [];
|
||||
@Input() tags: Array<Tag> = [];
|
||||
|
||||
|
||||
openPerson(queryParamName: FilterField, filter: Person) {
|
||||
if (queryParamName === FilterField.None) return;
|
||||
this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter.id}`).subscribe();
|
||||
}
|
||||
|
||||
openGeneric(queryParamName: FilterField, filter: string | number) {
|
||||
if (queryParamName === FilterField.None) return;
|
||||
this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe();
|
||||
}
|
||||
|
||||
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
}
|
||||
|
|
@ -0,0 +1,610 @@
|
|||
<ng-container *transloco="let t; read: 'edit-chapter-modal'">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{t('title')}} <app-entity-title [libraryType]="libraryType" [entity]="chapter" [prioritizeTitleName]="false"></app-entity-title></h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal" [ngClass]="{'d-flex': utilityService.getActiveBreakpoint() !== Breakpoint.Mobile}">
|
||||
|
||||
<form [formGroup]="editForm">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeId" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
|
||||
<!-- General Tab -->
|
||||
<li [ngbNavItem]="TabID.General">
|
||||
<a ngbNavLink>{{t(TabID.General)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-9 col-sm-12 mb-3">
|
||||
<app-setting-item [title]="t('title-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||
<ng-template #view>
|
||||
@if (editForm.get('titleName'); as formControl) {
|
||||
<div class="input-group" [ngClass]="{'lock-active': chapter.titleNameLocked}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'titleNameLocked' }"></ng-container>
|
||||
<input class="form-control" formControlName="titleName" type="text"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
@if (formControl.errors; as errors) {
|
||||
<div class="invalid-feedback">
|
||||
@if (errors.required) {
|
||||
<div>{{t('required-field')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 mb-3">
|
||||
<app-setting-item [title]="t('sort-order-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||
<ng-template #view>
|
||||
@if (editForm.get('sortOrder'); as formControl) {
|
||||
<div class="input-group" [ngClass]="{'lock-active': chapter.sortOrderLocked}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'sortOrderLocked' }"></ng-container>
|
||||
<input class="form-control" formControlName="sortOrder" type="number" min="0" step="0.1" inputmode="numeric"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
@if (formControl.errors; as errors) {
|
||||
<div class="invalid-feedback">
|
||||
@if (errors.required) {
|
||||
<div>{{t('required-field')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-9 col-sm-12 mb-3">
|
||||
<app-setting-item [title]="t('isbn-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||
<ng-template #view>
|
||||
@if (editForm.get('isbn'); as formControl) {
|
||||
<div class="input-group" [ngClass]="{'lock-active': chapter.isbnLocked}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'isbnLocked' }"></ng-container>
|
||||
<input class="form-control" formControlName="isbn" type="text"
|
||||
[class.is-invalid]="formControl.invalid && formControl.touched">
|
||||
@if (formControl.errors; as errors) {
|
||||
<div class="invalid-feedback">
|
||||
@if (errors.required) {
|
||||
<div>{{t('required-field')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 mb-3">
|
||||
<app-setting-item [title]="t('age-rating-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||
<ng-template #view>
|
||||
@if (editForm.get('ageRating'); as formControl) {
|
||||
<div class="input-group" [ngClass]="{'lock-active': chapter.ageRatingLocked}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'ageRatingLocked' }"></ng-container>
|
||||
<select class="form-select" id="age-rating" formControlName="ageRating">
|
||||
@for(opt of ageRatings; track opt.value) {
|
||||
<option [value]="opt.value">{{opt.title | titlecase}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-9 col-md-12">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('language-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updateLanguage($event)" [settings]="languageSettings"
|
||||
[(locked)]="chapter.languageLocked" (onUnlock)="chapter.languageLocked = false"
|
||||
(newItemAdded)="chapter.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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-12">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('release-date-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<div class="input-group" [ngClass]="{'lock-active': chapter.releaseDateLocked}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'releaseDateLocked' }"></ng-container>
|
||||
<input
|
||||
class="form-control"
|
||||
formControlName="releaseDate"
|
||||
type="date"
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<app-setting-item [title]="t('summary-label')" [showEdit]="false" [toggleOnViewClick]="false">
|
||||
<ng-template #view>
|
||||
@if (editForm.get('summary'); as formControl) {
|
||||
<div class="input-group" [ngClass]="{'lock-active': chapter.summaryLocked}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: chapter, field: 'summaryLocked' }"></ng-container>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Tags Tab -->
|
||||
<li [ngbNavItem]="TabID.Tags">
|
||||
<a ngbNavLink>{{t(TabID.Tags)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<!-- genre & tag -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('genres-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updateGenres($event);chapter.genresLocked = true" [settings]="genreSettings"
|
||||
[(locked)]="chapter.genresLocked" (onUnlock)="chapter.genresLocked = false"
|
||||
(newItemAdded)="chapter.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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('tags-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updateTags($event);chapter.tagsLocked = true" [settings]="tagsSettings"
|
||||
[(locked)]="chapter.tagsLocked" (onUnlock)="chapter.tagsLocked = false"
|
||||
(newItemAdded)="chapter.tagsLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- imprint & publisher -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('imprint-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Imprint);chapter.imprintLocked = true" [settings]="getPersonsSettings(PersonRole.Imprint)"
|
||||
[(locked)]="chapter.imprintLocked" (onUnlock)="chapter.imprintLocked = false"
|
||||
(newItemAdded)="chapter.imprintLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('publisher-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Publisher);chapter.publisherLocked = true" [settings]="getPersonsSettings(PersonRole.Publisher)"
|
||||
[(locked)]="chapter.publisherLocked" (onUnlock)="chapter.publisherLocked = false"
|
||||
(newItemAdded)="chapter.publisherLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- team & location -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('team-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Team);chapter.teamLocked = true" [settings]="getPersonsSettings(PersonRole.Team)"
|
||||
[(locked)]="chapter.teamLocked" (onUnlock)="chapter.teamLocked = false"
|
||||
(newItemAdded)="chapter.teamLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('location-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Location);chapter.locationLocked = true" [settings]="getPersonsSettings(PersonRole.Location)"
|
||||
[(locked)]="chapter.locationLocked" (onUnlock)="chapter.locationLocked = false"
|
||||
(newItemAdded)="chapter.locationLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- character -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-12 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('character-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Character);chapter.characterLocked = true" [settings]="getPersonsSettings(PersonRole.Character)"
|
||||
[(locked)]="chapter.characterLocked" (onUnlock)="chapter.characterLocked = false"
|
||||
(newItemAdded)="chapter.characterLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- People Tab -->
|
||||
<li [ngbNavItem]="TabID.People">
|
||||
<a ngbNavLink>{{t(TabID.People)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<!-- writer & cover artist -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('writer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Writer);chapter.writerLocked = true" [settings]="getPersonsSettings(PersonRole.Writer)"
|
||||
[(locked)]="chapter.writerLocked" (onUnlock)="chapter.writerLocked = false"
|
||||
(newItemAdded)="chapter.writerLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('cover-artist-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.CoverArtist);chapter.coverArtistLocked = true" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
|
||||
[(locked)]="chapter.coverArtistLocked" (onUnlock)="chapter.coverArtistLocked = false"
|
||||
(newItemAdded)="chapter.coverArtistLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- penciller & colorist -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('penciller-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Penciller);chapter.pencillerLocked = true" [settings]="getPersonsSettings(PersonRole.Penciller)"
|
||||
[(locked)]="chapter.pencillerLocked" (onUnlock)="chapter.pencillerLocked = false"
|
||||
(newItemAdded)="chapter.pencillerLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('colorist-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Colorist);chapter.coloristLocked = true" [settings]="getPersonsSettings(PersonRole.Colorist)"
|
||||
[(locked)]="chapter.coloristLocked" (onUnlock)="chapter.coloristLocked = false"
|
||||
(newItemAdded)="chapter.coloristLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- inker & letterer -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('inker-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Inker);chapter.inkerLocked = true" [settings]="getPersonsSettings(PersonRole.Inker)"
|
||||
[(locked)]="chapter.inkerLocked" (onUnlock)="chapter.inkerLocked = false"
|
||||
(newItemAdded)="chapter.inkerLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('letterer-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Letterer);chapter.lettererLocked = true" [settings]="getPersonsSettings(PersonRole.Letterer)"
|
||||
[(locked)]="chapter.lettererLocked" (onUnlock)="chapter.lettererLocked = false"
|
||||
(newItemAdded)="chapter.lettererLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- translator -->
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-12 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('translator-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator);chapter.translatorLocked = true" [settings]="getPersonsSettings(PersonRole.Translator)"
|
||||
[(locked)]="chapter.translatorLocked" (onUnlock)="chapter.translatorLocked = false"
|
||||
(newItemAdded)="chapter.translatorLocked = 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>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Cover Tab -->
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-warning" role="alert">
|
||||
{{t('cover-image-description')}}
|
||||
</p>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)"
|
||||
[showReset]="chapter.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Info Tab -->
|
||||
<li [ngbNavItem]="TabID.Info">
|
||||
<a ngbNavLink>{{t(TabID.Info)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('pages-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{t('pages-count', {num: chapter.pages | compactNumber})}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('words-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{t('words-count', {num: chapter.wordCount | compactNumber})}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('read-time-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{chapter | readTime }}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('size-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{size | bytes}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('date-added-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{chapter.createdUtc | utcToLocalTime | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('id-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{chapter.id}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@if (WebLinks.length > 0) {
|
||||
<div class="setting-section-break"></div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('links-title')" [clickable]="false" fontClasses="fa-solid fa-link" [title]="t('links-title')">
|
||||
@for(link of WebLinks; track link) {
|
||||
<a class="me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"
|
||||
[errorImage]="imageService.errorWebLinkImage"></app-image>
|
||||
</a>
|
||||
}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
@for (file of chapter.files; track file.id) {
|
||||
<div>
|
||||
<span>{{file.filePath}}</span><span class="ms-2 me-2">•</span><span>{{file.bytes | bytes}}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Progress Tab -->
|
||||
<li [ngbNavItem]="TabID.Progress">
|
||||
<a ngbNavLink>{{t(TabID.Progress)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-edit-chapter-progress [chapter]="chapter"></app-edit-chapter-progress>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Tasks Tab -->
|
||||
<li [ngbNavItem]="TabID.Tasks">
|
||||
<a ngbNavLink>{{t(TabID.Tasks)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@for(task of tasks; track task.action) {
|
||||
<div class="mt-3 mb-3">
|
||||
<app-setting-button [subtitle]="task.description">
|
||||
<button class="btn btn-{{task.action === Action.Delete ? 'danger' : 'secondary'}} btn-sm mb-2" (click)="runTask(task)">{{task.title}}</button>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<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()">{{t('close')}}</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!editForm.valid" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
</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">{{t('field-locked-alt')}}</span>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.lock-active {
|
||||
> .input-group-text {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,534 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
|
||||
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
|
||||
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
DecimalPipe,
|
||||
NgClass,
|
||||
NgTemplateOutlet,
|
||||
TitleCasePipe
|
||||
} from "@angular/common";
|
||||
import {
|
||||
NgbActiveModal,
|
||||
NgbInputDatepicker,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavOutlet
|
||||
} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {Chapter} from "../../_models/chapter";
|
||||
import {LibraryType} from "../../_models/library/library";
|
||||
import {TypeaheadSettings} from "../../typeahead/_models/typeahead-settings";
|
||||
import {Tag} from "../../_models/tag";
|
||||
import {Language} from "../../_models/metadata/language";
|
||||
import {Person, PersonRole} from "../../_models/metadata/person";
|
||||
import {Genre} from "../../_models/metadata/genre";
|
||||
import {AgeRatingDto} from "../../_models/metadata/age-rating-dto";
|
||||
import {SeriesService} from "../../_services/series.service";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {UploadService} from "../../_services/upload.service";
|
||||
import {MetadataService} from "../../_services/metadata.service";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {DownloadService} from "../../shared/_services/download.service";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component";
|
||||
import {forkJoin, Observable, of} from "rxjs";
|
||||
import {map} from "rxjs/operators";
|
||||
import {EntityTitleComponent} from "../../cards/entity-title/entity-title.component";
|
||||
import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component";
|
||||
import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component";
|
||||
import {EditChapterProgressComponent} from "../../cards/edit-chapter-progress/edit-chapter-progress.component";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {EntityInfoCardsComponent} from "../../cards/entity-info-cards/entity-info-cards.component";
|
||||
import {CompactNumberPipe} from "../../_pipes/compact-number.pipe";
|
||||
import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component";
|
||||
import {MangaFormat} from "../../_models/manga-format";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {TranslocoDatePipe} from "@jsverse/transloco-locale";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {BytesPipe} from "../../_pipes/bytes.pipe";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {ReadTimePipe} from "../../_pipes/read-time.pipe";
|
||||
import {ChapterService} from "../../_services/chapter.service";
|
||||
|
||||
enum TabID {
|
||||
General = 'general-tab',
|
||||
CoverImage = 'cover-image-tab',
|
||||
Info = 'info-tab',
|
||||
People = 'people-tab',
|
||||
Tasks = 'tasks-tab',
|
||||
Progress = 'progress-tab',
|
||||
Tags = 'tags-tab'
|
||||
}
|
||||
|
||||
export interface EditChapterModalCloseResult {
|
||||
success: boolean;
|
||||
chapter: Chapter;
|
||||
coverImageUpdate: boolean;
|
||||
needsReload: boolean;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList];
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-chapter-modal',
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavLink,
|
||||
TranslocoDirective,
|
||||
AsyncPipe,
|
||||
NgbNavOutlet,
|
||||
ReactiveFormsModule,
|
||||
NgbNavItem,
|
||||
SettingItemComponent,
|
||||
NgTemplateOutlet,
|
||||
NgClass,
|
||||
TypeaheadComponent,
|
||||
EntityTitleComponent,
|
||||
TitleCasePipe,
|
||||
SettingButtonComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditChapterProgressComponent,
|
||||
NgbInputDatepicker,
|
||||
EntityInfoCardsComponent,
|
||||
CompactNumberPipe,
|
||||
IconAndTitleComponent,
|
||||
DefaultDatePipe,
|
||||
TranslocoDatePipe,
|
||||
UtcToLocalTimePipe,
|
||||
BytesPipe,
|
||||
ImageComponent,
|
||||
SafeHtmlPipe,
|
||||
DecimalPipe,
|
||||
DatePipe,
|
||||
ReadTimePipe
|
||||
],
|
||||
templateUrl: './edit-chapter-modal.component.html',
|
||||
styleUrl: './edit-chapter-modal.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditChapterModalComponent implements OnInit {
|
||||
|
||||
protected readonly modal = inject(NgbActiveModal);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
public readonly imageService = inject(ImageService);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
private readonly metadataService = inject(MetadataService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
protected readonly accountService = inject(AccountService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly downloadService = inject(DownloadService);
|
||||
private readonly chapterService = inject(ChapterService);
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly Action = Action;
|
||||
protected readonly PersonRole = PersonRole;
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
|
||||
@Input({required: true}) chapter!: Chapter;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
@Input({required: true}) seriesId!: number;
|
||||
|
||||
activeId = TabID.General;
|
||||
editForm: FormGroup = new FormGroup({});
|
||||
selectedCover: string = '';
|
||||
coverImageReset = false;
|
||||
|
||||
tagsSettings: TypeaheadSettings<Tag> = new TypeaheadSettings();
|
||||
languageSettings: TypeaheadSettings<Language> = new TypeaheadSettings();
|
||||
peopleSettings: {[PersonRole: string]: TypeaheadSettings<Person>} = {};
|
||||
genreSettings: TypeaheadSettings<Genre> = new TypeaheadSettings();
|
||||
|
||||
tags: Tag[] = [];
|
||||
genres: Genre[] = [];
|
||||
ageRatings: Array<AgeRatingDto> = [];
|
||||
validLanguages: Array<Language> = [];
|
||||
|
||||
tasks = this.actionFactoryService.getActionablesForSettingsPage(this.actionFactoryService.getChapterActions(this.runTask.bind(this)), blackList);
|
||||
/**
|
||||
* A copy of the chapter from init. This is used to compare values for name fields to see if lock was modified
|
||||
*/
|
||||
initChapter!: Chapter;
|
||||
imageUrls: Array<string> = [];
|
||||
size: number = 0;
|
||||
|
||||
get WebLinks() {
|
||||
if (this.chapter.webLinks === '') return [];
|
||||
return this.chapter.webLinks.split(',');
|
||||
}
|
||||
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.initChapter = Object.assign({}, this.chapter);
|
||||
this.imageUrls.push(this.imageService.getChapterCoverImage(this.chapter.id));
|
||||
|
||||
this.size = this.utilityService.asChapter(this.chapter).files.reduce((sum, v) => sum + v.bytes, 0);
|
||||
|
||||
|
||||
this.editForm.addControl('titleName', new FormControl(this.chapter.titleName, []));
|
||||
this.editForm.addControl('sortOrder', new FormControl(this.chapter.sortOrder, [Validators.required, Validators.min(0)]));
|
||||
this.editForm.addControl('summary', new FormControl(this.chapter.summary, []));
|
||||
this.editForm.addControl('language', new FormControl(this.chapter.language, []));
|
||||
this.editForm.addControl('isbn', new FormControl(this.chapter.isbn, []));
|
||||
this.editForm.addControl('ageRating', new FormControl(this.chapter.ageRating, []));
|
||||
this.editForm.addControl('releaseDate', new FormControl(this.chapter.releaseDate, []));
|
||||
|
||||
|
||||
this.editForm.addControl('genres', new FormControl(this.chapter.genres, []));
|
||||
this.editForm.addControl('tags', new FormControl(this.chapter.tags, []));
|
||||
|
||||
|
||||
this.editForm.addControl('coverImageIndex', new FormControl(0, []));
|
||||
this.editForm.addControl('coverImageLocked', new FormControl(this.chapter.coverImageLocked, []));
|
||||
|
||||
this.metadataService.getAllValidLanguages().subscribe(validLanguages => {
|
||||
this.validLanguages = validLanguages;
|
||||
this.setupLanguageTypeahead();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.metadataService.getAllAgeRatings().subscribe(ratings => {
|
||||
this.ageRatings = ratings;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editForm.get('titleName')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
|
||||
this.chapter.titleNameLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editForm.get('sortOrder')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
|
||||
this.chapter.sortOrderLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editForm.get('isbn')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
|
||||
this.chapter.isbnLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editForm.get('ageRating')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
|
||||
this.chapter.ageRatingLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editForm.get('summary')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
|
||||
this.chapter.summaryLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.editForm.get('releaseDate')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
|
||||
this.chapter.releaseDateLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.setupTypeaheads();
|
||||
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.dismiss();
|
||||
}
|
||||
|
||||
save() {
|
||||
const model = this.editForm.value;
|
||||
const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0;
|
||||
|
||||
this.chapter.releaseDate = model.releaseDate;
|
||||
|
||||
|
||||
const apis = [
|
||||
this.chapterService.updateChapter(this.chapter)
|
||||
];
|
||||
|
||||
// We only need to call updateSeries if we changed name, sort name, or localized name or reset a cover image
|
||||
const needsReload = this.editForm.get('titleName')?.dirty || this.editForm.get('sortOrder')?.dirty;
|
||||
|
||||
|
||||
if (selectedIndex > 0 || this.coverImageReset) {
|
||||
apis.push(this.uploadService.updateChapterCoverImage(this.chapter.id, this.selectedCover, !this.coverImageReset));
|
||||
}
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
this.modal.close({success: true, chapter: model, coverImageUpdate: selectedIndex > 0 || this.coverImageReset, needsReload: needsReload, isDeleted: false} as EditChapterModalCloseResult);
|
||||
});
|
||||
}
|
||||
|
||||
unlock(b: any, field: string) {
|
||||
if (b) {
|
||||
b[field] = !b[field];
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
async runTask(action: ActionItem<Chapter>) {
|
||||
switch (action.action) {
|
||||
|
||||
case Action.MarkAsRead:
|
||||
this.actionService.markChapterAsRead(this.libraryId, this.seriesId, this.chapter, (p) => {
|
||||
this.chapter.pagesRead = p.pagesRead;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markChapterAsUnread(this.libraryId, this.seriesId, this.chapter, (p) => {
|
||||
this.chapter.pagesRead = 0;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
await this.actionService.deleteChapter(this.chapter.id, (b) => {
|
||||
if (!b) return;
|
||||
this.modal.close({success: b, chapter: this.chapter, coverImageUpdate: false, needsReload: true, isDeleted: b} as EditChapterModalCloseResult);
|
||||
});
|
||||
break;
|
||||
case Action.Download:
|
||||
this.downloadService.download('chapter', this.chapter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setupTypeaheads() {
|
||||
forkJoin([
|
||||
this.setupTagSettings(),
|
||||
this.setupGenreTypeahead(),
|
||||
this.setupPersonTypeahead(),
|
||||
this.setupLanguageTypeahead()
|
||||
]).subscribe(results => {
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
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.selectionCompareFn = (a: Tag, b: Tag) => {
|
||||
return a.title.toLowerCase() == b.title.toLowerCase();
|
||||
}
|
||||
this.tagsSettings.compareFnForAdd = (options: Tag[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
|
||||
if (this.chapter.tags) {
|
||||
this.tagsSettings.savedData = this.chapter.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.compareFnForAdd = (options: Genre[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
|
||||
return a.title.toLowerCase() == b.title.toLowerCase();
|
||||
}
|
||||
|
||||
this.genreSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, title: title };
|
||||
});
|
||||
|
||||
if (this.chapter.genres) {
|
||||
this.genreSettings.savedData = this.chapter.genres;
|
||||
}
|
||||
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.compareFnForAdd = (options: Language[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.title, filter));
|
||||
}
|
||||
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||
|
||||
this.languageSettings.selectionCompareFn = (a: Language, b: Language) => {
|
||||
return a.isoCode == b.isoCode;
|
||||
}
|
||||
|
||||
const l = this.validLanguages.find(l => l.isoCode === this.chapter.language);
|
||||
if (l !== undefined) {
|
||||
this.languageSettings.savedData = l;
|
||||
}
|
||||
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 presetIds = presetField.map(p => p.id);
|
||||
personSettings.savedData = people.filter(person => presetIds.includes(person.id));
|
||||
this.peopleSettings[role] = personSettings;
|
||||
this.metadataService.updatePerson(this.chapter, personSettings.savedData as Person[], role);
|
||||
this.cdRef.markForCheck();
|
||||
return true;
|
||||
}));
|
||||
} else {
|
||||
this.peopleSettings[role] = personSettings;
|
||||
return of(true);
|
||||
}
|
||||
}
|
||||
|
||||
setupPersonTypeahead() {
|
||||
this.peopleSettings = {};
|
||||
|
||||
return forkJoin([
|
||||
this.updateFromPreset('writer', this.chapter.writers, PersonRole.Writer),
|
||||
this.updateFromPreset('character', this.chapter.characters, PersonRole.Character),
|
||||
this.updateFromPreset('colorist', this.chapter.colorists, PersonRole.Colorist),
|
||||
this.updateFromPreset('cover-artist', this.chapter.coverArtists, PersonRole.CoverArtist),
|
||||
this.updateFromPreset('editor', this.chapter.editors, PersonRole.Editor),
|
||||
this.updateFromPreset('inker', this.chapter.inkers, PersonRole.Inker),
|
||||
this.updateFromPreset('letterer', this.chapter.letterers, PersonRole.Letterer),
|
||||
this.updateFromPreset('penciller', this.chapter.pencillers, PersonRole.Penciller),
|
||||
this.updateFromPreset('publisher', this.chapter.publishers, PersonRole.Publisher),
|
||||
this.updateFromPreset('imprint', this.chapter.imprints, PersonRole.Imprint),
|
||||
this.updateFromPreset('translator', this.chapter.translators, PersonRole.Translator),
|
||||
this.updateFromPreset('teams', this.chapter.teams, PersonRole.Team),
|
||||
this.updateFromPreset('locations', this.chapter.locations, PersonRole.Location),
|
||||
]).pipe(map(results => {
|
||||
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.compareFnForAdd = (options: Person[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filterMatches(m.name, filter));
|
||||
}
|
||||
|
||||
personSettings.selectionCompareFn = (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;
|
||||
}
|
||||
|
||||
updateTags(tags: Tag[]) {
|
||||
this.tags = tags;
|
||||
this.chapter.tags = tags;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateGenres(genres: Genre[]) {
|
||||
this.genres = genres;
|
||||
this.chapter.genres = genres;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updatePerson(persons: Person[], role: PersonRole) {
|
||||
this.metadataService.updatePerson(this.chapter, persons, role);
|
||||
this.chapter.locationLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateLanguage(language: Array<Language>) {
|
||||
if (language.length === 0) {
|
||||
this.chapter.language = '';
|
||||
return;
|
||||
}
|
||||
this.chapter.language = language[0].isoCode;
|
||||
this.chapter.languageLocked = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.editForm.patchValue({
|
||||
coverImageIndex: index
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageReset = true;
|
||||
this.editForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
getPersonsSettings(role: PersonRole) {
|
||||
return this.peopleSettings[role];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<ng-container *transloco="let t; read: 'edit-volume-modal'">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{t('title')}} <app-entity-title [libraryType]="libraryType" [entity]="volume" [prioritizeTitleName]="false"></app-entity-title></h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal" [ngClass]="{'d-flex': utilityService.getActiveBreakpoint() !== Breakpoint.Mobile}">
|
||||
<form [formGroup]="editForm">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeId" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
|
||||
<!-- Cover Tab -->
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-warning" role="alert">
|
||||
{{t('cover-image-description')}}
|
||||
</p>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)"
|
||||
[showReset]="volume.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Info Tab -->
|
||||
<li [ngbNavItem]="TabID.Info">
|
||||
<a ngbNavLink>{{t(TabID.Info)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('pages-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{t('pages-count', {num: volume.pages | compactNumber})}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="col-lg-6 col-md-12 pe-2">-->
|
||||
<!-- <div class="mb-3">-->
|
||||
<!-- <app-setting-item [title]="t('words-label')" [toggleOnViewClick]="false" [showEdit]="false">-->
|
||||
<!-- <ng-template #view>-->
|
||||
<!-- {{t('words-count', {num: volume.wordCount | compactNumber})}}-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-setting-item>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('read-time-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{volume | readTime }}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('size-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{size | bytes}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('date-added-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{volume.createdUtc | utcToLocalTime | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<app-setting-item [title]="t('id-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
{{volume.id}}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
@for (file of files; track file.id) {
|
||||
<div>
|
||||
<span>{{file.filePath}}</span><span class="ms-2 me-2">•</span><span>{{file.bytes | bytes}}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Progress Tab -->
|
||||
<li [ngbNavItem]="TabID.Progress">
|
||||
<a ngbNavLink>{{t(TabID.Progress)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@for(chapter of volume.chapters; track chapter.id) {
|
||||
<h6><app-entity-title [entity]="chapter" [prioritizeTitleName]="false"></app-entity-title></h6>
|
||||
<app-edit-chapter-progress [chapter]="chapter"></app-edit-chapter-progress>
|
||||
<div class="setting-section-break"></div>
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<!-- Tasks Tab -->
|
||||
<li [ngbNavItem]="TabID.Tasks">
|
||||
<a ngbNavLink>{{t(TabID.Tasks)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@for(task of tasks; track task.action) {
|
||||
<div class="mt-3 mb-3">
|
||||
<app-setting-button [subtitle]="task.description">
|
||||
<button class="btn btn-{{task.action === Action.Delete ? 'danger' : 'secondary'}} btn-sm mb-2" (click)="runTask(task)">{{task.title}}</button>
|
||||
</app-setting-button>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<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()">{{t('close')}}</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!editForm.valid" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
|
||||
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {
|
||||
NgbActiveModal,
|
||||
NgbInputDatepicker,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavOutlet
|
||||
} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {AsyncPipe, DatePipe, DecimalPipe, NgClass, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component";
|
||||
import {EntityTitleComponent} from "../../cards/entity-title/entity-title.component";
|
||||
import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component";
|
||||
import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component";
|
||||
import {EditChapterProgressComponent} from "../../cards/edit-chapter-progress/edit-chapter-progress.component";
|
||||
import {EntityInfoCardsComponent} from "../../cards/entity-info-cards/entity-info-cards.component";
|
||||
import {CompactNumberPipe} from "../../_pipes/compact-number.pipe";
|
||||
import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {TranslocoDatePipe} from "@jsverse/transloco-locale";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {BytesPipe} from "../../_pipes/bytes.pipe";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {ReadTimePipe} from "../../_pipes/read-time.pipe";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
|
||||
import {Volume} from "../../_models/volume";
|
||||
import {SeriesService} from "../../_services/series.service";
|
||||
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {UploadService} from "../../_services/upload.service";
|
||||
import {MetadataService} from "../../_services/metadata.service";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {DownloadService} from "../../shared/_services/download.service";
|
||||
import {Chapter} from "../../_models/chapter";
|
||||
import {LibraryType} from "../../_models/library/library";
|
||||
import {TypeaheadSettings} from "../../typeahead/_models/typeahead-settings";
|
||||
import {Tag} from "../../_models/tag";
|
||||
import {Language} from "../../_models/metadata/language";
|
||||
import {Person, PersonRole} from "../../_models/metadata/person";
|
||||
import {Genre} from "../../_models/metadata/genre";
|
||||
import {AgeRatingDto} from "../../_models/metadata/age-rating-dto";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {forkJoin, Observable, of} from "rxjs";
|
||||
import {map} from "rxjs/operators";
|
||||
import {EditChapterModalCloseResult} from "../edit-chapter-modal/edit-chapter-modal.component";
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import {MangaFile} from "../../_models/manga-file";
|
||||
import {VolumeService} from "../../_services/volume.service";
|
||||
|
||||
enum TabID {
|
||||
General = 'general-tab',
|
||||
CoverImage = 'cover-image-tab',
|
||||
Info = 'info-tab',
|
||||
Tasks = 'tasks-tab',
|
||||
Progress = 'progress-tab',
|
||||
}
|
||||
|
||||
export interface EditVolumeModalCloseResult {
|
||||
success: boolean;
|
||||
volume: Volume;
|
||||
coverImageUpdate: boolean;
|
||||
needsReload: boolean;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList];
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-volume-modal',
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavLink,
|
||||
TranslocoDirective,
|
||||
AsyncPipe,
|
||||
NgbNavOutlet,
|
||||
ReactiveFormsModule,
|
||||
NgbNavItem,
|
||||
SettingItemComponent,
|
||||
NgTemplateOutlet,
|
||||
NgClass,
|
||||
TypeaheadComponent,
|
||||
EntityTitleComponent,
|
||||
TitleCasePipe,
|
||||
SettingButtonComponent,
|
||||
CoverImageChooserComponent,
|
||||
EditChapterProgressComponent,
|
||||
NgbInputDatepicker,
|
||||
EntityInfoCardsComponent,
|
||||
CompactNumberPipe,
|
||||
IconAndTitleComponent,
|
||||
DefaultDatePipe,
|
||||
TranslocoDatePipe,
|
||||
UtcToLocalTimePipe,
|
||||
BytesPipe,
|
||||
ImageComponent,
|
||||
SafeHtmlPipe,
|
||||
DecimalPipe,
|
||||
DatePipe,
|
||||
ReadTimePipe
|
||||
],
|
||||
templateUrl: './edit-volume-modal.component.html',
|
||||
styleUrl: './edit-volume-modal.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EditVolumeModalComponent implements OnInit {
|
||||
public readonly modal = inject(NgbActiveModal);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
public readonly imageService = inject(ImageService);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
public readonly accountService = inject(AccountService);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly downloadService = inject(DownloadService);
|
||||
private readonly volumeService = inject(VolumeService);
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly Action = Action;
|
||||
protected readonly PersonRole = PersonRole;
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
|
||||
@Input({required: true}) volume!: Volume;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
@Input({required: true}) seriesId!: number;
|
||||
|
||||
activeId = TabID.CoverImage;
|
||||
editForm: FormGroup = new FormGroup({});
|
||||
selectedCover: string = '';
|
||||
coverImageReset = false;
|
||||
|
||||
|
||||
tasks = this.actionFactoryService.getActionablesForSettingsPage(this.actionFactoryService.getVolumeActions(this.runTask.bind(this)), blackList);
|
||||
/**
|
||||
* A copy of the chapter from init. This is used to compare values for name fields to see if lock was modified
|
||||
*/
|
||||
initVolume!: Volume;
|
||||
imageUrls: Array<string> = [];
|
||||
size: number = 0;
|
||||
files: Array<MangaFile> = [];
|
||||
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.initVolume = Object.assign({}, this.volume);
|
||||
this.imageUrls.push(this.imageService.getVolumeCoverImage(this.volume.id));
|
||||
|
||||
this.files = this.volume.chapters.flatMap(c => c.files);
|
||||
this.size = this.files.reduce((sum, v) => sum + v.bytes, 0);
|
||||
|
||||
this.editForm.addControl('coverImageIndex', new FormControl(0, []));
|
||||
this.editForm.addControl('coverImageLocked', new FormControl(this.volume.coverImageLocked, []));
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.dismiss();
|
||||
}
|
||||
|
||||
save() {
|
||||
const model = this.editForm.value;
|
||||
const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0;
|
||||
|
||||
const apis = [];
|
||||
|
||||
|
||||
if (selectedIndex > 0 || this.coverImageReset) {
|
||||
apis.push(this.uploadService.updateVolumeCoverImage(model.id, this.selectedCover, !this.coverImageReset));
|
||||
}
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
this.modal.close({success: true, volume: model, coverImageUpdate: selectedIndex > 0 || this.coverImageReset, needsReload: false, isDeleted: false} as EditVolumeModalCloseResult);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async runTask(action: ActionItem<Volume>) {
|
||||
switch (action.action) {
|
||||
case Action.MarkAsRead:
|
||||
this.actionService.markVolumeAsRead(this.seriesId, this.volume, (p) => {
|
||||
this.volume.pagesRead = p.pagesRead;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markVolumeAsUnread(this.seriesId, this.volume, (p) => {
|
||||
this.volume.pagesRead = 0;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
await this.actionService.deleteVolume(this.volume.id, (b) => {
|
||||
if (!b) return;
|
||||
this.modal.close({success: b, volume: this.volume, coverImageUpdate: false, needsReload: true, isDeleted: b} as EditVolumeModalCloseResult);
|
||||
});
|
||||
break;
|
||||
case Action.Download:
|
||||
this.downloadService.download('volume', this.volume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.editForm.patchValue({
|
||||
coverImageIndex: index
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageReset = true;
|
||||
this.editForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,31 @@
|
|||
<ng-container *transloco="let t; read:'review-card'">
|
||||
<div class="card review-card clickable mb-3" (click)="showModal()">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
<i class="img-fluid rounded-start fa-solid fa-circle-user profile-image" aria-hidden="true"></i>
|
||||
<div class="col-md-2 d-none d-md-block p-2">
|
||||
@if (isMyReview) {
|
||||
<div class="my-review">
|
||||
<i class="fa-solid fa-star" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
<span class="visually-hidden">{{t('your-review')}}</span>
|
||||
</div>
|
||||
<i class="d-md-none fa-solid fa-star me-2" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
<img class="me-2" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="40" height="40" alt="">
|
||||
} @else {
|
||||
<img class="me-2" [ngSrc]="review.provider | providerImage" width="40" height="40" alt="">
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="card-body">
|
||||
<div class="card-body p-2">
|
||||
<!--
|
||||
<h6 class="card-title">
|
||||
{{review.isExternal ? t('external-review') : t('local-review')}}
|
||||
</h6>
|
||||
</h6>-->
|
||||
<p class="card-text no-images">
|
||||
<app-read-more [text]="(review.isExternal ? review.bodyJustText : review.body) || ''" [maxLength]="100" [showToggle]="false"></app-read-more>
|
||||
<app-read-more [text]="(review.isExternal ? review.bodyJustText : review.body) || ''" [maxLength]="150" [showToggle]="false"></app-read-more>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent text-muted">
|
||||
<div class="card-footer bg-transparent text-muted p-2">
|
||||
<div>
|
||||
@if (isMyReview) {
|
||||
<i class="d-md-none fa-solid fa-star me-2" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
<img class="me-2" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
||||
{{review.username}}
|
||||
} @else {
|
||||
<img class="me-2" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||
}
|
||||
|
||||
{{(isMyReview ? '' : review.username | defaultValue:'')}}
|
||||
</div>
|
||||
@if (review.isExternal){
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.review-card {
|
||||
max-width: 320px;
|
||||
max-height: 160px;
|
||||
height: 160px;
|
||||
max-height: 130px;
|
||||
height: 130px;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
|
|
@ -33,8 +33,6 @@
|
|||
}
|
||||
|
||||
.card-text.no-images {
|
||||
min-height: 63px;
|
||||
max-height: 63px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -49,10 +47,16 @@
|
|||
max-width: 319px;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
padding: .5rem 0;
|
||||
|
||||
& > * {
|
||||
margin: 0 5px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
min-height: 93.5px;
|
||||
max-height: 93.5px;
|
||||
}
|
||||
|
|
@ -51,9 +51,9 @@
|
|||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="externalSeries.staff" [libraryId]="0" [heading]="t('series-preview-drawer.staff-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<div class="card mb-3" style="max-width: 180px;">
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<ng-container *ngIf="item.imageUrl && !item.imageUrl.endsWith('default.jpg'); else localPerson">
|
||||
<app-image height="24px" width="24px" [styles]="{'object-fit': 'contain'}" [imageUrl]="item.imageUrl" classes="person-img"></app-image>
|
||||
</ng-container>
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
<i class="fa fa-user-circle align-self-center person-img" style="font-size: 28px;" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-9">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{item.name}}</h6>
|
||||
<p class="card-text" style="font-size: 14px"><small class="text-muted">{{item.role}}</small></p>
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
<div class="mt-3">
|
||||
<app-metadata-detail [tags]="localStaff" [libraryId]="0" [heading]="t('series-preview-drawer.staff-label')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<div class="card mb-3" style="max-width: 180px;">
|
||||
<div class="card mb-3">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<i class="fa fa-user-circle align-self-center" style="font-size: 28px; margin-top: 24px; margin-left: 24px" aria-hidden="true"></i>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue