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

View file

@ -42,6 +42,8 @@
"is-processed-header": "Is Processed",
"no-data": "No Data",
"volume-and-chapter-num": "Volume {{v}} Chapter {{n}}",
"volume-num": "Volume {{num}}",
"chapter-num": "Chapter {{num}}",
"rating": "Rating {{r}}",
"not-applicable": "Not Applicable",
"processed": "Processed",
@ -457,7 +459,8 @@
"side-story": "Side Story",
"spin-off": "Spin Off",
"parent": "Parent",
"edition": "Edition"
"edition": "Edition",
"annual": "Annual"
},
"publication-status-pipe": {
@ -479,7 +482,11 @@
"penciller": "Penciller",
"publisher": "Publisher",
"writer": "Writer",
"other": "Other"
"other": "Other",
"imprint": "Imprint",
"translator": "Translator",
"team": "{{filter-field-pipe.team}}",
"location": "{{filter-field-pipe.location}}"
},
"manga-format-pipe": {
@ -493,7 +500,10 @@
"library-type-pipe": {
"book": "Book",
"comic": "Comic",
"manga": "Manga"
"manga": "Manga",
"comicVine": "ComicVine",
"image": "Image",
"lightNovel": "Light Novel"
},
"age-rating-pipe": {
@ -753,6 +763,9 @@
"translators-title": "Translators",
"pencillers-title": "Pencillers",
"publishers-title": "Publishers",
"imprints-title": "Imprints",
"teams-title": "Teams",
"locations-title": "Locations",
"promoted": "{{common.promoted}}",
"see-more": "See More",
@ -928,7 +941,10 @@
"writers-title": "{{series-metadata-detail.writers-title}}",
"genres-title": "{{series-metadata-detail.genres-title}}",
"publishers-title": "{{series-metadata-detail.publishers-title}}",
"imprints-title": "{{series-metadata-detail.imprints-title}}",
"tags-title": "{{series-metadata-detail.tags-title}}",
"teams-title": "{{series-metadata-detail.teams-title}}",
"locations-title": "{{series-metadata-detail.locations-title}}",
"not-defined": "Not defined",
"read": "{{common.read}}",
"unread": "Unread",
@ -958,7 +974,9 @@
"inkers-title": "{{series-metadata-detail.inkers-title}}",
"pencillers-title": "{{series-metadata-detail.pencillers-title}}",
"cover-artists-title": "{{series-metadata-detail.cover-artists-title}}",
"editors-title": "{{series-metadata-detail.editors-title}}"
"editors-title": "{{series-metadata-detail.editors-title}}",
"teams-title": "{{series-metadata-detail.teams-title}}",
"locations-title": "{{series-metadata-detail.locations-title}}"
},
"cover-image-chooser": {
@ -1519,7 +1537,7 @@
"general-tab": "General",
"cover-image-tab": "Cover Image",
"close": "{{common.close}}",
"save": "{common.save}}",
"save": "{{common.save}}",
"year-validation": "Must be greater than 1000, 0 or blank",
"month-validation": "Must be between 1 and 12 or blank",
"name-unique-validation": "Name must be unique",
@ -1539,7 +1557,6 @@
"import-description": "To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.",
"validate-description": "All files have been validated to see if there are any operations to do on the list. Any lists have have failed will not move to the next step. Fix the CBL files and retry.",
"validate-warning": "There are issues with the CBL that will prevent an import. Correct these issues then try again.",
"validate-no-issue": "Looks good",
"validate-no-issue-description": "No issues found with CBL, press next.",
"dry-run-description": "This is a dry run and shows what will happen if you press Next and perform the import. All Failures will not be imported.",
"prev": "Prev",
@ -1549,7 +1566,8 @@
"import-step": "Import CBLs",
"validate-cbl-step": "Validate CBL",
"dry-run-step": "Dry Run",
"final-import-step": "Final Step"
"final-import-step": "Final Step",
"comicvine-parsing-label": "Use ComicVine Series matching"
},
"pdf-reader": {
@ -1681,6 +1699,7 @@
"cover-artist-label": "Cover Artist",
"writer-label": "Writer",
"publisher-label": "Publisher",
"imprint-label": "Imprint",
"penciller-label": "Penciller",
"letterer-label": "Letterer",
"inker-label": "Inker",
@ -1688,6 +1707,8 @@
"colorist-label": "Colorist",
"character-label": "Character",
"translator-label": "Translator",
"team-label": "{{filter-field-pipe.team}}",
"location-label": "{{filter-field-pipe.location}}",
"language-label": "Language",
"age-rating-label": "Age Rating",
"publication-status-label": "Publication Status",
@ -1726,7 +1747,9 @@
"highest-count-tooltip": "Highest Count found across all ComicInfo in the Series",
"max-issue-tooltip": "Max Issue or Volume field from all ComicInfo in the series",
"force-refresh": "Force Refresh",
"force-refresh-tooltip": "Force refresh external metadata from Kavita+"
"force-refresh-tooltip": "Force refresh external metadata from Kavita+",
"loose-leaf-volume": "Loose Leaf Chapters",
"specials-volume": "Specials"
},
"day-breakdown": {
@ -1935,11 +1958,14 @@
"formats": "Formats",
"genres": "Genres",
"inker": "Inker",
"team": "Team",
"location": "Location",
"languages": "Languages",
"libraries": "Libraries",
"letterer": "Letterer",
"publication-status": "Publication Status",
"penciller": "Penciller",
"imprint": "Imprint",
"publisher": "Publisher",
"read-progress": "Read Progress",
"read-time": "Read Time",