mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Light Novel Library Type (#2682)
This commit is contained in:
parent
a40c019ddb
commit
8b2649302c
38
.github/workflows/build-and-test.yml
vendored
38
.github/workflows/build-and-test.yml
vendored
@ -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
|
||||||
|
14
.github/workflows/release-workflow.yml
vendored
14
.github/workflows/release-workflow.yml
vendored
@ -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:
|
||||||
|
@ -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" />
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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:
|
||||||
|
@ -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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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 ",
|
||||||
|
35
openapi.json
35
openapi.json
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user