From 93dc6534fc1d59124373c6fff0237b33ce41e71f Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sun, 6 Apr 2025 13:14:04 -0500 Subject: [PATCH] Another round of bugfixes (#3707) --- API.Tests/Services/ScannerServiceTests.cs | 90 ++++--- .../TestCases/Sort Order - Manga.json | 5 + API/Controllers/SettingsController.cs | 3 +- .../Repositories/ReadingListRepository.cs | 9 + API/Helpers/Builders/ChapterBuilder.cs | 2 +- API/Services/ReadingListService.cs | 10 +- API/Services/SettingsService.cs | 8 +- .../Tasks/Scanner/ParseScannedFiles.cs | 5 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 2 +- UI/Web/package-lock.json | 223 +++++++++--------- UI/Web/package.json | 32 +-- UI/Web/src/app/_services/action.service.ts | 46 ++-- .../details-tab/details-tab.component.html | 43 ++-- .../details-tab/details-tab.component.ts | 2 + .../manage-email-settings.component.ts | 6 +- .../manage-settings.component.ts | 6 +- .../manage-users/manage-users.component.html | 2 +- .../manage-users/manage-users.component.ts | 19 +- .../book-reader/book-reader.component.ts | 3 +- .../volume-card/volume-card.component.html | 11 +- .../draggable-ordered-list.component.html | 16 +- .../draggable-ordered-list.component.ts | 7 +- .../reading-list-detail.component.html | 2 + .../series-detail/series-detail.component.ts | 10 +- .../setting-switch.component.scss | 2 + .../file-breakdown-stats.component.html | 108 ++++----- .../file-breakdown-stats.component.scss | 6 +- .../file-breakdown-stats.component.ts | 36 +-- .../publication-status-stats.component.html | 18 +- .../_components/typeahead.component.html | 1 - UI/Web/src/assets/langs/en.json | 2 +- UI/Web/src/theme/utilities/_global.scss | 12 +- 32 files changed, 412 insertions(+), 335 deletions(-) create mode 100644 API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 5addc767d..4554820fb 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -90,7 +90,7 @@ public class ScannerServiceTests : AbstractDbTest [Fact] 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 scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); @@ -106,7 +106,7 @@ public class ScannerServiceTests : AbstractDbTest [Fact] 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 scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); @@ -121,7 +121,7 @@ public class ScannerServiceTests : AbstractDbTest [Fact] 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 scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); @@ -302,38 +302,38 @@ public class ScannerServiceTests : AbstractDbTest } - [Fact] - public async Task ScanLibrary_PublishersInheritFromChapters() + [Fact] + public async Task ScanLibrary_PublishersInheritFromChapters() + { + const string testcase = "Flat Special - Manga.json"; + + var infos = new Dictionary(); + 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(); - 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 library = await _scannerHelper.GenerateScannerData(testcase, infos); - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var scanner = _scannerHelper.CreateServices(); + await scanner.ScanLibrary(library.Id); + var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - Assert.NotNull(postLib); - Assert.Single(postLib.Series); - var publishers = postLib.Series.First().Metadata.People - .Where(p => p.Role == PersonRole.Publisher); - Assert.Equal(3, publishers.Count()); - } + Assert.NotNull(postLib); + Assert.Single(postLib.Series); + var publishers = postLib.Series.First().Metadata.People + .Where(p => p.Role == PersonRole.Publisher); + Assert.Equal(3, publishers.Count()); + } /// @@ -908,4 +908,34 @@ public class ScannerServiceTests : AbstractDbTest Assert.Equal(6, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); } + + + /// + /// Ensure when Kavita scans, the sort order of chapters is correct + /// + [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)); + } } diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json new file mode 100644 index 000000000..0b2dd765d --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json @@ -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" +] diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index e9477ab46..0610c8705 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -151,7 +151,8 @@ public class SettingsController : BaseApiController try { - return Ok(await _settingsService.UpdateSettings(updateSettingsDto)); + var d = await _settingsService.UpdateSettings(updateSettingsDto); + return Ok(d); } catch (KavitaException ex) { diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 83f2685f4..6d4a14bd9 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -49,6 +49,7 @@ public interface IReadingListRepository Task> GetRandomCoverImagesAsync(int readingListId); Task> GetAllCoverImagesAsync(); Task ReadingListExists(string name); + Task ReadingListExistsForUser(string name, int userId); IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role); Task GetReadingListAllPeopleAsync(int readingListId); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); @@ -109,6 +110,7 @@ public class ReadingListRepository : IReadingListRepository .SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage)) .Where(t => !string.IsNullOrEmpty(t)) .ToListAsync(); + return data .OrderBy(_ => random.Next()) .Take(4) @@ -123,6 +125,13 @@ public class ReadingListRepository : IReadingListRepository .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); } + public async Task 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 GetReadingListPeopleAsync(int readingListId, PersonRole role) { return _context.ReadingListItem diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index e7c458eeb..f85c21595 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -25,7 +25,7 @@ public class ChapterBuilder : IEntityBuilder MinNumber = Parser.MinNumberFromRange(number), MaxNumber = Parser.MaxNumberFromRange(number), SortOrder = Parser.MinNumberFromRange(number), - Files = new List(), + Files = [], Pages = 1, CreatedUtc = DateTime.UtcNow }; diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index a1c5fe6df..8c4f63430 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -473,6 +473,7 @@ public class ReadingListService : IReadingListService _logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name); var user = await _unitOfWork.UserRepository.GetDefaultAdminUser(); series.Metadata ??= new SeriesMetadataBuilder().Build(); + foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters)) { var pairs = new List>(); @@ -578,14 +579,14 @@ public class ReadingListService : IReadingListService { CblName = cblReading.Name, Success = CblImportResult.Success, - Results = new List(), + Results = [], SuccessfulInserts = new List() }; if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl; - // Is there another reading list with the same name? - if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name)) + // Is there another reading list with the same name on the user's account? + if (await _unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId)) { importSummary.Success = CblImportResult.Fail; importSummary.Results.Add(new CblBookResult @@ -600,9 +601,6 @@ public class ReadingListService : IReadingListService var userSeries = (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) { // Report that no series exist in the reading list diff --git a/API/Services/SettingsService.cs b/API/Services/SettingsService.cs index 172bcfedb..a7b8cc490 100644 --- a/API/Services/SettingsService.cs +++ b/API/Services/SettingsService.cs @@ -21,8 +21,8 @@ namespace API.Services; public interface ISettingsService { - Task> UpdateMetadataSettings(MetadataSettingsDto dto); - Task> UpdateSettings(ServerSettingDto updateSettingsDto); + Task UpdateMetadataSettings(MetadataSettingsDto dto); + Task UpdateSettings(ServerSettingDto updateSettingsDto); } @@ -50,7 +50,7 @@ public class SettingsService : ISettingsService /// /// /// - public async Task> UpdateMetadataSettings(MetadataSettingsDto dto) + public async Task UpdateMetadataSettings(MetadataSettingsDto dto) { var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings(); existingMetadataSetting.Enabled = dto.Enabled; @@ -108,7 +108,7 @@ public class SettingsService : ISettingsService /// /// /// - public async Task> UpdateSettings(ServerSettingDto updateSettingsDto) + public async Task UpdateSettings(ServerSettingDto updateSettingsDto) { // We do not allow CacheDirectory changes, so we will ignore. var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 01cb4a4d0..c3f36ef2e 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -871,7 +871,10 @@ public class ParseScannedFiles var prevIssue = string.Empty; 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 counter = parsedChapter; diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 46bc74a65..12987b18b 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -24,7 +24,7 @@ public static partial class Parser 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 EpubFileExtension = @"\.epub"; public const string PdfFileExtension = @"\.pdf"; diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index f7384900b..42c749900 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -9,16 +9,16 @@ "version": "0.7.12.1", "dependencies": { "@angular-slider/ngx-slider": "^19.0.0", - "@angular/animations": "^19.2.3", - "@angular/cdk": "^19.2.6", - "@angular/common": "^19.2.3", - "@angular/compiler": "^19.2.3", - "@angular/core": "^19.2.3", - "@angular/forms": "^19.2.3", - "@angular/localize": "^19.2.3", - "@angular/platform-browser": "^19.2.3", - "@angular/platform-browser-dynamic": "^19.2.3", - "@angular/router": "^19.2.3", + "@angular/animations": "^19.2.5", + "@angular/cdk": "^19.2.8", + "@angular/common": "^19.2.5", + "@angular/compiler": "^19.2.5", + "@angular/core": "^19.2.5", + "@angular/forms": "^19.2.5", + "@angular/localize": "^19.2.5", + "@angular/platform-browser": "^19.2.5", + "@angular/platform-browser-dynamic": "^19.2.5", + "@angular/router": "^19.2.5", "@fortawesome/fontawesome-free": "^6.7.2", "@iharbeck/ngx-virtual-scroller": "^19.0.1", "@iplab/ngx-file-upload": "^19.0.3", @@ -36,10 +36,10 @@ "bootstrap": "^5.3.2", "charts.css": "^1.1.0", "file-saver": "^2.0.5", - "luxon": "^3.5.0", + "luxon": "^3.6.1", "ng-circle-progress": "^1.7.1", "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-extended-pdf-viewer": "^23.0.0-alpha.7", "ngx-file-drop": "^16.0.0", @@ -58,12 +58,12 @@ "@angular-eslint/eslint-plugin-template": "^19.3.0", "@angular-eslint/schematics": "^19.3.0", "@angular-eslint/template-parser": "^19.3.0", - "@angular/build": "^19.2.4", - "@angular/cli": "^19.2.4", - "@angular/compiler-cli": "^19.2.3", + "@angular/build": "^19.2.6", + "@angular/cli": "^19.2.6", + "@angular/compiler-cli": "^19.2.5", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", - "@types/luxon": "^3.4.0", + "@types/luxon": "^3.6.2", "@types/node": "^22.13.13", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", @@ -97,12 +97,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1902.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.4.tgz", - "integrity": "sha512-YTLiJ7uVItZTAxRuSgASP0M5qILESWH8xGmfR+YWR1JiJem09DWEOpWeLdha1BFzUui5L+6j1btzh4FUHJOtAg==", + "version": "0.1902.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.6.tgz", + "integrity": "sha512-Dx6yPxpaE5AhP6UtrVRDCc9Ihq9B65LAbmIh3dNOyeehratuaQS0TYNKjbpaevevJojW840DTg80N+CrlfYp9g==", "dev": true, "dependencies": { - "@angular-devkit/core": "19.2.4", + "@angular-devkit/core": "19.2.6", "rxjs": "7.8.1" }, "engines": { @@ -121,9 +121,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.4.tgz", - "integrity": "sha512-dL6AmCQsKh+CFVvO/jxX8qZpamVwt9r4iIo7fYcAI2+mTSDGxxBGWbS+onIfdPFuRp2HgKa+AT6WiHmRqu63AA==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.6.tgz", + "integrity": "sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ==", "dev": true, "dependencies": { "ajv": "8.17.1", @@ -157,12 +157,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.4.tgz", - "integrity": "sha512-WaFe95ncm1A+QTlUHxQcFyGKIn67xgqGX7WCj8R0QlKOS0hLKx97SG4p19uwHlww0lmAcwk/QJP6G6sPL/CJ9w==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.6.tgz", + "integrity": "sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "19.2.4", + "@angular-devkit/core": "19.2.6", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -304,9 +304,9 @@ } }, "node_modules/@angular/animations": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.3.tgz", - "integrity": "sha512-HQexOmwEJFX3sHLspOCi7dVOdPW5Ad4jH6tJsf+zABkF0GjgIVf4jewe1uE5ZLKgoflr9f9vpcpy39IWl00kWw==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.5.tgz", + "integrity": "sha512-m4RtY3z1JuHFCh6OrOHxo25oKEigBDdR/XmdCfXIwfTiObZzNA7VQhysgdrb9IISO99kXbjZUYKDtLzgWT8Klg==", "dependencies": { "tslib": "^2.3.0" }, @@ -314,17 +314,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.2.3" + "@angular/common": "19.2.5", + "@angular/core": "19.2.5" } }, "node_modules/@angular/build": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.4.tgz", - "integrity": "sha512-poCXvmwKri3snWa9zVJ2sW7wyJatZjkwnH6GUBdJrM2dXRQ+LYLk/oXqEjlSRBYNna7P1ZcKNqBbzu0/SnnngA==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.6.tgz", + "integrity": "sha512-+VBLb4ZPLswwJmgfsTFzGex+Sq/WveNc+uaIWyHYjwnuI17NXe1qAAg1rlp72CqGn0cirisfOyAUwPc/xZAgTg==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.4", + "@angular-devkit/architect": "0.1902.6", "@babel/core": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", @@ -347,7 +348,7 @@ "sass": "1.85.0", "semver": "7.7.1", "source-map-support": "0.5.21", - "vite": "6.2.0", + "vite": "6.2.4", "watchpack": "2.4.2" }, "engines": { @@ -364,7 +365,7 @@ "@angular/localize": "^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/ssr": "^19.2.4", + "@angular/ssr": "^19.2.6", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", @@ -464,15 +465,13 @@ } }, "node_modules/@angular/cdk": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.6.tgz", - "integrity": "sha512-AneN/NeAYU4+AwzicTwtYE9CkMcWA0cAJ41SNfSyoHaaHNXSkryzwSmTYS3FO+taqd7OGnBePeWJbW2uJXcvfA==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.8.tgz", + "integrity": "sha512-ZZqWVYFF80TdjWkk2sc9Pn2luhiYeC78VH3Yjeln4wXMsTGDsvKPBcuOxSxxpJ31saaVBehDjBUuXMqGRj8KuA==", "dependencies": { + "parse5": "^7.1.2", "tslib": "^2.3.0" }, - "optionalDependencies": { - "parse5": "^7.1.2" - }, "peerDependencies": { "@angular/common": "^19.0.0 || ^20.0.0", "@angular/core": "^19.0.0 || ^20.0.0", @@ -480,17 +479,17 @@ } }, "node_modules/@angular/cli": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.4.tgz", - "integrity": "sha512-YmZYrxdGBwSZsrnpS6vr9gQ8+PrZHzwyYW/3jU2NRAMtl0ZlipDyfpLIDgrfqYPeumzr7+SKtJYVvlsMnjkN4g==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.6.tgz", + "integrity": "sha512-eZhFOSsDUHKaciwcWdU5C54ViAvPPdZJf42So93G2vZWDtEq6Uk47huocn1FY9cMhDvURfYLNrrLMpUDtUSsSA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1902.4", - "@angular-devkit/core": "19.2.4", - "@angular-devkit/schematics": "19.2.4", + "@angular-devkit/architect": "0.1902.6", + "@angular-devkit/core": "19.2.6", + "@angular-devkit/schematics": "19.2.6", "@inquirer/prompts": "7.3.2", "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.2.4", + "@schematics/angular": "19.2.6", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", @@ -513,9 +512,9 @@ } }, "node_modules/@angular/common": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.3.tgz", - "integrity": "sha512-cYOMRXFb6Sjtg9BI3bE/Ave+xU234jQmHYj7hBxr3MiqRSVJL4niy10KiA/Jiz6y76V5QfZfS+0aE65VuoqAvg==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.5.tgz", + "integrity": "sha512-vFCBdas4C5PxP6ts/4TlRddWD3DUmI3aaO0QZdZvqyLHy428t84ruYdsJXKaeD8ie2U4/9F3a1tsklclRG/BBA==", "dependencies": { "tslib": "^2.3.0" }, @@ -523,14 +522,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.2.3", + "@angular/core": "19.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.3.tgz", - "integrity": "sha512-TL/JIU7vzSWD+IrMq2PAiHZi7IUFSRhdHo8q6/WuZ8SQmbuXCK2pJvHZpTtUdLswdPeD/UVhkhTAhQzEyEdZVg==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.5.tgz", + "integrity": "sha512-34J+HubQjwkbZ0AUtU5sa4Zouws9XtP/fKaysMQecoYJTZ3jewzLSRu3aAEZX1Y4gIrcVVKKIxM6oWoXKwYMOA==", "dependencies": { "tslib": "^2.3.0" }, @@ -539,9 +538,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.3.tgz", - "integrity": "sha512-ePh/7A6eEDAyfVn8QgLcAvrxhXBAf6mTqB/3+HwQeXLaka1gtN6xvZ6cjLEegP4s6kcYGhdfdLwzCcy0kjsY5g==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.5.tgz", + "integrity": "sha512-b2cG41r6lilApXLlvja1Ra2D00dM3BxmQhoElKC1tOnpD6S3/krlH1DOnBB2I55RBn9iv4zdmPz1l8zPUSh7DQ==", "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -561,7 +560,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.2.3", + "@angular/compiler": "19.2.5", "typescript": ">=5.5 <5.9" } }, @@ -592,9 +591,9 @@ } }, "node_modules/@angular/core": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.3.tgz", - "integrity": "sha512-uNDbQBDWdAfL8JhgG2l9eTEbikovZ+SthLUKERyR4fL7AVGSx85LjNySRuq4WAL4eiD1cRN1UUgu8o+WKqF/ow==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.5.tgz", + "integrity": "sha512-NNEz1sEZz1mBpgf6Tz3aJ9b8KjqpTiMYhHfCYA9h9Ipe4D8gUmOsvPHPK2M755OX7p7PmUmzp1XCUHYrZMVHRw==", "dependencies": { "tslib": "^2.3.0" }, @@ -607,9 +606,9 @@ } }, "node_modules/@angular/forms": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.3.tgz", - "integrity": "sha512-JEgNKiZd3taYBg9lsMvoana5cv1QGke8xkuryc9zesHPJjhw9QHllmDPOW2HyUuwPqXZ/YkHiuCMOk+4qPjsAw==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.5.tgz", + "integrity": "sha512-2Zvy3qK1kOxiAX9fdSaeG48q7oyO/4RlMYlg1w+ra9qX1SrgwF3OQ2P2Vs+ojg1AxN3z9xFp4aYaaID/G2LZAw==", "dependencies": { "tslib": "^2.3.0" }, @@ -617,16 +616,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.3", - "@angular/core": "19.2.3", - "@angular/platform-browser": "19.2.3", + "@angular/common": "19.2.5", + "@angular/core": "19.2.5", + "@angular/platform-browser": "19.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.2.3.tgz", - "integrity": "sha512-4YUvii9uJCT5D1n3RR5W68UizrwsFr85ucIB9G3rQXL9y1LA2GbeKQkgazpF9nTCBDAfrux7tKvS27owofY5ww==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.2.5.tgz", + "integrity": "sha512-oAc19bubk6Z/2Vv6OkV0MsjdgC8cUaUwBmwdc6blFVe1NCX1KjdaqDyC2EQAO3nWfcdV4uvOOuu8myxB64bamw==", "dependencies": { "@babel/core": "7.26.9", "@types/babel__core": "7.20.5", @@ -642,14 +641,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.2.3", - "@angular/compiler-cli": "19.2.3" + "@angular/compiler": "19.2.5", + "@angular/compiler-cli": "19.2.5" } }, "node_modules/@angular/platform-browser": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.3.tgz", - "integrity": "sha512-bz5mvUkCS8SxaMInjPgi/2dD7vpWkZePQesvr/bBRNQbYSE4cGTbovXcVl3X5hIxs5JoC6Het0lS2IxDq7j6qg==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.5.tgz", + "integrity": "sha512-Lshy++X16cvl6OPvfzMySpsqEaCPKEJmDjz7q7oSt96oxlh6LvOeOUVLjsNyrNaIt9NadpWoqjlu/I9RTPJkpw==", "dependencies": { "tslib": "^2.3.0" }, @@ -657,9 +656,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "19.2.3", - "@angular/common": "19.2.3", - "@angular/core": "19.2.3" + "@angular/animations": "19.2.5", + "@angular/common": "19.2.5", + "@angular/core": "19.2.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -668,9 +667,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.3.tgz", - "integrity": "sha512-PHmmtdGxSfe9HL8xR4g3PspnEaPqTEOGyzNviAHugfkZCgXCdSbYs36d3i0nPwhExMAeuIVXbbJyouDn2kyeOw==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.5.tgz", + "integrity": "sha512-15in8u4552EcdWNTXY2h0MKuJbk3AuXwWr0zVTum4CfB/Ss2tNTrDEdWhgAbhnUI0e9jZQee/fhBbA1rleMYrA==", "dependencies": { "tslib": "^2.3.0" }, @@ -678,16 +677,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.3", - "@angular/compiler": "19.2.3", - "@angular/core": "19.2.3", - "@angular/platform-browser": "19.2.3" + "@angular/common": "19.2.5", + "@angular/compiler": "19.2.5", + "@angular/core": "19.2.5", + "@angular/platform-browser": "19.2.5" } }, "node_modules/@angular/router": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.3.tgz", - "integrity": "sha512-yYVMT7CceKqE+fBXxkhkAqEQUEdY/BHtLQr1vP9rEnAf30vwKghDEresugfegY6Ch4IGKTBtDG/QGmxWszgUAQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.5.tgz", + "integrity": "sha512-9pSfmdNXLjaOKj0kd4UxBC7sFdCFOnRGbftp397G3KWqsLsGSKmNFzqhXNeA5QHkaVxnpmpm8HzXU+zYV5JwSg==", "dependencies": { "tslib": "^2.3.0" }, @@ -695,9 +694,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.3", - "@angular/core": "19.2.3", - "@angular/platform-browser": "19.2.3", + "@angular/common": "19.2.5", + "@angular/core": "19.2.5", + "@angular/platform-browser": "19.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -3562,13 +3561,13 @@ ] }, "node_modules/@schematics/angular": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.4.tgz", - "integrity": "sha512-P7fphIPbqHHYRVRPiFl7RAHYPYhINGSUYOXrcThVBBsgKQBX18oNdUWvhZA6ylwK/9T21XB20VyLjNy0d78H1Q==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.6.tgz", + "integrity": "sha512-fmbF9ONmEZqxHocCwOSWG2mHp4a22d1uW+DZUBUgZSBUFIrnFw42deOxDq8mkZOZ1Tc73UpLN2GKI7iJeUqS2A==", "dev": true, "dependencies": { - "@angular-devkit/core": "19.2.4", - "@angular-devkit/schematics": "19.2.4", + "@angular-devkit/core": "19.2.6", + "@angular-devkit/schematics": "19.2.6", "jsonc-parser": "3.3.1" }, "engines": { @@ -4063,9 +4062,9 @@ "dev": true }, "node_modules/@types/luxon": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", - "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", "dev": true }, "node_modules/@types/node": { @@ -5372,7 +5371,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "devOptional": true, "engines": { "node": ">=0.12" }, @@ -7008,9 +7006,9 @@ } }, "node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", "engines": { "node": ">=12" } @@ -7414,9 +7412,9 @@ } }, "node_modules/ng-select2-component": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.2.tgz", - "integrity": "sha512-dAeUSqmjU9Gexi47vMEz1bXGQkl3Be2O0wl6QqpYwFvM+QEfUyQiY0zWpYvB8shO1sIHoCQNKt9yTFcRzvzW0g==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-17.2.3.tgz", + "integrity": "sha512-JNik7OWqya4ERuqlfnYiJHkaqyZtHqUhATIZ9yUxmadWWNIn8I3Lwa7qt0KtPpR01O9HJC0PtHXhvev88Cju2A==", "dependencies": { "ngx-infinite-scroll": ">=18.0.0 || >=19.0.0", "tslib": "^2.3.0" @@ -7946,7 +7944,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "devOptional": true, "dependencies": { "entities": "^4.4.0" }, @@ -9212,9 +9209,9 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", + "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", "dev": true, "dependencies": { "esbuild": "^0.25.0", diff --git a/UI/Web/package.json b/UI/Web/package.json index 1ffbb1c07..912434a63 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -17,16 +17,16 @@ "private": true, "dependencies": { "@angular-slider/ngx-slider": "^19.0.0", - "@angular/animations": "^19.2.3", - "@angular/cdk": "^19.2.6", - "@angular/common": "^19.2.3", - "@angular/compiler": "^19.2.3", - "@angular/core": "^19.2.3", - "@angular/forms": "^19.2.3", - "@angular/localize": "^19.2.3", - "@angular/platform-browser": "^19.2.3", - "@angular/platform-browser-dynamic": "^19.2.3", - "@angular/router": "^19.2.3", + "@angular/animations": "^19.2.5", + "@angular/cdk": "^19.2.8", + "@angular/common": "^19.2.5", + "@angular/compiler": "^19.2.5", + "@angular/core": "^19.2.5", + "@angular/forms": "^19.2.5", + "@angular/localize": "^19.2.5", + "@angular/platform-browser": "^19.2.5", + "@angular/platform-browser-dynamic": "^19.2.5", + "@angular/router": "^19.2.5", "@fortawesome/fontawesome-free": "^6.7.2", "@iharbeck/ngx-virtual-scroller": "^19.0.1", "@iplab/ngx-file-upload": "^19.0.3", @@ -44,10 +44,10 @@ "bootstrap": "^5.3.2", "charts.css": "^1.1.0", "file-saver": "^2.0.5", - "luxon": "^3.5.0", + "luxon": "^3.6.1", "ng-circle-progress": "^1.7.1", "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-extended-pdf-viewer": "^23.0.0-alpha.7", "ngx-file-drop": "^16.0.0", @@ -66,12 +66,12 @@ "@angular-eslint/eslint-plugin-template": "^19.3.0", "@angular-eslint/schematics": "^19.3.0", "@angular-eslint/template-parser": "^19.3.0", - "@angular/build": "^19.2.4", - "@angular/cli": "^19.2.4", - "@angular/compiler-cli": "^19.2.3", + "@angular/build": "^19.2.6", + "@angular/cli": "^19.2.6", + "@angular/compiler-cli": "^19.2.5", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", - "@types/luxon": "^3.4.0", + "@types/luxon": "^3.6.2", "@types/node": "^22.13.13", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index d520a4a7b..8bf6cdacd 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -1,23 +1,27 @@ import {inject, Injectable} from '@angular/core'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ToastrService } from 'ngx-toastr'; -import { take } from 'rxjs/operators'; -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 { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component'; -import { ConfirmService } from '../shared/confirm.service'; -import { LibrarySettingsModalComponent } from '../sidenav/_modals/library-settings-modal/library-settings-modal.component'; -import { Chapter } from '../_models/chapter'; -import { Device } from '../_models/device/device'; -import { Library } from '../_models/library/library'; -import { ReadingList } from '../_models/reading-list'; -import { Series } from '../_models/series'; -import { Volume } from '../_models/volume'; -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 {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import {ToastrService} from 'ngx-toastr'; +import {take} from 'rxjs/operators'; +import {BulkAddToCollectionComponent} from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.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 {ConfirmService} from '../shared/confirm.service'; +import { + LibrarySettingsModalComponent +} from '../sidenav/_modals/library-settings-modal/library-settings-modal.component'; +import {Chapter} from '../_models/chapter'; +import {Device} from '../_models/device/device'; +import {Library} from '../_models/library/library'; +import {ReadingList} from '../_models/reading-list'; +import {Series} from '../_models/series'; +import {Volume} from '../_models/volume'; +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 {UserCollection} from "../_models/collection-tag"; import {CollectionTagService} from "./collection-tag.service"; @@ -652,7 +656,7 @@ export class ActionService { } 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.closed.pipe(take(1)).subscribe((list) => { if (callback && list !== undefined) { @@ -773,7 +777,7 @@ export class ActionService { } 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.closed.subscribe(saved => { if (callback) { diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html index 6f307a55d..1087f3d3b 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html @@ -47,30 +47,33 @@ } - + @if (!suppressEmptyGenres || genres.length > 0) { + - -
-

{{t('genres-title')}}

-
- - - {{item.title}} - - +
+

{{t('genres-title')}}

+
+ + + {{item.title}} + + +
-
+ } -
-

