Light Novel Library Type (#2682)

This commit is contained in:
Joe Milazzo 2024-02-08 12:35:05 -06:00 committed by GitHub
parent a40c019ddb
commit 8b2649302c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 237 additions and 148 deletions

View file

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

View file

@ -27,5 +27,8 @@
<ng-container *ngSwitchCase="LibraryType.Book">
{{volumeTitle}}
</ng-container>
<ng-container *ngSwitchCase="LibraryType.LightNovel">
{{volumeTitle}}
</ng-container>
</ng-container>
</ng-container>

View file

@ -139,7 +139,7 @@
</div>
<div class="row" [ngClass]="{'pt-3': !seriesMetadata || seriesMetadata?.summary?.length === 0}">
<div class="row" [ngClass]="{'pt-3': !seriesMetadata || seriesMetadata.summary.length === 0}">
<app-carousel-reel [items]="reviews" [alwaysShow]="true" [title]="t('user-reviews-alt')"
iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}"
[clickableTitle]="true" (sectionClick)="openReviewModal()">
@ -153,7 +153,7 @@
<ng-container *ngIf="series">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)">
<li [ngbNavItem]="TabID.Storyline" *ngIf="ShowStorylineTab">
<a ngbNavLink>{{t('storyline-tab')}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1">
@ -181,8 +181,8 @@
</ng-template>
</li>
<li [ngbNavItem]="TabID.Volumes" *ngIf="volumes.length > 0">
<a ngbNavLink>{{libraryType === LibraryType.Book ? t('books-tab') : t('volumes-tab')}}</a>
<li [ngbNavItem]="TabID.Volumes" *ngIf="ShowVolumeTab">
<a ngbNavLink>{{UseBookLogic ? t('books-tab') : t('volumes-tab')}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else volumeListLayout">
@ -202,7 +202,7 @@
</ng-template>
</li>
<li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0">
<li [ngbNavItem]="TabID.Chapters" *ngIf="ShowChaptersTab">
<a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1">

View file

@ -48,6 +48,7 @@
.image-container {
max-height: 400px;
max-width: 280px;
}
.info-container {
@ -63,4 +64,4 @@
border-bottom-right-radius: 6px !important;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}
}

View file

@ -137,7 +137,7 @@ interface StoryLineItem {
isChapter: boolean;
}
const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.Book];
const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.LightNovel];
@Component({
selector: 'app-series-detail',
@ -337,6 +337,21 @@ 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);
}
get ShowVolumeTab() {
return this.volumes.length > 0;
}
get ShowChaptersTab() {
return this.chapters.length > 0;
}
get UseBookLogic() {
return this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel;
}
get ScrollingBlockHeight() {
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
const navbar = this.document.querySelector('.navbar') as HTMLElement;
@ -614,6 +629,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.loadPlusMetadata(this.seriesId, this.libraryType);
}
if (this.libraryType === LibraryType.LightNovel) {
this.renderMode = PageLayoutMode.List;
this.pageExtrasGroup.get('renderMode')?.setValue(this.renderMode);
this.cdRef.markForCheck();
}
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
@ -694,12 +716,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
*/
updateSelectedTab() {
// Book libraries only have Volumes or Specials enabled
if (this.libraryType === LibraryType.Book) {
if (this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel) {
if (this.volumes.length === 0) {
this.activeTabId = TabID.Specials;
} else {
this.activeTabId = TabID.Volumes;
}
this.cdRef.markForCheck();
return;
}
@ -708,6 +731,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
} else {
this.activeTabId = TabID.Storyline;
}
this.cdRef.markForCheck();
}

View file

