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

@ -26,48 +26,10 @@ jobs:
- name: Install dependencies
run: dotnet restore
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- uses: actions/upload-artifact@v3
with:
name: csproj
path: Kavita.Common/Kavita.Common.csproj
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~\sonar\cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
uses: actions/cache@v3
with:
path: .\.sonar\scanner
key: ${{ runner.os }}-sonar-scanner
restore-keys: ${{ runner.os }}-sonar-scanner
- name: Install SonarCloud scanner
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
shell: powershell
run: |
New-Item -Path .\.sonar\scanner -ItemType Directory
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
- name: Sonar Scan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: powershell
run: |
.\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
dotnet build --configuration Release
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
- name: Test
run: dotnet test --no-restore --verbosity normal

View File

@ -59,6 +59,13 @@ jobs:
id: parse-body
run: |
body="${{ steps.findPr.outputs.body }}"
body=${body//\'/}
body=${body//'%'/'%25'}
body=${body//$'\n'/'%0A'}
body=${body//$'\r'/'%0D'}
body=${body//$'`'/'%60'}
body=${body//$'>'/'%3E'}
if [[ ${#body} -gt 1870 ]] ; then
body=${body:0:1870}
body="${body}...and much more.
@ -66,16 +73,9 @@ jobs:
Read full changelog: https://github.com/Kareadita/Kavita/releases/latest"
fi
body=${body//\'/}
body=${body//'%'/'%25'}
body=${body//$'\n'/'%0A'}
body=${body//$'\r'/'%0D'}
body=${body//$'`'/'%60'}
body=${body//$'>'/'%3E'}
echo $body
echo "BODY=$body" >> $GITHUB_OUTPUT
- name: Check Out Repo
uses: actions/checkout@v3
with:

View File

@ -69,7 +69,7 @@
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.57" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.58" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />

View File

@ -24,4 +24,9 @@ public enum LibraryType
/// </summary>
[Description("Image")]
Image = 3,
/// <summary>
/// Allows Books to Scrobble with AniList for Kavita+
/// </summary>
[Description("Light Novel")]
LightNovel = 4,
}

View File

@ -20,7 +20,7 @@ public class LibraryBuilder : IEntityBuilder<Library>
Series = new List<Series>(),
Folders = new List<FolderPath>(),
AppUsers = new List<AppUser>(),
AllowScrobbling = type is LibraryType.Book or LibraryType.Manga
AllowScrobbling = type is LibraryType.LightNovel or LibraryType.Manga
};
}

View File

@ -13,7 +13,7 @@ public static class LibraryTypeHelper
{
LibraryType.Manga => MediaFormat.Manga,
LibraryType.Comic => MediaFormat.Comic,
LibraryType.Book => MediaFormat.LightNovel,
LibraryType.LightNovel => MediaFormat.LightNovel,
};
}
}

View File

@ -69,7 +69,7 @@ public class ExternalMetadataService : IExternalMetadataService
private readonly IMapper _mapper;
private readonly ILicenseService _licenseService;
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30);
public static readonly ImmutableArray<LibraryType> NonEligibleLibraryTypes = ImmutableArray.Create<LibraryType>(LibraryType.Comic);
public static readonly ImmutableArray<LibraryType> NonEligibleLibraryTypes = ImmutableArray.Create<LibraryType>(LibraryType.Comic, LibraryType.Book);
private readonly SeriesDetailPlusDto _defaultReturn = new()
{
Recommendations = null,
@ -420,6 +420,7 @@ public class ExternalMetadataService : IExternalMetadataService
LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga,
LibraryType.Comic => MediaFormat.Comic,
LibraryType.Book => MediaFormat.Book,
LibraryType.LightNovel => MediaFormat.LightNovel,
_ => MediaFormat.Unknown
};
}

View File

@ -80,6 +80,9 @@ public class ScrobblingService : IScrobblingService
private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90)
private static readonly IList<ScrobbleProvider> BookProviders = new List<ScrobbleProvider>()
{
};
private static readonly IList<ScrobbleProvider> LightNovelProviders = new List<ScrobbleProvider>()
{
ScrobbleProvider.AniList
};
@ -877,6 +880,12 @@ public class ScrobblingService : IScrobblingService
return true;
}
if (readEvent.Series.Library.Type == LibraryType.LightNovel &&
LightNovelProviders.Intersect(userProviders).Any())
{
return true;
}
return false;
}

View File

@ -776,6 +776,7 @@ public class ReaderService : IReaderService
}
return "Issue" + (includeSpace ? " " : string.Empty);
case LibraryType.Book:
case LibraryType.LightNovel:
return "Book" + (includeSpace ? " " : string.Empty);
default:
throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null);

View File

@ -489,7 +489,7 @@ public class SeriesService : ISeriesService
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
var processedVolumes = new List<VolumeDto>();
if (libraryType == LibraryType.Book)
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
{
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
foreach (var volume in volumes)
@ -533,7 +533,7 @@ public class SeriesService : ISeriesService
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
IEnumerable<ChapterDto> retChapters;
if (libraryType == LibraryType.Book)
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
{
retChapters = Array.Empty<ChapterDto>();
} else
@ -576,7 +576,7 @@ public class SeriesService : ISeriesService
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume")
{
if (libraryType == LibraryType.Book)
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
{
if (string.IsNullOrEmpty(firstChapter.TitleName))
{
@ -587,6 +587,7 @@ public class SeriesService : ISeriesService
}
else if (volume.Name != "0")
{
// If the titleName has Volume inside it, let's just send that back?
volume.Name += $" - {firstChapter.TitleName}";
}
// else
@ -614,6 +615,7 @@ public class SeriesService : ISeriesService
return libraryType switch
{
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle),
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterTitle),
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle),
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle),
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
@ -636,6 +638,7 @@ public class SeriesService : ISeriesService
return (libraryType switch
{
LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty),
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", string.Empty),
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty),
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
@ -723,7 +726,8 @@ public class SeriesService : ISeriesService
{
throw new UnauthorizedAccessException("user-no-access-library-from-series");
}
if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book)
if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) ||
(series.Library.Type is LibraryType.Book or LibraryType.LightNovel))
{
return _emptyExpectedChapter;
}
@ -803,6 +807,7 @@ public class SeriesService : ISeriesService
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber),
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber),
LibraryType.Book => await _localizationService.Translate(userId, "book-num", result.ChapterNumber),
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", result.ChapterNumber),
_ => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber)
};
}

