Localization - First Pass (#2174)
* Started designing the backend localization service * Worked in Transloco for initial PoC * Worked in Transloco for initial PoC * Translated the login screen * translated dashboard screen * Started work on the backend * Fixed a logic bug * translated edit-user screen * Hooked up the backend for having a locale property. * Hooked up the ability to view the available locales and switch to them. * Made the localization service languages be derived from what's in langs/ directory. * Fixed up localization switching * Switched when we check for a license on UI bootstrap * Tweaked some code * Fixed the bug where dashboard wasn't loading and made it so language switching is working. * Fixed a bug on dashboard with languagePath * Converted user-scrobble-history.component.html * Converted spoiler.component.html * Converted review-series-modal.component.html * Converted review-card-modal.component.html * Updated the readme * Translated using Weblate (English) Currently translated at 100.0% (54 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/ * Converted review-card.component.html * Deleted dead component * Converted want-to-read.component.html * Added translation using Weblate (Korean) * Translated using Weblate (Spanish) Currently translated at 40.7% (22 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/ * Translated using Weblate (Korean) Currently translated at 62.9% (34 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-preferences.component.html * Translated using Weblate (Korean) Currently translated at 92.5% (50 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-holds.component.html * Converted theme-manager.component.html * Converted restriction-selector.component.html * Converted manage-devices.component.html * Converted edit-device.component.html * Converted change-password.component.html * Converted change-email.component.html * Converted change-age-restriction.component.html * Converted api-key.component.html * Converted anilist-key.component.html * Converted typeahead.component.html * Converted user-stats-info-cards.component.html * Converted user-stats.component.html * Converted top-readers.component.html * Converted some pipes and ensure translation is loaded before the app. * Finished all but one pipe for localization * Converted directory-picker.component.html * Converted library-access-modal.component.html * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Merged weblate in * ... -> … update * Updated the readme * Updateded all fonts to be woff2 * Cleaned up some strings to increase re-use * Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita. * Converted Series detail * Lots more converted * Lots more converted & hooked up the ability to flatten during prod build the language files. * Lots more converted * Lots more converted & fixed a bunch of broken pipes due to inject() * Lots more converted * Lots more converted * Lots more converted & fixed some bad keys * Lots more converted * Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection * Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission * Fixed a stupid build issue again * Started adding errors for interceptor and backend. * Finished off manga-reader * More translations * Few fixes * Fixed a bug where character tag badges weren't showing the name on chapter info * All components are translated * All toasts are translated * All confirm/alerts are translated * Trying something new for the backend * Migrated the localization strings for the backend into a new file. * Updated the localization service to be able to do backend localization with fallback to english. * Cleaned up some external reviews code to reduce looping * Localized AccountController.cs * 60% done with controllers * All controllers are done * All KavitaExceptions are covered * Some shakeout fixes * Prep for initial merge * Everything is done except options and basic shakeout proves response times are good. Unit tests are broken. * Fixed up the unit tests * All unit tests are now working * Removed some quantifier * I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: majora2007 <kavitareader@gmail.com> Co-authored-by: expertjun <jtrobin@naver.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
parent
670bf82c38
commit
3b23d63234
389 changed files with 13652 additions and 7925 deletions
|
|
@ -1,44 +1,45 @@
|
|||
<ng-container *transloco="let t; read: 'bulk-add-to-collection'">
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Add to Collection</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<form style="width: 100%" [formGroup]="listForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3" *ngIf="lists.length >= 5">
|
||||
<label for="filter" class="form-label">Filter</label>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<form style="width: 100%" [formGroup]="listForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3" *ngIf="lists.length >= 5">
|
||||
<label for="filter" class="form-label">{{t('filter-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item clickable" tabindex="0" role="option" *ngFor="let collectionTag of lists | filter: filterList; let i = index; trackBy: collectionTitleTrackby" (click)="addToCollection(collectionTag)">
|
||||
{{collectionTag.title}} <i class="fa fa-angle-double-up" *ngIf="collectionTag.promoted" title="Promoted"></i>
|
||||
{{collectionTag.title}} <i class="fa fa-angle-double-up" *ngIf="collectionTag.promoted" [title]="t('promoted')"></i>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
|
||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">{{t('no-data')}}</li>
|
||||
<li class="list-group-item" *ngIf="loading">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content: normal">
|
||||
<div style="width: 100%;">
|
||||
<div class="d-flex">
|
||||
<div class="col-9 col-lg-10">
|
||||
<label class="form-label visually-hidden" for="add-rlist">Collection</label>
|
||||
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button type="submit" class="btn btn-primary" (click)="create()">Create</button>
|
||||
</div>
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-footer" style="justify-content: normal">
|
||||
<div style="width: 100%;">
|
||||
<div class="d-flex">
|
||||
<div class="col-9 col-lg-10">
|
||||
<label class="form-label visually-hidden" for="add-rlist">{{t('collection-label')}}</label>
|
||||
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button type="submit" class="btn btn-primary" (click)="create()">{{t('create')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';
|
||||
import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
|
@ -7,11 +18,12 @@ import { ReadingList } from 'src/app/_models/reading-list';
|
|||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {FilterPipe} from "../../../pipe/filter.pipe";
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-add-to-collection',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, FilterPipe, NgbModalModule],
|
||||
imports: [CommonModule, ReactiveFormsModule, FilterPipe, NgbModalModule, TranslocoModule],
|
||||
templateUrl: './bulk-add-to-collection.component.html',
|
||||
styleUrls: ['./bulk-add-to-collection.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None, // This is needed as per the bootstrap modal documentation to get styles to work.
|
||||
|
|
@ -34,6 +46,8 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
|||
|
||||
collectionTitleTrackby = (index: number, item: CollectionTag) => `${item.title}`;
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||
|
||||
|
||||
|
|
@ -69,7 +83,7 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
|||
create() {
|
||||
const tagName = this.listForm.value.title;
|
||||
this.collectionService.addByMultiple(0, this.seriesIds, tagName).subscribe(() => {
|
||||
this.toastr.success('Series added to ' + tagName + ' collection');
|
||||
this.toastr.success(this.translocoService.translate('toasts.series-added-to-collection', {collectionName: tagName}));
|
||||
this.modal.close();
|
||||
});
|
||||
}
|
||||
|
|
@ -78,7 +92,7 @@ export class BulkAddToCollectionComponent implements OnInit, AfterViewInit {
|
|||
if (this.seriesIds.length === 0) return;
|
||||
|
||||
this.collectionService.addByMultiple(tag.id, this.seriesIds, '').subscribe(() => {
|
||||
this.toastr.success('Series added to ' + tag.title + ' collection');
|
||||
this.toastr.success(this.translocoService.translate('toasts.series-added-to-collection', {collectionName: tag.title}));
|
||||
this.modal.close();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,95 +1,98 @@
|
|||
<ng-container *transloco="let t; read: 'edit-collection-tags'">
|
||||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Edit {{tag.title}} Collection</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{t('title', {collectionName: tag.title})}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills"
|
||||
orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="TabID.General">
|
||||
<a ngbNavLink>{{TabID.General}}</a>
|
||||
orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
|
||||
<li [ngbNavItem]="TabID.General">
|
||||
<a ngbNavLink>{{t(TabID.General)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<form [formGroup]="collectionTagForm">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<label for="library-name" class="form-label">Name</label>
|
||||
<input id="library-name" class="form-control" formControlName="title" type="text"
|
||||
[class.is-invalid]="collectionTagForm.get('title')?.invalid && collectionTagForm.get('title')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="collectionTagForm.dirty || collectionTagForm.touched">
|
||||
<div *ngIf="collectionTagForm.get('title')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="collectionTagForm.get('title')?.errors?.duplicateName">
|
||||
Name must be unique
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 ms-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="tag-promoted" role="switch" formControlName="promoted" class="form-check-input"
|
||||
aria-labelledby="auto-close-label" aria-describedby="tag-promoted-help">
|
||||
<label class="form-check-label me-1" for="tag-promoted">Promote</label>
|
||||
<i class="fa fa-info-circle" aria-hidden="true" placement="left" [ngbTooltip]="promotedTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #promotedTooltip>Promotion means that the tag can be seen server-wide, not just for admin users. All series that have this tag will still have user-access restrictions placed on them.</ng-template>
|
||||
<span class="visually-hidden" id="tag-promoted-help"><ng-container [ngTemplateOutlet]="promotedTooltip"></ng-container></span>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="collectionTagForm">
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<label for="library-name" class="form-label">{{t('name-label')}}</label>
|
||||
<input id="library-name" class="form-control" formControlName="title" type="text"
|
||||
[class.is-invalid]="collectionTagForm.get('title')?.invalid && collectionTagForm.get('title')?.touched">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="collectionTagForm.dirty || collectionTagForm.touched">
|
||||
<div *ngIf="collectionTagForm.get('title')?.errors?.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
<div *ngIf="collectionTagForm.get('title')?.errors?.duplicateName">
|
||||
{{t('name-validation')}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-3">
|
||||
<label for="summary" class="form-label">Summary</label>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12 ms-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="tag-promoted" role="switch" formControlName="promoted" class="form-check-input"
|
||||
aria-labelledby="auto-close-label" aria-describedby="tag-promoted-help">
|
||||
<label class="form-check-label me-1" for="tag-promoted">{{t('promote-label')}}</label>
|
||||
<i class="fa fa-info-circle" aria-hidden="true" placement="left" [ngbTooltip]="promotedTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #promotedTooltip>{{t('promote-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="tag-promoted-help"><ng-container [ngTemplateOutlet]="promotedTooltip"></ng-container></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<div class="row g-0 mb-3">
|
||||
<label for="summary" class="form-label">{{t('summary-label')}}</label>
|
||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</ng-template>
|
||||
</li>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.Series">
|
||||
<a ngbNavLink>{{TabID.Series}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<h6>Applies to Series</h6>
|
||||
<div class="form-check">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let item of series; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="series-{{i}}" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selections.isSelected(item)" (change)="handleSelection(item)">
|
||||
<label attr.for="series-{{i}}" class="form-check-label">{{item.name}} ({{libraryName(item.libraryId)}})</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex justify-content-center" *ngIf="pagination && series.length !== 0">
|
||||
<ngb-pagination
|
||||
*ngIf="pagination.totalPages > 1"
|
||||
[(page)]="pagination.currentPage"
|
||||
[pageSize]="pagination.itemsPerPage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
[rotate]="false" [ellipses]="false" [boundaryLinks]="true"
|
||||
[collectionSize]="pagination.totalItems"></ngb-pagination>
|
||||
</div>
|
||||
<li [ngbNavItem]="TabID.Series">
|
||||
<a ngbNavLink>{{t(TabID.Series)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<h6>{{t('series-title')}}</h6>
|
||||
<div class="form-check">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? t('deselect-all') : t('select-all')}}</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let item of series; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="series-{{i}}" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selections.isSelected(item)" (change)="handleSelection(item)">
|
||||
<label attr.for="series-{{i}}" class="form-check-label">{{item.name}} ({{libraryName(item.libraryId)}})</label>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex justify-content-center" *ngIf="pagination && series.length !== 0">
|
||||
<ngb-pagination
|
||||
*ngIf="pagination.totalPages > 1"
|
||||
[(page)]="pagination.currentPage"
|
||||
[pageSize]="pagination.itemsPerPage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
[rotate]="false" [ellipses]="false" [boundaryLinks]="true"
|
||||
[collectionSize]="pagination.totalItems"></ngb-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{TabID.CoverImage}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)"
|
||||
(selectedBase64Url)="updateSelectedImage($event)" [showReset]="tag.coverImageLocked"
|
||||
(resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)"
|
||||
(selectedBase64Url)="updateSelectedImage($event)" [showReset]="tag.coverImageLocked"
|
||||
(resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="collectionTagForm.invalid" (click)="save()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('cancel')}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="collectionTagForm.invalid" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -34,18 +34,19 @@ import { UploadService } from 'src/app/_services/upload.service';
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {CoverImageChooserComponent} from "../../cover-image-chooser/cover-image-chooser.component";
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
|
||||
enum TabID {
|
||||
General = 'General',
|
||||
CoverImage = 'Cover Image',
|
||||
Series = 'Series'
|
||||
General = 'general-tab',
|
||||
CoverImage = 'cover-image-tab',
|
||||
Series = 'series-tab'
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-collection-tags',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ReactiveFormsModule, FormsModule, NgbPagination, CoverImageChooserComponent, NgbNavOutlet, NgbTooltip],
|
||||
imports: [CommonModule, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ReactiveFormsModule, FormsModule, NgbPagination, CoverImageChooserComponent, NgbNavOutlet, NgbTooltip, TranslocoModule],
|
||||
templateUrl: './edit-collection-tags.component.html',
|
||||
styleUrls: ['./edit-collection-tags.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -65,6 +66,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
imageUrls: Array<string> = [];
|
||||
selectedCover: string = '';
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
get hasSomeSelected() {
|
||||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
|
|
@ -80,7 +82,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||
private confirmService: ConfirmService, private libraryService: LibraryService,
|
||||
private imageService: ImageService, private uploadService: UploadService,
|
||||
public utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
|
|
@ -170,7 +172,8 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
const tag = this.collectionTagForm.value;
|
||||
tag.id = this.tag.id;
|
||||
|
||||
if (unselectedIds.length == this.series.length && !await this.confirmSerivce.confirm('Warning! No series are selected, saving will delete the tag. Are you sure you want to continue?')) {
|
||||
if (unselectedIds.length == this.series.length &&
|
||||
!await this.confirmService.confirm(this.translocoService.translate('toasts.no-series-collection-warning'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +188,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
|
||||
forkJoin(apis).subscribe(() => {
|
||||
this.modal.close({success: true, coverImageUpdated: selectedIndex > 0});
|
||||
this.toastr.success('Tag updated');
|
||||
this.toastr.success(this.translocoService.translate('toasts.collection-updated'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,492 +1,493 @@
|
|||
<div class="modal-container" *ngIf="series !== undefined">
|
||||
<ng-container *transloco="let t; read: 'edit-series-modal'">
|
||||
<div class="modal-container" *ngIf="series !== undefined">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
{{this.series.name}} Details</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
{{t('title', {seriesName: this.series.name})}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></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;">
|
||||
<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[TabID.General]">
|
||||
<a ngbNavLink>{{tabs[TabID.General]}}</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">
|
||||
<input id="name" class="form-control" formControlName="name" type="text" readonly
|
||||
[class.is-invalid]="editSeriesForm.get('name')?.invalid && editSeriesForm.get('name')?.touched">
|
||||
<ng-container *ngIf="editSeriesForm.get('name')?.errors as errors">
|
||||
<div class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li [ngbNavItem]="tabs[TabID.General]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.General])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="name" class="form-label">{{t('name-label')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="name" class="form-control" formControlName="name" type="text" readonly
|
||||
[class.is-invalid]="editSeriesForm.get('name')?.invalid && editSeriesForm.get('name')?.touched">
|
||||
<ng-container *ngIf="editSeriesForm.get('name')?.errors as errors">
|
||||
<div class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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' : ''}}"
|
||||
[class.is-invalid]="editSeriesForm.get('sortName')?.invalid && editSeriesForm.get('sortName')?.touched">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'sortNameLocked' }"></ng-container>
|
||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
|
||||
<ng-container *ngIf="editSeriesForm.get('sortName')?.errors as errors">
|
||||
<div class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="sort-name" class="form-label">{{t('sort-name-label')}}</label>
|
||||
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}"
|
||||
[class.is-invalid]="editSeriesForm.get('sortName')?.invalid && editSeriesForm.get('sortName')?.touched">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'sortNameLocked' }"></ng-container>
|
||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
|
||||
<ng-container *ngIf="editSeriesForm.get('sortName')?.errors as errors">
|
||||
<div class="invalid-feedback" *ngIf="errors.required">
|
||||
{{t('required-field')}}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="row g-0">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="localized-name" class="form-label">{{t('localized-name-label')}}</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="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 class="row g-0" *ngIf="metadata">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="summary" class="form-label">{{t('summary-label')}}</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>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Metadata]" *ngIf="metadata">
|
||||
<a ngbNavLink>{{tabs[TabID.Metadata]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<li [ngbNavItem]="tabs[TabID.Metadata]" *ngIf="metadata">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Metadata])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-8 col-md-12 pe-2">
|
||||
<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 class="col-lg-4 col-md-12">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="release-year" class="form-label">Release Year</label>
|
||||
<div class="input-group {{metadata.releaseYearLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'releaseYearLocked' }"></ng-container>
|
||||
<input type="number" inputmode="numeric" class="form-control" id="release-year" formControlName="releaseYear" maxlength="4" minlength="4" [class.is-invalid]="editSeriesForm.get('releaseYear')?.invalid && editSeriesForm.get('releaseYear')?.touched">
|
||||
<ng-container *ngIf="editSeriesForm.get('releaseYear')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.pattern">
|
||||
This must be a valid year greater than 1000 and 4 characters long
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</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-lg-4 col-md-12 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-lg-4 col-md-12 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-lg-4 col-md-12">
|
||||
<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[TabID.People]">
|
||||
<a ngbNavLink>{{tabs[TabID.People]}}</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[TabID.WebLinks]" *ngIf="metadata">
|
||||
<a ngbNavLink>{{tabs[TabID.WebLinks]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>Here you can add many different links to external services.</p>
|
||||
<div class="row g-0 mb-3" *ngFor="let link of WebLinks; let i = index;">
|
||||
<div class="col-lg-8 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="web-link--{{i}}" class="visually-hidden">Web Link</label>
|
||||
<input type="text" class="form-control" formControlName="link{{i}}" attr.id="web-link--{{i}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary me-1" (click)="addWebLink()">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Add Link</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" (click)="removeWebLink(i)">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Remove Link</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.CoverImage]">
|
||||
<a ngbNavLink>{{tabs[TabID.CoverImage]}}</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.
|
||||
</p>
|
||||
<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[TabID.Related]">
|
||||
<a ngbNavLink>{{tabs[TabID.Related]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-edit-series-relation [series]="series" [save]="saveNestedComponents"></app-edit-series-relation>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="tabs[TabID.Info]">
|
||||
<a ngbNavLink>{{tabs[TabID.Info]}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4>Information</h4>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
|
||||
<div class="col-md-6">Format: <app-tag-badge>{{series.format | mangaFormat}}</app-tag-badge></div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6">Created: {{series.created | date:'shortDate'}}</div>
|
||||
<div class="col-md-6">Last Read: {{series.latestReadDate | defaultDate | timeAgo}}</div>
|
||||
<div class="col-md-6">Last Added To: {{series.lastChapterAdded | defaultDate | timeAgo}}</div>
|
||||
<div class="col-md-6">Last Scanned: {{series.lastFolderScanned | defaultDate | timeAgo}}</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-8 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="collections" class="form-label">{{t('collections-label')}}</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 class="col-lg-4 col-md-12">
|
||||
<div class="mb-3" style="width: 100%">
|
||||
<label for="release-year" class="form-label">{{t('release-year-label')}}</label>
|
||||
<div class="input-group {{metadata.releaseYearLocked ? 'lock-active' : ''}}">
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'releaseYearLocked' }"></ng-container>
|
||||
<input type="number" inputmode="numeric" class="form-control" id="release-year" formControlName="releaseYear" maxlength="4" minlength="4" [class.is-invalid]="editSeriesForm.get('releaseYear')?.invalid && editSeriesForm.get('releaseYear')?.touched">
|
||||
<ng-container *ngIf="editSeriesForm.get('releaseYear')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.pattern">
|
||||
This must be a valid year greater than 1000 and 4 characters long
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-auto">Folder Path: {{series.folderPath | defaultValue}}</div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2" *ngIf="metadata">
|
||||
<div class="col-md-6">
|
||||
Max Items: {{metadata.maxCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Highest Count found across all ComicInfo in the Series" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
Total Items: {{metadata.totalCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" ngbTooltip="Max Issue or Volume field from all ComicInfo in the series" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6">Publication Status: {{metadata.publicationStatus | publicationStatus}}</div>
|
||||
<div class="col-md-6">Total Pages: {{series.pages}}</div>
|
||||
<div class="col-md-6">Size: {{size | bytes}}</div>
|
||||
</div>
|
||||
<h4>Volumes</h4>
|
||||
<div class="spinner-border text-secondary" role="status" *ngIf="isLoadingVolumes">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
<ul class="list-unstyled" *ngIf="!isLoadingVolumes">
|
||||
<li class="d-flex my-4" *ngFor="let volume of seriesVolumes">
|
||||
<app-image class="me-3" style="width: 74px;" width="74px" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">Volume {{volume.name}}</h5>
|
||||
<div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Added: {{volume.created | date: 'short'}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Last Modified: {{volume.lastModified | date: 'short'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()"
|
||||
[attr.aria-expanded]="!volumeCollapsed[volume.name]">
|
||||
View Files
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{volume.pages}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="genres" class="form-label">{{t('genres-label')}}</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 #collapse="ngbCollapse" [(ngbCollapse)]="volumeCollapsed[volume.name]">
|
||||
<ul class="list-group mt-2">
|
||||
<li *ngFor="let file of volume.volumeFiles.sort()" class="list-group-item">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Chapter: {{file.chapter}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{file.pages}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Format: <span class="badge badge-secondary">{{utilityService.mangaFormatToText(file.format)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="tags" class="form-label">{{t('tags-label')}}</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-lg-4 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="language" class="form-label">{{t('language-label')}}</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-lg-4 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="age-rating" class="form-label">{{t('age-rating-label')}}</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-lg-4 col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="publication-status" class="form-label">{{t('publication-status-label')}}</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[TabID.People]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.People])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="writer" class="form-label">{{t('writer-label')}}</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">{{t('cover-artist-label')}}</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">{{t('publisher-label')}}</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">{{t('penciller-label')}}</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">{{t('letterer-label')}}</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">{{t('inker-label')}}</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">{{t('editor-label')}}</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">{{t('colorist-label')}}</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">{{t('character-label')}}</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">{{t('translator-label')}}</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[TabID.WebLinks]" *ngIf="metadata">
|
||||
<a ngbNavLink>{{t(tabs[TabID.WebLinks])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>{{t('web-link-description')}}</p>
|
||||
<div class="row g-0 mb-3" *ngFor="let link of WebLinks; let i = index;">
|
||||
<div class="col-lg-8 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="web-link--{{i}}" class="visually-hidden">{{t('web-link-label')}}</label>
|
||||
<input type="text" class="form-control" formControlName="link{{i}}" attr.id="web-link--{{i}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary me-1" (click)="addWebLink()">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('add-link-alt')}}</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" (click)="removeWebLink(i)">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove-link-alt')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.CoverImage]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.CoverImage])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p class="alert alert-primary" role="alert">
|
||||
{{t('cover-image-description')}}
|
||||
</p>
|
||||
<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[TabID.Related]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Related])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-edit-series-relation [series]="series" [save]="saveNestedComponents"></app-edit-series-relation>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="tabs[TabID.Info]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Info])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4>{{t('info-title')}}</h4>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6" *ngIf="libraryName">{{t('library-title')}} {{libraryName | sentenceCase}}</div>
|
||||
<div class="col-md-6">{{t('format-title')}} <app-tag-badge>{{series.format | mangaFormat}}</app-tag-badge></div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6">{{t('created-title')}} {{series.created | date:'shortDate'}}</div>
|
||||
<div class="col-md-6">{{t('last-read-title')}} {{series.latestReadDate | defaultDate | timeAgo}}</div>
|
||||
<div class="col-md-6">{{t('last-added-title')}} {{series.lastChapterAdded | defaultDate | timeAgo}}</div>
|
||||
<div class="col-md-6">{{t('last-scanned-title')}} {{series.lastFolderScanned | defaultDate | timeAgo}}</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-auto">{{t('folder-path-title')}} {{series.folderPath | defaultValue}}</div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2" *ngIf="metadata">
|
||||
<div class="col-md-6">
|
||||
{{t('max-items-title')}} {{metadata.maxCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="t('highest-count-tooltip')" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{t('total-items-title')}} {{metadata.totalCount}}
|
||||
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="t('max-issue-tooltip')" role="button" tabindex="0"></i>
|
||||
</div>
|
||||
<div class="col-md-6">{{t('publication-status-title')}} {{metadata.publicationStatus | publicationStatus}}</div>
|
||||
<div class="col-md-6">{{t('total-pages-title')}} {{series.pages}}</div>
|
||||
<div class="col-md-6">{{t('size-title')}} {{size | bytes}}</div>
|
||||
</div>
|
||||
<h4>Volumes</h4>
|
||||
<div class="spinner-border text-secondary" role="status" *ngIf="isLoadingVolumes">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
</div>
|
||||
<ul class="list-unstyled" *ngIf="!isLoadingVolumes">
|
||||
<li class="d-flex my-4" *ngFor="let volume of seriesVolumes">
|
||||
<app-image class="me-3" style="width: 74px;" width="74px" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">{{t('volume-num')}} {{volume.name}}</h5>
|
||||
<div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('added-title')}} {{volume.created | date: 'short'}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{t('last-modified-title')}} {{volume.lastModified | date: 'short'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()"
|
||||
[attr.aria-expanded]="!volumeCollapsed[volume.name]">
|
||||
{{t('view-files')}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
{{t('pages-title')}} {{volume.pages}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="volumeCollapsed[volume.name]">
|
||||
<ul class="list-group mt-2">
|
||||
<li *ngFor="let file of volume.volumeFiles.sort()" class="list-group-item">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('chapter-title')}} {{file.chapter}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{t('pages-title')}} {{file.pages}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{t('format-title')}} <span class="badge badge-secondary">{{utilityService.mangaFormatToText(file.format)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</ul>
|
||||
</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>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!editSeriesForm.valid" (click)="save()">Save</button>
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!editSeriesForm.valid" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #lock let-item="item" let-field="field">
|
||||
<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 class="visually-hidden">{{t('field-locked-alt')}}</span>
|
||||
</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavModule, NgbNavOutlet,
|
||||
NgbNavOutlet,
|
||||
NgbTooltip
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
|
|
@ -38,7 +38,7 @@ import { MetadataService } from 'src/app/_services/metadata.service';
|
|||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {CommonModule, NgTemplateOutlet} from "@angular/common";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {TypeaheadComponent} from "../../../typeahead/_components/typeahead.component";
|
||||
import {CoverImageChooserComponent} from "../../cover-image-chooser/cover-image-chooser.component";
|
||||
import {EditSeriesRelationComponent} from "../../edit-series-relation/edit-series-relation.component";
|
||||
|
|
@ -51,6 +51,7 @@ import {PublicationStatusPipe} from "../../../pipe/publication-status.pipe";
|
|||
import {BytesPipe} from "../../../pipe/bytes.pipe";
|
||||
import {ImageComponent} from "../../../shared/image/image.component";
|
||||
import {DefaultValuePipe} from "../../../pipe/default-value.pipe";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
enum TabID {
|
||||
General = 0,
|
||||
|
|
@ -87,6 +88,7 @@ enum TabID {
|
|||
NgbCollapse,
|
||||
NgbNavOutlet,
|
||||
DefaultValuePipe,
|
||||
TranslocoModule,
|
||||
|
||||
],
|
||||
templateUrl: './edit-series-modal.component.html',
|
||||
|
|
@ -104,7 +106,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
initSeries!: Series;
|
||||
|
||||
volumeCollapsed: any = {};
|
||||
tabs = ['General', 'Metadata', 'People', 'Web Links', 'Cover Image', 'Related', 'Info'];
|
||||
tabs = ['general-tab', 'metadata-tab', 'people-tab', 'web-links-tab', 'cover-image-tab', 'related-tab', 'info-tab'];
|
||||
active = this.tabs[0];
|
||||
editSeriesForm!: FormGroup;
|
||||
libraryName: string | undefined = undefined;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
<ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount">
|
||||
<ng-container *transloco="let t; read: 'bulk-operations'">
|
||||
<ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount">
|
||||
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 fixed-top" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
|
||||
<span class="highlight">
|
||||
<i class="fa fa-check me-1" aria-hidden="true"></i>
|
||||
{{selectionCount | number}} items selected
|
||||
{{t('items-selected',{num: selectionCount | number})}}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<button *ngIf="hasMarkAsUnread" class="btn btn-icon" (click)="executeAction(Action.MarkAsUnread)" ngbTooltip="Mark as Unread" placement="bottom">
|
||||
<span>
|
||||
<button *ngIf="hasMarkAsUnread" class="btn btn-icon" (click)="executeAction(Action.MarkAsUnread)" [ngbTooltip]="t('mark-as-unread')" placement="bottom">
|
||||
<i class="fa-regular fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Mark as Unread</span>
|
||||
<span class="visually-hidden">{{t('mark-as-unread')}}</span>
|
||||
</button>
|
||||
<button *ngIf="hasMarkAsRead" class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" ngbTooltip="Mark as Read" placement="bottom">
|
||||
<button *ngIf="hasMarkAsRead" class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" [ngbTooltip]="t('mark-as-read')" placement="bottom">
|
||||
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Mark as Read</span>
|
||||
<span class="visually-hidden">{{t('mark-as-read')}}</span>
|
||||
</button>
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times me-1" aria-hidden="true"></i>{{t('deselect-all')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ import {
|
|||
DestroyRef,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { BulkSelectionService } from '../bulk-selection.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {AsyncPipe, CommonModule} from "@angular/common";
|
||||
import {CardActionablesComponent} from "../card-item/card-actionables/card-actionables.component";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-operations',
|
||||
|
|
@ -21,7 +21,9 @@ import {CardActionablesComponent} from "../card-item/card-actionables/card-actio
|
|||
imports: [
|
||||
CommonModule,
|
||||
AsyncPipe,
|
||||
CardActionablesComponent
|
||||
CardActionablesComponent,
|
||||
TranslocoModule,
|
||||
NgbTooltip
|
||||
],
|
||||
templateUrl: './bulk-operations.component.html',
|
||||
styleUrls: ['./bulk-operations.component.scss'],
|
||||
|
|
|
|||
|
|
@ -1,169 +1,165 @@
|
|||
<div class="offcanvas-header">
|
||||
<ng-container *transloco="let t; read: 'card-detail-drawer'">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">
|
||||
<span class="modal-title" id="modal-basic-title">
|
||||
<app-entity-title [libraryType]="libraryType" [entity]="data" [seriesName]="parentName"></app-entity-title>
|
||||
</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" aria-label="Close" (click)="activeOffcanvas.dismiss()"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas-body pb-3">
|
||||
<div class="offcanvas-body pb-3">
|
||||
<div class="d-flex">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[TabID.General]">
|
||||
<a ngbNavLink>General</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="container-fluid" style="overflow: auto">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[TabID.General]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.General].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="container-fluid" style="overflow: auto">
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="d-none d-md-block col-md-2 col-lg-1">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="coverImageUrl"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-lg-11">
|
||||
<ng-container *ngIf="summary.length > 0; else noSummary">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</ng-container>
|
||||
<ng-template #noSummary>
|
||||
No Summary available.
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="d-none d-md-block col-md-2 col-lg-1">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="coverImageUrl"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-lg-11">
|
||||
<ng-container *ngIf="summary.length > 0; else noSummary">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</ng-container>
|
||||
<ng-template #noSummary>
|
||||
{{t('no-summary')}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-entity-info-cards [entity]="data" [libraryId]="libraryId"></app-entity-info-cards>
|
||||
|
||||
|
||||
<!-- 2 rows to show some tags-->
|
||||
<ng-container *ngIf="chapterMetadata !== undefined">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>{{t('writers-title')}}</h6>
|
||||
<ng-container *ngIf="chapterMetadata.writers.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>{{t('genres-title')}}</h6>
|
||||
<ng-container *ngIf="chapterMetadata.genres.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>{{t('publishers-title')}}</h6>
|
||||
<ng-container *ngIf="chapterMetadata.publishers.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>{{t('tags-title')}}</h6>
|
||||
<ng-container *ngIf="chapterMetadata.tags.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #noBadges>
|
||||
{{t('not-defined')}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Metadata]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Metadata].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-chapter-metadata-detail [chapter]="chapterMetadata"></app-chapter-metadata-detail>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Cover].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
[showReset]="chapter.coverImageLocked"
|
||||
[showApplyButton]="true"
|
||||
(applyCover)="applyCoverImage($event)"
|
||||
(resetCover)="resetCoverImage()"
|
||||
>
|
||||
</app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="(isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Files].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span>
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">{{t('unread') | uppercase}}</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">{{t('read') | uppercase}}</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>{{t('files')}}</ng-template>
|
||||
</h5>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('pages')}} {{file.pages | number:''}}
|
||||
</div>
|
||||
|
||||
<app-entity-info-cards [entity]="data" [libraryId]="libraryId"></app-entity-info-cards>
|
||||
|
||||
|
||||
<!-- 2 rows to show some tags-->
|
||||
<ng-container *ngIf="chapterMetadata !== undefined">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>Authors/Writers</h6>
|
||||
<ng-container *ngIf="chapterMetadata.writers.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>Genres</h6>
|
||||
<ng-container *ngIf="chapterMetadata.genres.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>Publisher</h6>
|
||||
<ng-container *ngIf="chapterMetadata.publishers.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<h6>Tags</h6>
|
||||
<ng-container *ngIf="chapterMetadata.tags.length > 0; else noBadges">
|
||||
<app-badge-expander [items]="chapterMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #noBadges>
|
||||
Not defined
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Metadata]">
|
||||
<a ngbNavLink>{{tabs[TabID.Metadata].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-chapter-metadata-detail [chapter]="chapterMetadata"></app-chapter-metadata-detail>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{tabs[TabID.Cover].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
[showReset]="chapter.coverImageLocked"
|
||||
[showApplyButton]="true"
|
||||
(applyCover)="applyCoverImage($event)"
|
||||
(resetCover)="resetCoverImage()"
|
||||
>
|
||||
</app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="(isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{tabs[TabID.Files].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span >
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>Files</ng-template>
|
||||
</h5>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
Pages: {{file.pages | number:''}}
|
||||
</div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
Added:
|
||||
<!-- TODO: This data.created can be removed after v0.5.5 release -->
|
||||
<ng-container *ngIf="file.created === '0001-01-01T00:00:00'; else fileDate">
|
||||
{{data.created | date: 'short' | defaultDate}}
|
||||
</ng-container>
|
||||
<ng-template #fileDate>
|
||||
{{file.created | date: 'short' | defaultDate}}
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="col">
|
||||
Size: {{file.bytes | bytes}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
{{t('added')}} {{file.created | date: 'short' | defaultDate}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{t('size')}} {{file.bytes | bytes}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import {BytesPipe} from "../../pipe/bytes.pipe";
|
|||
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
|
||||
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
|
||||
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
enum TabID {
|
||||
General = 0,
|
||||
|
|
@ -60,7 +61,7 @@ enum TabID {
|
|||
@Component({
|
||||
selector: 'app-card-detail-drawer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, EntityTitleComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ImageComponent, ReadMoreComponent, EntityInfoCardsComponent, CoverImageChooserComponent, ChapterMetadataDetailComponent, CardActionablesComponent, DefaultDatePipe, BytesPipe, NgbNavOutlet, BadgeExpanderComponent, TagBadgeComponent, PersonBadgeComponent],
|
||||
imports: [CommonModule, EntityTitleComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ImageComponent, ReadMoreComponent, EntityInfoCardsComponent, CoverImageChooserComponent, ChapterMetadataDetailComponent, CardActionablesComponent, DefaultDatePipe, BytesPipe, NgbNavOutlet, BadgeExpanderComponent, TagBadgeComponent, PersonBadgeComponent, TranslocoModule],
|
||||
templateUrl: './card-detail-drawer.component.html',
|
||||
styleUrls: ['./card-detail-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -72,6 +73,8 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
@Input() libraryId: number = 0;
|
||||
@Input({required: true}) data!: Volume | Chapter;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
|
||||
|
||||
/**
|
||||
* If this is a volume, this will be first chapter for said volume.
|
||||
|
|
@ -94,7 +97,12 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
libraryType: LibraryType = LibraryType.Manga;
|
||||
|
||||
|
||||
tabs = [{title: 'General', disabled: false}, {title: 'Metadata', disabled: false}, {title: 'Cover', disabled: false}, {title: 'Info', disabled: false}];
|
||||
tabs = [
|
||||
{title: 'general-tab', disabled: false},
|
||||
{title: 'metadata-tab', disabled: false},
|
||||
{title: 'cover-tab', disabled: false},
|
||||
{title: 'info-tab', disabled: false}
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
chapterMetadata!: ChapterMetadata;
|
||||
|
|
@ -201,7 +209,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
|
||||
resetCoverImage() {
|
||||
this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => {
|
||||
this.toastr.info('A job has been enqueued to regenerate the cover image');
|
||||
this.toastr.info(this.translocoService.translate('toasts.regen-cover'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +262,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
|
||||
readChapter(chapter: Chapter, incognito: boolean = false) {
|
||||
if (chapter.pages === 0) {
|
||||
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
|
||||
this.toastr.error(this.translocoService.translate('toasts.no-pages'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +273,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
|
||||
download(chapter: Chapter) {
|
||||
if (this.downloadInProgress) {
|
||||
this.toastr.info('Download is already in progress. Please wait.');
|
||||
this.toastr.info(this.translocoService.translate('toasts.download-in-progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,60 +1,62 @@
|
|||
<ng-container *transloco="let t; read: 'card-detail-layout'">
|
||||
|
||||
<div class="row mt-2 g-0 pb-2" *ngIf="header !== undefined && header.length > 0">
|
||||
<div class="row mt-2 g-0 pb-2" *ngIf="header !== undefined && header.length > 0">
|
||||
<div class="col me-auto">
|
||||
<h2>
|
||||
<h2>
|
||||
<span *ngIf="actions.length > 0" class="">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
||||
</span>
|
||||
<span *ngIf="header !== undefined && header.length > 0">
|
||||
<span *ngIf="header !== undefined && header.length > 0">
|
||||
{{header}}
|
||||
<span class="badge bg-primary rounded-pill" attr.aria-label="{{pagination.totalItems}} total items" *ngIf="pagination !== undefined">{{pagination.totalItems}}</span>
|
||||
<span class="badge bg-primary rounded-pill" [attr.aria-label]="t('total-items', {count: pagination.totalItems})" *ngIf="pagination !== undefined">{{pagination.totalItems}}</span>
|
||||
</span>
|
||||
</h2>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
|
||||
<div class="viewport-container" [ngClass]="{'empty': items.length === 0 && !isLoading}">
|
||||
</div>
|
||||
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
|
||||
<div class="viewport-container" [ngClass]="{'empty': items.length === 0 && !isLoading}">
|
||||
<div class="content-container">
|
||||
<div class="card-container mt-2 mb-2">
|
||||
<p *ngIf="items.length === 0 && !isLoading">
|
||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||
</p>
|
||||
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="1" [parentScroll]="parentScroll">
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2" (click)="tryToSaveJumpKey(item)" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: scroll.viewPortInfo.startIndexWithBuffer + i }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
</div>
|
||||
<div class="card-container mt-2 mb-2">
|
||||
<p *ngIf="items.length === 0 && !isLoading">
|
||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||
</p>
|
||||
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="1" [parentScroll]="parentScroll">
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2" (click)="tryToSaveJumpKey(item)" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: scroll.viewPortInfo.startIndexWithBuffer + i }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="jumpBarKeysToRender.length >= 4 && items.length > 0 && scroll.viewPortInfo.maxScrollPosition > 0" [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
|
||||
</div>
|
||||
<ng-template #cardTemplate>
|
||||
</div>
|
||||
<ng-template #cardTemplate>
|
||||
<virtual-scroller #scroll [items]="items" [bufferAmount]="1">
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2" (click)="tryToSaveJumpKey(item)" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
</div>
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2" (click)="tryToSaveJumpKey(item)" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
|
||||
<div class="mx-auto" *ngIf="items.length === 0 && !isLoading" style="width: 200px;">
|
||||
<p>
|
||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||
</p>
|
||||
<p>
|
||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
|
||||
<ng-template #jumpBar>
|
||||
<ng-template #jumpBar>
|
||||
<div class="jump-bar">
|
||||
<ng-container *ngFor="let jumpKey of jumpBarKeysToRender; let i = index;">
|
||||
<button class="btn btn-link" [ngClass]="{'disabled': hasCustomSort()}" (click)="scrollTo(jumpKey)" [ngbTooltip]="jumpKey.size + ' Series'" placement="left">
|
||||
{{jumpKey.title}}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let jumpKey of jumpBarKeysToRender; let i = index;">
|
||||
<button class="btn btn-link" [ngClass]="{'disabled': hasCustomSort()}" (click)="scrollTo(jumpKey)" [ngbTooltip]="jumpKey.size + ' Series'" placement="left">
|
||||
{{jumpKey.title}}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@ import {LoadingComponent} from "../../shared/loading/loading.component";
|
|||
import {CardActionablesComponent} from "../card-item/card-actionables/card-actionables.component";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {MetadataFilterComponent} from "../../metadata-filter/metadata-filter.component";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-detail-layout',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LoadingComponent, VirtualScrollerModule, CardActionablesComponent, NgbTooltip, MetadataFilterComponent],
|
||||
imports: [CommonModule, LoadingComponent, VirtualScrollerModule, CardActionablesComponent, NgbTooltip, MetadataFilterComponent, TranslocoModule],
|
||||
templateUrl: './card-detail-layout.component.html',
|
||||
styleUrls: ['./card-detail-layout.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<div class="card {{selected ? 'selected-highlight' : ''}}">
|
||||
<ng-container *transloco="let t; read: 'card-item'">
|
||||
<div class="card {{selected ? 'selected-highlight' : ''}}">
|
||||
<div class="overlay" (click)="handleClick($event)">
|
||||
<ng-container *ngIf="total > 0 || suppressArchiveWarning">
|
||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageUrl"></app-image>
|
||||
|
|
@ -16,12 +17,12 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="error-banner" *ngIf="total === 0 && !suppressArchiveWarning">
|
||||
Cannot Read
|
||||
{{t('cannot-read')}}
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="read === 0 && total > 0">
|
||||
<div class="badge-container">
|
||||
<div class="not-read-badge" ></div>
|
||||
<div class="not-read-badge"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
|
@ -62,4 +63,6 @@
|
|||
{{libraryName | sentenceCase}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {CardActionablesComponent} from "./card-actionables/card-actionables.comp
|
|||
import {SentenceCasePipe} from "../../pipe/sentence-case.pipe";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {RouterLink} from "@angular/router";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-item',
|
||||
|
|
@ -55,7 +56,8 @@ import {RouterLink} from "@angular/router";
|
|||
MangaFormatIconPipe,
|
||||
CardActionablesComponent,
|
||||
SentenceCasePipe,
|
||||
RouterLink
|
||||
RouterLink,
|
||||
TranslocoModule
|
||||
],
|
||||
templateUrl: './card-item.component.html',
|
||||
styleUrls: ['./card-item.component.scss'],
|
||||
|
|
|
|||
|
|
@ -1,102 +1,102 @@
|
|||
<ng-container *ngIf="chapter !== undefined">
|
||||
<ng-container>
|
||||
<span *ngIf="chapter.writers.length === 0 && chapter.coverArtists.length === 0
|
||||
<ng-container *transloco="let t; read: 'chapter-metadata-detail'">
|
||||
<ng-container *ngIf="chapter !== undefined">
|
||||
<span *ngIf="chapter.writers.length === 0 && chapter.coverArtists.length === 0
|
||||
&& chapter.pencillers.length === 0 && chapter.inkers.length === 0
|
||||
&& chapter.colorists.length === 0 && chapter.letterers.length === 0
|
||||
&& chapter.editors.length === 0 && chapter.publishers.length === 0
|
||||
&& chapter.characters.length === 0 && chapter.translators.length === 0">
|
||||
No metadata available
|
||||
{{t('no-data')}}
|
||||
</span>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<h6>Writers</h6>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.coverArtists && chapter.coverArtists.length > 0">
|
||||
<h6>Cover Artists</h6>
|
||||
<app-badge-expander [items]="chapter.coverArtists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.pencillers && chapter.pencillers.length > 0">
|
||||
<h6>Pencillers</h6>
|
||||
<app-badge-expander [items]="chapter.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.inkers && chapter.inkers.length > 0">
|
||||
<h6>Inkers</h6>
|
||||
<app-badge-expander [items]="chapter.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.colorists && chapter.colorists.length > 0">
|
||||
<h6>Colorists</h6>
|
||||
<app-badge-expander [items]="chapter.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.letterers && chapter.letterers.length > 0">
|
||||
<h6>Letterers</h6>
|
||||
<app-badge-expander [items]="chapter.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.editors && chapter.editors.length > 0">
|
||||
<h6>Editors</h6>
|
||||
<app-badge-expander [items]="chapter.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.publishers && chapter.publishers.length > 0">
|
||||
<h6>Publishers</h6>
|
||||
<app-badge-expander [items]="chapter.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<h6>Characters</h6>
|
||||
<app-badge-expander [items]="chapter.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
<div class="col-auto mt-2" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<h6>Translators</h6>
|
||||
<app-badge-expander [items]="chapter.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<h6>{{t('writers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.coverArtists && chapter.coverArtists.length > 0">
|
||||
<h6>{{t('cover-artists-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.coverArtists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.pencillers && chapter.pencillers.length > 0">
|
||||
<h6>{{t('pencillers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.inkers && chapter.inkers.length > 0">
|
||||
<h6>{{t('inkers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.colorists && chapter.colorists.length > 0">
|
||||
<h6>{{t('colorists-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.letterers && chapter.letterers.length > 0">
|
||||
<h6>{{t('letterers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.editors && chapter.editors.length > 0">
|
||||
<h6>{{t('editors-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.publishers && chapter.publishers.length > 0">
|
||||
<h6>{{t('publishers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<h6>{{t('characters-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
<div class="col-auto mt-2" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<h6>{{t('translators-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,16 @@ import { ChapterMetadata } from 'src/app/_models/metadata/chapter-metadata';
|
|||
import {CommonModule} from "@angular/common";
|
||||
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
|
||||
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-chapter-metadata-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BadgeExpanderComponent, PersonBadgeComponent],
|
||||
imports: [CommonModule, BadgeExpanderComponent, PersonBadgeComponent, TranslocoModule],
|
||||
templateUrl: './chapter-metadata-detail.component.html',
|
||||
styleUrls: ['./chapter-metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ChapterMetadataDetailComponent {
|
||||
@Input() chapter: ChapterMetadata | undefined;
|
||||
|
||||
constructor() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +1,80 @@
|
|||
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
|
||||
<ng-container *transloco="let t; read: 'cover-image-chooser'">
|
||||
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
|
||||
<form [formGroup]="form">
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false"
|
||||
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
|
||||
<div class="mx-auto">
|
||||
<div class="row g-0 mb-3">
|
||||
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false"
|
||||
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
|
||||
<div class="mx-auto">
|
||||
<div class="row g-0 mb-3">
|
||||
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="changeMode('url')"><span class="phone-hidden">Enter a </span>Url</a>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<span style="padding-right:0px" href="javascript:void(0)">Drag and drop</span>
|
||||
<span class="ps-1 pe-1" style="padding-right:0px">•</span>
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">Upload<span class="phone-hidden"> an image</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="changeMode('url')"><span class="phone-hidden">Enter a </span>Url</a>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<span style="padding-right:0px" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
|
||||
<span class="ps-1 pe-1" style="padding-right:0px">•</span>
|
||||
<a style="padding-right:0px" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="mode === 'url'">
|
||||
<div class="row g-0 mt-3 pb-3 ms-md-2 me-md-2">
|
||||
<div class="input-group col-auto me-md-2" style="width: 83%">
|
||||
<label class="input-group-text" for="load-image">Url</label>
|
||||
<input type="text" autofocus autocomplete="off" class="form-control" formControlName="coverImageUrl" placeholder="https://" id="load-image" class="form-control">
|
||||
<button class="btn btn-outline-secondary" type="button" id="load-image-addon" (click)="loadImage(); mode='all';" [disabled]="form.get('coverImageUrl')?.value.length === 0">
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary col-auto" href="javascript:void(0)" (click)="mode = 'all'" aria-label="Back">
|
||||
<i class="fas fa-share" aria-hidden="true" style="transform: rotateY(180deg)"></i>
|
||||
<span class="phone-hidden">Back</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-template>
|
||||
</ngx-file-drop>
|
||||
|
||||
<ng-template>
|
||||
<ng-container *ngIf="mode === 'url'">
|
||||
<div class="row g-0 mt-3 pb-3 ms-md-2 me-md-2">
|
||||
<div class="input-group col-auto me-md-2" style="width: 83%">
|
||||
<label class="input-group-text" for="load-image">{{t('url-label')}}</label>
|
||||
<input type="text" autofocus autocomplete="off" class="form-control" formControlName="coverImageUrl" placeholder="https://" id="load-image" class="form-control">
|
||||
<button class="btn btn-outline-secondary" type="button" id="load-image-addon" (click)="loadImage(); mode='all';" [disabled]="form.get('coverImageUrl')?.value.length === 0">
|
||||
{{t('load')}}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary col-auto" href="javascript:void(0)" (click)="mode = 'all'">
|
||||
<i class="fas fa-share" aria-hidden="true" style="transform: rotateY(180deg)"></i>
|
||||
<span class="phone-hidden">{{t('back')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-template>
|
||||
</ngx-file-drop>
|
||||
|
||||
<ng-template>
|
||||
|
||||
</ng-template>
|
||||
</form>
|
||||
|
||||
<div class="row g-0 chooser" style="padding-top: 10px">
|
||||
<div class="image-card col-auto"
|
||||
*ngIf="showReset" tabindex="0" aria-label="Reset cover image" (click)="reset()"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
|
||||
<app-image class="card-img-top" title="Reset Cover Image" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button style="width: 100%;" class="btn btn-secondary" aria-label="Reset to generated image" (click)="resetImage()">Reset</button>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
<div class="image-card col-auto"
|
||||
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" attr.aria-label="Image {{idx + 1}}" (click)="selectImage(idx)"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
||||
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button class="btn btn-primary" style="width: 100%;" aria-label="Apply for uploaded image"
|
||||
(click)="applyImage(idx)">
|
||||
{{appliedIndex === idx ? 'Applied' : 'Apply'}}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="image-card col-auto"
|
||||
*ngIf="showReset" tabindex="0" (click)="reset()"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
|
||||
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
<div class="image-card col-auto"
|
||||
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
||||
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button class="btn btn-primary" style="width: 100%;"
|
||||
(click)="applyImage(idx)">
|
||||
{{appliedIndex === idx ? t('applied') : t('apply')}}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
|||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {translate, TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-cover-image-chooser',
|
||||
|
|
@ -17,7 +18,8 @@ import {ImageComponent} from "../../shared/image/image.component";
|
|||
ReactiveFormsModule,
|
||||
NgxFileDropModule,
|
||||
CommonModule,
|
||||
ImageComponent
|
||||
ImageComponent,
|
||||
TranslocoModule
|
||||
],
|
||||
templateUrl: './cover-image-chooser.component.html',
|
||||
styleUrls: ['./cover-image-chooser.component.scss'],
|
||||
|
|
@ -116,7 +118,7 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
img.src = imgUrl;
|
||||
img.onload = (e) => this.handleUrlImageAdd(img, index);
|
||||
img.onerror = (e) => {
|
||||
this.toastr.error('The image could not be fetched due to server refusing request. Please download and upload from file instead.');
|
||||
this.toastr.error(translate('errors.rejected-cover-upload'));
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
};
|
||||
|
|
@ -156,7 +158,7 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
img.src = this.imageService.getCoverUploadImage(filename);
|
||||
img.onload = (e) => this.handleUrlImageAdd(img);
|
||||
img.onerror = (e) => {
|
||||
this.toastr.error('The image could not be fetched due to server refusing request. Please download and upload from file instead.');
|
||||
this.toastr.error(translate('errors.rejected-cover-upload'));
|
||||
this.form.get('coverImageUrl')?.setValue('');
|
||||
this.cdRef.markForCheck();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<span class="download" *ngIf="download$ | async as download">
|
||||
<ng-container *transloco="let t; read: 'download-indicator'">
|
||||
<span class="download" *ngIf="download$ | async as download">
|
||||
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
||||
<span class="visually-hidden" role="status">
|
||||
{{download.progress}}% downloaded
|
||||
{{t('progress',{percentage: download.progress})}}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import { Download } from 'src/app/shared/_models/download';
|
|||
import { DownloadEvent } from 'src/app/shared/_services/download.service';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {CircularLoaderComponent} from "../../shared/circular-loader/circular-loader.component";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-download-indicator',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CircularLoaderComponent],
|
||||
imports: [CommonModule, CircularLoaderComponent, TranslocoModule],
|
||||
templateUrl: './download-indicator.component.html',
|
||||
styleUrls: ['./download-indicator.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -1,38 +1,41 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read: 'edit-series-relation'">
|
||||
<div class="container-fluid">
|
||||
|
||||
<p>
|
||||
Not sure what relationship to add? See our <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/series-relationships" target="_blank" rel="noopener noreferrer" referrerpolicy="no-refer">wiki for hints</a>.
|
||||
{{t('description-part-1')}} <a href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/series-relationships" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{t('description-part-2')}}</a>
|
||||
</p>
|
||||
|
||||
<div class="row g-0" *ngIf="relations.length > 0">
|
||||
<label class="form-label col-md-7">Target Series</label>
|
||||
<label class="form-label col-md-5">Relationship</label>
|
||||
<label class="form-label col-md-7">{{t('target-series')}}</label>
|
||||
<label class="form-label col-md-5">{{t('relationship')}}</label>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<div class="row g-0" *ngFor="let relation of relations; let idx = index; let isLast = last;">
|
||||
<div class="col-sm-12 col-md-12 col-lg-7 mb-3">
|
||||
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}" [focus]="focusTypeahead">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-10 col-lg-3 mb-3">
|
||||
<select class="form-select" [formControl]="relation.formControl">
|
||||
<option [value]="RelationKind.Parent" disabled>Parent</option>
|
||||
<option *ngFor="let opt of relationOptions" [value]="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="col-sm-auto col-md-2 mb-3 btn btn-outline-secondary" (click)="removeRelation(idx)">
|
||||
<i class="fa fa-trash"></i><span class="visually-hidden">Remove</span></button>
|
||||
<div class="row g-0" *ngFor="let relation of relations; let idx = index; let isLast = last;">
|
||||
<div class="col-sm-12 col-md-12 col-lg-7 mb-3">
|
||||
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}" [focus]="focusTypeahead">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}} ({{libraryNames[item.libraryId]}})
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-10 col-lg-3 mb-3">
|
||||
<select class="form-select" [formControl]="relation.formControl">
|
||||
<option [value]="RelationKind.Parent" disabled>{{t('parent')}}</option>
|
||||
<option *ngFor="let opt of relationOptions" [value]="opt.value">{{opt.value | relationship }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="col-sm-auto col-md-2 mb-3 btn btn-outline-secondary" (click)="removeRelation(idx)">
|
||||
<i class="fa fa-trash"></i><span class="visually-hidden">{{t('remove')}}</span></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row g-0 mt-3 mb-3">
|
||||
<button class="btn btn-outline-secondary col-md-12" (click)="addNewRelation()">Add Relationship</button>
|
||||
<button class="btn btn-outline-secondary col-md-12" (click)="addNewRelation()">{{t('add-relationship')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import { SeriesService } from 'src/app/_services/series.service';
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component";
|
||||
import {CommonModule, NgForOf, NgIf} from "@angular/common";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {RelationshipPipe} from "../../pipe/relationship.pipe";
|
||||
|
||||
interface RelationControl {
|
||||
series: {id: number, name: string} | undefined; // Will add type as well
|
||||
|
|
@ -37,6 +39,8 @@ interface RelationControl {
|
|||
TypeaheadComponent,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
TranslocoModule,
|
||||
RelationshipPipe,
|
||||
],
|
||||
templateUrl: './edit-series-relation.component.html',
|
||||
styleUrls: ['./edit-series-relation.component.scss'],
|
||||
|
|
@ -54,7 +58,6 @@ export class EditSeriesRelationComponent implements OnInit {
|
|||
|
||||
relationOptions = RelationKinds;
|
||||
relations: Array<RelationControl> = [];
|
||||
seriesSettings: TypeaheadSettings<SearchResult> = new TypeaheadSettings();
|
||||
libraryNames: {[key:number]: string} = {};
|
||||
|
||||
focusTypeahead = new EventEmitter();
|
||||
|
|
|
|||
|
|
@ -1,121 +1,124 @@
|
|||
<ng-container *transloco="let t; read: 'entity-info-cards'">
|
||||
|
||||
<div class="mt-4 mb-3">
|
||||
<div class="row g-0" *ngIf="chapterMetadata ">
|
||||
<!-- Tags and Characters are used a lot of Hentai and Doujinshi type content, so showing in list item has value add on first glance -->
|
||||
<app-metadata-detail [tags]="chapterMetadata.tags" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
<div class="mt-4 mb-3">
|
||||
<div class="row g-0" *ngIf="chapterMetadata ">
|
||||
<!-- Tags and Characters are used a lot of Hentai and Doujinshi type content, so showing in list item has value add on first glance -->
|
||||
<app-metadata-detail [tags]="chapterMetadata.tags" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="chapterMetadata.characters" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="chapterMetadata.characters" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||
<ng-template #titleTemplate let-item>{{item.name}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Release Date" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release">
|
||||
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-tooltip')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-date-title')">
|
||||
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Age Rating" [clickable]="false" fontClasses="fas fa-eye" title="Age Rating">
|
||||
{{chapter.ageRating | ageRating | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="false" fontClasses="fas fa-eye" [title]="t('age-rating-title')">
|
||||
{{chapter.ageRating | ageRating}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="totalPages > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
||||
{{totalPages | compactNumber}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines" [title]="t('pages-title')">
|
||||
{{t('pages-count', {num: totalPages | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{totalWordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{t('words-count', {num: totalWordCount | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime"><1 Hour</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Date Added" [clickable]="false" fontClasses="fa-solid fa-file-import" title="Date Added">
|
||||
{{chapter.created | date:'short' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('date-added-title')" [clickable]="false" fontClasses="fa-solid fa-file-import" [title]="t('date-added-title')">
|
||||
{{chapter.created | date:'short' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && size > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="ID">
|
||||
{{size | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('size-title')" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" [title]="t('size-title')">
|
||||
{{size | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('id-title')" [clickable]="false" fontClasses="fa-solid fa-fingerprint" [title]="t('id-title')">
|
||||
{{entity.id}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<ng-container *ngIf="WebLinks.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ID" [clickable]="false" fontClasses="fa-solid fa-fingerprint" title="ID">
|
||||
{{entity.id}}
|
||||
</app-icon-and-title>
|
||||
<app-icon-and-title [label]="t('links-title')" [clickable]="false" fontClasses="fa-solid fa-link" [title]="t('links-title')">
|
||||
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<img width="24" height="24" #img class="lazyload img-placeholder"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
[attr.data-src]="imageService.getWebLinkImage(link)"
|
||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||
aria-hidden="true" alt="">
|
||||
</a>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<ng-container *ngIf="WebLinks.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Links" [clickable]="false" fontClasses="fa-solid fa-link" title="Links">
|
||||
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<img width="24" height="24" #img class="lazyload img-placeholder"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
[attr.data-src]="imageService.getWebLinkImage(link)"
|
||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||
aria-hidden="true" alt="">
|
||||
</a>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ISBN" [clickable]="false" fontClasses="fa-solid fa-barcode" title="ISBN">
|
||||
{{chapter.isbn}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('isbn-title')" [clickable]="false" fontClasses="fa-solid fa-barcode" [title]="t('isbn-title')">
|
||||
{{chapter.isbn}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Last Read" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
||||
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
||||
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -26,11 +26,12 @@ import {AgeRatingPipe} from "../../pipe/age-rating.pipe";
|
|||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
|
||||
import {FilterQueryParam} from "../../shared/_services/filter-utilities.service";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-entity-info-cards',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent],
|
||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent, TranslocoModule],
|
||||
templateUrl: './entity-info-cards.component.html',
|
||||
styleUrls: ['./entity-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
<ng-container [ngSwitch]="libraryType">
|
||||
<ng-container *transloco="let t; read: 'entity-title'">
|
||||
<ng-container [ngSwitch]="libraryType">
|
||||
<ng-container *ngSwitchCase="LibraryType.Comic">
|
||||
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullComicTitle">
|
||||
{{titleName}}
|
||||
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullComicTitle">
|
||||
{{titleName}}
|
||||
</ng-container>
|
||||
<ng-template #fullComicTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
|
||||
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
<ng-template #fullComicTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
|
||||
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
{{entity.number !== 0 ? (isChapter ? 'Issue #' + entity.number : volumeTitle) : 'Special'}}
|
||||
</ng-template>
|
||||
{{entity.number !== 0 ? (isChapter ? t('issue-num') + entity.number : volumeTitle) : t('special')}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Manga">
|
||||
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullMangaTitle">
|
||||
{{titleName}}
|
||||
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullMangaTitle">
|
||||
{{titleName}}
|
||||
</ng-container>
|
||||
<ng-template #fullMangaTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
|
||||
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
<ng-template #fullMangaTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
|
||||
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
{{entity.number !== 0 ? (isChapter ? 'Chapter ' + entity.number : volumeTitle) : 'Special'}}
|
||||
</ng-template>
|
||||
{{entity.number !== 0 ? (isChapter ? (t('chapter') + ' ') + entity.number : volumeTitle) : t('special')}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Book">
|
||||
{{volumeTitle}}
|
||||
{{volumeTitle}}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ import { Chapter } from 'src/app/_models/chapter';
|
|||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import {CommonModule, NgSwitch} from "@angular/common";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-entity-title',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgSwitch
|
||||
NgSwitch,
|
||||
TranslocoModule
|
||||
],
|
||||
templateUrl: './entity-title.component.html',
|
||||
styleUrls: ['./entity-title.component.scss'],
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
<ng-container *ngIf="data !== undefined">
|
||||
<div class="card">
|
||||
<div class="overlay" (click)="handleClick()">
|
||||
<ng-container>
|
||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="data.coverUrl"></app-image>
|
||||
</ng-container>
|
||||
<ng-container *transloco="let t; read: 'external-series-card'">
|
||||
<ng-container *ngIf="data !== undefined">
|
||||
<div class="card">
|
||||
<div class="overlay" (click)="handleClick()">
|
||||
<ng-container>
|
||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="data.coverUrl"></app-image>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container>
|
||||
<div class="badge-container">
|
||||
<div class="not-read-badge" ></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<div class="badge-container">
|
||||
<div class="not-read-badge" ></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="card-overlay"></div>
|
||||
</div>
|
||||
<div class="card-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-body" *ngIf="data.name.length > 0">
|
||||
<div>
|
||||
<div class="card-body" *ngIf="data.name.length > 0">
|
||||
<div>
|
||||
<span class="card-title" placement="top" id="{{data.name}}" [ngbTooltip]="data.name" (click)="handleClick()" tabindex="0">
|
||||
{{data.name}}
|
||||
</span>
|
||||
<span class="card-actions float-end">
|
||||
<span class="card-actions float-end">
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<a #link class="card-title library" [href]="data.url" target="_blank" rel="noreferrer nofollow">{{t('open-external')}}</a>
|
||||
</div>
|
||||
<a #link class="card-title library" [href]="data.url" target="_blank" rel="noreferrer nofollow">Open External</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ import {RouterLinkActive} from "@angular/router";
|
|||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-external-series-card',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ImageComponent, NgbProgressbar, NgbTooltip, ReactiveFormsModule, RouterLinkActive],
|
||||
imports: [CommonModule, ImageComponent, NgbProgressbar, NgbTooltip, ReactiveFormsModule, RouterLinkActive, TranslocoModule],
|
||||
templateUrl: './external-series-card.component.html',
|
||||
styleUrls: ['./external-series-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -1,37 +1,40 @@
|
|||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<ng-container *transloco="let t; read: 'list-item'">
|
||||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" maxHeight="200px" [width]="imageWidth"></app-image>
|
||||
<div class="not-read-badge" *ngIf="pagesRead === 0 && totalPages > 0"></div>
|
||||
<span class="download">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" maxHeight="200px" [width]="imageWidth"></app-image>
|
||||
<div class="not-read-badge" *ngIf="pagesRead === 0 && totalPages > 0"></div>
|
||||
<span class="download">
|
||||
<app-download-indicator [download$]="download$"></app-download-indicator>
|
||||
</span>
|
||||
<div class="progress-banner" *ngIf="pagesRead < totalPages && totalPages > 0 && pagesRead !== totalPages"
|
||||
ngbTooltip="{{(pagesRead / totalPages) | number:'1.0-1'}}% Read">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
||||
</div>
|
||||
<div class="progress-banner" *ngIf="pagesRead < totalPages && totalPages > 0 && pagesRead !== totalPages"
|
||||
ngbTooltip="{{(pagesRead / totalPages) | number:'1.0-1'}}% Read">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-0">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="seriesName" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
<button class="btn btn-primary float-end" (click)="read.emit()">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-0">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="seriesName" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
<button class="btn btn-primary float-end" (click)="read.emit()">
|
||||
<span>
|
||||
<i class="fa fa-book me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">Read</span>
|
||||
</button>
|
||||
</h5>
|
||||
<span class="d-none d-sm-inline-block">{{t('read')}}</span>
|
||||
</button>
|
||||
</h5>
|
||||
|
||||
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" *ngIf="Title !== '' && showTitle">{{Title}}</h6>
|
||||
<ng-container *ngIf="summary.length > 0">
|
||||
<div class="mt-2 ps-2">
|
||||
<app-read-more [text]="summary" [blur]="pagesRead === 0 && blur" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="ps-2 d-none d-md-inline-block">
|
||||
<app-entity-info-cards [entity]="entity" [libraryId]="libraryId" [includeMetadata]="ShowExtended" [showExtendedProperties]="ShowExtended"></app-entity-info-cards>
|
||||
</div>
|
||||
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" *ngIf="Title !== '' && showTitle">{{Title}}</h6>
|
||||
<ng-container *ngIf="summary.length > 0">
|
||||
<div class="mt-2 ps-2">
|
||||
<app-read-more [text]="summary" [blur]="pagesRead === 0 && blur" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="ps-2 d-none d-md-inline-block">
|
||||
<app-entity-info-cards [entity]="entity" [libraryId]="libraryId" [includeMetadata]="ShowExtended" [showExtendedProperties]="ShowExtended"></app-entity-info-cards>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -26,11 +26,12 @@ import {DownloadIndicatorComponent} from "../download-indicator/download-indicat
|
|||
import {EntityInfoCardsComponent} from "../entity-info-cards/entity-info-cards.component";
|
||||
import {CardActionablesComponent} from "../card-item/card-actionables/card-actionables.component";
|
||||
import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReadMoreComponent, ImageComponent, DownloadIndicatorComponent, EntityInfoCardsComponent, CardActionablesComponent, NgbProgressbar, NgbTooltip],
|
||||
imports: [CommonModule, ReadMoreComponent, ImageComponent, DownloadIndicatorComponent, EntityInfoCardsComponent, CardActionablesComponent, NgbProgressbar, NgbTooltip, TranslocoModule],
|
||||
templateUrl: './list-item.component.html',
|
||||
styleUrls: ['./list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -89,6 +90,7 @@ export class ListItemComponent implements OnInit {
|
|||
|
||||
@Output() read: EventEmitter<void> = new EventEmitter<void>();
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
summary: string = '';
|
||||
|
|
@ -134,7 +136,7 @@ export class ListItemComponent implements OnInit {
|
|||
performAction(action: ActionItem<any>) {
|
||||
if (action.action == Action.Download) {
|
||||
if (this.downloadInProgress) {
|
||||
this.toastr.info('Download is already in progress. Please wait.');
|
||||
this.toastr.info(this.translocoService.translate('toasts.download-in-progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {
|
|||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
EventEmitter, inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
|
|
@ -22,6 +22,7 @@ import {CommonModule} from "@angular/common";
|
|||
import {CardItemComponent} from "../card-item/card-item.component";
|
||||
import {RelationshipPipe} from "../../pipe/relationship.pipe";
|
||||
import {Device} from "../../_models/device/device";
|
||||
import {TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-card',
|
||||
|
|
@ -66,6 +67,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
actions: ActionItem<Series>[] = [];
|
||||
imageUrl: string = '';
|
||||
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
|
||||
constructor(private router: Router, private cdRef: ChangeDetectorRef,
|
||||
private seriesService: SeriesService, private toastr: ToastrService,
|
||||
private modalService: NgbModal, private imageService: ImageService,
|
||||
|
|
@ -84,11 +87,12 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
if (this.data) {
|
||||
this.actions = this.actionFactoryService.getSeriesActions((action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||
if (this.isOnDeck) {
|
||||
const othersIndex = this.actions.findIndex(obj => obj.title === 'Others');
|
||||
const otherStr = this.translocoService.translate('actionable.others');
|
||||
const othersIndex = this.actions.findIndex(obj => obj.title === otherStr);
|
||||
if (this.actions[othersIndex].children.findIndex(o => o.action === Action.RemoveFromOnDeck) < 0) {
|
||||
this.actions[othersIndex].children.push({
|
||||
action: Action.RemoveFromOnDeck,
|
||||
title: 'Remove From On Deck',
|
||||
title: this.translocoService.translate('actionable.remove-from-on-deck'),
|
||||
callback: (action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series),
|
||||
class: 'danger',
|
||||
requiresAdmin: false,
|
||||
|
|
@ -171,7 +175,7 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
|
||||
async scanLibrary(series: Series) {
|
||||
this.seriesService.scan(series.libraryId, series.id).subscribe((res: any) => {
|
||||
this.toastr.success('Scan queued for ' + series.name);
|
||||
this.toastr.success(this.translocoService.translate('toasts.scan-queued', {name: series.name}));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,123 +1,126 @@
|
|||
<div class="row g-0 mt-3">
|
||||
<ng-container *transloco="let t; read: 'series-info-cards'">
|
||||
<div class="row g-0 mt-3">
|
||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Release" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release Year">
|
||||
{{seriesMetadata.releaseYear}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-title')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-year-tooltip')">
|
||||
{{seriesMetadata.releaseYear}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata">
|
||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Age Rating" [clickable]="true" fontClasses="fas fa-eye" (click)="handleGoTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" title="Age Rating">
|
||||
{{this.seriesMetadata.ageRating | ageRating | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="true" fontClasses="fas fa-eye" (click)="handleGoTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" [title]="t('age-rating-title')">
|
||||
{{this.seriesMetadata.ageRating | ageRating}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata.language !== null">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Language" [clickable]="true" fontClasses="fas fa-language" (click)="handleGoTo(FilterQueryParam.Languages, seriesMetadata.language)" title="Language">
|
||||
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="seriesMetadata.language !== null">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('language-title')" [clickable]="true" fontClasses="fas fa-language" (click)="handleGoTo(FilterQueryParam.Languages, seriesMetadata.language)" [title]="t('language-title')">
|
||||
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
|
||||
<app-icon-and-title label="Publication" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === 'Ongoing' ? 'empty' : 'end'}}"
|
||||
(click)="handleGoTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||
ngbTooltip="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})">
|
||||
{{pubStatus}}
|
||||
</app-icon-and-title>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="vr m-2 d-none d-lg-block"></div>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
|
||||
<app-icon-and-title [label]="t('publication-status-title')" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}"
|
||||
(click)="handleGoTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||
[ngbTooltip]="t('publication-status-tooltip') + ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')'">
|
||||
{{pubStatus}}
|
||||
</app-icon-and-title>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="vr m-2 d-none d-lg-block"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="accountService.hasValidLicense$ | async">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Scrobbling" [clickable]="libraryAllowsScrobbling"
|
||||
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
|
||||
(click)="toggleScrobbling($event)"
|
||||
ngbTooltip="Scrobbling Status">
|
||||
<ng-container *ngIf="libraryAllowsScrobbling; else noScrobble">
|
||||
{{ isScrobbling ? 'On' : 'Off' }}
|
||||
</ng-container>
|
||||
<ng-template #noScrobble>
|
||||
Disabled
|
||||
</ng-template>
|
||||
<app-icon-and-title [label]="t('scrobbling-title')" [clickable]="libraryAllowsScrobbling"
|
||||
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
|
||||
(click)="toggleScrobbling($event)"
|
||||
[ngbTooltip]="t('scrobbling-tooltip')">
|
||||
<ng-container *ngIf="libraryAllowsScrobbling; else noScrobble">
|
||||
{{ isScrobbling ? t('on') : t('off') }}
|
||||
</ng-container>
|
||||
<ng-template #noScrobble>
|
||||
{{t('disabled')}}
|
||||
</ng-template>
|
||||
|
||||
</app-icon-and-title>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series">
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Format" [clickable]="true"
|
||||
[fontClasses]="'fa ' + (series.format | mangaFormatIcon)"
|
||||
(click)="handleGoTo(FilterQueryParam.Format, series.format)" title="Format">
|
||||
{{series.format | mangaFormat}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('format-title')" [clickable]="true"
|
||||
[fontClasses]="'fa ' + (series.format | mangaFormatIcon)"
|
||||
(click)="handleGoTo(FilterQueryParam.Format, series.format)" [title]="t('format-title')">
|
||||
{{series.format | mangaFormat}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [title]="t('last-read-title')">
|
||||
{{series.latestReadDate | timeAgo}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
||||
<ng-container *ngIf="series.wordCount > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{t('words-count', {num: series.wordCount | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Last Read" [clickable]="false" fontClasses="fa-regular fa-clock" title="Last Read">
|
||||
{{series.latestReadDate | timeAgo}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #showPages>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{t('pages-count', {num: series.pages | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
||||
<ng-container *ngIf="series.wordCount > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{series.wordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
<ng-template #showPages>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{series.pages | compactNumber}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime"><1 Hour</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||
~{{readingTimeLeft.avgHours}} Hour{{readingTimeLeft.avgHours > 1 ? 's' : ''}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||
~{{readingTimeLeft.avgHours}} {{readingTimeLeft.avgHours > 1 ? t('hours') : t('hour')}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -33,11 +33,12 @@ import {TimeAgoPipe} from "../../pipe/time-ago.pipe";
|
|||
import {CompactNumberPipe} from "../../pipe/compact-number.pipe";
|
||||
import {MangaFormatIconPipe} from "../../pipe/manga-format-icon.pipe";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-info-cards',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconAndTitleComponent, AgeRatingPipe, DefaultValuePipe, LanguageNamePipe, PublicationStatusPipe, MangaFormatPipe, TimeAgoPipe, CompactNumberPipe, MangaFormatIconPipe, NgbTooltip],
|
||||
imports: [CommonModule, IconAndTitleComponent, AgeRatingPipe, DefaultValuePipe, LanguageNamePipe, PublicationStatusPipe, MangaFormatPipe, TimeAgoPipe, CompactNumberPipe, MangaFormatIconPipe, NgbTooltip, TranslocoModule],
|
||||
templateUrl: './series-info-cards.component.html',
|
||||
styleUrls: ['./series-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue