Comic Rework, New Scanner, Foundation Overahul (is this a full release?) (#2780)

This commit is contained in:
Joe Milazzo 2024-03-17 12:58:32 -05:00 committed by GitHub
parent d7e9e7c832
commit 7552c3f5fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
182 changed files with 27630 additions and 3046 deletions

View file

@ -1,7 +1,8 @@
import { MangaFile } from './manga-file';
import { AgeRating } from './metadata/age-rating';
export const LooseLeafOrSpecialNumber = 0;
export const LooseLeafOrDefaultNumber = -100000;
export const SpecialVolumeNumber = 100000;
/**
* Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields.
@ -9,7 +10,12 @@ export const LooseLeafOrSpecialNumber = 0;
export interface Chapter {
id: number;
range: string;
/**
* @deprecated Use minNumber/maxNumber
*/
number: string;
minNumber: number;
maxNumber: number;
files: Array<MangaFile>;
/**
* This is used in the UI, it is not updated or sent to Backend
@ -44,4 +50,5 @@ export interface Chapter {
webLinks: string;
isbn: string;
lastReadingProgress: string;
sortOrder: number;
}

View file

@ -5,7 +5,8 @@ export enum LibraryType {
Comic = 1,
Book = 2,
Images = 3,
LightNovel = 4
LightNovel = 4,
ComicVine = 5
}
export interface Library {

View file

@ -18,8 +18,8 @@ export interface ChapterMetadata {
count: number;
totalCount: number;
wordCount: number;
genres: Array<Genre>;
tags: Array<Tag>;
@ -29,11 +29,14 @@ export interface ChapterMetadata {
characters: Array<Person>;
pencillers: Array<Person>;
inkers: Array<Person>;
imprints: Array<Person>;
colorists: Array<Person>;
letterers: Array<Person>;
editors: Array<Person>;
translators: Array<Person>;
teams: Array<Person>;
locations: Array<Person>;
}
}

View file

@ -1,20 +1,23 @@
export enum PersonRole {
Other = 1,
Artist = 2,
Writer = 3,
Penciller = 4,
Inker = 5,
Colorist = 6,
Letterer = 7,
CoverArtist = 8,
Editor = 9,
Publisher = 10,
Character = 11,
Translator = 12
Other = 1,
Artist = 2,
Writer = 3,
Penciller = 4,
Inker = 5,
Colorist = 6,
Letterer = 7,
CoverArtist = 8,
Editor = 9,
Publisher = 10,
Character = 11,
Translator = 12,
Imprint = 13,
Team = 14,
Location = 15
}
export interface Person {
id: number;
name: string;
role: PersonRole;
}
}

View file

@ -21,10 +21,13 @@ export interface SeriesMetadata {
characters: Array<Person>;
pencillers: Array<Person>;
inkers: Array<Person>;
imprints: Array<Person>;
colorists: Array<Person>;
letterers: Array<Person>;
editors: Array<Person>;
translators: Array<Person>;
teams: Array<Person>;
locations: Array<Person>;
ageRating: AgeRating;
releaseYear: number;
language: string;
@ -40,10 +43,13 @@ export interface SeriesMetadata {
characterLocked: boolean;
pencillerLocked: boolean;
inkerLocked: boolean;
imprintLocked: boolean;
coloristLocked: boolean;
lettererLocked: boolean;
editorLocked: boolean;
translatorLocked: boolean;
teamLocked: boolean;
locationLocked: boolean;
ageRatingLocked: boolean;
releaseYearLocked: boolean;
languageLocked: boolean;

View file

@ -29,7 +29,10 @@ export enum FilterField
FilePath = 25,
WantToRead = 26,
ReadingDate = 27,
AverageRating = 28
AverageRating = 28,
Imprint = 29,
Team = 30,
Location = 31
}

View file

@ -15,4 +15,5 @@ export interface RelatedSeries {
doujinshis: Array<Series>;
parent: Array<Series>;
editions: Array<Series>;
annuals: Array<Series>;
}

View file

@ -14,7 +14,8 @@ export enum RelationKind {
* This is UI only. Backend will generate Parent series for everything but Prequel/Sequel
*/
Parent = 12,
Edition = 13
Edition = 13,
Annual = 14
}
const RelationKindsUnsorted = [
@ -22,6 +23,7 @@ const RelationKindsUnsorted = [
{text: 'Sequel', value: RelationKind.Sequel},
{text: 'Spin Off', value: RelationKind.SpinOff},
{text: 'Adaptation', value: RelationKind.Adaptation},
{text: 'Annual', value: RelationKind.Annual},
{text: 'Alternative Setting', value: RelationKind.AlternativeSetting},
{text: 'Alternative Version', value: RelationKind.AlternativeVersion},
{text: 'Side Story', value: RelationKind.SideStory},

View file

@ -8,7 +8,6 @@ import {TranslocoService} from "@ngneat/transloco";
})
export class DefaultDatePipe implements PipeTransform {
// TODO: Figure out how to translate Never
constructor(private translocoService: TranslocoService) {
}
transform(value: any, replacementString = 'default-date-pipe.never'): string {

View file

@ -28,6 +28,12 @@ export class FilterFieldPipe implements PipeTransform {
return translate('filter-field-pipe.genres');
case FilterField.Inker:
return translate('filter-field-pipe.inker');
case FilterField.Imprint:
return translate('filter-field-pipe.imprint');
case FilterField.Team:
return translate('filter-field-pipe.team');
case FilterField.Location:
return translate('filter-field-pipe.location');
case FilterField.Languages:
return translate('filter-field-pipe.languages');
case FilterField.Libraries:

View file

@ -18,8 +18,14 @@ export class LibraryTypePipe implements PipeTransform {
return this.translocoService.translate('library-type-pipe.book');
case LibraryType.Comic:
return this.translocoService.translate('library-type-pipe.comic');
case LibraryType.ComicVine:
return this.translocoService.translate('library-type-pipe.comicVine');
case LibraryType.Images:
return this.translocoService.translate('library-type-pipe.image');
case LibraryType.Manga:
return this.translocoService.translate('library-type-pipe.manga');
case LibraryType.LightNovel:
return this.translocoService.translate('library-type-pipe.lightNovel');
default:
return '';
}

View file

@ -29,8 +29,16 @@ export class PersonRolePipe implements PipeTransform {
return this.translocoService.translate('person-role-pipe.penciller');
case PersonRole.Publisher:
return this.translocoService.translate('person-role-pipe.publisher');
case PersonRole.Imprint:
return this.translocoService.translate('person-role-pipe.imprint');
case PersonRole.Writer:
return this.translocoService.translate('person-role-pipe.writer');
case PersonRole.Team:
return this.translocoService.translate('person-role-pipe.team');
case PersonRole.Location:
return this.translocoService.translate('person-role-pipe.location');
case PersonRole.Translator:
return this.translocoService.translate('person-role-pipe.translator');
case PersonRole.Other:
return this.translocoService.translate('person-role-pipe.other');
default:

View file

@ -39,6 +39,8 @@ export class RelationshipPipe implements PipeTransform {
return this.translocoService.translate('relationship-pipe.parent');
case RelationKind.Edition:
return this.translocoService.translate('relationship-pipe.edition');
case RelationKind.Annual:
return this.translocoService.translate('relationship-pipe.annual');
default:
return '';
}

View file

@ -199,10 +199,11 @@ export class SeriesService {
updateRelationships(seriesId: number, adaptations: Array<number>, characters: Array<number>,
contains: Array<number>, others: Array<number>, prequels: Array<number>,
sequels: Array<number>, sideStories: Array<number>, spinOffs: Array<number>,
alternativeSettings: Array<number>, alternativeVersions: Array<number>, doujinshis: Array<number>, editions: Array<number>) {
alternativeSettings: Array<number>, alternativeVersions: Array<number>,
doujinshis: Array<number>, editions: Array<number>, annuals: Array<number>) {
return this.httpClient.post(this.baseUrl + 'series/update-related?seriesId=' + seriesId,
{seriesId, adaptations, characters, sequels, prequels, contains, others, sideStories, spinOffs,
alternativeSettings, alternativeVersions, doujinshis, editions});
alternativeSettings, alternativeVersions, doujinshis, editions, annuals});
}
getSeriesDetail(seriesId: number) {

View file

@ -62,7 +62,15 @@
<td>
<ng-container [ngSwitch]="item.scrobbleEventType">
<ng-container *ngSwitchCase="ScrobbleEventType.ChapterRead">
{{t('volume-and-chapter-num', {v: item.volumeNumber, n: item.chapterNumber})}}
@if(item.volumeNumber === SpecialVolumeNumber) {
{{t('chapter-num', {num: item.volumeNumber})}}
} @else if (item.chapterNumber === LooseLeafOrDefaultNumber) {
{{t('volume-num', {num: item.volumeNumber})}}
} @else if (item.chapterNumber === LooseLeafOrDefaultNumber && item.volumeNumber === SpecialVolumeNumber) {
} @else {
{{t('volume-and-chapter-num', {v: item.volumeNumber, n: item.chapterNumber})}}
}
</ng-container>
<ng-container *ngSwitchCase="ScrobbleEventType.ScoreUpdated">
{{t('rating', {r: item.rating})}}

View file

@ -16,6 +16,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
import {ToastrService} from "ngx-toastr";
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
@Component({
selector: 'app-user-scrobble-history',
@ -101,4 +102,6 @@ export class UserScrobbleHistoryComponent implements OnInit {
}
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
protected readonly LooseLeafOrDefaultNumber = LooseLeafOrDefaultNumber;
}

View file

@ -49,7 +49,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy {
this.hubConnection.on('SendLogAsObject', resp => {
const payload = resp.arguments[0] as LogMessage;
const logMessage = {timestamp: payload.timestamp, level: payload.level, message: payload.message, exception: payload.exception};
// TODO: It might be better to just have a queue to show this
// NOTE: It might be better to just have a queue to show this
const values = this.logsSource.getValue();
values.push(logMessage);
this.logsSource.next(values);
@ -60,7 +60,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
// unsubscrbe from signalr connection
// unsubscribe from signalr connection
if (this.hubConnection) {
this.hubConnection.stop().catch(err => console.error(err));
console.log('Stoping log connection');

View file

@ -229,6 +229,23 @@
</app-typeahead>
</div>
</div>
<div class="row g-0">
<div class="mb-3">
<label for="imprint" class="form-label">{{t('imprint-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Imprint);metadata.publisherLocked = true" [settings]="getPersonsSettings(PersonRole.Imprint)"
[(locked)]="metadata.imprintLocked" (onUnlock)="metadata.imprintLocked = false"
(newItemAdded)="metadata.imprintLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.name}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="row g-0">
<div class="mb-3">
<label for="penciller" class="form-label">{{t('penciller-label')}}</label>
@ -310,7 +327,21 @@
</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);metadata.translatorLocked = true;" [settings]="getPersonsSettings(PersonRole.Translator)"
[(locked)]="metadata.translatorLocked" (onUnlock)="metadata.translatorLocked = false"
(newItemAdded)="metadata.translatorLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.name}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="row g-0">
<div class="mb-3">
@ -327,12 +358,29 @@
</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);metadata.translatorLocked = true;" [settings]="getPersonsSettings(PersonRole.Translator)"
[(locked)]="metadata.translatorLocked" (onUnlock)="metadata.translatorLocked = false"
(newItemAdded)="metadata.translatorLocked = true">
<label for="team" class="form-label">{{t('team-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Character);metadata.teamLocked = true" [settings]="getPersonsSettings(PersonRole.Team)"
[(locked)]="metadata.teamLocked" (onUnlock)="metadata.teamLocked = false"
(newItemAdded)="metadata.teamLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.name}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="row g-0">
<div class="mb-3">
<label for="location" class="form-label">{{t('location-label')}}</label>
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Location);metadata.locationLocked = true" [settings]="getPersonsSettings(PersonRole.Location)"
[(locked)]="metadata.locationLocked" (onUnlock)="metadata.locationLocked = false"
(newItemAdded)="metadata.locationLocked = true">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -408,7 +456,7 @@
<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>
<h5 class="mt-0 mb-1">{{formatVolumeName(volume)}}</h5>
<div>
<div class="row g-0">
<div class="col">
@ -432,7 +480,7 @@
<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">
<li *ngFor="let file of volume.volumeFiles" class="list-group-item">
<span>{{file.filePath}}</span>
<div class="row g-0">
<div class="col">

View file

@ -21,7 +21,7 @@ import { forkJoin, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { TypeaheadSettings } from 'src/app/typeahead/_models/typeahead-settings';
import { Chapter } from 'src/app/_models/chapter';
import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter';
import { CollectionTag } from 'src/app/_models/collection-tag';
import { Genre } from 'src/app/_models/metadata/genre';
import { AgeRatingDto } from 'src/app/_models/metadata/age-rating-dto';
@ -58,6 +58,7 @@ import {EditListComponent} from "../../../shared/edit-list/edit-list.component";
import {AccountService} from "../../../_services/account.service";
import {LibraryType} from "../../../_models/library/library";
import {ToastrService} from "ngx-toastr";
import {Volume} from "../../../_models/volume";
enum TabID {
General = 0,
@ -296,8 +297,10 @@ export class EditSeriesModalComponent implements OnInit {
this.volumeCollapsed[v.name] = true;
});
this.seriesVolumes.forEach(vol => {
vol.volumeFiles = vol.chapters?.sort(this.utilityService.sortChapters).map((c: Chapter) => c.files.map((f: any) => {
f.chapter = c.number;
//.sort(this.utilityService.sortChapters) (no longer needed, all data is sorted on the backend)
vol.volumeFiles = vol.chapters?.map((c: Chapter) => c.files.map((f: any) => {
// TODO: Identify how to fix this hack
f.chapter = c.range;
return f;
})).flat();
});
@ -315,6 +318,15 @@ export class EditSeriesModalComponent implements OnInit {
});
}
formatVolumeName(volume: Volume) {
if (volume.minNumber === LooseLeafOrDefaultNumber) {
return translate('edit-series-modal.loose-leaf-volume');
} else if (volume.minNumber === SpecialVolumeNumber) {
return translate('edit-series-modal.specials-volume');
}
return translate('edit-series-modal.volume-num') + ' ' + volume.name;
}
setupTypeaheads() {
forkJoin([
@ -475,7 +487,10 @@ export class EditSeriesModalComponent implements OnInit {
this.updateFromPreset('letterer', this.metadata.letterers, PersonRole.Letterer),
this.updateFromPreset('penciller', this.metadata.pencillers, PersonRole.Penciller),
this.updateFromPreset('publisher', this.metadata.publishers, PersonRole.Publisher),
this.updateFromPreset('translator', this.metadata.translators, PersonRole.Translator)
this.updateFromPreset('imprint', this.metadata.imprints, PersonRole.Imprint),
this.updateFromPreset('translator', this.metadata.translators, PersonRole.Translator),
this.updateFromPreset('teams', this.metadata.teams, PersonRole.Team),
this.updateFromPreset('locations', this.metadata.locations, PersonRole.Location),
]).pipe(map(results => {
return of(true);
}));
@ -598,6 +613,10 @@ export class EditSeriesModalComponent implements OnInit {
updatePerson(persons: Person[], role: PersonRole) {
switch (role) {
case PersonRole.Other:
break;
case PersonRole.Artist:
break;
case PersonRole.CoverArtist:
this.metadata.coverArtists = persons;
break;
@ -622,11 +641,22 @@ export class EditSeriesModalComponent implements OnInit {
case PersonRole.Publisher:
this.metadata.publishers = persons;
break;
case PersonRole.Imprint:
this.metadata.imprints = persons;
break;
case PersonRole.Team:
this.metadata.teams = persons;
break;
case PersonRole.Location:
this.metadata.locations = persons;
break;
case PersonRole.Writer:
this.metadata.writers = persons;
break;
case PersonRole.Translator:
this.metadata.translators = persons;
break;
}
this.cdRef.markForCheck();
}

View file

@ -114,7 +114,7 @@
<ul class="list-unstyled">
<li class="d-flex my-4" *ngFor="let chapter of chapters">
<!-- TODO: Localize title -->
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read">
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
</a>
<div class="flex-grow-1">
@ -123,7 +123,7 @@
<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">
<ng-container *ngIf="chapter.minNumber !== LooseLeafOrSpecialNumber; else specialHeader">
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
</ng-container>
</span>

View file

@ -20,7 +20,7 @@ import { ToastrService } from 'ngx-toastr';
import { Observable, of, map, shareReplay } from 'rxjs';
import { DownloadService } from 'src/app/shared/_services/download.service';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter } from 'src/app/_models/chapter';
import {Chapter, LooseLeafOrDefaultNumber} from 'src/app/_models/chapter';
import { ChapterMetadata } from 'src/app/_models/metadata/chapter-metadata';
import { Device } from 'src/app/_models/device/device';
import { LibraryType } from 'src/app/_models/library/library';
@ -74,6 +74,7 @@ export class CardDetailDrawerComponent implements OnInit {
protected readonly Breakpoint = Breakpoint;
protected readonly LibraryType = LibraryType;
protected readonly TabID = TabID;
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
@Input() parentName = '';
@Input() seriesId: number = 0;
@ -182,10 +183,10 @@ export class CardDetailDrawerComponent implements OnInit {
}
formatChapterNumber(chapter: Chapter) {
if (chapter.number === '0') {
if (chapter.minNumber === LooseLeafOrDefaultNumber) {
return '1';
}
return chapter.number;
return chapter.range + '';
}
performAction(action: ActionItem<any>, chapter: Chapter) {
@ -281,5 +282,4 @@ export class CardDetailDrawerComponent implements OnInit {
this.cdRef.markForCheck();
});
}
}

View file

@ -198,13 +198,14 @@ export class CardItemComponent implements OnInit {
this.format = (this.entity as Series).format;
if (this.utilityService.isChapter(this.entity)) {
const chapterTitle = this.utilityService.asChapter(this.entity).titleName;
const chapter = this.utilityService.asChapter(this.entity);
const chapterTitle = chapter.titleName;
if (chapterTitle === '' || chapterTitle === null || chapterTitle === undefined) {
const volumeTitle = this.utilityService.asChapter(this.entity).volumeTitle
const volumeTitle = chapter.volumeTitle
if (volumeTitle === '' || volumeTitle === null || volumeTitle === undefined) {
this.tooltipTitle = (this.title).trim();
} else {
this.tooltipTitle = (this.utilityService.asChapter(this.entity).volumeTitle + ' ' + this.title).trim();
this.tooltipTitle = (volumeTitle + ' ' + this.title).trim();
}
} else {
this.tooltipTitle = chapterTitle;

View file

@ -4,10 +4,12 @@
&& 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">
&& chapter.characters.length === 0 && chapter.translators.length === 0
&& chapter.imprints.length === 0 && chapter.locations.length === 0
&& chapter.teams.length === 0">
{{t('no-data')}}
</span>
<div class="row g-0">
<div class="container-flex row row-cols-auto row-cols-lg-5 g-2 g-lg-3 me-0 mt-2">
<div class="col-auto mt-2" *ngIf="chapter.writers && chapter.writers.length > 0">
<h6>{{t('writers-title')}}</h6>
<app-badge-expander [items]="chapter.writers">
@ -81,6 +83,15 @@
</app-badge-expander>
</div>
<div class="col-auto mt-2" *ngIf="chapter.imprints && chapter.imprints.length > 0">
<h6>{{t('imprints-title')}}</h6>
<app-badge-expander [items]="chapter.imprints">
<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">
@ -89,6 +100,25 @@
</ng-template>
</app-badge-expander>
</div>
<div class="col-auto mt-2" *ngIf="chapter.teams && chapter.teams.length > 0">
<h6>{{t('teams-title')}}</h6>
<app-badge-expander [items]="chapter.teams">
<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.locations && chapter.locations.length > 0">
<h6>{{t('locations-title')}}</h6>
<app-badge-expander [items]="chapter.locations">
<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">

View file

@ -71,7 +71,7 @@ export class EditSeriesRelationComponent implements OnInit {
focusTypeahead = new EventEmitter();
ngOnInit(): void {
this.seriesService.getRelatedForSeries(this.series.id).subscribe(async relations => {
this.seriesService.getRelatedForSeries(this.series.id).subscribe( relations => {
this.setupRelationRows(relations.prequels, RelationKind.Prequel);
this.setupRelationRows(relations.sequels, RelationKind.Sequel);
this.setupRelationRows(relations.sideStories, RelationKind.SideStory);
@ -85,6 +85,7 @@ export class EditSeriesRelationComponent implements OnInit {
this.setupRelationRows(relations.contains, RelationKind.Contains);
this.setupRelationRows(relations.parent, RelationKind.Parent);
this.setupRelationRows(relations.editions, RelationKind.Edition);
this.setupRelationRows(relations.annuals, RelationKind.Annual);
this.cdRef.detectChanges();
});
@ -181,9 +182,10 @@ export class EditSeriesRelationComponent implements OnInit {
const alternativeVersions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.AlternativeVersion && item.series !== undefined).map(item => item.series!.id);
const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id);
const editions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Edition && item.series !== undefined).map(item => item.series!.id);
const annuals = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Annual && item.series !== undefined).map(item => item.series!.id);
// NOTE: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis, editions).subscribe(() => {});
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis, editions, annuals).subscribe(() => {});
}

View file

@ -7,11 +7,25 @@
<ng-template #fullComicTitle>
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
{{Number !== LooseLeafOrSpecialNumber ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
{{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</ng-container>
{{Number !== LooseLeafOrSpecialNumber ? (isChapter ? t('issue-num') + Number : volumeTitle) : t('special')}}
{{Number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + Number : volumeTitle) : t('special')}}
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="LibraryType.ComicVine">
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullComicTitle">
{{titleName}}
</ng-container>
<ng-template #fullComicTitle>
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
{{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</ng-container>
{{Number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + Number : volumeTitle) : t('special')}}
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="LibraryType.Manga">
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullMangaTitle">
{{titleName}}
@ -19,9 +33,9 @@
<ng-template #fullMangaTitle>
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
{{Number !== LooseLeafOrSpecialNumber ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
{{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</ng-container>
{{Number !== LooseLeafOrSpecialNumber ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}}
{{Number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}}
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="LibraryType.Book">
@ -30,5 +44,8 @@
<ng-container *ngSwitchCase="LibraryType.LightNovel">
{{volumeTitle}}
</ng-container>
<ng-container *ngSwitchCase="LibraryType.Images">
{{Number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}}
</ng-container>
</ng-container>
</ng-container>

View file

@ -1,11 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter, LooseLeafOrSpecialNumber } from 'src/app/_models/chapter';
import { Chapter, LooseLeafOrDefaultNumber } from 'src/app/_models/chapter';
import { LibraryType } from 'src/app/_models/library/library';
import { Volume } from 'src/app/_models/volume';
import {CommonModule, NgSwitch} from "@angular/common";
import {TranslocoModule} from "@ngneat/transloco";
/**
* This is primarily used for list item
*/
@Component({
selector: 'app-entity-title',
standalone: true,
@ -20,7 +23,9 @@ import {TranslocoModule} from "@ngneat/transloco";
})
export class EntityTitleComponent implements OnInit {
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrSpecialNumber;
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
protected readonly LooseLeafOrSpecial = LooseLeafOrDefaultNumber + "";
protected readonly LibraryType = LibraryType;
/**
* Library type for which the entity belongs
@ -42,19 +47,18 @@ export class EntityTitleComponent implements OnInit {
volumeTitle: string = '';
get Number() {
if (this.utilityService.isVolume(this.entity)) return (this.entity as Volume).minNumber;
return (this.entity as Chapter).number;
if (this.isChapter) return (this.entity as Chapter).range;
return (this.entity as Volume).name;
}
get LibraryType() {
return LibraryType;
}
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.isChapter = this.utilityService.isChapter(this.entity);
if (this.isChapter) {
const c = (this.entity as Chapter);
this.volumeTitle = c.volumeTitle || '';

View file

@ -64,7 +64,8 @@ const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, Fi
FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer,
FilterField.Colorist, FilterField.Inker, FilterField.Penciller,
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags,
FilterField.Imprint, FilterField.Team, FilterField.Location
];
const BooleanFields = [FilterField.WantToRead];
const DateFields = [FilterField.ReadingDate];
@ -297,6 +298,9 @@ export class MetadataFilterRowComponent implements OnInit {
case FilterField.Letterer: return this.getPersonOptions(PersonRole.Letterer);
case FilterField.Penciller: return this.getPersonOptions(PersonRole.Penciller);
case FilterField.Publisher: return this.getPersonOptions(PersonRole.Publisher);
case FilterField.Imprint: return this.getPersonOptions(PersonRole.Imprint);
case FilterField.Team: return this.getPersonOptions(PersonRole.Imprint);
case FilterField.Location: return this.getPersonOptions(PersonRole.Imprint);
case FilterField.Translators: return this.getPersonOptions(PersonRole.Translator);
case FilterField.Writers: return this.getPersonOptions(PersonRole.Writer);
}

View file

@ -129,7 +129,8 @@
<ng-container *ngIf="item.files.length > 0">
<app-series-format [format]="item.files?.[0].format"></app-series-format>
</ng-container>
<span>{{item.titleName}}</span>
<!-- TODO: this needs the series name before the chapter issue -->
<span>{{item.titleName || item.range}}</span>
</div>
</div>
</ng-template>

View file

@ -148,6 +148,8 @@ export class NavHeaderComponent implements OnInit {
this.clearSearch();
filter = filter + '';
switch(role) {
case PersonRole.Other:
break;
case PersonRole.Writer:
this.goTo({field: FilterField.Writers, comparison: FilterComparison.Equal, value: filter});
break;
@ -178,9 +180,19 @@ export class NavHeaderComponent implements OnInit {
case PersonRole.Publisher:
this.goTo({field: FilterField.Publisher, comparison: FilterComparison.Equal, value: filter});
break;
case PersonRole.Imprint:
this.goTo({field: FilterField.Imprint, comparison: FilterComparison.Equal, value: filter});
break;
case PersonRole.Team:
this.goTo({field: FilterField.Team, comparison: FilterComparison.Equal, value: filter});
break;
case PersonRole.Location:
this.goTo({field: FilterField.Location, comparison: FilterComparison.Equal, value: filter});
break;
case PersonRole.Translator:
this.goTo({field: FilterField.Translators, comparison: FilterComparison.Equal, value: filter});
break;
}
}

View file

@ -21,40 +21,20 @@
<ng-container *ngIf="currentStepIndex === Step.Validate">
<p>{{t('validate-description')}}</p>
<div class="row g-0">
<div ngbAccordion #accordion="ngbAccordion">
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
<div ngbAccordionItem *ngIf="fileToProcess.validateSummary as summary">
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
@if(summary.results.length > 0) {
<h5>{{t('validate-warning')}}</h5>
<ol class="list-group list-group-numbered list-group-flush" >
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
[innerHTML]="result | cblConflictReason | safeHtml">
</li>
</ol>
} @else {
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
{{t('validate-no-issue')}}
</div>
</div>
{{t('validate-no-issue-description')}}
</div>
}
<div ngbAccordionItem *ngIf="fileToProcess.validateSummary as summary">
<h5 ngbAccordionHeader>
<button ngbAccordionButton>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</button>
</h5>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-container [ngTemplateOutlet]="validationList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</div>
</div>
</div>
</div>
}
</div>
</div>
@ -105,6 +85,38 @@
</ng-container>
</div>
<ng-template #validationList let-summary="summary">
@if (summary.results.length > 0) {
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-triangle-exclamation" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
{{t('validate-warning')}}
</div>
</div>
</div>
<ol class="list-group list-group-numbered list-group-flush" >
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
[innerHTML]="result | cblConflictReason | safeHtml">
</li>
</ol>
}
@else {
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
{{t('validate-no-issue-description')}}
</div>
</div>
</div>
}
</ng-template>
<ng-template #resultsList let-summary="summary">
<ul class="list-group list-group-flush">
@for(result of summary.results; track result.order) {
@ -115,23 +127,46 @@
</ng-template>
<ng-template #heading let-filename="filename" let-summary="summary">
<ng-container *ngIf="summary.success | cblImportResult as success">
<ng-container [ngSwitch]="summary.success">
<span *ngSwitchCase="CblImportResult.Success" class="badge bg-primary me-1">{{success}}</span>
<span *ngSwitchCase="CblImportResult.Fail" class="badge bg-danger me-1">{{success}}</span>
<span *ngSwitchCase="CblImportResult.Partial" class="badge bg-warning me-1">{{success}}</span>
</ng-container>
</ng-container>
<span>{{filename}}<span *ngIf="summary.cblName">: ({{summary.cblName}})</span></span>
@switch (summary.success) {
@case (CblImportResult.Success) {
<span class="badge heading-badge bg-primary me-1">{{summary.success | cblImportResult}}</span>
}
@case (CblImportResult.Fail) {
<span class="badge heading-badge bg-danger me-1">{{summary.success | cblImportResult}}</span>
}
@case (CblImportResult.Partial) {
<span class="badge heading-badge bg-warning me-1">{{summary.success | cblImportResult}}</span>
}
}
<span>{{filename}}<span *ngIf="summary.cblName">: ({{summary.cblName}})</span></span>
</ng-template>
</div>
<div class="modal-footer">
<a class="btn btn-icon" href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/reading-lists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{t(NextButtonLabel)}}</button>
<form [formGroup]="cblSettingsForm" class="row align-items-center">
<div class="col-auto">
<div class="form-check form-switch">
<input type="checkbox" id="settings-comicvine-mode" role="switch" formControlName="comicVineMatching" class="form-check-input"
aria-labelledby="auto-close-label">
<label class="form-check-label" for="settings-comicvine-mode">{{t('comicvine-parsing-label')}}</label>
</div>
</div>
</form>
<!-- Spacer -->
<div class="col" aria-hidden="true"></div>
<div class="col-auto">
<a class="btn btn-icon" href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/reading-lists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>
</div>
<div class="col-auto">
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{t(NextButtonLabel)}}</button>
</div>
</div>

View file

@ -2,6 +2,10 @@
display: none;
}
.heading-badge {
color: var(--bs-badge-color);
}
::ng-deep .file-info {
width: 83%;
float: left;
@ -38,4 +42,4 @@ file-upload {
::ng-deep .reading-list-fail--item {
color: var(--error-color);
}
}

View file

@ -46,8 +46,12 @@ enum Step {
})
export class ImportCblModalComponent {
protected readonly CblImportResult = CblImportResult;
protected readonly Step = Step;
@ViewChild('fileUpload') fileUpload!: ElementRef<HTMLInputElement>;
fileUploadControl = new FormControl<undefined | Array<File>>(undefined, [
FileUploadValidators.accept(['.cbl']),
]);
@ -55,6 +59,9 @@ export class ImportCblModalComponent {
uploadForm = new FormGroup({
files: this.fileUploadControl
});
cblSettingsForm = new FormGroup({
comicVineMatching: new FormControl(true, [])
});
isLoading: boolean = false;
@ -70,10 +77,6 @@ export class ImportCblModalComponent {
failedFiles: Array<FileStep> = [];
get Breakpoint() { return Breakpoint; }
get Step() { return Step; }
get CblImportResult() { return CblImportResult; }
get NextButtonLabel() {
switch(this.currentStepIndex) {
case Step.DryRun:
@ -105,11 +108,12 @@ export class ImportCblModalComponent {
return;
}
// Load each file into filesToProcess and group their data
let pages = [];
const pages = [];
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('cbl', files[i]);
formData.append('dryRun', true + '');
formData.append('dryRun', 'true');
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
pages.push(this.readingListService.validateCbl(formData));
}
forkJoin(pages).subscribe(results => {
@ -195,12 +199,13 @@ export class ImportCblModalComponent {
const filenamesAllowedToProcess = this.filesToProcess.map(p => p.fileName);
const files = (this.uploadForm.get('files')?.value || []).filter(f => filenamesAllowedToProcess.includes(f.name));
let pages = [];
const pages = [];
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('cbl', files[i]);
formData.append('dryRun', 'true');
pages.push(this.readingListService.importCbl(formData));
formData.append('cbl', files[i]);
formData.append('dryRun', 'true');
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
pages.push(this.readingListService.importCbl(formData));
}
forkJoin(pages).subscribe(results => {
results.forEach(cblImport => {
@ -224,6 +229,7 @@ export class ImportCblModalComponent {
const formData = new FormData();
formData.append('cbl', files[i]);
formData.append('dryRun', 'false');
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
pages.push(this.readingListService.importCbl(formData));
}
forkJoin(pages).subscribe(results => {

View file

@ -308,7 +308,7 @@
<ng-container *ngIf="nextExpectedChapter">
<ng-container [ngSwitch]="tabId">
<ng-container *ngSwitchCase="TabID.Volumes">
<app-next-expected-card *ngIf="nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === LooseLeafOrSpecialNumber"
<app-next-expected-card *ngIf="nextExpectedChapter.volumeNumber !== SpecialVolumeNumber && nextExpectedChapter.chapterNumber === LooseLeafOrSpecialNumber"
class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter"
[imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
</ng-container>

View file

@ -55,7 +55,7 @@ import {
import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component';
import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.service';
import {KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
import {Chapter} from 'src/app/_models/chapter';
import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter';
import {Device} from 'src/app/_models/device/device';
import {ScanSeriesEvent} from 'src/app/_models/events/scan-series-event';
import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event';
@ -67,7 +67,6 @@ import {RelationKind} from 'src/app/_models/series-detail/relation-kind';
import {SeriesMetadata} from 'src/app/_models/metadata/series-metadata';
import {User} from 'src/app/_models/user';
import {Volume} from 'src/app/_models/volume';
import {LooseLeafOrSpecialNumber} from 'src/app/_models/chapter';
import {AccountService} from 'src/app/_services/account.service';
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
import {ActionService} from 'src/app/_services/action.service';
@ -184,7 +183,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
protected readonly PageLayoutMode = PageLayoutMode;
protected readonly TabID = TabID;
protected readonly TagBadgeCursor = TagBadgeCursor;
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrSpecialNumber;
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
@ -241,7 +241,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
/**
* Track by function for Chapter to tell when to refresh card data
*/
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.volumeId}_${item.pagesRead}`;
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.minNumber}_${item.maxNumber}_${item.volumeId}_${item.pagesRead}`;
trackByRelatedSeriesIdentify = (index: number, item: RelatedSeriesPair) => `${item.series.name}_${item.series.libraryId}_${item.series.pagesRead}_${item.relation}`;
trackBySeriesIdentify = (index: number, item: Series) => `${item.name}_${item.libraryId}_${item.pagesRead}`;
trackByStoryLineIdentity = (index: number, item: StoryLineItem) => {
@ -338,12 +338,20 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
get ShowStorylineTab() {
return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel) && (this.volumes.length > 0 || this.chapters.length > 0);
if (this.libraryType === LibraryType.ComicVine) return false;
return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel && this.libraryType !== LibraryType.Comic)
&& (this.volumes.length > 0 || this.chapters.length > 0);
}
get ShowVolumeTab() {
if (this.libraryType === LibraryType.ComicVine) {
if (this.volumes.length > 1) return true;
if (this.specials.length === 0 && this.chapters.length === 0) return true;
return false;
}
return this.volumes.length > 0;
}
get ShowChaptersTab() {
return this.chapters.length > 0;
}
@ -371,13 +379,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
// This is a lone chapter
if (vol.length === 0) {
return 'Ch ' + this.currentlyReadingChapter.number;
return 'Ch ' + this.currentlyReadingChapter.minNumber; // TODO: Refactor this to use DisplayTitle (or Range) and Localize it
}
if (this.currentlyReadingChapter.number === "0") {
if (this.currentlyReadingChapter.minNumber === LooseLeafOrDefaultNumber) {
return 'Vol ' + vol[0].minNumber;
}
return 'Vol ' + vol[0].minNumber + ' Ch ' + this.currentlyReadingChapter.number;
return 'Vol ' + vol[0].minNumber + ' Ch ' + this.currentlyReadingChapter.minNumber;
}
return this.currentlyReadingChapter.title;
@ -661,6 +669,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
...relations.doujinshis.map(item => this.createRelatedSeries(item, RelationKind.Doujinshi)),
...relations.parent.map(item => this.createRelatedSeries(item, RelationKind.Parent)),
...relations.editions.map(item => this.createRelatedSeries(item, RelationKind.Edition)),
...relations.annuals.map(item => this.createRelatedSeries(item, RelationKind.Annual)),
];
if (this.relations.length > 0) {
this.hasRelations = true;
@ -729,7 +738,16 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
if (this.volumes.length === 0 && this.chapters.length === 0 && this.specials.length > 0) {
this.activeTabId = TabID.Specials;
} else {
this.activeTabId = TabID.Storyline;
if (this.libraryType == LibraryType.Comic || this.libraryType == LibraryType.ComicVine) {
if (this.chapters.length === 0) {
this.activeTabId = TabID.Specials;
} else {
this.activeTabId = TabID.Chapters;
}
} else {
this.activeTabId = TabID.Storyline;
}
}
this.cdRef.markForCheck();
}

View file

@ -76,15 +76,6 @@
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
<!-- @if (libraryType === LibraryType.Comic || libraryType === LibraryType.Images) {-->
<!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- -->
<!-- }-->
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">
<ng-template #itemTemplate let-item>
@ -92,26 +83,6 @@
</ng-template>
</app-metadata-detail>
<!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<!-- <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">-->
<!-- <ng-template #itemTemplate let-item>-->
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
<!-- </ng-template>-->
<!-- </app-metadata-detail>-->
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterField.Colorist" [heading]="t('colorists-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
@ -136,24 +107,48 @@
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterField.Penciller" [heading]="t('pencillers-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.teams" [libraryId]="series.libraryId" [queryParam]="FilterField.Team" [heading]="t('teams-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.locations" [libraryId]="series.libraryId" [queryParam]="FilterField.Location" [heading]="t('locations-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterField.Publisher" [heading]="t('publishers-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.imprints" [libraryId]="series.libraryId" [queryParam]="FilterField.Imprint" [heading]="t('imprints-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
</div>
<div class="row g-0">

View file

@ -4,7 +4,7 @@ import {
Component,
inject,
Input,
OnChanges,
OnChanges, OnInit,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
@ -46,7 +46,7 @@ import {Rating} from "../../../_models/rating";
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class SeriesMetadataDetailComponent implements OnChanges {
export class SeriesMetadataDetailComponent implements OnChanges, OnInit {
protected readonly imageService = inject(ImageService);
protected readonly utilityService = inject(UtilityService);
@ -83,13 +83,26 @@ export class SeriesMetadataDetailComponent implements OnChanges {
return this.seriesMetadata?.webLinks.split(',') || [];
}
constructor() {
ngOnInit() {
// If on desktop, we can just have all the data expanded by default:
this.isCollapsed = this.utilityService.getActiveBreakpoint() < Breakpoint.Desktop;
// Check if there is a lot of extended data, if so, re-collapse
const sum = (this.seriesMetadata.colorists.length + this.seriesMetadata.editors.length
+ this.seriesMetadata.coverArtists.length + this.seriesMetadata.inkers.length
+ this.seriesMetadata.letterers.length + this.seriesMetadata.pencillers.length
+ this.seriesMetadata.publishers.length + this.seriesMetadata.characters.length
+ this.seriesMetadata.imprints.length + this.seriesMetadata.translators.length
+ this.seriesMetadata.writers.length + this.seriesMetadata.teams.length + this.seriesMetadata.locations.length) / 13;
if (sum > 10) {
this.isCollapsed = true;
}
this.cdRef.markForCheck();
}
ngOnChanges(changes: SimpleChanges): void {
this.hasExtendedProperties = this.seriesMetadata.colorists.length > 0 ||
this.seriesMetadata.editors.length > 0 ||
this.seriesMetadata.coverArtists.length > 0 ||
@ -98,7 +111,11 @@ export class SeriesMetadataDetailComponent implements OnChanges {
this.seriesMetadata.pencillers.length > 0 ||
this.seriesMetadata.publishers.length > 0 ||
this.seriesMetadata.characters.length > 0 ||
this.seriesMetadata.translators.length > 0;
this.seriesMetadata.imprints.length > 0 ||
this.seriesMetadata.teams.length > 0 ||
this.seriesMetadata.locations.length > 0 ||
this.seriesMetadata.translators.length > 0
;
this.seriesSummary = (this.seriesMetadata?.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');

View file

@ -119,7 +119,7 @@ export class DownloadService {
case 'volume':
return (downloadEntity as Volume).minNumber + '';
case 'chapter':
return (downloadEntity as Chapter).number;
return (downloadEntity as Chapter).minNumber + '';
case 'bookmark':
return '';
case 'logs':

View file

@ -43,7 +43,7 @@ export class UtilityService {
sortChapters = (a: Chapter, b: Chapter) => {
return parseFloat(a.number) - parseFloat(b.number);
return a.minNumber - b.minNumber;
}
mangaFormatToText(format: MangaFormat): string {
@ -67,6 +67,7 @@ export class UtilityService {
case LibraryType.LightNovel:
return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : '');
case LibraryType.Comic:
case LibraryType.ComicVine:
if (includeHash) {
return this.translocoService.translate('common.issue-hash-num');
}

View file

@ -188,6 +188,7 @@ export class SideNavComponent implements OnInit {
case LibraryType.LightNovel:
return 'fa-book';
case LibraryType.Comic:
case LibraryType.ComicVine:
case LibraryType.Manga:
return 'fa-book-open';
case LibraryType.Images:

View file

@ -172,6 +172,7 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
break;
case LibraryType.Comic:
case LibraryType.ComicVine:
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(true);
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
@ -196,6 +197,9 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
break;
}
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible);
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();

View file

@ -6,5 +6,5 @@ export interface ReadHistoryEvent {
libraryId: number;
readDate: string;
chapterId: number;
chapterNumber: string;
}
chapterNumber: number;
}