{{t('tags-title')}}

-
- - - {{item.title}} - - + @if (!suppressEmptyTags || tags.length > 0) { +
+

{{t('tags-title')}}

+
+ + + {{item.title}} + + +
-
+ }
diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts index 72d1df227..096826964 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts @@ -61,6 +61,8 @@ export class DetailsTabComponent { @Input() genres: Array = []; @Input() tags: Array = []; @Input() webLinks: Array = []; + @Input() suppressEmptyGenres: boolean = false; + @Input() suppressEmptyTags: boolean = false; openGeneric(queryParamName: FilterField, filter: string | number) { diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index df079d33c..d9aa1decb 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; 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 {ServerSettings} from '../_models/server-settings'; import {translate, TranslocoModule} from "@jsverse/transloco"; @@ -30,7 +30,7 @@ export class ManageEmailSettingsComponent implements OnInit { settingsForm: FormGroup = new FormGroup({}); ngOnInit(): void { - this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { + this.settingsService.getServerSettings().subscribe((settings: ServerSettings) => { this.serverSettings = settings; 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() { const modelSettings = Object.assign({}, this.serverSettings); + + modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value; modelSettings.hostName = this.settingsForm.get('hostName')?.value; diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index 35be97349..33941f768 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -47,15 +47,15 @@ export class ManageSettingsComponent implements OnInit { translate('manage-settings.allow-stats-tooltip-part-2'); ngOnInit(): void { - this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => { + this.settingsService.getTaskFrequencies().subscribe(frequencies => { this.taskFrequencies = frequencies; this.cdRef.markForCheck(); }); - this.settingsService.getLoggingLevels().pipe(take(1)).subscribe(levels => { + this.settingsService.getLoggingLevels().subscribe(levels => { this.logLevels = levels; this.cdRef.markForCheck(); }); - this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { + this.settingsService.getServerSettings().subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required])); this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required])); diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.html b/UI/Web/src/app/admin/manage-users/manage-users.component.html index 7991a4d25..72c345edd 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.html +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.html @@ -37,7 +37,7 @@ @if (member.libraries.length > 0) { - @if (hasAdminRole(member)) { + @if (hasAdminRole(member) || member.libraries.length === libraryCount) { {{t('all-libraries')}} } @else { @if (member.libraries.length > 5) { diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.ts b/UI/Web/src/app/admin/manage-users/manage-users.component.ts index 4a85c37c4..6b62d6dc1 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.ts +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.ts @@ -13,7 +13,7 @@ import {EditUserComponent} from '../edit-user/edit-user.component'; import {Router} from '@angular/router'; import {TagBadgeComponent} from '../../shared/tag-badge/tag-badge.component'; 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 {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; @@ -50,6 +50,7 @@ export class ManageUsersComponent implements OnInit { members: Member[] = []; loggedInUsername = ''; loadingMembers = false; + libraryCount: number = 0; constructor() { @@ -81,7 +82,11 @@ export class ManageUsersComponent implements OnInit { if (nameA < nameB) return -1; if (nameA > nameB) return 1; 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.cdRef.markForCheck(); }); @@ -142,16 +147,8 @@ export class ManageUsersComponent implements OnInit { 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) { - return member.roles.indexOf('Admin') >= 0; + return member.roles.indexOf(Role.Admin) >= 0; } getRoles(member: Member) { diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index a595013fb..6abd619f8 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -722,6 +722,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { // Update the window Height this.updateWidthAndHeightCalcs(); this.updateImageSizes(); + const resumeElement = this.getFirstVisibleElementXPath(); if (this.layoutMode !== BookPageLayoutMode.Default && resumeElement !== null && resumeElement !== undefined) { 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.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.cdRef.markForCheck(); diff --git a/UI/Web/src/app/cards/volume-card/volume-card.component.html b/UI/Web/src/app/cards/volume-card/volume-card.component.html index b89fd19e4..c9845b5e3 100644 --- a/UI/Web/src/app/cards/volume-card/volume-card.component.html +++ b/UI/Web/src/app/cards/volume-card/volume-card.component.html @@ -52,12 +52,15 @@ }
- @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) { + @if (libraryType !== LibraryType.Images) {
- - +
- {{volume.name}} + @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) { + {{volume.name}} + } @else { + {{volume.chapters[0].titleName}} + }
@if (actions && actions.length > 0) { diff --git a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html index 801df6699..4d5f89531 100644 --- a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html +++ b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.html @@ -2,15 +2,15 @@ @if (items.length > virtualizeAfter) {
-
- -
- - - - + @for (item of scroll.viewPortItems; track trackByIdentity(i, item); let i = $index) { +
+
+ + + +
-
+ }
} @else { diff --git a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts index 1f0f0e75b..3a3087632 100644 --- a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts +++ b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts @@ -13,7 +13,7 @@ import { TrackByFunction } from '@angular/core'; 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 {BulkSelectionService} from "../../../cards/bulk-selection.service"; import {FormsModule} from "@angular/forms"; @@ -36,13 +36,14 @@ export interface ItemRemoveEvent { templateUrl: './draggable-ordered-list.component.html', styleUrls: ['./draggable-ordered-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [VirtualScrollerModule, NgFor, NgTemplateOutlet, CdkDropList, CdkDrag, + imports: [VirtualScrollerModule, NgTemplateOutlet, CdkDropList, CdkDrag, CdkDragHandle, TranslocoDirective, NgClass, FormsModule] }) export class DraggableOrderedListComponent { protected readonly bulkSelectionService = inject(BulkSelectionService); 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); } - constructor(private readonly cdRef: ChangeDetectorRef) { + constructor() { this.bulkSelectionService.selections$.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((s) => { diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html index 8196f15e5..7a7f8ac4d 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html @@ -252,6 +252,8 @@ @defer (when activeTabId === TabID.Details; prefetch on idle) { } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 6a781b886..890237be4 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -636,7 +636,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } } - handleChapterActionCallback(action: ActionItem, chapter: Chapter) { + async handleChapterActionCallback(action: ActionItem, chapter: Chapter) { switch (action.action) { case(Action.MarkAsRead): this.markChapterAsRead(chapter); @@ -657,6 +657,14 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { const device = (action._extra!.data as Device); this.actionService.sendToDevice([chapter.id], device); 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: break; } diff --git a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.scss b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.scss index b27f531d7..06ba1fc2c 100644 --- a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.scss +++ b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.scss @@ -1,3 +1,5 @@ .text-muted { font-size: 14px; } + + diff --git a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html index 0f6889382..99dea4d79 100644 --- a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html +++ b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html @@ -18,69 +18,69 @@ {{t('format-tooltip')}} + @let files = files$ | async; - - - - - - + @if (formControl.value) { + + } @else { +
- - - - - - - + + + + + + + - - - - - - - + @for (item of files; track item.extension; let idx = $index) { + + + + + + + + } - - - - - - + + + + + +
- {{t('extension-header')}} - - {{t('format-header')}} - - {{t('total-size-header')}} - - {{t('total-files-header')}} - {{t('download-file-for-extension-header')}}
+ {{t('extension-header')}} + + {{t('format-header')}} + + {{t('total-size-header')}} + + {{t('total-files-header')}} + {{t('download-file-for-extension-header')}}
- {{item.extension || t('not-classified')}} - - {{item.format | mangaFormat}} - - {{item.totalSize | bytes}} - - {{item.totalFiles | number:'1.0-0'}} - - -
+ {{item.extension || t('not-classified')}} + + {{item.format | mangaFormat}} + + {{item.totalSize | bytes}} + + {{item.totalFiles | number:'1.0-0'}} + + +
{{t('total-file-size-title')}}{{((rawData$ | async)?.totalFileSize || 0) | bytes}}
{{t('total-file-size-title')}}{{((rawData$ | async)?.totalFileSize || 0) | bytes}}
- - - +
+ }
diff --git a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.scss b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.scss index dc37ca9e7..12b3e18ed 100644 --- a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.scss +++ b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.scss @@ -9,4 +9,8 @@ display: flex; flex-flow: column; box-sizing: border-box; -} \ No newline at end of file +} + +tfoot { + color: var(--bs-body); +} diff --git a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.ts b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.ts index e5a2963ae..039ee3332 100644 --- a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.ts +++ b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.ts @@ -1,26 +1,29 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, DestroyRef, inject, - QueryList, TemplateRef, ViewChild, + QueryList, + TemplateRef, + ViewChild, ViewChildren } from '@angular/core'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; -import { PieChartModule } from '@swimlane/ngx-charts'; -import {Observable, BehaviorSubject, combineLatest, map, shareReplay, switchMap} from 'rxjs'; -import { StatisticsService } from 'src/app/_services/statistics.service'; -import { SortableHeader, SortEvent, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive'; -import { FileExtension, FileExtensionBreakdown } from '../../_models/file-breakdown'; -import { PieDataItem } from '../../_models/pie-data-item'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {PieChartModule} from '@swimlane/ngx-charts'; +import {BehaviorSubject, combineLatest, map, Observable, shareReplay} from 'rxjs'; +import {StatisticsService} from 'src/app/_services/statistics.service'; +import {compare, SortableHeader, SortEvent} from 'src/app/_single-module/table/_directives/sortable-header.directive'; +import {FileExtension, FileExtensionBreakdown} from '../../_models/file-breakdown'; +import {PieDataItem} from '../../_models/pie-data-item'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import { MangaFormatPipe } from '../../../_pipes/manga-format.pipe'; -import { BytesPipe } from '../../../_pipes/bytes.pipe'; -import { NgIf, NgFor, AsyncPipe, DecimalPipe } from '@angular/common'; -import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco"; -import {Pagination} from "../../../_models/pagination"; -import {DownloadService} from "../../../shared/_services/download.service"; +import {MangaFormatPipe} from '../../../_pipes/manga-format.pipe'; +import {BytesPipe} from '../../../_pipes/bytes.pipe'; +import {AsyncPipe, DecimalPipe, NgFor, NgIf} from '@angular/common'; +import {TranslocoDirective, TranslocoService} from "@jsverse/transloco"; 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 { name: string, @@ -32,7 +35,7 @@ export interface StackedBarChartDataItem { templateUrl: './file-breakdown-stats.component.html', styleUrls: ['./file-breakdown-stats.component.scss'], 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 { @@ -103,4 +106,5 @@ export class FileBreakdownStatsComponent { }); } + protected readonly ColumnMode = ColumnMode; } diff --git a/UI/Web/src/app/statistics/_components/publication-status-stats/publication-status-stats.component.html b/UI/Web/src/app/statistics/_components/publication-status-stats/publication-status-stats.component.html index d04a544d9..46f6cd21f 100644 --- a/UI/Web/src/app/statistics/_components/publication-status-stats/publication-status-stats.component.html +++ b/UI/Web/src/app/statistics/_components/publication-status-stats/publication-status-stats.component.html @@ -16,16 +16,12 @@
+ @let statuses = publicationStatues$ | async; - - - - - - - + @if (formControl.value) { + + } @else { +
@@ -48,8 +44,8 @@
- - +
+ }
diff --git a/UI/Web/src/app/typeahead/_components/typeahead.component.html b/UI/Web/src/app/typeahead/_components/typeahead.component.html index 7f8311c8c..e757132e2 100644 --- a/UI/Web/src/app/typeahead/_components/typeahead.component.html +++ b/UI/Web/src/app/typeahead/_components/typeahead.component.html @@ -53,7 +53,6 @@
  • - {{settings.trackByIdentityFn(index, option)}}
  • } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 311901bbc..4d8a07210 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1074,7 +1074,7 @@ "donate": "Donate", "donate-tooltip": "You can remove this by subscribing to Kavita+", "back": "Back", - "cancel-edit": "Cancel Edit", + "cancel-edit": "Close Reorder", "more": "More", "customize": "{{settings.customize}}", "edit": "{{common.edit}}" diff --git a/UI/Web/src/theme/utilities/_global.scss b/UI/Web/src/theme/utilities/_global.scss index 2a96114a6..f08705dcd 100644 --- a/UI/Web/src/theme/utilities/_global.scss +++ b/UI/Web/src/theme/utilities/_global.scss @@ -7,7 +7,7 @@ html { html, body { height: 100%; overflow: hidden; } body { margin: 0; - font-family: var(--body-font-family); + font-family: var(--body-font-family), serif; color: var(--body-text-color); color-scheme: var(--color-scheme); max-height: 100%; @@ -21,13 +21,13 @@ body { hr { background-color: var(--hr-color); - border-top: 0px; + border-top: 0; } .accent { background-color: var(--accent-bg-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-style: italic; padding: 10px; @@ -38,6 +38,12 @@ hr { color: var(--text-muted-color) !important; } +.form-check-input { + cursor: pointer !important; +} + + + .form-switch .form-check-input:checked { background-color: var(--primary-color); }