Round 3 of Bugfixing (#3318)

This commit is contained in:
Joe Milazzo 2024-10-28 18:13:48 -05:00 committed by GitHub
parent 727fbd353b
commit abdf15b895
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 482 additions and 489 deletions

View file

@ -1,3 +1,5 @@
import {IHasCover} from "../common/i-has-cover";
export enum PersonRole {
Other = 1,
Artist = 2,
@ -16,7 +18,7 @@ export enum PersonRole {
Location = 15
}
export interface Person {
export interface Person extends IHasCover {
id: number;
name: string;
description: string;
@ -26,6 +28,6 @@ export interface Person {
aniListId?: number;
hardcoverId?: string;
asin?: string;
primaryColor?: string;
secondaryColor?: string;
primaryColor: string;
secondaryColor: string;
}

View file

@ -9,16 +9,10 @@ import {shareReplay} from "rxjs/operators";
})
export class LanguageNamePipe implements PipeTransform {
constructor(private metadataService: MetadataService) {
}
constructor(private metadataService: MetadataService) {}
transform(isoCode: string): Observable<string> {
// TODO: See if we can speed this up. It rarely changes and is quite heavy to download on each page
return this.metadataService.getAllValidLanguages().pipe(map(lang => {
const l = lang.filter(l => l.isoCode === isoCode);
if (l.length > 0) return l[0].title;
return '';
}), shareReplay());
return this.metadataService.getLanguageNameForCode(isoCode).pipe(shareReplay());
}
}

View file

@ -18,6 +18,7 @@ import {FilterStatement} from "../_models/metadata/v2/filter-statement";
import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus";
import {LibraryType} from "../_models/library/library";
import {IHasCast} from "../_models/common/i-has-cast";
import {TextResonse} from "../_types/text-response";
@Injectable({
providedIn: 'root'
@ -77,6 +78,10 @@ export class MetadataService {
return this.httpClient.get<Array<Language>>(this.baseUrl + method);
}
getLanguageNameForCode(code: string) {
return this.httpClient.get<string>(`${this.baseUrl}metadata/language-title?code=${code}`, TextResonse);
}
/**
* All the potential language tags there can be

View file

@ -1,5 +1,43 @@
<ng-container *transloco="let t; read: 'details-tab'">
<div class="details pb-3">
@if (readingTime) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('read-time-title')}}</h4>
<div class="ms-3">
{{readingTime | readTime}}
</div>
</div>
}
@if (releaseYear) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('release-title')}}</h4>
<div class="ms-3">
{{releaseYear}}
</div>
</div>
}
@if (language) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('language-title')}}</h4>
<div class="ms-3">
{{language | languageName | async}}
</div>
</div>
}
<div class="mb-3 ms-1">
<h4 class="header">{{t('format-title')}}</h4>
<div class="ms-3">
<app-series-format [format]="format"></app-series-format> {{format | mangaFormat }}
</div>
</div>
<div class="setting-section-break" aria-hidden="true"></div>
<div class="mb-3 ms-1">
<h4 class="header">{{t('genres-title')}}</h4>
<div class="ms-3">

View file

@ -3,18 +3,25 @@ import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/ca
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
import {TranslocoDirective} from "@jsverse/transloco";
import {IHasCast} from "../../_models/common/i-has-cast";
import {Person, PersonRole} from "../../_models/metadata/person";
import {Router} from "@angular/router";
import {PersonRole} from "../../_models/metadata/person";
import {FilterField} from "../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
import {Genre} from "../../_models/metadata/genre";
import {Tag} from "../../_models/tag";
import {TagBadgeComponent, TagBadgeCursor} from "../../shared/tag-badge/tag-badge.component";
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
import {ImageComponent} from "../../shared/image/image.component";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {ImageService} from "../../_services/image.service";
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
import {IHasReadingTime} from "../../_models/common/i-has-reading-time";
import {ReadTimePipe} from "../../_pipes/read-time.pipe";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
import {MangaFormat} from "../../_models/manga-format";
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
import {MangaFormatPipe} from "../../_pipes/manga-format.pipe";
import {LanguageNamePipe} from "../../_pipes/language-name.pipe";
import {AsyncPipe} from "@angular/common";
@Component({
selector: 'app-details-tab',
@ -26,7 +33,13 @@ import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander
TagBadgeComponent,
ImageComponent,
SafeHtmlPipe,
BadgeExpanderComponent
BadgeExpanderComponent,
ReadTimePipe,
SentenceCasePipe,
SeriesFormatComponent,
MangaFormatPipe,
LanguageNamePipe,
AsyncPipe
],
templateUrl: './details-tab.component.html',
styleUrl: './details-tab.component.scss',
@ -41,6 +54,10 @@ export class DetailsTabComponent {
protected readonly FilterField = FilterField;
@Input({required: true}) metadata!: IHasCast;
@Input() readingTime: IHasReadingTime | undefined;
@Input() language: string | undefined;
@Input() format: MangaFormat = MangaFormat.UNKNOWN;
@Input() releaseYear: number | undefined;
@Input() genres: Array<Genre> = [];
@Input() tags: Array<Tag> = [];
@Input() webLinks: Array<string> = [];
@ -50,4 +67,6 @@ export class DetailsTabComponent {
if (queryParamName === FilterField.None) return;
this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe();
}
protected readonly MangaFormat = MangaFormat;
}

View file

@ -493,7 +493,7 @@ export class EditChapterModalComponent implements OnInit {
};
personSettings.addTransformFn = ((title: string) => {
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false };
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
});
return personSettings;

View file

@ -52,7 +52,7 @@
<div class="form-group mb-3">
<label for="discordId">{{t('activate-discordId-label')}}</label>
<i class="fa fa-circle-info ms-1" aria-hidden="true" [ngbTooltip]="t('activate-discordId-tooltip')"></i>
<a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">Help</a>
<a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
<input id="discordId" type="text" class="form-control" formControlName="discordId" autocomplete="off" [class.is-invalid]="formGroup.get('discordId')?.invalid && formGroup.get('discordId')?.touched"/>
@if (formGroup.dirty || formGroup.touched) {
<div id="inviteForm-validations" class="invalid-feedback">

View file

@ -14,6 +14,6 @@
</app-side-nav-companion-bar>
<app-manage-smart-filters></app-manage-smart-filters>
<app-manage-smart-filters [target]="'_self'"></app-manage-smart-filters>
</div>
</ng-container>

View file

@ -198,7 +198,7 @@
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'publicationStatusLocked' }"></ng-container>
<select class="form-select" id="publication-status" formControlName="publicationStatus">
@for (opt of publicationStatuses; track opt.value) {
<option [value]="opt.value">{{opt.title | titlecase}}</option>
<option [value]="opt.value">{{opt.value | publicationStatus}}</option>
}
</select>
</div>

View file

@ -515,7 +515,7 @@ export class EditSeriesModalComponent implements OnInit {
};
personSettings.addTransformFn = ((title: string) => {
return {id: 0, name: title, description: '', coverImageLocked: false };
return {id: 0, name: title, description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
});
return personSettings;
@ -551,6 +551,7 @@ export class EditSeriesModalComponent implements OnInit {
// We only need to call updateSeries if we changed name, sort name, or localized name or reset a cover image
const nameFieldsDirty = this.editSeriesForm.get('name')?.dirty || this.editSeriesForm.get('sortName')?.dirty || this.editSeriesForm.get('localizedName')?.dirty;
const nameFieldLockChanged = this.series.nameLocked !== this.initSeries.nameLocked || this.series.sortNameLocked !== this.initSeries.sortNameLocked || this.series.localizedNameLocked !== this.initSeries.localizedNameLocked;
if (nameFieldsDirty || nameFieldLockChanged || this.coverImageReset) {
model.nameLocked = this.series.nameLocked;
model.sortNameLocked = this.series.sortNameLocked;

View file

@ -46,12 +46,12 @@
<div class="card-overlay"></div>
<div class="chapter overlay-information">
<div class="overlay-information--centered">
<span class="card-title library mx-auto" style="width: auto;" (click)="read($event)">
<!-- Card Image -->
<div>
<i class="fa-solid fa-book" aria-hidden="true"></i>
</div>
</span>
<span class="card-title library mx-auto" style="width: auto;" (click)="read($event)">
<!-- Card Image -->
<div>
<i class="fa-solid fa-book" aria-hidden="true"></i>
</div>
</span>
</div>
</div>
</div>
@ -85,10 +85,10 @@
</a>
</span>
<span class="card-actions">
@if (actions && actions.length > 0) {
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
}
</span>
@if (actions && actions.length > 0) {
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
}
</span>
</div>
</div>

View file

@ -1,32 +1,31 @@
<ng-container *transloco="let t; read: 'cover-image-chooser'">
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
<div class="container-fluid" style="padding-left: 0; padding-right: 0">
<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>
@if (mode === 'all') {
<div class="row g-0 mt-3 pb-3">
<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 class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
</a>
<span class="ps-1 pe-1"></span>
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
<a class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
</a>
<span class="ps-1 pe-1"></span>
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
</div>
</div>
</div>
</div>
</div>
<ng-container *ngIf="mode === 'url'">
} @else if (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>
@ -42,9 +41,7 @@
<span class="phone-hidden">{{t('back')}}</span>
</button>
</div>
</ng-container>
}
</ng-template>
</ngx-file-drop>
@ -54,28 +51,32 @@
</form>
<div class="row g-0 chooser" style="padding-top: 10px">
<div class="clickable 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="232.91px" width="160px" [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>
@if (showReset) {
<div class="clickable col-auto" tabindex="0" (click)="reset()"
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="232.91px" width="160px" [imageUrl]="imageService.resetCoverImage"></app-image>
@if (showApplyButton) {
<br>
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
}
</div>
}
@for (url of imageUrls; track url; let idx = $index) {
<div class="clickable col-auto" 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="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
@if (showApplyButton) {
<br>
<button class="btn btn-primary" style="width: 100%;"
(click)="applyImage(idx)">
{{appliedIndex === idx ? t('applied') : t('apply')}}
</button>
}
</div>
}
</div>
<div class="clickable 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="232.91px" width="160px" [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>

View file

@ -17,7 +17,7 @@ import { ToastrService } from 'ngx-toastr';
import { ImageService } from 'src/app/_services/image.service';
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 {DOCUMENT, NgClass} from '@angular/common';
import {ImageComponent} from "../../shared/image/image.component";
import {translate, TranslocoModule} from "@jsverse/transloco";
@ -27,9 +27,9 @@ import {translate, TranslocoModule} from "@jsverse/transloco";
imports: [
ReactiveFormsModule,
NgxFileDropModule,
CommonModule,
ImageComponent,
TranslocoModule
TranslocoModule,
NgClass
],
templateUrl: './cover-image-chooser.component.html',
styleUrls: ['./cover-image-chooser.component.scss'],

View file

@ -1,87 +1,94 @@
<ng-container *transloco="let t; read: 'entity-title'">
@switch (libraryType) {
@case (LibraryType.Comic) {
@if (titleName !== '' && prioritizeTitleName) {
@if (isChapter && includeChapter) {
{{t('issue-num') + ' ' + number + ' - ' }}
}
{{renderText | defaultValue}}
<!-- @switch (libraryType) {-->
<!-- @case (LibraryType.Comic) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
<!-- }-->
{{titleName}}
} @else {
@if (includeVolume && volumeTitle !== '') {
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
}
{{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}
}
}
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
<!-- }-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}-->
<!-- }-->
<!-- }-->
@case (LibraryType.ComicVine) {
@if (titleName !== '' && prioritizeTitleName) {
@if (isChapter && includeChapter) {
{{t('issue-num') + ' ' + number + ' - ' }}
}
<!-- @case (LibraryType.ComicVine) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
<!-- }-->
{{titleName}}
} @else {
@if (includeVolume && volumeTitle !== '') {
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
}
{{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}
}
}
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
<!-- }-->
<!-- @if (number !== LooseLeafOrSpecial) {-->
<!-- {{isChapter ? t('issue-num') + number : volumeTitle}}-->
<!-- } @else {-->
<!-- {{t('special')}}-->
<!-- }-->
<!-- }-->
<!-- }-->
@case (LibraryType.Manga) {
@if (titleName !== '' && prioritizeTitleName) {
@if (isChapter && includeChapter) {
@if (number === LooseLeafOrSpecial) {
{{t('chapter') + ' - ' }}
} @else {
{{t('chapter') + ' ' + number + ' - ' }}
}
<!-- @case (LibraryType.Manga) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- @if (number === LooseLeafOrSpecial) {-->
<!-- {{t('chapter') + ' - ' }}-->
<!-- } @else {-->
<!-- {{t('chapter') + ' ' + number + ' - ' }}-->
<!-- }-->
}
{{titleName}}
} @else {
@if (includeVolume && volumeTitle !== '') {
@if (number !== LooseLeafOrSpecial && isChapter && includeVolume) {
{{volumeTitle}}
}
}
<!-- }-->
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- @if (number !== LooseLeafOrSpecial && isChapter && includeVolume) {-->
<!-- {{volumeTitle}}-->
<!-- }-->
<!-- }-->
@if (number !== LooseLeafOrSpecial) {
@if (isChapter) {
{{t('chapter') + ' ' + number}}
} @else {
{{volumeTitle}}
}
} @else {
{{t('special')}}
}
}
}
<!-- @if (number !== LooseLeafOrSpecial) {-->
<!-- @if (isChapter) {-->
<!-- {{t('chapter') + ' ' + number}}-->
<!-- } @else {-->
<!-- {{volumeTitle}}-->
<!-- }-->
<!-- } @else if (fallbackToVolume && isChapter && volumeTitle) {-->
<!-- {{t('vol-num', {num: volumeTitle})}}-->
<!-- } @else {-->
<!-- {{t('special')}}-->
<!-- }-->
<!-- }-->
<!-- }-->
@case (LibraryType.Book) {
@if (titleName !== '' && prioritizeTitleName) {
{{titleName}}
} @else if (number === LooseLeafOrSpecial) {
{{null | defaultValue}}
} @else {
{{t('book-num', {num: volumeTitle})}}
}
}
<!-- @case (LibraryType.Book) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- {{titleName}}-->
<!-- } @else if (number === LooseLeafOrSpecial) {-->
<!-- {{null | defaultValue}}-->
<!-- } @else {-->
<!-- {{t('book-num', {num: volumeTitle})}}-->
<!-- }-->
<!-- }-->
@case (LibraryType.LightNovel) {
@if (titleName !== '' && prioritizeTitleName) {
{{titleName}}
} @else if (number === LooseLeafOrSpecial) {
{{null | defaultValue}}
} @else {
{{t('book-num', {num: (isChapter ? number : volumeTitle)})}}
}
}
<!-- @case (LibraryType.LightNovel) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- {{titleName}}-->
<!-- } @else if (number === LooseLeafOrSpecial) {-->
<!-- {{null | defaultValue}}-->
<!-- } @else {-->
<!-- {{t('book-num', {num: (isChapter ? number : volumeTitle)})}}-->
<!-- }-->
<!-- }-->
@case (LibraryType.Images) {
{{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}
}
}
<!-- @case (LibraryType.Images) {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}-->
<!-- }-->
<!-- }-->
</ng-container>

View file

@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter, LooseLeafOrDefaultNumber } from 'src/app/_models/chapter';
import { LibraryType } from 'src/app/_models/library/library';
import { Volume } from 'src/app/_models/volume';
import {TranslocoModule} from "@jsverse/transloco";
import {translate, TranslocoModule} from "@jsverse/transloco";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
/**
@ -22,6 +22,9 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
})
export class EntityTitleComponent implements OnInit {
private readonly utilityService = inject(UtilityService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly LooseLeafOrSpecial = LooseLeafOrDefaultNumber + "";
protected readonly LibraryType = LibraryType;
@ -42,16 +45,18 @@ export class EntityTitleComponent implements OnInit {
* When a titleName (aka a title) is available on the entity, show it over Volume X Chapter Y
*/
@Input() prioritizeTitleName: boolean = true;
/**
* When there is no meaningful title to display and the chapter is just a single volume, show the volume number
*/
@Input() fallbackToVolume: boolean = true;
isChapter = false;
titleName: string = '';
volumeTitle: string = '';
number: string = '';
renderText: string = '';
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.isChapter = this.utilityService.isChapter(this.entity);
@ -60,7 +65,6 @@ export class EntityTitleComponent implements OnInit {
this.volumeTitle = c.volumeTitle || '';
this.titleName = c.titleName || '';
this.number = c.range;
} else {
const v = this.utilityService.asVolume(this.entity);
this.volumeTitle = v.name || '';
@ -70,6 +74,125 @@ export class EntityTitleComponent implements OnInit {
}
this.number = v.name;
}
this.calculateRenderText();
this.cdRef.markForCheck();
}
private calculateRenderText() {
switch (this.libraryType) {
case LibraryType.Manga:
this.renderText = this.calculateMangaRenderText();
break;
case LibraryType.Comic:
this.renderText = this.calculateComicRenderText();
break;
case LibraryType.Book:
this.renderText = this.calculateBookRenderText();
break;
case LibraryType.Images:
this.renderText = this.calculateImageRenderText();
break;
case LibraryType.LightNovel:
this.renderText = this.calculateLightNovelRenderText();
break;
case LibraryType.ComicVine:
this.renderText = this.calculateComicRenderText();
break;
}
this.cdRef.markForCheck();
}
private calculateBookRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
renderText = this.titleName;
} else if (this.number === this.LooseLeafOrSpecial) {
renderText = '';
} else {
renderText = translate('entity-title.book-num', {num: this.volumeTitle});
}
return renderText;
}
private calculateLightNovelRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
renderText = this.titleName;
} else if (this.number === this.LooseLeafOrSpecial) {
renderText = '';
} else {
const bookNum = this.isChapter ? this.number : this.volumeTitle;
renderText = translate('entity-title.book-num', {num: bookNum});
}
return renderText;
}
private calculateMangaRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
if (this.isChapter && this.includeChapter) {
if (this.number === this.LooseLeafOrSpecial) {
renderText = translate('entity-title.chapter') + ' - ';
} else {
renderText = translate('entity-title.chapter') + ' ' + this.number + ' - ';
}
}
renderText += this.titleName;
} else {
if (this.includeVolume && this.volumeTitle !== '') {
if (this.number !== this.LooseLeafOrSpecial && this.isChapter && this.includeVolume) {
renderText = this.volumeTitle;
}
}
if (this.number !== this.LooseLeafOrSpecial) {
if (this.isChapter) {
renderText = translate('entity-title.chapter') + ' ' + this.number;
} else {
renderText = this.volumeTitle;
}
} else if (this.fallbackToVolume && this.isChapter && this.volumeTitle) {
renderText = translate('entity-title.vol-num', {num: this.volumeTitle});
} else if (this.fallbackToVolume && this.isChapter) { // this.volumeTitle === '' (this is a single volume on volume detail page)
renderText = translate('entity-title.single-volume');
} else {
renderText = translate('entity-title.special');
}
}
return renderText;
}
private calculateImageRenderText() {
let renderText = '';
if (this.number !== this.LooseLeafOrSpecial) {
if (this.isChapter) {
renderText = translate('entity-title.chapter') + ' ' + this.number;
} else {
renderText = this.volumeTitle;
}
} else {
renderText = translate('entity-title.special');
}
return renderText;
}
private calculateComicRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
if (this.isChapter && this.includeChapter) {
renderText = translate('entity-title.issue-num') + ' ' + this.number + ' - ';
}
renderText += this.titleName;
}
return renderText;
}
}

