Another round of bugfixes (#3707)

This commit is contained in:
Joe Milazzo 2025-04-06 13:14:04 -05:00 committed by GitHub
parent cbb97208b8
commit 93dc6534fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 412 additions and 335 deletions

View File

@ -90,7 +90,7 @@ public class ScannerServiceTests : AbstractDbTest
[Fact] [Fact]
public async Task ScanLibrary_FlatSeries() public async Task ScanLibrary_FlatSeries()
{ {
var testcase = "Flat Series - Manga.json"; const string testcase = "Flat Series - Manga.json";
var library = await _scannerHelper.GenerateScannerData(testcase); var library = await _scannerHelper.GenerateScannerData(testcase);
var scanner = _scannerHelper.CreateServices(); var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id); await scanner.ScanLibrary(library.Id);
@ -106,7 +106,7 @@ public class ScannerServiceTests : AbstractDbTest
[Fact] [Fact]
public async Task ScanLibrary_FlatSeriesWithSpecialFolder() public async Task ScanLibrary_FlatSeriesWithSpecialFolder()
{ {
var testcase = "Flat Series with Specials Folder Alt Naming - Manga.json"; const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
var library = await _scannerHelper.GenerateScannerData(testcase); var library = await _scannerHelper.GenerateScannerData(testcase);
var scanner = _scannerHelper.CreateServices(); var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id); await scanner.ScanLibrary(library.Id);
@ -121,7 +121,7 @@ public class ScannerServiceTests : AbstractDbTest
[Fact] [Fact]
public async Task ScanLibrary_FlatSeriesWithSpecialFolder_AlternativeNaming() public async Task ScanLibrary_FlatSeriesWithSpecialFolder_AlternativeNaming()
{ {
var testcase = "Flat Series with Specials Folder Alt Naming - Manga.json"; const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
var library = await _scannerHelper.GenerateScannerData(testcase); var library = await _scannerHelper.GenerateScannerData(testcase);
var scanner = _scannerHelper.CreateServices(); var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id); await scanner.ScanLibrary(library.Id);
@ -302,38 +302,38 @@ public class ScannerServiceTests : AbstractDbTest
} }
[Fact] [Fact]
public async Task ScanLibrary_PublishersInheritFromChapters() public async Task ScanLibrary_PublishersInheritFromChapters()
{
const string testcase = "Flat Special - Manga.json";
var infos = new Dictionary<string, ComicInfo>();
infos.Add("Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz", new ComicInfo()
{ {
const string testcase = "Flat Special - Manga.json"; Publisher = "Correct Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", new ComicInfo()
{
Publisher = "Special Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", new ComicInfo()
{
Publisher = "Chapter Publisher"
});
var infos = new Dictionary<string, ComicInfo>(); var library = await _scannerHelper.GenerateScannerData(testcase, infos);
infos.Add("Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz", new ComicInfo()
{
Publisher = "Correct Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz", new ComicInfo()
{
Publisher = "Special Publisher"
});
infos.Add("Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz", new ComicInfo()
{
Publisher = "Chapter Publisher"
});
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
var scanner = _scannerHelper.CreateServices(); var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id); await scanner.ScanLibrary(library.Id);
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
Assert.NotNull(postLib); Assert.NotNull(postLib);
Assert.Single(postLib.Series); Assert.Single(postLib.Series);
var publishers = postLib.Series.First().Metadata.People var publishers = postLib.Series.First().Metadata.People
.Where(p => p.Role == PersonRole.Publisher); .Where(p => p.Role == PersonRole.Publisher);
Assert.Equal(3, publishers.Count()); Assert.Equal(3, publishers.Count());
} }
/// <summary> /// <summary>
@ -908,4 +908,34 @@ public class ScannerServiceTests : AbstractDbTest
Assert.Equal(6, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); Assert.Equal(6, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count));
} }
/// <summary>
/// Ensure when Kavita scans, the sort order of chapters is correct
/// </summary>
[Fact]
public async Task ScanLibrary_SortOrderWorks()
{
const string testcase = "Sort Order - Manga.json";
var library = await _scannerHelper.GenerateScannerData(testcase);
var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id);
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
Assert.NotNull(postLib);
// Get the loose leaf volume and confirm each chapter aligns with expectation of Sort Order
var series = postLib.Series.First();
Assert.NotNull(series);
var volume = series.Volumes.FirstOrDefault();
Assert.NotNull(volume);
var sortedChapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList();
Assert.True(sortedChapters[0].SortOrder.Is(1f));
Assert.True(sortedChapters[1].SortOrder.Is(4f));
Assert.True(sortedChapters[2].SortOrder.Is(5f));
}
} }

View File

@ -0,0 +1,5 @@
[
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 1-3.cbz",
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 4.cbz",
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 5.cbz"
]

View File

@ -151,7 +151,8 @@ public class SettingsController : BaseApiController
try try
{ {
return Ok(await _settingsService.UpdateSettings(updateSettingsDto)); var d = await _settingsService.UpdateSettings(updateSettingsDto);
return Ok(d);
} }
catch (KavitaException ex) catch (KavitaException ex)
{ {

View File

@ -49,6 +49,7 @@ public interface IReadingListRepository
Task<IList<string>> GetRandomCoverImagesAsync(int readingListId); Task<IList<string>> GetRandomCoverImagesAsync(int readingListId);
Task<IList<string>> GetAllCoverImagesAsync(); Task<IList<string>> GetAllCoverImagesAsync();
Task<bool> ReadingListExists(string name); Task<bool> ReadingListExists(string name);
Task<bool> ReadingListExistsForUser(string name, int userId);
IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role); IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role);
Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId); Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId);
Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
@ -109,6 +110,7 @@ public class ReadingListRepository : IReadingListRepository
.SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage)) .SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage))
.Where(t => !string.IsNullOrEmpty(t)) .Where(t => !string.IsNullOrEmpty(t))
.ToListAsync(); .ToListAsync();
return data return data
.OrderBy(_ => random.Next()) .OrderBy(_ => random.Next())
.Take(4) .Take(4)
@ -123,6 +125,13 @@ public class ReadingListRepository : IReadingListRepository
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
} }
public async Task<bool> ReadingListExistsForUser(string name, int userId)
{
var normalized = name.ToNormalized();
return await _context.ReadingList
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId);
}
public IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role) public IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role)
{ {
return _context.ReadingListItem return _context.ReadingListItem

View File

@ -25,7 +25,7 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
MinNumber = Parser.MinNumberFromRange(number), MinNumber = Parser.MinNumberFromRange(number),
MaxNumber = Parser.MaxNumberFromRange(number), MaxNumber = Parser.MaxNumberFromRange(number),
SortOrder = Parser.MinNumberFromRange(number), SortOrder = Parser.MinNumberFromRange(number),
Files = new List<MangaFile>(), Files = [],
Pages = 1, Pages = 1,
CreatedUtc = DateTime.UtcNow CreatedUtc = DateTime.UtcNow
}; };

View File