View File

@ -595,7 +595,8 @@ public class StatisticService : IStatisticService
{
UserId = userId,
Username = users.First(u => u.Id == userId).UserName,
BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0,
BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0 +
(user[userId].TryGetValue(LibraryType.LightNovel, out var bookTime2) ? bookTime2 : 0),
ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0,
MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 0,
})

View File

@ -152,7 +152,9 @@ public class ScannerService : IScannerService
_logger.LogCritical("[ScannerService] Multiple series map to this folder. Library scan will be used for ScanFolder");
}
}
if (series != null && series.Library.Type != LibraryType.Book)
// TODO: Figure out why we have the library type restriction here
if (series != null && (series.Library.Type != LibraryType.Book || series.Library.Type != LibraryType.LightNovel))
{
if (TaskScheduler.HasScanTaskRunningForSeries(series.Id))
{

View File

@ -5,11 +5,9 @@ using System.Threading.Tasks;
using API.DTOs.Update;
using API.SignalR;
using Flurl.Http;
using HtmlAgilityPack;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using MarkdownDeep;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace API.Services.Tasks;

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Company>kavitareader.com</Company>
<Product>Kavita</Product>
<AssemblyVersion>0.7.14.0</AssemblyVersion>
<AssemblyVersion>0.8.0.0</AssemblyVersion>
<NeutralLanguage>en</NeutralLanguage>
<TieredPGO>true</TieredPGO>
</PropertyGroup>

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() {

View File

@ -811,6 +811,8 @@
"last-scanned-label": "Last Scanned:",
"type-label": "Type",
"type-tooltip": "Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but have different naming in the UI.",
"kavitaplus-eligible-label": "Kavita+ Eligible",
"kavitaplus-eligible-tooltip": "Will Kavita+ pull information or support Scrobbling",
"folder-description": "Add folders to your library",
"browse": "Browse for Media Folders",
"help-us-part-1": "Help us out by following ",

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.14.0"
"version": "0.8.0.0"
},
"servers": [
{
@ -2908,7 +2908,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -2920,7 +2921,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -2932,7 +2934,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -3615,7 +3618,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -13519,7 +13523,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -14111,7 +14116,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"description": "Library type",
@ -15879,7 +15885,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -15992,7 +15999,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -16981,7 +16989,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -17033,7 +17042,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"
@ -19371,7 +19381,8 @@
0,
1,
2,
3
3,
4
],
"type": "integer",
"format": "int32"