View file

@ -163,7 +163,13 @@
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="chapter" [genres]="chapter.genres" [tags]="chapter.tags" [webLinks]="weblinks"></app-details-tab>
<app-details-tab [metadata]="chapter"
[genres]="chapter.genres"
[tags]="chapter.tags"
[webLinks]="weblinks"
[readingTime]="chapter"
[language]="chapter.language"
[format]="series.format"></app-details-tab>
}
</ng-template>
</li>

View file

@ -59,7 +59,9 @@
<div class="row mt-2">
<app-carousel-reel [items]="(chaptersByRole[role] | async)!" [title]="t('individual-role-title', {role: (role | personRole)})" (sectionClick)="loadFilterByRole(role)">
<ng-template #carouselItem let-item>
<app-chapter-card [chapter]="item" [libraryId]="item.libraryId" [libraryType]="item.libraryType" [seriesId]="item.seriesId"></app-chapter-card>
<app-chapter-card [chapter]="item" [libraryId]="item.libraryId" [libraryType]="item.libraryType" [seriesId]="item.seriesId">
</app-chapter-card>
</ng-template>
</app-carousel-reel>
</div>

View file

@ -165,7 +165,7 @@
<!-- Spacer -->
<div class="col" aria-hidden="true"></div>
<div class="col-auto ms-1">
<a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">Help</a>
<a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
</div>
<div class="col-auto ms-1">
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>