@ -473,6 +473,7 @@ public class ReadingListService : IReadingListService
_logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name); _logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name);
var user = await _unitOfWork.UserRepository.GetDefaultAdminUser(); var user = await _unitOfWork.UserRepository.GetDefaultAdminUser();
series.Metadata ??= new SeriesMetadataBuilder().Build(); series.Metadata ??= new SeriesMetadataBuilder().Build();
foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters)) foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters))
{ {
var pairs = new List<Tuple<string, string>>(); var pairs = new List<Tuple<string, string>>();
@ -578,14 +579,14 @@ public class ReadingListService : IReadingListService
{ {
CblName = cblReading.Name, CblName = cblReading.Name,
Success = CblImportResult.Success, Success = CblImportResult.Success,
Results = new List<CblBookResult>(), Results = [],
SuccessfulInserts = new List<CblBookResult>() SuccessfulInserts = new List<CblBookResult>()
}; };
if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl; if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl;
// Is there another reading list with the same name? // Is there another reading list with the same name on the user's account?
if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name)) if (await _unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId))
{ {
importSummary.Success = CblImportResult.Fail; importSummary.Success = CblImportResult.Fail;
importSummary.Results.Add(new CblBookResult importSummary.Results.Add(new CblBookResult
@ -600,9 +601,6 @@ public class ReadingListService : IReadingListService
var userSeries = var userSeries =
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); (await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
// How can we match properly with ComicVine library when year is part of the series unless we do this in 2 passes and see which has a better match
if (userSeries.Count == 0) if (userSeries.Count == 0)
{ {
// Report that no series exist in the reading list // Report that no series exist in the reading list

View File

@ -21,8 +21,8 @@ namespace API.Services;
public interface ISettingsService public interface ISettingsService
{ {
Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto); Task<MetadataSettingsDto> UpdateMetadataSettings(MetadataSettingsDto dto);
Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto); Task<ServerSettingDto> UpdateSettings(ServerSettingDto updateSettingsDto);
} }
@ -50,7 +50,7 @@ public class SettingsService : ISettingsService
/// </summary> /// </summary>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <returns></returns> /// <returns></returns>
public async Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto) public async Task<MetadataSettingsDto> UpdateMetadataSettings(MetadataSettingsDto dto)
{ {
var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings(); var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings();
existingMetadataSetting.Enabled = dto.Enabled; existingMetadataSetting.Enabled = dto.Enabled;
@ -108,7 +108,7 @@ public class SettingsService : ISettingsService
/// <param name="updateSettingsDto"></param> /// <param name="updateSettingsDto"></param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="KavitaException"></exception> /// <exception cref="KavitaException"></exception>
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto) public async Task<ServerSettingDto> UpdateSettings(ServerSettingDto updateSettingsDto)
{ {
// We do not allow CacheDirectory changes, so we will ignore. // We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();

View File

@ -871,7 +871,10 @@ public class ParseScannedFiles
var prevIssue = string.Empty; var prevIssue = string.Empty;
foreach (var chapter in chapters) foreach (var chapter in chapters)
{ {
if (float.TryParse(chapter.Chapters, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedChapter)) // Use MinNumber in case there is a range, as otherwise sort order will cause it to be processed last
var chapterNum =
$"{Parser.Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}";
if (float.TryParse(chapterNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedChapter))
{ {
// Parsed successfully, use the numeric value // Parsed successfully, use the numeric value
counter = parsedChapter; counter = parsedChapter;

View File

@ -24,7 +24,7 @@ public static partial class Parser
public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser public const string ImageFileExtensions = @"(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser
public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt";
public const string EpubFileExtension = @"\.epub"; public const string EpubFileExtension = @"\.epub";
public const string PdfFileExtension = @"\.pdf"; public const string PdfFileExtension = @"\.pdf";

223
UI/Web/package-lock.json generated
View File

@ -9,16 +9,16 @@
"version": "0.7.12.1", "version": "0.7.12.1",
"dependencies": { "dependencies": {
"@angular-slider/ngx-slider": "^19.0.0", "@angular-slider/ngx-slider": "^19.0.0",
"@angular/animations": "^19.2.3", "@angular/animations": "^19.2.5",
"@angular/cdk": "^19.2.6", "@angular/cdk": "^19.2.8",
"@angular/common": "^19.2.3", "@angular/common": "^19.2.5",
"@angular/compiler": "^19.2.3", "@angular/compiler": "^19.2.5",
"@angular/core": "^19.2.3", "@angular/core": "^19.2.5",
"@angular/forms": "^19.2.3", "@angular/forms": "^19.2.5",
"@angular/localize": "^19.2.3", "@angular/localize": "^19.2.5",
"@angular/platform-browser": "^19.2.3", "@angular/platform-browser": "^19.2.5",
"@angular/platform-browser-dynamic": "^19.2.3", "@angular/platform-browser-dynamic": "^19.2.5",
"@angular/router": "^19.2.3", "@angular/router": "^19.2.5",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@iharbeck/ngx-virtual-scroller": "^19.0.1", "@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@iplab/ngx-file-upload": "^19.0.3", "@iplab/ngx-file-upload": "^19.0.3",
@ -36,10 +36,10 @@
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"charts.css": "^1.1.0", "charts.css": "^1.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"luxon": "^3.5.0", "luxon": "^3.6.1",
"ng-circle-progress": "^1.7.1", "ng-circle-progress": "^1.7.1",
"ng-lazyload-image": "^9.1.3", "ng-lazyload-image": "^9.1.3",
"ng-select2-component": "^17.2.2", "ng-select2-component": "^17.2.3",
"ngx-color-picker": "^19.0.0", "ngx-color-picker": "^19.0.0",
"ngx-extended-pdf-viewer": "^23.0.0-alpha.7", "ngx-extended-pdf-viewer": "^23.0.0-alpha.7",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
@ -58,12 +58,12 @@
"@angular-eslint/eslint-plugin-template": "^19.3.0", "@angular-eslint/eslint-plugin-template": "^19.3.0",
"@angular-eslint/schematics": "^19.3.0", "@angular-eslint/schematics": "^19.3.0",
"@angular-eslint/template-parser": "^19.3.0", "@angular-eslint/template-parser": "^19.3.0",
"@angular/build": "^19.2.4", "@angular/build": "^19.2.6",
"@angular/cli": "^19.2.4", "@angular/cli": "^19.2.6",
"@angular/compiler-cli": "^19.2.3", "@angular/compiler-cli": "^19.2.5",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/luxon": "^3.4.0", "@types/luxon": "^3.6.2",
"@types/node": "^22.13.13", "@types/node": "^22.13.13",
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.28.0",
@ -97,12 +97,12 @@
} }
}, },
"node_modules/@angular-devkit/architect": { "node_modules/@angular-devkit/architect": {
"version": "0.1902.4", "version": "0.1902.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.4.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.6.tgz",
"integrity": "sha512-YTLiJ7uVItZTAxRuSgASP0M5qILESWH8xGmfR+YWR1JiJem09DWEOpWeLdha1BFzUui5L+6j1btzh4FUHJOtAg==", "integrity": "sha512-Dx6yPxpaE5AhP6UtrVRDCc9Ihq9B65LAbmIh3dNOyeehratuaQS0TYNKjbpaevevJojW840DTg80N+CrlfYp9g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@angular-devkit/core": "19.2.4", "@angular-devkit/core": "19.2.6",
"rxjs": "7.8.1" "rxjs": "7.8.1"
}, },
"engines": { "engines": {
@ -121,9 +121,9 @@
} }
}, },
"node_modules/@angular-devkit/core": { "node_modules/@angular-devkit/core": {
"version": "19.2.4", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.6.tgz",
"integrity": "sha512-dL6AmCQsKh+CFVvO/jxX8qZpamVwt9r4iIo7fYcAI2+mTSDGxxBGWbS+onIfdPFuRp2HgKa+AT6WiHmRqu63AA==", "integrity": "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ajv": "8.17.1", "ajv": "8.17.1",
@ -157,12 +157,12 @@
} }
}, },
"node_modules/@angular-devkit/schematics": { "node_modules/@angular-devkit/schematics": {
"version": "19.2.4", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.6.tgz",
"integrity": "sha512-WaFe95ncm1A+QTlUHxQcFyGKIn67xgqGX7WCj8R0QlKOS0hLKx97SG4p19uwHlww0lmAcwk/QJP6G6sPL/CJ9w==", "integrity": "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@angular-devkit/core": "19.2.4", "@angular-devkit/core": "19.2.6",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"magic-string": "0.30.17", "magic-string": "0.30.17",
"ora": "5.4.1", "ora": "5.4.1",
@ -304,9 +304,9 @@
} }
}, },
"node_modules/@angular/animations": { "node_modules/@angular/animations": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.5.tgz",
"integrity": "sha512-HQexOmwEJFX3sHLspOCi7dVOdPW5Ad4jH6tJsf+zABkF0GjgIVf4jewe1uE5ZLKgoflr9f9vpcpy39IWl00kWw==", "integrity": "sha512-m4RtY3z1JuHFCh6OrOHxo25oKEigBDdR/XmdCfXIwfTiObZzNA7VQhysgdrb9IISO99kXbjZUYKDtLzgWT8Klg==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -314,17 +314,18 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "19.2.3" "@angular/common": "19.2.5",
"@angular/core": "19.2.5"
} }
}, },
"node_modules/@angular/build": { "node_modules/@angular/build": {
"version": "19.2.4", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.6.tgz",
"integrity": "sha512-poCXvmwKri3snWa9zVJ2sW7wyJatZjkwnH6GUBdJrM2dXRQ+LYLk/oXqEjlSRBYNna7P1ZcKNqBbzu0/SnnngA==", "integrity": "sha512-+VBLb4ZPLswwJmgfsTFzGex+Sq/WveNc+uaIWyHYjwnuI17NXe1qAAg1rlp72CqGn0cirisfOyAUwPc/xZAgTg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "2.3.0", "@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "0.1902.4", "@angular-devkit/architect": "0.1902.6",
"@babel/core": "7.26.10", "@babel/core": "7.26.10",
"@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-annotate-as-pure": "7.25.9",
"@babel/helper-split-export-declaration": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7",
@ -347,7 +348,7 @@
"sass": "1.85.0", "sass": "1.85.0",
"semver": "7.7.1", "semver": "7.7.1",
"source-map-support": "0.5.21", "source-map-support": "0.5.21",
"vite": "6.2.0", "vite": "6.2.4",
"watchpack": "2.4.2" "watchpack": "2.4.2"
}, },
"engines": { "engines": {
@ -364,7 +365,7 @@
"@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/localize": "^19.0.0 || ^19.2.0-next.0",
"@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0",
"@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0",
"@angular/ssr": "^19.2.4", "@angular/ssr": "^19.2.6",
"karma": "^6.4.0", "karma": "^6.4.0",
"less": "^4.2.0", "less": "^4.2.0",
"ng-packagr": "^19.0.0 || ^19.2.0-next.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0",
@ -464,15 +465,13 @@
} }
}, },
"node_modules/@angular/cdk": { "node_modules/@angular/cdk": {
"version": "19.2.6", "version": "19.2.8",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.6.tgz", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.8.tgz",
"integrity": "sha512-AneN/NeAYU4+AwzicTwtYE9CkMcWA0cAJ41SNfSyoHaaHNXSkryzwSmTYS3FO+taqd7OGnBePeWJbW2uJXcvfA==", "integrity": "sha512-ZZqWVYFF80TdjWkk2sc9Pn2luhiYeC78VH3Yjeln4wXMsTGDsvKPBcuOxSxxpJ31saaVBehDjBUuXMqGRj8KuA==",
"dependencies": { "dependencies": {
"parse5": "^7.1.2",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"optionalDependencies": {
"parse5": "^7.1.2"
},
"peerDependencies": { "peerDependencies": {
"@angular/common": "^19.0.0 || ^20.0.0", "@angular/common": "^19.0.0 || ^20.0.0",
"@angular/core": "^19.0.0 || ^20.0.0", "@angular/core": "^19.0.0 || ^20.0.0",
@ -480,17 +479,17 @@
} }
}, },
"node_modules/@angular/cli": { "node_modules/@angular/cli": {
"version": "19.2.4", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.6.tgz",
"integrity": "sha512-YmZYrxdGBwSZsrnpS6vr9gQ8+PrZHzwyYW/3jU2NRAMtl0ZlipDyfpLIDgrfqYPeumzr7+SKtJYVvlsMnjkN4g==", "integrity": "sha512-eZhFOSsDUHKaciwcWdU5C54ViAvPPdZJf42So93G2vZWDtEq6Uk47huocn1FY9cMhDvURfYLNrrLMpUDtUSsSA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@angular-devkit/architect": "0.1902.4", "@angular-devkit/architect": "0.1902.6",
"@angular-devkit/core": "19.2.4", "@angular-devkit/core": "19.2.6",
"@angular-devkit/schematics": "19.2.4", "@angular-devkit/schematics": "19.2.6",
"@inquirer/prompts": "7.3.2", "@inquirer/prompts": "7.3.2",
"@listr2/prompt-adapter-inquirer": "2.0.18", "@listr2/prompt-adapter-inquirer": "2.0.18",
"@schematics/angular": "19.2.4", "@schematics/angular": "19.2.6",
"@yarnpkg/lockfile": "1.1.0", "@yarnpkg/lockfile": "1.1.0",
"ini": "5.0.0", "ini": "5.0.0",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
@ -513,9 +512,9 @@
} }
}, },
"node_modules/@angular/common": { "node_modules/@angular/common": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.5.tgz",
"integrity": "sha512-cYOMRXFb6Sjtg9BI3bE/Ave+xU234jQmHYj7hBxr3MiqRSVJL4niy10KiA/Jiz6y76V5QfZfS+0aE65VuoqAvg==", "integrity": "sha512-vFCBdas4C5PxP6ts/4TlRddWD3DUmI3aaO0QZdZvqyLHy428t84ruYdsJXKaeD8ie2U4/9F3a1tsklclRG/BBA==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -523,14 +522,14 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "19.2.3", "@angular/core": "19.2.5",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/compiler": { "node_modules/@angular/compiler": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.5.tgz",
"integrity": "sha512-TL/JIU7vzSWD+IrMq2PAiHZi7IUFSRhdHo8q6/WuZ8SQmbuXCK2pJvHZpTtUdLswdPeD/UVhkhTAhQzEyEdZVg==", "integrity": "sha512-34J+HubQjwkbZ0AUtU5sa4Zouws9XtP/fKaysMQecoYJTZ3jewzLSRu3aAEZX1Y4gIrcVVKKIxM6oWoXKwYMOA==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -539,9 +538,9 @@
} }
}, },
"node_modules/@angular/compiler-cli": { "node_modules/@angular/compiler-cli": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.5.tgz",
"integrity": "sha512-ePh/7A6eEDAyfVn8QgLcAvrxhXBAf6mTqB/3+HwQeXLaka1gtN6xvZ6cjLEegP4s6kcYGhdfdLwzCcy0kjsY5g==", "integrity": "sha512-b2cG41r6lilApXLlvja1Ra2D00dM3BxmQhoElKC1tOnpD6S3/krlH1DOnBB2I55RBn9iv4zdmPz1l8zPUSh7DQ==",
"dependencies": { "dependencies": {
"@babel/core": "7.26.9", "@babel/core": "7.26.9",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
@ -561,7 +560,7 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "19.2.3", "@angular/compiler": "19.2.5",
"typescript": ">=5.5 <5.9" "typescript": ">=5.5 <5.9"
} }
}, },
@ -592,9 +591,9 @@
} }
}, },
"node_modules/@angular/core": { "node_modules/@angular/core": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.5.tgz",
"integrity": "sha512-uNDbQBDWdAfL8JhgG2l9eTEbikovZ+SthLUKERyR4fL7AVGSx85LjNySRuq4WAL4eiD1cRN1UUgu8o+WKqF/ow==", "integrity": "sha512-NNEz1sEZz1mBpgf6Tz3aJ9b8KjqpTiMYhHfCYA9h9Ipe4D8gUmOsvPHPK2M755OX7p7PmUmzp1XCUHYrZMVHRw==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -607,9 +606,9 @@
} }
}, },
"node_modules/@angular/forms": { "node_modules/@angular/forms": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.5.tgz",
"integrity": "sha512-JEgNKiZd3taYBg9lsMvoana5cv1QGke8xkuryc9zesHPJjhw9QHllmDPOW2HyUuwPqXZ/YkHiuCMOk+4qPjsAw==", "integrity": "sha512-2Zvy3qK1kOxiAX9fdSaeG48q7oyO/4RlMYlg1w+ra9qX1SrgwF3OQ2P2Vs+ojg1AxN3z9xFp4aYaaID/G2LZAw==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -617,16 +616,16 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "19.2.3", "@angular/common": "19.2.5",
"@angular/core": "19.2.3", "@angular/core": "19.2.5",
"@angular/platform-browser": "19.2.3", "@angular/platform-browser": "19.2.5",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/localize": { "node_modules/@angular/localize": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.2.5.tgz",
"integrity": "sha512-4YUvii9uJCT5D1n3RR5W68UizrwsFr85ucIB9G3rQXL9y1LA2GbeKQkgazpF9nTCBDAfrux7tKvS27owofY5ww==", "integrity": "sha512-oAc19bubk6Z/2Vv6OkV0MsjdgC8cUaUwBmwdc6blFVe1NCX1KjdaqDyC2EQAO3nWfcdV4uvOOuu8myxB64bamw==",
"dependencies": { "dependencies": {
"@babel/core": "7.26.9", "@babel/core": "7.26.9",
"@types/babel__core": "7.20.5", "@types/babel__core": "7.20.5",
@ -642,14 +641,14 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "19.2.3", "@angular/compiler": "19.2.5",
"@angular/compiler-cli": "19.2.3" "@angular/compiler-cli": "19.2.5"
} }
}, },
"node_modules/@angular/platform-browser": { "node_modules/@angular/platform-browser": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.5.tgz",
"integrity": "sha512-bz5mvUkCS8SxaMInjPgi/2dD7vpWkZePQesvr/bBRNQbYSE4cGTbovXcVl3X5hIxs5JoC6Het0lS2IxDq7j6qg==", "integrity": "sha512-Lshy++X16cvl6OPvfzMySpsqEaCPKEJmDjz7q7oSt96oxlh6LvOeOUVLjsNyrNaIt9NadpWoqjlu/I9RTPJkpw==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -657,9 +656,9 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/animations": "19.2.3", "@angular/animations": "19.2.5",
"@angular/common": "19.2.3", "@angular/common": "19.2.5",
"@angular/core": "19.2.3" "@angular/core": "19.2.5"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@angular/animations": { "@angular/animations": {
@ -668,9 +667,9 @@
} }
}, },
"node_modules/@angular/platform-browser-dynamic": { "node_modules/@angular/platform-browser-dynamic": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.5.tgz",
"integrity": "sha512-PHmmtdGxSfe9HL8xR4g3PspnEaPqTEOGyzNviAHugfkZCgXCdSbYs36d3i0nPwhExMAeuIVXbbJyouDn2kyeOw==", "integrity": "sha512-15in8u4552EcdWNTXY2h0MKuJbk3AuXwWr0zVTum4CfB/Ss2tNTrDEdWhgAbhnUI0e9jZQee/fhBbA1rleMYrA==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -678,16 +677,16 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "19.2.3", "@angular/common": "19.2.5",
"@angular/compiler": "19.2.3", "@angular/compiler": "19.2.5",
"@angular/core": "19.2.3", "@angular/core": "19.2.5",
"@angular/platform-browser": "19.2.3" "@angular/platform-browser": "19.2.5"
} }
}, },
"node_modules/@angular/router": { "node_modules/@angular/router": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.5.tgz",
"integrity": "sha512-yYVMT7CceKqE+fBXxkhkAqEQUEdY/BHtLQr1vP9rEnAf30vwKghDEresugfegY6Ch4IGKTBtDG/QGmxWszgUAQ==", "integrity": "sha512-9pSfmdNXLjaOKj0kd4UxBC7sFdCFOnRGbftp397G3KWqsLsGSKmNFzqhXNeA5QHkaVxnpmpm8HzXU+zYV5JwSg==",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -695,9 +694,9 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "19.2.3", "@angular/common": "19.2.5",
"@angular/core": "19.2.3", "@angular/core": "19.2.5",
"@angular/platform-browser": "19.2.3", "@angular/platform-browser": "19.2.5",
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
@ -3562,13 +3561,13 @@
] ]
}, },
"node_modules/@schematics/angular": { "node_modules/@schematics/angular": {
"version": "19.2.4", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.6.tgz",
"integrity": "sha512-P7fphIPbqHHYRVRPiFl7RAHYPYhINGSUYOXrcThVBBsgKQBX18oNdUWvhZA6ylwK/9T21XB20VyLjNy0d78H1Q==", "integrity": "sha512-fmbF9ONmEZqxHocCwOSWG2mHp4a22d1uW+DZUBUgZSBUFIrnFw42deOxDq8mkZOZ1Tc73UpLN2GKI7iJeUqS2A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@angular-devkit/core": "19.2.4", "@angular-devkit/core": "19.2.6",
"@angular-devkit/schematics": "19.2.4", "@angular-devkit/schematics": "19.2.6",
"jsonc-parser": "3.3.1" "jsonc-parser": "3.3.1"
}, },
"engines": { "engines": {
@ -4063,9 +4062,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.4.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
@ -5372,7 +5371,6 @@
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"devOptional": true,
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
}, },
@ -7008,9 +7006,9 @@
} }
}, },
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.5.0", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@ -7414,9 +7412,9 @@
} }
}, },
"node_modules/ng-select2-component": { "node_modules/ng-select2-component": {
"version": "17.2.2", "version": "17.2.3",
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.2.tgz", "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.3.tgz",
"integrity": "sha512-dAeUSqmjU9Gexi47vMEz1bXGQkl3Be2O0wl6QqpYwFvM+QEfUyQiY0zWpYvB8shO1sIHoCQNKt9yTFcRzvzW0g==", "integrity": "sha512-JNik7OWqya4ERuqlfnYiJHkaqyZtHqUhATIZ9yUxmadWWNIn8I3Lwa7qt0KtPpR01O9HJC0PtHXhvev88Cju2A==",
"dependencies": { "dependencies": {
"ngx-infinite-scroll": ">=18.0.0 || >=19.0.0", "ngx-infinite-scroll": ">=18.0.0 || >=19.0.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"
@ -7946,7 +7944,6 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
"devOptional": true,
"dependencies": { "dependencies": {
"entities": "^4.4.0" "entities": "^4.4.0"
}, },
@ -9212,9 +9209,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.0", "version": "6.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",

View File

@ -17,16 +17,16 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular-slider/ngx-slider": "^19.0.0", "@angular-slider/ngx-slider": "^19.0.0",
"@angular/animations": "^19.2.3", "@angular/animations": "^19.2.5",
"@angular/cdk": "^19.2.6", "@angular/cdk": "^19.2.8",
"@angular/common": "^19.2.3", "@angular/common": "^19.2.5",
"@angular/compiler": "^19.2.3", "@angular/compiler": "^19.2.5",
"@angular/core": "^19.2.3", "@angular/core": "^19.2.5",
"@angular/forms": "^19.2.3", "@angular/forms": "^19.2.5",
"@angular/localize": "^19.2.3", "@angular/localize": "^19.2.5",
"@angular/platform-browser": "^19.2.3", "@angular/platform-browser": "^19.2.5",
"@angular/platform-browser-dynamic": "^19.2.3", "@angular/platform-browser-dynamic": "^19.2.5",
"@angular/router": "^19.2.3", "@angular/router": "^19.2.5",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@iharbeck/ngx-virtual-scroller": "^19.0.1", "@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@iplab/ngx-file-upload": "^19.0.3", "@iplab/ngx-file-upload": "^19.0.3",
@ -44,10 +44,10 @@
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"charts.css": "^1.1.0", "charts.css": "^1.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"luxon": "^3.5.0", "luxon": "^3.6.1",
"ng-circle-progress": "^1.7.1", "ng-circle-progress": "^1.7.1",
"ng-lazyload-image": "^9.1.3", "ng-lazyload-image": "^9.1.3",
"ng-select2-component": "^17.2.2", "ng-select2-component": "^17.2.3",
"ngx-color-picker": "^19.0.0", "ngx-color-picker": "^19.0.0",
"ngx-extended-pdf-viewer": "^23.0.0-alpha.7", "ngx-extended-pdf-viewer": "^23.0.0-alpha.7",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
@ -66,12 +66,12 @@
"@angular-eslint/eslint-plugin-template": "^19.3.0", "@angular-eslint/eslint-plugin-template": "^19.3.0",
"@angular-eslint/schematics": "^19.3.0", "@angular-eslint/schematics": "^19.3.0",
"@angular-eslint/template-parser": "^19.3.0", "@angular-eslint/template-parser": "^19.3.0",
"@angular/build": "^19.2.4", "@angular/build": "^19.2.6",
"@angular/cli": "^19.2.4", "@angular/cli": "^19.2.6",
"@angular/compiler-cli": "^19.2.3", "@angular/compiler-cli": "^19.2.5",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/luxon": "^3.4.0", "@types/luxon": "^3.6.2",
"@types/node": "^22.13.13", "@types/node": "^22.13.13",
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.28.0",

View File

@ -1,23 +1,27 @@
import {inject, Injectable} from '@angular/core'; import {inject, Injectable} from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import { take } from 'rxjs/operators'; import {take} from 'rxjs/operators';
import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component'; import {BulkAddToCollectionComponent} from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component';
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component'; import {ADD_FLOW, AddToListModalComponent} from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component'; import {
import { ConfirmService } from '../shared/confirm.service'; EditReadingListModalComponent
import { LibrarySettingsModalComponent } from '../sidenav/_modals/library-settings-modal/library-settings-modal.component'; } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';
import { Chapter } from '../_models/chapter'; import {ConfirmService} from '../shared/confirm.service';
import { Device } from '../_models/device/device'; import {
import { Library } from '../_models/library/library'; LibrarySettingsModalComponent
import { ReadingList } from '../_models/reading-list'; } from '../sidenav/_modals/library-settings-modal/library-settings-modal.component';
import { Series } from '../_models/series'; import {Chapter} from '../_models/chapter';
import { Volume } from '../_models/volume'; import {Device} from '../_models/device/device';
import { DeviceService } from './device.service'; import {Library} from '../_models/library/library';
import { LibraryService } from './library.service'; import {ReadingList} from '../_models/reading-list';
import { MemberService } from './member.service'; import {Series} from '../_models/series';
import { ReaderService } from './reader.service'; import {Volume} from '../_models/volume';
import { SeriesService } from './series.service'; import {DeviceService} from './device.service';
import {LibraryService} from './library.service';
import {MemberService} from './member.service';
import {ReaderService} from './reader.service';
import {SeriesService} from './series.service';
import {translate} from "@jsverse/transloco"; import {translate} from "@jsverse/transloco";
import {UserCollection} from "../_models/collection-tag"; import {UserCollection} from "../_models/collection-tag";
import {CollectionTagService} from "./collection-tag.service"; import {CollectionTagService} from "./collection-tag.service";
@ -652,7 +656,7 @@ export class ActionService {
} }
editReadingList(readingList: ReadingList, callback?: ReadingListActionCallback) { editReadingList(readingList: ReadingList, callback?: ReadingListActionCallback) {
const readingListModalRef = this.modalService.open(EditReadingListModalComponent, { scrollable: true, size: 'lg', fullscreen: 'md' }); const readingListModalRef = this.modalService.open(EditReadingListModalComponent, DefaultModalOptions);
readingListModalRef.componentInstance.readingList = readingList; readingListModalRef.componentInstance.readingList = readingList;
readingListModalRef.closed.pipe(take(1)).subscribe((list) => { readingListModalRef.closed.pipe(take(1)).subscribe((list) => {
if (callback && list !== undefined) { if (callback && list !== undefined) {
@ -773,7 +777,7 @@ export class ActionService {
} }
matchSeries(series: Series, callback?: BooleanActionCallback) { matchSeries(series: Series, callback?: BooleanActionCallback) {
const ref = this.modalService.open(MatchSeriesModalComponent, {size: 'lg'}); const ref = this.modalService.open(MatchSeriesModalComponent, DefaultModalOptions);
ref.componentInstance.series = series; ref.componentInstance.series = series;
ref.closed.subscribe(saved => { ref.closed.subscribe(saved => {
if (callback) { if (callback) {

View File

@ -47,30 +47,33 @@
} }
<div class="setting-section-break" aria-hidden="true"></div> @if (!suppressEmptyGenres || genres.length > 0) {
<div class="setting-section-break" aria-hidden="true"></div>
<div class="mb-3 ms-1">
<div class="mb-3 ms-1"> <h4 class="header">{{t('genres-title')}}</h4>
<h4 class="header">{{t('genres-title')}}</h4> <div class="ms-3">
<div class="ms-3"> <app-badge-expander [includeComma]="true" [items]="genres" [itemsTillExpander]="3" [defaultExpanded]="true">
<app-badge-expander [includeComma]="true" [items]="genres" [itemsTillExpander]="3" [defaultExpanded]="true"> <ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last"> <a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Genres, item.id)">{{item.title}}</a>
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Genres, item.id)">{{item.title}}</a> </ng-template>
</ng-template> </app-badge-expander>
</app-badge-expander> </div>
</div> </div>
</div> }
<div class="mb-3 ms-1"> @if (!suppressEmptyTags || tags.length > 0) {
<h4 class="header">{{t('tags-title')}}</h4> <div class="mb-3 ms-1">
<div class="ms-3"> <h4 class="header">{{t('tags-title')}}</h4>
<app-badge-expander [includeComma]="true" [items]="tags" [itemsTillExpander]="3" [defaultExpanded]="true"> <div class="ms-3">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last"> <app-badge-expander [includeComma]="true" [items]="tags" [itemsTillExpander]="3" [defaultExpanded]="true">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Tags, item.id)">{{item.title}}</a> <ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
</ng-template> <a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Tags, item.id)">{{item.title}}</a>
</app-badge-expander> </ng-template>
</app-badge-expander>
</div>
</div> </div>
</div> }
<div class="mb-3"> <div class="mb-3">
<app-carousel-reel [items]="webLinks" [title]="t('weblinks-title')"> <app-carousel-reel [items]="webLinks" [title]="t('weblinks-title')">

View File

@ -61,6 +61,8 @@ export class DetailsTabComponent {
@Input() genres: Array<Genre> = []; @Input() genres: Array<Genre> = [];
@Input() tags: Array<Tag> = []; @Input() tags: Array<Tag> = [];
@Input() webLinks: Array<string> = []; @Input() webLinks: Array<string> = [];
@Input() suppressEmptyGenres: boolean = false;
@Input() suppressEmptyTags: boolean = false;
openGeneric(queryParamName: FilterField, filter: string | number) { openGeneric(queryParamName: FilterField, filter: string | number) {

View File

@ -1,7 +1,7 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ToastrService} from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import {debounceTime, distinctUntilChanged, filter, switchMap, take, tap} from 'rxjs'; import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from 'rxjs';
import {SettingsService} from '../settings.service'; import {SettingsService} from '../settings.service';
import {ServerSettings} from '../_models/server-settings'; import {ServerSettings} from '../_models/server-settings';
import {translate, TranslocoModule} from "@jsverse/transloco"; import {translate, TranslocoModule} from "@jsverse/transloco";
@ -30,7 +30,7 @@ export class ManageEmailSettingsComponent implements OnInit {
settingsForm: FormGroup = new FormGroup({}); settingsForm: FormGroup = new FormGroup({});
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.settingsService.getServerSettings().subscribe((settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)])); this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
@ -100,6 +100,8 @@ export class ManageEmailSettingsComponent implements OnInit {
packData() { packData() {
const modelSettings = Object.assign({}, this.serverSettings); const modelSettings = Object.assign({}, this.serverSettings);
modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value; modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value;
modelSettings.hostName = this.settingsForm.get('hostName')?.value; modelSettings.hostName = this.settingsForm.get('hostName')?.value;

View File

@ -47,15 +47,15 @@ export class ManageSettingsComponent implements OnInit {
translate('manage-settings.allow-stats-tooltip-part-2'); translate('manage-settings.allow-stats-tooltip-part-2');
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => { this.settingsService.getTaskFrequencies().subscribe(frequencies => {
this.taskFrequencies = frequencies; this.taskFrequencies = frequencies;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.settingsService.getLoggingLevels().pipe(take(1)).subscribe(levels => { this.settingsService.getLoggingLevels().subscribe(levels => {
this.logLevels = levels; this.logLevels = levels;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.settingsService.getServerSettings().subscribe((settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;
this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required])); this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required]));
this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required])); this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));

View File

@ -37,7 +37,7 @@
</td> </td>
<td> <td>
@if (member.libraries.length > 0) { @if (member.libraries.length > 0) {
@if (hasAdminRole(member)) { @if (hasAdminRole(member) || member.libraries.length === libraryCount) {
{{t('all-libraries')}} {{t('all-libraries')}}
} @else { } @else {
@if (member.libraries.length > 5) { @if (member.libraries.length > 5) {

View File

@ -13,7 +13,7 @@ import {EditUserComponent} from '../edit-user/edit-user.component';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {TagBadgeComponent} from '../../shared/tag-badge/tag-badge.component'; import {TagBadgeComponent} from '../../shared/tag-badge/tag-badge.component';
import {AsyncPipe, NgClass, TitleCasePipe} from '@angular/common'; import {AsyncPipe, NgClass, TitleCasePipe} from '@angular/common';
import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco"; import {TranslocoModule, TranslocoService} from "@jsverse/transloco";
import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
@ -50,6 +50,7 @@ export class ManageUsersComponent implements OnInit {
members: Member[] = []; members: Member[] = [];
loggedInUsername = ''; loggedInUsername = '';
loadingMembers = false; loadingMembers = false;
libraryCount: number = 0;
constructor() { constructor() {
@ -81,7 +82,11 @@ export class ManageUsersComponent implements OnInit {
if (nameA < nameB) return -1; if (nameA < nameB) return -1;
if (nameA > nameB) return 1; if (nameA > nameB) return 1;
return 0; return 0;
}) });
// Get the admin and get their library count
this.libraryCount = this.members.filter(m => this.hasAdminRole(m))[0].libraries.length;
this.loadingMembers = false; this.loadingMembers = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
@ -142,16 +147,8 @@ export class ManageUsersComponent implements OnInit {
modalRef.componentInstance.member = member; modalRef.componentInstance.member = member;
} }
formatLibraries(member: Member) {
if (member.libraries.length === 0) {
return translate('manage-users.none');
}
return member.libraries.map(item => item.name).join(', ');
}
hasAdminRole(member: Member) { hasAdminRole(member: Member) {
return member.roles.indexOf('Admin') >= 0; return member.roles.indexOf(Role.Admin) >= 0;
} }
getRoles(member: Member) { getRoles(member: Member) {

View File

@ -722,6 +722,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Update the window Height // Update the window Height
this.updateWidthAndHeightCalcs(); this.updateWidthAndHeightCalcs();
this.updateImageSizes(); this.updateImageSizes();
const resumeElement = this.getFirstVisibleElementXPath(); const resumeElement = this.getFirstVisibleElementXPath();
if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) { if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) {
this.scrollTo(resumeElement); // This works pretty well, but not perfect this.scrollTo(resumeElement); // This works pretty well, but not perfect
@ -944,7 +945,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => { this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => {
this.isSingleImagePage = this.checkSingleImagePage(content) // This needs be performed before we set this.page to avoid image jumping this.isSingleImagePage = this.checkSingleImagePage(content) // This needs be performed before we set this.page to avoid image jumping
this.updateSingleImagePageStyles() this.updateSingleImagePageStyles();
this.page = this.domSanitizer.bypassSecurityTrustHtml(content); // PERF: Potential optimization to prefetch next/prev page and store in localStorage this.page = this.domSanitizer.bypassSecurityTrustHtml(content); // PERF: Potential optimization to prefetch next/prev page and store in localStorage
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -52,12 +52,15 @@
} }
</div> </div>
@if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) { @if (libraryType !== LibraryType.Images) {
<div class="card-body meta-title"> <div class="card-body meta-title">
<span class="card-format"> <span class="card-format"></span>
</span>
<div class="card-content d-flex justify-content-center align-items-center text-center" style="width:100%;min-height:58px;"> <div class="card-content d-flex justify-content-center align-items-center text-center" style="width:100%;min-height:58px;">
{{volume.name}} @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) {
{{volume.name}}
} @else {
{{volume.chapters[0].titleName}}
}
</div> </div>
@if (actions && actions.length > 0) { @if (actions && actions.length > 0) {

View File

@ -2,15 +2,15 @@
@if (items.length > virtualizeAfter) { @if (items.length > virtualizeAfter) {
<div class="example-list list-group-flush"> <div class="example-list list-group-flush">
<virtual-scroller #scroll [items]="items" [bufferAmount]="BufferAmount" [parentScroll]="parentScroll"> <virtual-scroller #scroll [items]="items" [bufferAmount]="BufferAmount" [parentScroll]="parentScroll">
<div class="example-box" *ngFor="let item of scroll.viewPortItems; index as i; trackBy: trackByIdentity"> @for (item of scroll.viewPortItems; track trackByIdentity(i, item); let i = $index) {
<div class="example-box">
<div class="d-flex list-container"> <div class="d-flex list-container">
<ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container> <ng-container [ngTemplateOutlet]="handle" [ngTemplateOutletContext]="{ $implicit: item, idx: i, isVirtualized: true }"></ng-container>
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container> <ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container>
<ng-container [ngTemplateOutlet]="removeBtn" [ngTemplateOutletContext]="{$implicit: item, idx: i}"></ng-container> </div>
</div> </div>
</div> }
</virtual-scroller> </virtual-scroller>
</div> </div>
} @else { } @else {

View File

@ -13,7 +13,7 @@ import {
TrackByFunction TrackByFunction
} from '@angular/core'; } from '@angular/core';
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller'; import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {NgClass, NgFor, NgTemplateOutlet} from '@angular/common'; import {NgClass, NgTemplateOutlet} from '@angular/common';
import {TranslocoDirective} from "@jsverse/transloco"; import {TranslocoDirective} from "@jsverse/transloco";
import {BulkSelectionService} from "../../../cards/bulk-selection.service"; import {BulkSelectionService} from "../../../cards/bulk-selection.service";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
@ -36,13 +36,14 @@ export interface ItemRemoveEvent {
templateUrl: './draggable-ordered-list.component.html', templateUrl: './draggable-ordered-list.component.html',
styleUrls: ['./draggable-ordered-list.component.scss'], styleUrls: ['./draggable-ordered-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag, imports: [VirtualScrollerModule, NgTemplateOutlet, CdkDropList, CdkDrag,
CdkDragHandle, TranslocoDirective, NgClass, FormsModule] CdkDragHandle, TranslocoDirective, NgClass, FormsModule]
}) })
export class DraggableOrderedListComponent { export class DraggableOrderedListComponent {
protected readonly bulkSelectionService = inject(BulkSelectionService); protected readonly bulkSelectionService = inject(BulkSelectionService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly cdRef = inject(ChangeDetectorRef);
/** /**
@ -84,7 +85,7 @@ export class DraggableOrderedListComponent {
return Math.min(this.items.length / 20, 20); return Math.min(this.items.length / 20, 20);
} }
constructor(private readonly cdRef: ChangeDetectorRef) { constructor() {
this.bulkSelectionService.selections$.pipe( this.bulkSelectionService.selections$.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((s) => { ).subscribe((s) => {

View File

@ -252,6 +252,8 @@
@defer (when activeTabId === TabID.Details; prefetch on idle) { @defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="castInfo" <app-details-tab [metadata]="castInfo"
[readingTime]="rlInfo" [readingTime]="rlInfo"
[suppressEmptyGenres]="true"
[suppressEmptyTags]="true"
[ageRating]="readingList.ageRating"/> [ageRating]="readingList.ageRating"/>
} }
</ng-template> </ng-template>

View File

@ -636,7 +636,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
} }
} }
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) { async handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
switch (action.action) { switch (action.action) {
case(Action.MarkAsRead): case(Action.MarkAsRead):
this.markChapterAsRead(chapter); this.markChapterAsRead(chapter);
@ -657,6 +657,14 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
const device = (action._extra!.data as Device); const device = (action._extra!.data as Device);
this.actionService.sendToDevice([chapter.id], device); this.actionService.sendToDevice([chapter.id], device);
break; break;
case (Action.Delete):
await this.actionService.deleteChapter(chapter.id, (success) => {
if (!success) return;
this.chapters = this.chapters.filter(c => c.id != chapter.id);
this.cdRef.markForCheck();
});
break;
default: default:
break; break;
} }

View File

@ -1,3 +1,5 @@
.text-muted { .text-muted {
font-size: 14px; font-size: 14px;
} }

View File

@ -18,69 +18,69 @@
<ng-template #tooltip>{{t('format-tooltip')}}</ng-template> <ng-template #tooltip>{{t('format-tooltip')}}</ng-template>
@let files = files$ | async;
<ng-container *ngIf="files$ | async as files"> @if (formControl.value) {
<ng-container *ngIf="formControl.value; else tableLayout"> <ngx-charts-advanced-pie-chart [results]="vizData2$ | async" />
<ngx-charts-advanced-pie-chart [results]="vizData2$ | async"></ngx-charts-advanced-pie-chart> } @else {
</ng-container> <div style="height: 242px; overflow-y: auto;">
<ng-template #tableLayout>
<table class="table table-striped table-striped table-sm scrollable"> <table class="table table-striped table-striped table-sm scrollable">
<thead> <thead>
<tr> <tr>
<th scope="col" sortable="extension" direction="asc" (sort)="onSort($event)"> <th scope="col" sortable="extension" direction="asc" (sort)="onSort($event)">
{{t('extension-header')}} {{t('extension-header')}}
</th> </th>
<th scope="col" sortable="format" (sort)="onSort($event)"> <th scope="col" sortable="format" (sort)="onSort($event)">
{{t('format-header')}} {{t('format-header')}}
</th> </th>
<th scope="col" sortable="totalSize" (sort)="onSort($event)"> <th scope="col" sortable="totalSize" (sort)="onSort($event)">
{{t('total-size-header')}} {{t('total-size-header')}}
</th> </th>
<th scope="col" sortable="totalFiles" (sort)="onSort($event)"> <th scope="col" sortable="totalFiles" (sort)="onSort($event)">
{{t('total-files-header')}} {{t('total-files-header')}}
</th> </th>
<th scope="col">{{t('download-file-for-extension-header')}}</th> <th scope="col">{{t('download-file-for-extension-header')}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let item of files; let idx = index;"> @for (item of files; track item.extension; let idx = $index) {
<td id="adhoctask--{{idx}}"> <tr>
{{item.extension || t('not-classified')}} <td id="adhoctask--{{idx}}">
</td> {{item.extension || t('not-classified')}}
<td> </td>
{{item.format | mangaFormat}} <td>
</td> {{item.format | mangaFormat}}
<td> </td>
{{item.totalSize | bytes}} <td>
</td> {{item.totalSize | bytes}}
<td> </td>
{{item.totalFiles | number:'1.0-0'}} <td>
</td> {{item.totalFiles | number:'1.0-0'}}
<td> </td>
<button class="btn btn-icon" style="color: var(--primary-color)" (click)="export(item.extension)" [disabled]="downloadInProgress[item.extension]"> <td>
@if (downloadInProgress[item.extension]) { <button class="btn btn-icon" style="color: var(--primary-color)" (click)="export(item.extension)" [disabled]="downloadInProgress[item.extension]">
<div class="spinner-border spinner-border-sm" aria-hidden="true"></div> @if (downloadInProgress[item.extension]) {
} @else { <div class="spinner-border spinner-border-sm" aria-hidden="true"></div>
<i class="fa-solid fa-file-arrow-down" aria-hidden="true"></i> } @else {
} <i class="fa-solid fa-file-arrow-down" aria-hidden="true"></i>
<span class="visually-hidden">{{t('download-file-for-extension-alt"', {extension: item.extension})}}</span> }
</button> <span class="visually-hidden">{{t('download-file-for-extension-alt', {extension: item.extension})}}</span>
</td> </button>
</tr> </td>
</tr>
}
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td>{{t('total-file-size-title')}}</td> <td>{{t('total-file-size-title')}}</td>
<td></td> <td></td>
<td></td> <td></td>
<td>{{((rawData$ | async)?.totalFileSize || 0) | bytes}}</td> <td>{{((rawData$ | async)?.totalFileSize || 0) | bytes}}</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</ng-template> </div>
</ng-container> }
</div> </div>
</ng-container> </ng-container>

View File

@ -9,4 +9,8 @@
display: flex; display: flex;
flex-flow: column; flex-flow: column;
box-sizing: border-box; box-sizing: border-box;
} }
tfoot {
color: var(--bs-body);
}

View File

@ -1,26 +1,29 @@
import { import {
ChangeDetectionStrategy, ChangeDetectorRef, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
inject, inject,
QueryList, TemplateRef, ViewChild, QueryList,
TemplateRef,
ViewChild,
ViewChildren ViewChildren
} from '@angular/core'; } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import {FormControl, ReactiveFormsModule} from '@angular/forms';
import { PieChartModule } from '@swimlane/ngx-charts'; import {PieChartModule} from '@swimlane/ngx-charts';
import {Observable, BehaviorSubject, combineLatest, map, shareReplay, switchMap} from 'rxjs'; import {BehaviorSubject, combineLatest, map, Observable, shareReplay} from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service'; import {StatisticsService} from 'src/app/_services/statistics.service';
import { SortableHeader, SortEvent, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive'; import {compare, SortableHeader, SortEvent} from 'src/app/_single-module/table/_directives/sortable-header.directive';
import { FileExtension, FileExtensionBreakdown } from '../../_models/file-breakdown'; import {FileExtension, FileExtensionBreakdown} from '../../_models/file-breakdown';
import { PieDataItem } from '../../_models/pie-data-item'; import {PieDataItem} from '../../_models/pie-data-item';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { MangaFormatPipe } from '../../../_pipes/manga-format.pipe'; import {MangaFormatPipe} from '../../../_pipes/manga-format.pipe';
import { BytesPipe } from '../../../_pipes/bytes.pipe'; import {BytesPipe} from '../../../_pipes/bytes.pipe';
import { NgIf, NgFor, AsyncPipe, DecimalPipe } from '@angular/common'; import {AsyncPipe, DecimalPipe, NgFor, NgIf} from '@angular/common';
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco"; import {TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {Pagination} from "../../../_models/pagination";
import {DownloadService} from "../../../shared/_services/download.service";
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable";
import {UtcToLocalTimePipe} from "../../../_pipes/utc-to-local-time.pipe";
export interface StackedBarChartDataItem { export interface StackedBarChartDataItem {
name: string, name: string,
@ -32,7 +35,7 @@ export interface StackedBarChartDataItem {
templateUrl: './file-breakdown-stats.component.html', templateUrl: './file-breakdown-stats.component.html',
styleUrls: ['./file-breakdown-stats.component.scss'], styleUrls: ['./file-breakdown-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, NgFor, AsyncPipe, DecimalPipe, BytesPipe, MangaFormatPipe, TranslocoDirective, SortableHeader] imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, NgFor, AsyncPipe, DecimalPipe, BytesPipe, MangaFormatPipe, TranslocoDirective, SortableHeader, NgxDatatableModule, UtcToLocalTimePipe]
}) })
export class FileBreakdownStatsComponent { export class FileBreakdownStatsComponent {
@ -103,4 +106,5 @@ export class FileBreakdownStatsComponent {
}); });
} }
protected readonly ColumnMode = ColumnMode;
} }

View File

@ -16,16 +16,12 @@
</div> </div>
</div> </div>
@let statuses = publicationStatues$ | async;
@if (formControl.value) {
<ng-container *ngIf="publicationStatues$ | async as statuses"> <ngx-charts-advanced-pie-chart [results]="statuses" />
<ng-container *ngIf="formControl.value; else tableLayout"> } @else {
<ngx-charts-advanced-pie-chart <div style="height: 242px; overflow-y: auto;">
[results]="statuses"
>
</ngx-charts-advanced-pie-chart>
</ng-container>
<ng-template #tableLayout>
<table class="table table-striped table-striped table-sm scrollable"> <table class="table table-striped table-striped table-sm scrollable">
<thead> <thead>
<tr> <tr>
@ -48,8 +44,8 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</ng-template> </div>
</ng-container> }
</div> </div>

View File

@ -53,7 +53,6 @@
<li (click)="handleOptionClick(option)" <li (click)="handleOptionClick(option)"
class="list-group-item" role="option" [attr.data-index]="index" class="list-group-item" role="option" [attr.data-index]="index"
(mouseenter)="focusedIndex = index + (showAddItem ? 1 : 0); updateHighlight();"> (mouseenter)="focusedIndex = index + (showAddItem ? 1 : 0); updateHighlight();">
{{settings.trackByIdentityFn(index, option)}}
<ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index, value: typeaheadControl.value }"></ng-container> <ng-container [ngTemplateOutlet]="optionTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index, value: typeaheadControl.value }"></ng-container>
</li> </li>
} }

View File

@ -1074,7 +1074,7 @@
"donate": "Donate", "donate": "Donate",
"donate-tooltip": "You can remove this by subscribing to Kavita+", "donate-tooltip": "You can remove this by subscribing to Kavita+",
"back": "Back", "back": "Back",
"cancel-edit": "Cancel Edit", "cancel-edit": "Close Reorder",
"more": "More", "more": "More",
"customize": "{{settings.customize}}", "customize": "{{settings.customize}}",
"edit": "{{common.edit}}" "edit": "{{common.edit}}"

View File

@ -7,7 +7,7 @@ html {
html, body { height: 100%; overflow: hidden; } html, body { height: 100%; overflow: hidden; }
body { body {
margin: 0; margin: 0;
font-family: var(--body-font-family); font-family: var(--body-font-family), serif;
color: var(--body-text-color); color: var(--body-text-color);
color-scheme: var(--color-scheme); color-scheme: var(--color-scheme);
max-height: 100%; max-height: 100%;
@ -21,13 +21,13 @@ body {
hr { hr {
background-color: var(--hr-color); background-color: var(--hr-color);
border-top: 0px; border-top: 0;
} }
.accent { .accent {
background-color: var(--accent-bg-color) !important; background-color: var(--accent-bg-color) !important;
color: var(--accent-text-color) !important; color: var(--accent-text-color) !important;
box-shadow: inset 0px 0px 8px 1px var(--accent-bg-color) !important; box-shadow: inset 0 0 8px 1px var(--accent-bg-color) !important;
font-size: var(--accent-text-size) !important; font-size: var(--accent-text-size) !important;
font-style: italic; font-style: italic;
padding: 10px; padding: 10px;
@ -38,6 +38,12 @@ hr {
color: var(--text-muted-color) !important; color: var(--text-muted-color) !important;
} }
.form-check-input {
cursor: pointer !important;
}
.form-switch .form-check-input:checked { .form-switch .form-check-input:checked {
background-color: var(--primary-color); background-color: var(--primary-color);
} }