@ -3,14 +3,14 @@
<app-read-more [text]="seriesSummary" [maxLength]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 1000 : 250"></app-read-more>
</div>
<!-- Ratings -->
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" [heading]="t('rating-title')">
<ng-template #itemTemplate let-item>
<app-external-rating [seriesId]="series.id" [ratings]="ratings" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
</ng-template>
</app-metadata-detail>
<!-- Weblinks -->
<ng-container *ngIf="WebLinks as links">
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
<ng-template #itemTemplate let-item>
@ -24,14 +24,17 @@
</ng-container>
<!-- Genres -->
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterField.Genres" [heading]="t('genres-title')">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<!-- Tags -->
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterField.Tags" [heading]="t('tags-title')">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<!-- Collections -->
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" [heading]="t('collections-title')">
<ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
@ -40,7 +43,7 @@
</ng-template>
</app-metadata-detail>
<!-- Reading Lists -->
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" [heading]="t('reading-lists-title')">
<ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
@ -53,6 +56,16 @@
</ng-template>
</app-metadata-detail>
<!-- Key Person Information -->
<!-- @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {-->
<!-- -->
<!-- <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.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">
<ng-template #itemTemplate let-item>
@ -62,70 +75,95 @@
<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>
<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>
</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.editors" [libraryId]="series.libraryId" [queryParam]="FilterField.Editor" [heading]="t('editors-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.inkers" [libraryId]="series.libraryId" [queryParam]="FilterField.Inker" [heading]="t('inkers-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.letterers" [libraryId]="series.libraryId" [queryParam]="FilterField.Letterer" [heading]="t('letterers-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.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.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>
</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.editors" [libraryId]="series.libraryId" [queryParam]="FilterField.Editor" [heading]="t('editors-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.inkers" [libraryId]="series.libraryId" [queryParam]="FilterField.Inker" [heading]="t('inkers-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.letterers" [libraryId]="series.libraryId" [queryParam]="FilterField.Letterer" [heading]="t('letterers-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>
<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.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>
</div>
<div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperties" >
<a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
{{isCollapsed ? t('see-more') : t('see-less')}}
</a>
</div>
<div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperties" >
<a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
{{isCollapsed ? t('see-more') : t('see-less')}}
</a>
</div>
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>

View file

@ -64,6 +64,7 @@ export class UtilityService {
formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) {
switch(libraryType) {
case LibraryType.Book:
case LibraryType.LightNovel:
return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : '');
case LibraryType.Comic:
if (includeHash) {

View file

@ -185,6 +185,7 @@ export class SideNavComponent implements OnInit {
getLibraryTypeIcon(format: LibraryType) {
switch (format) {
case LibraryType.Book:
case LibraryType.LightNovel:
return 'fa-book';
case LibraryType.Comic:
case LibraryType.Manga:

View file

@ -2,7 +2,7 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">
<ng-container *ngIf="!isAddLibrary; else addLibraryTitle">
{{t('edit-title', {name: library.name | sentenceCase})}}
{{t('edit-title', {name: library!.name | sentenceCase})}}
</ng-container>
<ng-template #addLibraryTitle>
{{t('add-title')}}
@ -33,8 +33,15 @@
<div class="mb-3">
<label for="library-type" class="form-label">{{t('type-label')}}</label>
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
<i class="fa fa-info-circle ms-1" placement="top" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
@if(IsKavitaPlusEligible) {
<span class="float-end">
{{t('kavitaplus-eligible-label')}}
<i class="fa fa-info-circle ms-1" placement="top" [ngbTooltip]="kavitaplusEligibleTooltip" role="button" tabindex="0"></i>
</span>
}
<ng-template #typeTooltip>{{t('type-tooltip')}}</ng-template>
<ng-template #kavitaplusEligibleTooltip>{{t('kavitaplus-eligible-tooltip')}}</ng-template>
<span class="visually-hidden" id="library-type-help">
<ng-container [ngTemplateOutlet]="typeTooltip"></ng-container>
</span>
@ -44,7 +51,7 @@
</div>
<div *ngIf="!isAddLibrary">
{{t('last-scanned-label')}}
<span>{{library.lastScanned | date: 'short' | defaultDate}}</span>
<span>{{library?.lastScanned | date: 'short' | defaultDate}}</span>
</div>
</ng-template>
</li>

View file

@ -72,9 +72,23 @@ enum StepID {
})
export class LibrarySettingsModalComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
protected readonly LibraryType = LibraryType;
protected readonly Breakpoint = Breakpoint;
protected readonly TabID = TabID;
@Input({required: true}) library!: Library;
public readonly utilityService = inject(UtilityService);
public readonly modal = inject(NgbActiveModal);
private readonly destroyRef = inject(DestroyRef);
private readonly uploadService = inject(UploadService);
private readonly modalService = inject(NgbModal);
private readonly settingService = inject(SettingsService);
private readonly confirmService = inject(ConfirmService);
private readonly libraryService = inject(LibraryService);
private readonly toastr = inject(ToastrService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly imageService = inject(ImageService);
@Input({required: true}) library!: Library | undefined;
active = TabID.General;
imageUrls: Array<string> = [];
@ -101,14 +115,10 @@ export class LibrarySettingsModalComponent implements OnInit {
fileTypeGroups = allFileTypeGroup;
excludePatterns: Array<string> = [''];
protected readonly Breakpoint = Breakpoint;
protected readonly TabID = TabID;
constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal,
private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService,
private libraryService: LibraryService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef,
private imageService: ImageService) { }
get IsKavitaPlusEligible() {
const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType;
return libType === LibraryType.Manga || libType === LibraryType.LightNovel;
}
ngOnInit(): void {
this.settingService.getLibraryTypes().subscribe((types) => {
@ -126,7 +136,7 @@ export class LibrarySettingsModalComponent implements OnInit {
this.cdRef.markForCheck();
}
if (this.library && this.library.type === LibraryType.Comic) {
if (this.library && (this.library.type === LibraryType.Comic || this.library.type === LibraryType.Book)) {
this.libraryForm.get('allowScrobbling')?.setValue(false);
this.libraryForm.get('allowScrobbling')?.disable();
}
@ -173,6 +183,12 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(true);
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(true);
break;
case LibraryType.LightNovel:
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(true);
break;
case LibraryType.Images:
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true);
@ -237,8 +253,8 @@ export class LibrarySettingsModalComponent implements OnInit {
}
forceScan() {
this.libraryService.scan(this.library.id, true)
.subscribe(() => this.toastr.info(translate('toasts.forced-scan-queued', {name: this.library.name})));
this.libraryService.scan(this.library!.id, true)
.subscribe(() => this.toastr.info(translate('toasts.forced-scan-queued', {name: this.library!.name})));
}
async save() {
@ -295,7 +311,7 @@ export class LibrarySettingsModalComponent implements OnInit {
}
applyCoverImage(coverUrl: string) {
this.uploadService.updateLibraryCoverImage(this.library.id, coverUrl).subscribe(() => {});
this.uploadService.updateLibraryCoverImage(this.library!.id, coverUrl).subscribe(() => {});
}
updateCoverImageIndex(selectedIndex: number) {
@ -304,7 +320,7 @@ export class LibrarySettingsModalComponent implements OnInit {
}
resetCoverImage() {
this.uploadService.updateLibraryCoverImage(this.library.id, '').subscribe(() => {});
this.uploadService.updateLibraryCoverImage(this.library!.id, '').subscribe(() => {});
}
openDirectoryPicker() {