View file

@ -187,7 +187,7 @@
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for(item of scroll.viewPortItems; let idx = $index; track item) {
@if (item.isChapter) {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container>
} @else {
@ -214,7 +214,7 @@
@defer (when activeTabId === TabID.Volumes; prefetch on idle) {
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + + '_volumes') {
<ng-container [ngTemplateOutlet]="nonChapterVolumeCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: volumes.length}"></ng-container>
}
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container>
@ -235,7 +235,7 @@
@defer (when activeTabId === TabID.Chapters; prefetch on idle) {
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_chapters') {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: chapters.length}"></ng-container>
}
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container>
@ -256,7 +256,7 @@
@defer (when activeTabId === TabID.Specials; prefetch on idle) {
<virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_specials') {
<ng-container [ngTemplateOutlet]="specialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, chaptersLength: chapters.length}"></ng-container>
}
</div>
@ -338,7 +338,15 @@
<a ngbNavLink>{{t(TabID.Details)}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="seriesMetadata" [genres]="seriesMetadata.genres" [tags]="seriesMetadata.tags" [webLinks]="WebLinks"></app-details-tab>
<app-details-tab [metadata]="seriesMetadata"
[genres]="seriesMetadata.genres"
[tags]="seriesMetadata.tags"
[webLinks]="WebLinks"
[readingTime]="series"
[releaseYear]="seriesMetadata.releaseYear"
[language]="seriesMetadata.language"
[format]="series.format">
</app-details-tab>
}
</ng-template>
</li>

View file

@ -19,7 +19,7 @@
<i class="fa-solid fa-triangle-exclamation red me-2" [ngbTooltip]="t('errored')"></i>
<span class="visually-hidden">{{t('errored')}}</span>
}
<a [href]="baseUrl + 'all-series?' + f.filter" target="_blank">{{f.name}}</a>
<a [href]="baseUrl + 'all-series?' + f.filter" [target]="target">{{f.name}}</a>
</span>
<button class="btn btn-danger float-end" (click)="deleteFilter(f)">
<i class="fa-solid fa-trash" aria-hidden="true"></i>

View file

@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input} from '@angular/core';
import {FilterService} from "../../../_services/filter.service";
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
import {TranslocoDirective} from "@jsverse/transloco";
@ -24,6 +24,8 @@ export class ManageSmartFiltersComponent {
private readonly actionService = inject(ActionService);
protected readonly baseUrl = inject(APP_BASE_HREF);
@Input() target: '_self' | '_blank' = '_blank';
filters: Array<SmartFilter> = [];
listForm: FormGroup = new FormGroup({
'filterQuery': new FormControl('', [])

View file

@ -196,7 +196,12 @@
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="volumeCast" [genres]="genres" [tags]="tags"></app-details-tab>
<app-details-tab [metadata]="volumeCast"
[genres]="genres"
[tags]="tags"
[readingTime]="volume"
[language]="volume.chapters[0].language"
[format]="series.format"></app-details-tab>
}
</ng-template>
</li>