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
|
||||
run: dotnet restore
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: csproj
|
||||
path: Kavita.Common/Kavita.Common.csproj
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||
|
||||
- name: Sonar Scan
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
|
||||
dotnet build --configuration Release
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --verbosity normal
|
||||
|
14
.github/workflows/release-workflow.yml
vendored
14
.github/workflows/release-workflow.yml
vendored
@ -59,6 +59,13 @@ jobs:
|
||||
id: parse-body
|
||||
run: |
|
||||
body="${{ steps.findPr.outputs.body }}"
|
||||
body=${body//\'/}
|
||||
body=${body//'%'/'%25'}
|
||||
body=${body//$'\n'/'%0A'}
|
||||
body=${body//$'\r'/'%0D'}
|
||||
body=${body//$'`'/'%60'}
|
||||
body=${body//$'>'/'%3E'}
|
||||
|
||||
if [[ ${#body} -gt 1870 ]] ; then
|
||||
body=${body:0:1870}
|
||||
body="${body}...and much more.
|
||||
@ -66,16 +73,9 @@ jobs:
|
||||
Read full changelog: https://github.com/Kareadita/Kavita/releases/latest"
|
||||
fi
|
||||
|
||||
body=${body//\'/}
|
||||
body=${body//'%'/'%25'}
|
||||
body=${body//$'\n'/'%0A'}
|
||||
body=${body//$'\r'/'%0D'}
|
||||
body=${body//$'`'/'%60'}
|
||||
body=${body//$'>'/'%3E'}
|
||||
echo $body
|
||||
echo "BODY=$body" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -69,7 +69,7 @@
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.57" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.58" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
|
@ -24,4 +24,9 @@ public enum LibraryType
|
||||
/// </summary>
|
||||
[Description("Image")]
|
||||
Image = 3,
|
||||
/// <summary>
|
||||
/// Allows Books to Scrobble with AniList for Kavita+
|
||||
/// </summary>
|
||||
[Description("Light Novel")]
|
||||
LightNovel = 4,
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ public class LibraryBuilder : IEntityBuilder<Library>
|
||||
Series = new List<Series>(),
|
||||
Folders = new List<FolderPath>(),
|
||||
AppUsers = new List<AppUser>(),
|
||||
AllowScrobbling = type is LibraryType.Book or LibraryType.Manga
|
||||
AllowScrobbling = type is LibraryType.LightNovel or LibraryType.Manga
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ public static class LibraryTypeHelper
|
||||
{
|
||||
LibraryType.Manga => MediaFormat.Manga,
|
||||
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 ILicenseService _licenseService;
|
||||
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30);
|
||||
public static readonly ImmutableArray<LibraryType> NonEligibleLibraryTypes = ImmutableArray.Create<LibraryType>(LibraryType.Comic);
|
||||
public static readonly ImmutableArray<LibraryType> NonEligibleLibraryTypes = ImmutableArray.Create<LibraryType>(LibraryType.Comic, LibraryType.Book);
|
||||
private readonly SeriesDetailPlusDto _defaultReturn = new()
|
||||
{
|
||||
Recommendations = null,
|
||||
@ -420,6 +420,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga,
|
||||
LibraryType.Comic => MediaFormat.Comic,
|
||||
LibraryType.Book => MediaFormat.Book,
|
||||
LibraryType.LightNovel => MediaFormat.LightNovel,
|
||||
_ => MediaFormat.Unknown
|
||||
};
|
||||
}
|
||||
|
@ -80,6 +80,9 @@ public class ScrobblingService : IScrobblingService
|
||||
private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90)
|
||||
|
||||
private static readonly IList<ScrobbleProvider> BookProviders = new List<ScrobbleProvider>()
|
||||
{
|
||||
};
|
||||
private static readonly IList<ScrobbleProvider> LightNovelProviders = new List<ScrobbleProvider>()
|
||||
{
|
||||
ScrobbleProvider.AniList
|
||||
};
|
||||
@ -877,6 +880,12 @@ public class ScrobblingService : IScrobblingService
|
||||
return true;
|
||||
}
|
||||
|
||||
if (readEvent.Series.Library.Type == LibraryType.LightNovel &&
|
||||
LightNovelProviders.Intersect(userProviders).Any())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -776,6 +776,7 @@ public class ReaderService : IReaderService
|
||||
}
|
||||
return "Issue" + (includeSpace ? " " : string.Empty);
|
||||
case LibraryType.Book:
|
||||
case LibraryType.LightNovel:
|
||||
return "Book" + (includeSpace ? " " : string.Empty);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null);
|
||||
|
@ -489,7 +489,7 @@ public class SeriesService : ISeriesService
|
||||
|
||||
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
|
||||
var processedVolumes = new List<VolumeDto>();
|
||||
if (libraryType == LibraryType.Book)
|
||||
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
|
||||
{
|
||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||
foreach (var volume in volumes)
|
||||
@ -533,7 +533,7 @@ public class SeriesService : ISeriesService
|
||||
|
||||
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
|
||||
IEnumerable<ChapterDto> retChapters;
|
||||
if (libraryType == LibraryType.Book)
|
||||
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
|
||||
{
|
||||
retChapters = Array.Empty<ChapterDto>();
|
||||
} else
|
||||
@ -576,7 +576,7 @@ public class SeriesService : ISeriesService
|
||||
|
||||
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume")
|
||||
{
|
||||
if (libraryType == LibraryType.Book)
|
||||
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
|
||||
{
|
||||
if (string.IsNullOrEmpty(firstChapter.TitleName))
|
||||
{
|
||||
@ -587,6 +587,7 @@ public class SeriesService : ISeriesService
|
||||
}
|
||||
else if (volume.Name != "0")
|
||||
{
|
||||
// If the titleName has Volume inside it, let's just send that back?
|
||||
volume.Name += $" - {firstChapter.TitleName}";
|
||||
}
|
||||
// else
|
||||
@ -614,6 +615,7 @@ public class SeriesService : ISeriesService
|
||||
return libraryType switch
|
||||
{
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle),
|
||||
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterTitle),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
@ -636,6 +638,7 @@ public class SeriesService : ISeriesService
|
||||
return (libraryType switch
|
||||
{
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty),
|
||||
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", string.Empty),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
@ -723,7 +726,8 @@ public class SeriesService : ISeriesService
|
||||
{
|
||||
throw new UnauthorizedAccessException("user-no-access-library-from-series");
|
||||
}
|
||||
if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book)
|
||||
if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) ||
|
||||
(series.Library.Type is LibraryType.Book or LibraryType.LightNovel))
|
||||
{
|
||||
return _emptyExpectedChapter;
|
||||
}
|
||||
@ -803,6 +807,7 @@ public class SeriesService : ISeriesService
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber),
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", result.ChapterNumber),
|
||||
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", result.ChapterNumber),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber)
|
||||
};
|
||||
}
|
||||
|
@ -595,7 +595,8 @@ public class StatisticService : IStatisticService
|
||||
{
|
||||
UserId = userId,
|
||||
Username = users.First(u => u.Id == userId).UserName,
|
||||
BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0,
|
||||
BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0 +
|
||||
(user[userId].TryGetValue(LibraryType.LightNovel, out var bookTime2) ? bookTime2 : 0),
|
||||
ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0,
|
||||
MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 0,
|
||||
})
|
||||
|
@ -152,7 +152,9 @@ public class ScannerService : IScannerService
|
||||
_logger.LogCritical("[ScannerService] Multiple series map to this folder. Library scan will be used for ScanFolder");
|
||||
}
|
||||
}
|
||||
if (series != null && series.Library.Type != LibraryType.Book)
|
||||
|
||||
// TODO: Figure out why we have the library type restriction here
|
||||
if (series != null && (series.Library.Type != LibraryType.Book || series.Library.Type != LibraryType.LightNovel))
|
||||
{
|
||||
if (TaskScheduler.HasScanTaskRunningForSeries(series.Id))
|
||||
{
|
||||
|
@ -5,11 +5,9 @@ using System.Threading.Tasks;
|
||||
using API.DTOs.Update;
|
||||
using API.SignalR;
|
||||
using Flurl.Http;
|
||||
using HtmlAgilityPack;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Kavita.Common.Helpers;
|
||||
using MarkdownDeep;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services.Tasks;
|
||||
|
@ -4,7 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Company>kavitareader.com</Company>
|
||||
<Product>Kavita</Product>
|
||||
<AssemblyVersion>0.7.14.0</AssemblyVersion>
|
||||
<AssemblyVersion>0.8.0.0</AssemblyVersion>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
</PropertyGroup>
|
||||
|
@ -4,7 +4,8 @@ export enum LibraryType {
|
||||
Manga = 0,
|
||||
Comic = 1,
|
||||
Book = 2,
|
||||
Images = 3
|
||||
Images = 3,
|
||||
LightNovel = 4
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
|
@ -27,5 +27,8 @@
|
||||
<ng-container *ngSwitchCase="LibraryType.Book">
|
||||
{{volumeTitle}}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.LightNovel">
|
||||
{{volumeTitle}}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -139,7 +139,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" [ngClass]="{'pt-3': !seriesMetadata || seriesMetadata?.summary?.length === 0}">
|
||||
<div class="row" [ngClass]="{'pt-3': !seriesMetadata || seriesMetadata.summary.length === 0}">
|
||||
<app-carousel-reel [items]="reviews" [alwaysShow]="true" [title]="t('user-reviews-alt')"
|
||||
iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}"
|
||||
[clickableTitle]="true" (sectionClick)="openReviewModal()">
|
||||
@ -153,7 +153,7 @@
|
||||
<ng-container *ngIf="series">
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
|
||||
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)">
|
||||
<li [ngbNavItem]="TabID.Storyline" *ngIf="ShowStorylineTab">
|
||||
<a ngbNavLink>{{t('storyline-tab')}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
@ -181,8 +181,8 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.Volumes" *ngIf="volumes.length > 0">
|
||||
<a ngbNavLink>{{libraryType === LibraryType.Book ? t('books-tab') : t('volumes-tab')}}</a>
|
||||
<li [ngbNavItem]="TabID.Volumes" *ngIf="ShowVolumeTab">
|
||||
<a ngbNavLink>{{UseBookLogic ? t('books-tab') : t('volumes-tab')}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else volumeListLayout">
|
||||
@ -202,7 +202,7 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0">
|
||||
<li [ngbNavItem]="TabID.Chapters" *ngIf="ShowChaptersTab">
|
||||
<a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
|
@ -48,6 +48,7 @@
|
||||
|
||||
.image-container {
|
||||
max-height: 400px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
@ -63,4 +64,4 @@
|
||||
border-bottom-right-radius: 6px !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ interface StoryLineItem {
|
||||
isChapter: boolean;
|
||||
}
|
||||
|
||||
const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.Book];
|
||||
const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.LightNovel];
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-detail',
|
||||
@ -337,6 +337,21 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
}
|
||||
|
||||
get ShowStorylineTab() {
|
||||
return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel) && (this.volumes.length > 0 || this.chapters.length > 0);
|
||||
}
|
||||
|
||||
get ShowVolumeTab() {
|
||||
return this.volumes.length > 0;
|
||||
}
|
||||
get ShowChaptersTab() {
|
||||
return this.chapters.length > 0;
|
||||
}
|
||||
|
||||
get UseBookLogic() {
|
||||
return this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel;
|
||||
}
|
||||
|
||||
get ScrollingBlockHeight() {
|
||||
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
|
||||
const navbar = this.document.querySelector('.navbar') as HTMLElement;
|
||||
@ -614,6 +629,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
this.loadPlusMetadata(this.seriesId, this.libraryType);
|
||||
}
|
||||
|
||||
if (this.libraryType === LibraryType.LightNovel) {
|
||||
this.renderMode = PageLayoutMode.List;
|
||||
this.pageExtrasGroup.get('renderMode')?.setValue(this.renderMode);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
|
||||
|
||||
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
|
||||
@ -694,12 +716,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
*/
|
||||
updateSelectedTab() {
|
||||
// Book libraries only have Volumes or Specials enabled
|
||||
if (this.libraryType === LibraryType.Book) {
|
||||
if (this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel) {
|
||||
if (this.volumes.length === 0) {
|
||||
this.activeTabId = TabID.Specials;
|
||||
} else {
|
||||
this.activeTabId = TabID.Volumes;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -708,6 +731,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
} else {
|
||||
this.activeTabId = TabID.Storyline;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,14 +3,14 @@
|
||||
<app-read-more [text]="seriesSummary" [maxLength]="utilityService.getActiveBreakpoint() >= Breakpoint.Desktop ? 1000 : 250"></app-read-more>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Ratings -->
|
||||
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" [heading]="t('rating-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-external-rating [seriesId]="series.id" [ratings]="ratings" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<!-- Weblinks -->
|
||||
<ng-container *ngIf="WebLinks as links">
|
||||
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
@ -24,14 +24,17 @@
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- Genres -->
|
||||
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterField.Genres" [heading]="t('genres-title')">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<!-- Tags -->
|
||||
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterField.Tags" [heading]="t('tags-title')">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<!-- Collections -->
|
||||
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" [heading]="t('collections-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
@ -40,7 +43,7 @@
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<!-- Reading Lists -->
|
||||
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" [heading]="t('reading-lists-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
@ -53,6 +56,16 @@
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<!-- Key Person Information -->
|
||||
<!-- @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {-->
|
||||
<!-- -->
|
||||
|
||||
<!-- <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">-->
|
||||
<!-- <ng-template #itemTemplate let-item>-->
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-metadata-detail>-->
|
||||
<!-- }-->
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
@ -62,70 +75,95 @@
|
||||
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
||||
|
||||
<!-- @if (libraryType === LibraryType.Comic || libraryType === LibraryType.Images) {-->
|
||||
<!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
|
||||
<!-- <ng-template #itemTemplate let-item>-->
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-metadata-detail>-->
|
||||
|
||||
<!-- -->
|
||||
<!-- }-->
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterField.Colorist" [heading]="t('colorists-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
<!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
|
||||
<!-- <ng-template #itemTemplate let-item>-->
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-metadata-detail>-->
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterField.Editor" [heading]="t('editors-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterField.Inker" [heading]="t('inkers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
<!-- <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">-->
|
||||
<!-- <ng-template #itemTemplate let-item>-->
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-metadata-detail>-->
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterField.Letterer" [heading]="t('letterers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterField.Colorist" [heading]="t('colorists-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterField.Penciller" [heading]="t('pencillers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
<app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterField.Editor" [heading]="t('editors-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterField.Publisher" [heading]="t('publishers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
<app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterField.Inker" [heading]="t('inkers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterField.Letterer" [heading]="t('letterers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterField.Penciller" [heading]="t('pencillers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterField.Publisher" [heading]="t('publishers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<hr class="col mt-3" *ngIf="hasExtendedProperties" >
|
||||
<a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
|
||||
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
|
||||
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
|
||||
{{isCollapsed ? t('see-more') : t('see-less')}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<hr class="col mt-3" *ngIf="hasExtendedProperties" >
|
||||
<a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
|
||||
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
|
||||
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
|
||||
{{isCollapsed ? t('see-more') : t('see-less')}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
|
||||
|
||||
|
@ -64,6 +64,7 @@ export class UtilityService {
|
||||
formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) {
|
||||
switch(libraryType) {
|
||||
case LibraryType.Book:
|
||||
case LibraryType.LightNovel:
|
||||
return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : '');
|
||||
case LibraryType.Comic:
|
||||
if (includeHash) {
|
||||
|
@ -185,6 +185,7 @@ export class SideNavComponent implements OnInit {
|
||||
getLibraryTypeIcon(format: LibraryType) {
|
||||
switch (format) {
|
||||
case LibraryType.Book:
|
||||
case LibraryType.LightNovel:
|
||||
return 'fa-book';
|
||||
case LibraryType.Comic:
|
||||
case LibraryType.Manga:
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">
|
||||
<ng-container *ngIf="!isAddLibrary; else addLibraryTitle">
|
||||
{{t('edit-title', {name: library.name | sentenceCase})}}
|
||||
{{t('edit-title', {name: library!.name | sentenceCase})}}
|
||||
</ng-container>
|
||||
<ng-template #addLibraryTitle>
|
||||
{{t('add-title')}}
|
||||
@ -33,8 +33,15 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="library-type" class="form-label">{{t('type-label')}}</label>
|
||||
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
|
||||
<i class="fa fa-info-circle ms-1" placement="top" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
|
||||
@if(IsKavitaPlusEligible) {
|
||||
<span class="float-end">
|
||||
{{t('kavitaplus-eligible-label')}}
|
||||
<i class="fa fa-info-circle ms-1" placement="top" [ngbTooltip]="kavitaplusEligibleTooltip" role="button" tabindex="0"></i>
|
||||
</span>
|
||||
}
|
||||
<ng-template #typeTooltip>{{t('type-tooltip')}}</ng-template>
|
||||
<ng-template #kavitaplusEligibleTooltip>{{t('kavitaplus-eligible-tooltip')}}</ng-template>
|
||||
<span class="visually-hidden" id="library-type-help">
|
||||
<ng-container [ngTemplateOutlet]="typeTooltip"></ng-container>
|
||||
</span>
|
||||
@ -44,7 +51,7 @@
|
||||
</div>
|
||||
<div *ngIf="!isAddLibrary">
|
||||
{{t('last-scanned-label')}}
|
||||
<span>{{library.lastScanned | date: 'short' | defaultDate}}</span>
|
||||
<span>{{library?.lastScanned | date: 'short' | defaultDate}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
@ -72,9 +72,23 @@ enum StepID {
|
||||
})
|
||||
export class LibrarySettingsModalComponent implements OnInit {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
|
||||
@Input({required: true}) library!: Library;
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
public readonly modal = inject(NgbActiveModal);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
private readonly modalService = inject(NgbModal);
|
||||
private readonly settingService = inject(SettingsService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly imageService = inject(ImageService);
|
||||
|
||||
@Input({required: true}) library!: Library | undefined;
|
||||
|
||||
active = TabID.General;
|
||||
imageUrls: Array<string> = [];
|
||||
@ -101,14 +115,10 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||
fileTypeGroups = allFileTypeGroup;
|
||||
excludePatterns: Array<string> = [''];
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly TabID = TabID;
|
||||
|
||||
|
||||
constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal,
|
||||
private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService,
|
||||
private libraryService: LibraryService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef,
|
||||
private imageService: ImageService) { }
|
||||
get IsKavitaPlusEligible() {
|
||||
const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType;
|
||||
return libType === LibraryType.Manga || libType === LibraryType.LightNovel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingService.getLibraryTypes().subscribe((types) => {
|
||||
@ -126,7 +136,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
if (this.library && this.library.type === LibraryType.Comic) {
|
||||
if (this.library && (this.library.type === LibraryType.Comic || this.library.type === LibraryType.Book)) {
|
||||
this.libraryForm.get('allowScrobbling')?.setValue(false);
|
||||
this.libraryForm.get('allowScrobbling')?.disable();
|
||||
}
|
||||
@ -173,6 +183,12 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(true);
|
||||
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(true);
|
||||
break;
|
||||
case LibraryType.LightNovel:
|
||||
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false);
|
||||
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false);
|
||||
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
|
||||
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(true);
|
||||
break;
|
||||
case LibraryType.Images:
|
||||
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false);
|
||||
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true);
|
||||
@ -237,8 +253,8 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||
}
|
||||
|
||||
forceScan() {
|
||||
this.libraryService.scan(this.library.id, true)
|
||||
.subscribe(() => this.toastr.info(translate('toasts.forced-scan-queued', {name: this.library.name})));
|
||||
this.libraryService.scan(this.library!.id, true)
|
||||
.subscribe(() => this.toastr.info(translate('toasts.forced-scan-queued', {name: this.library!.name})));
|
||||
}
|
||||
|
||||
async save() {
|
||||
@ -295,7 +311,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||
}
|
||||
|
||||
applyCoverImage(coverUrl: string) {
|
||||
this.uploadService.updateLibraryCoverImage(this.library.id, coverUrl).subscribe(() => {});
|
||||
this.uploadService.updateLibraryCoverImage(this.library!.id, coverUrl).subscribe(() => {});
|
||||
}
|
||||
|
||||
updateCoverImageIndex(selectedIndex: number) {
|
||||
@ -304,7 +320,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||
}
|
||||
|
||||
resetCoverImage() {
|
||||
this.uploadService.updateLibraryCoverImage(this.library.id, '').subscribe(() => {});
|
||||
this.uploadService.updateLibraryCoverImage(this.library!.id, '').subscribe(() => {});
|
||||
}
|
||||
|
||||
openDirectoryPicker() {
|
||||
|
@ -811,6 +811,8 @@
|
||||
"last-scanned-label": "Last Scanned:",
|
||||
"type-label": "Type",
|
||||
"type-tooltip": "Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but have different naming in the UI.",
|
||||
"kavitaplus-eligible-label": "Kavita+ Eligible",
|
||||
"kavitaplus-eligible-tooltip": "Will Kavita+ pull information or support Scrobbling",
|
||||
"folder-description": "Add folders to your library",
|
||||
"browse": "Browse for Media Folders",
|
||||
"help-us-part-1": "Help us out by following ",
|
||||
|
35
openapi.json
35
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.14.0"
|
||||
"version": "0.8.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -2908,7 +2908,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -2920,7 +2921,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -2932,7 +2934,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -3615,7 +3618,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -13519,7 +13523,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -14111,7 +14116,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"description": "Library type",
|
||||
@ -15879,7 +15885,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -15992,7 +15999,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -16981,7 +16989,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -17033,7 +17042,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -19371,7 +19381,8 @@
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
3,
|
||||
4
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
Loading…
x
Reference in New Issue
Block a user