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 - name: Install dependencies
run: dotnet restore run: dotnet restore
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: csproj name: csproj
path: Kavita.Common/Kavita.Common.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 - name: Test
run: dotnet test --no-restore --verbosity normal run: dotnet test --no-restore --verbosity normal

View File

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

View File

@ -69,7 +69,7 @@
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" /> <PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" /> <PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.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="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />

View File

@ -24,4 +24,9 @@ public enum LibraryType
/// </summary> /// </summary>
[Description("Image")] [Description("Image")]
Image = 3, 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>(), Series = new List<Series>(),
Folders = new List<FolderPath>(), Folders = new List<FolderPath>(),
AppUsers = new List<AppUser>(), 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.Manga => MediaFormat.Manga,
LibraryType.Comic => MediaFormat.Comic, 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 IMapper _mapper;
private readonly ILicenseService _licenseService; private readonly ILicenseService _licenseService;
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30); 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() private readonly SeriesDetailPlusDto _defaultReturn = new()
{ {
Recommendations = null, Recommendations = null,
@ -420,6 +420,7 @@ public class ExternalMetadataService : IExternalMetadataService
LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga, LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga,
LibraryType.Comic => MediaFormat.Comic, LibraryType.Comic => MediaFormat.Comic,
LibraryType.Book => MediaFormat.Book, LibraryType.Book => MediaFormat.Book,
LibraryType.LightNovel => MediaFormat.LightNovel,
_ => MediaFormat.Unknown _ => 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 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> BookProviders = new List<ScrobbleProvider>()
{
};
private static readonly IList<ScrobbleProvider> LightNovelProviders = new List<ScrobbleProvider>()
{ {
ScrobbleProvider.AniList ScrobbleProvider.AniList
}; };
@ -877,6 +880,12 @@ public class ScrobblingService : IScrobblingService
return true; return true;
} }
if (readEvent.Series.Library.Type == LibraryType.LightNovel &&
LightNovelProviders.Intersect(userProviders).Any())
{
return true;
}
return false; return false;
} }

View File

@ -776,6 +776,7 @@ public class ReaderService : IReaderService
} }
return "Issue" + (includeSpace ? " " : string.Empty); return "Issue" + (includeSpace ? " " : string.Empty);
case LibraryType.Book: case LibraryType.Book:
case LibraryType.LightNovel:
return "Book" + (includeSpace ? " " : string.Empty); return "Book" + (includeSpace ? " " : string.Empty);
default: default:
throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null); 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. // 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>(); 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); var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
foreach (var volume in volumes) 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) // 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; IEnumerable<ChapterDto> retChapters;
if (libraryType == LibraryType.Book) if (libraryType is LibraryType.Book or LibraryType.LightNovel)
{ {
retChapters = Array.Empty<ChapterDto>(); retChapters = Array.Empty<ChapterDto>();
} else } else
@ -576,7 +576,7 @@ public class SeriesService : ISeriesService
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume") 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)) if (string.IsNullOrEmpty(firstChapter.TitleName))
{ {
@ -587,6 +587,7 @@ public class SeriesService : ISeriesService
} }
else if (volume.Name != "0") else if (volume.Name != "0")
{ {
// If the titleName has Volume inside it, let's just send that back?
volume.Name += $" - {firstChapter.TitleName}"; volume.Name += $" - {firstChapter.TitleName}";
} }
// else // else
@ -614,6 +615,7 @@ public class SeriesService : ISeriesService
return libraryType switch return libraryType switch
{ {
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle), 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.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle),
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle),
_ => await _localizationService.Translate(userId, "chapter-num", ' ') _ => await _localizationService.Translate(userId, "chapter-num", ' ')
@ -636,6 +638,7 @@ public class SeriesService : ISeriesService
return (libraryType switch return (libraryType switch
{ {
LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty), 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.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty),
_ => await _localizationService.Translate(userId, "chapter-num", ' ') _ => await _localizationService.Translate(userId, "chapter-num", ' ')
@ -723,7 +726,8 @@ public class SeriesService : ISeriesService
{ {
throw new UnauthorizedAccessException("user-no-access-library-from-series"); 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; return _emptyExpectedChapter;
} }
@ -803,6 +807,7 @@ public class SeriesService : ISeriesService
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber),
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber),
LibraryType.Book => await _localizationService.Translate(userId, "book-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) _ => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber)
}; };
} }

View File

@ -595,7 +595,8 @@ public class StatisticService : IStatisticService
{ {
UserId = userId, UserId = userId,
Username = users.First(u => u.Id == userId).UserName, 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, ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0,
MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 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"); _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)) if (TaskScheduler.HasScanTaskRunningForSeries(series.Id))
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -137,7 +137,7 @@ interface StoryLineItem {
isChapter: boolean; isChapter: boolean;
} }
const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.Book]; const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.LightNovel];
@Component({ @Component({
selector: 'app-series-detail', 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() { get ScrollingBlockHeight() {
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)'; if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
const navbar = this.document.querySelector('.navbar') as HTMLElement; const navbar = this.document.querySelector('.navbar') as HTMLElement;
@ -614,6 +629,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.loadPlusMetadata(this.seriesId, this.libraryType); 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.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this)) this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
@ -694,12 +716,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
*/ */
updateSelectedTab() { updateSelectedTab() {
// Book libraries only have Volumes or Specials enabled // 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) { if (this.volumes.length === 0) {
this.activeTabId = TabID.Specials; this.activeTabId = TabID.Specials;
} else { } else {
this.activeTabId = TabID.Volumes; this.activeTabId = TabID.Volumes;
} }
this.cdRef.markForCheck();
return; return;
} }
@ -708,6 +731,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
} else { } else {
this.activeTabId = TabID.Storyline; 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> <app-read-more [text]="seriesSummary" [maxLength]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 1000 : 250"></app-read-more>
</div> </div>
<!-- Ratings -->
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" [heading]="t('rating-title')"> <app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" [heading]="t('rating-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-external-rating [seriesId]="series.id" [ratings]="ratings" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating> <app-external-rating [seriesId]="series.id" [ratings]="ratings" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
</ng-template> </ng-template>
</app-metadata-detail> </app-metadata-detail>
<!-- Weblinks -->
<ng-container *ngIf="WebLinks as links"> <ng-container *ngIf="WebLinks as links">
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')"> <app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
@ -24,14 +24,17 @@
</ng-container> </ng-container>
<!-- Genres -->
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterField.Genres" [heading]="t('genres-title')"> <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> <ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail> </app-metadata-detail>
<!-- Tags -->
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterField.Tags" [heading]="t('tags-title')"> <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> <ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail> </app-metadata-detail>
<!-- Collections -->
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" [heading]="t('collections-title')"> <app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" [heading]="t('collections-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable"> <app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
@ -40,7 +43,7 @@
</ng-template> </ng-template>
</app-metadata-detail> </app-metadata-detail>
<!-- Reading Lists -->
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" [heading]="t('reading-lists-title')"> <app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" [heading]="t('reading-lists-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable"> <app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
@ -53,6 +56,16 @@
</ng-template> </ng-template>
</app-metadata-detail> </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')"> <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
@ -62,70 +75,95 @@
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata"> <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')"> <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge> <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template> </ng-template>
</app-metadata-detail> </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')"> <!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
<ng-template #itemTemplate let-item> <!-- <ng-template #itemTemplate let-item>-->
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge> <!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
</ng-template> <!-- </ng-template>-->
</app-metadata-detail> <!-- </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')"> <!-- <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">-->
<ng-template #itemTemplate let-item> <!-- <ng-template #itemTemplate let-item>-->
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge> <!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
</ng-template> <!-- </ng-template>-->
</app-metadata-detail> <!-- </app-metadata-detail>-->
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterField.Letterer" [heading]="t('letterers-title')"> <app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge> <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template> </ng-template>
</app-metadata-detail> </app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')"> <app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterField.Colorist" [heading]="t('colorists-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge> <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template> </ng-template>
</app-metadata-detail> </app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterField.Penciller" [heading]="t('pencillers-title')"> <app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterField.Editor" [heading]="t('editors-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge> <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template> </ng-template>
</app-metadata-detail> </app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterField.Publisher" [heading]="t('publishers-title')"> <app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterField.Inker" [heading]="t('inkers-title')">
<ng-template #itemTemplate let-item> <ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge> <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template> </ng-template>
</app-metadata-detail> </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>
<div class="row g-0"> <div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperties" > <hr class="col mt-3" *ngIf="hasExtendedProperties" >
<a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties" <a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()"> 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> <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')}} {{isCollapsed ? t('see-more') : t('see-less')}}
</a> </a>
</div> </div>
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards> <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) { formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) {
switch(libraryType) { switch(libraryType) {
case LibraryType.Book: case LibraryType.Book:
case LibraryType.LightNovel:
return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : ''); return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : '');
case LibraryType.Comic: case LibraryType.Comic:
if (includeHash) { if (includeHash) {

View File

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

View File

@ -2,7 +2,7 @@
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title"> <h4 class="modal-title" id="modal-basic-title">
<ng-container *ngIf="!isAddLibrary; else addLibraryTitle"> <ng-container *ngIf="!isAddLibrary; else addLibraryTitle">
{{t('edit-title', {name: library.name | sentenceCase})}} {{t('edit-title', {name: library!.name | sentenceCase})}}
</ng-container> </ng-container>
<ng-template #addLibraryTitle> <ng-template #addLibraryTitle>
{{t('add-title')}} {{t('add-title')}}
@ -33,8 +33,15 @@
<div class="mb-3"> <div class="mb-3">
<label for="library-type" class="form-label">{{t('type-label')}}</label> <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 #typeTooltip>{{t('type-tooltip')}}</ng-template>
<ng-template #kavitaplusEligibleTooltip>{{t('kavitaplus-eligible-tooltip')}}</ng-template>
<span class="visually-hidden" id="library-type-help"> <span class="visually-hidden" id="library-type-help">
<ng-container [ngTemplateOutlet]="typeTooltip"></ng-container> <ng-container [ngTemplateOutlet]="typeTooltip"></ng-container>
</span> </span>
@ -44,7 +51,7 @@
</div> </div>
<div *ngIf="!isAddLibrary"> <div *ngIf="!isAddLibrary">
{{t('last-scanned-label')}} {{t('last-scanned-label')}}
<span>{{library.lastScanned | date: 'short' | defaultDate}}</span> <span>{{library?.lastScanned | date: 'short' | defaultDate}}</span>
</div> </div>
</ng-template> </ng-template>
</li> </li>

View File

@ -72,9 +72,23 @@ enum StepID {
}) })
export class LibrarySettingsModalComponent implements OnInit { 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; active = TabID.General;
imageUrls: Array<string> = []; imageUrls: Array<string> = [];
@ -101,14 +115,10 @@ export class LibrarySettingsModalComponent implements OnInit {
fileTypeGroups = allFileTypeGroup; fileTypeGroups = allFileTypeGroup;
excludePatterns: Array<string> = ['']; excludePatterns: Array<string> = [''];
protected readonly Breakpoint = Breakpoint; get IsKavitaPlusEligible() {
protected readonly TabID = TabID; const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType;
return libType === LibraryType.Manga || libType === LibraryType.LightNovel;
}
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) { }
ngOnInit(): void { ngOnInit(): void {
this.settingService.getLibraryTypes().subscribe((types) => { this.settingService.getLibraryTypes().subscribe((types) => {
@ -126,7 +136,7 @@ export class LibrarySettingsModalComponent implements OnInit {
this.cdRef.markForCheck(); 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')?.setValue(false);
this.libraryForm.get('allowScrobbling')?.disable(); 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.Pdf + '')?.setValue(true);
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(true); this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(true);
break; 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: case LibraryType.Images:
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false); this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true); this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true);
@ -237,8 +253,8 @@ export class LibrarySettingsModalComponent implements OnInit {
} }
forceScan() { forceScan() {
this.libraryService.scan(this.library.id, true) this.libraryService.scan(this.library!.id, true)
.subscribe(() => this.toastr.info(translate('toasts.forced-scan-queued', {name: this.library.name}))); .subscribe(() => this.toastr.info(translate('toasts.forced-scan-queued', {name: this.library!.name})));
} }
async save() { async save() {
@ -295,7 +311,7 @@ export class LibrarySettingsModalComponent implements OnInit {
} }
applyCoverImage(coverUrl: string) { applyCoverImage(coverUrl: string) {
this.uploadService.updateLibraryCoverImage(this.library.id, coverUrl).subscribe(() => {}); this.uploadService.updateLibraryCoverImage(this.library!.id, coverUrl).subscribe(() => {});
} }
updateCoverImageIndex(selectedIndex: number) { updateCoverImageIndex(selectedIndex: number) {
@ -304,7 +320,7 @@ export class LibrarySettingsModalComponent implements OnInit {
} }
resetCoverImage() { resetCoverImage() {
this.uploadService.updateLibraryCoverImage(this.library.id, '').subscribe(() => {}); this.uploadService.updateLibraryCoverImage(this.library!.id, '').subscribe(() => {});
} }
openDirectoryPicker() { openDirectoryPicker() {

View File

@ -811,6 +811,8 @@
"last-scanned-label": "Last Scanned:", "last-scanned-label": "Last Scanned:",
"type-label": "Type", "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.", "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", "folder-description": "Add folders to your library",
"browse": "Browse for Media Folders", "browse": "Browse for Media Folders",
"help-us-part-1": "Help us out by following ", "help-us-part-1": "Help us out by following ",

View File

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