From abdf15b895eab8fbf7d4453b4d540f405b4d5c55 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Mon, 28 Oct 2024 18:13:48 -0500 Subject: [PATCH] Round 3 of Bugfixing (#3318) --- API.Tests/Parsing/ComicParsingTests.cs | 6 +- API.Tests/Parsing/MangaParsingTests.cs | 1 - API.Tests/Parsing/ParsingTests.cs | 3 +- API.Tests/Services/ScannerServiceTests.cs | 30 ++ API.Tests/Services/TachiyomiServiceTests.cs | 2 +- .../TestCases/Series with Extra - Manga.json | 4 + API/Controllers/CBLController.cs | 1 + API/Controllers/MetadataController.cs | 25 +- API/Controllers/ReaderController.cs | 6 +- API/Controllers/SeriesController.cs | 2 +- API/Services/CacheService.cs | 2 +- API/Services/ReadingListService.cs | 4 - API/Services/TachiyomiService.cs | 4 +- .../Tasks/Scanner/Parser/ComicVineParser.cs | 4 +- .../Tasks/Scanner/Parser/ImageParser.cs | 2 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 26 +- UI/Web/package-lock.json | 285 +----------------- UI/Web/src/app/_models/metadata/person.ts | 8 +- UI/Web/src/app/_pipes/language-name.pipe.ts | 10 +- UI/Web/src/app/_services/metadata.service.ts | 5 + .../details-tab/details-tab.component.html | 38 +++ .../details-tab/details-tab.component.ts | 27 +- .../edit-chapter-modal.component.ts | 2 +- .../app/admin/license/license.component.html | 2 +- .../all-filters/all-filters.component.html | 2 +- .../edit-series-modal.component.html | 2 +- .../edit-series-modal.component.ts | 3 +- .../chapter-card/chapter-card.component.html | 20 +- .../cover-image-chooser.component.html | 87 +++--- .../cover-image-chooser.component.ts | 6 +- .../entity-title/entity-title.component.html | 159 +++++----- .../entity-title/entity-title.component.ts | 135 ++++++++- .../chapter-detail.component.html | 8 +- .../person-detail.component.html | 4 +- .../import-cbl/import-cbl.component.html | 2 +- .../series-detail.component.html | 18 +- .../manage-smart-filters.component.html | 2 +- .../manage-smart-filters.component.ts | 4 +- .../volume-detail.component.html | 7 +- UI/Web/src/assets/langs/en.json | 13 +- 40 files changed, 482 insertions(+), 489 deletions(-) create mode 100644 API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json diff --git a/API.Tests/Parsing/ComicParsingTests.cs b/API.Tests/Parsing/ComicParsingTests.cs index ad28e80a9..a0375a566 100644 --- a/API.Tests/Parsing/ComicParsingTests.cs +++ b/API.Tests/Parsing/ComicParsingTests.cs @@ -51,15 +51,15 @@ public class ComicParsingTests [InlineData("Demon 012 (Sep 1973) c2c", "Demon")] [InlineData("Dragon Age - Until We Sleep 01 (of 03)", "Dragon Age - Until We Sleep")] [InlineData("Green Lantern v2 017 - The Spy-Eye that doomed Green Lantern v2", "Green Lantern")] - [InlineData("Green Lantern - Circle of Fire Special - Adam Strange (2000)", "Green Lantern - Circle of Fire - Adam Strange")] - [InlineData("Identity Crisis Extra - Rags Morales Sketches (2005)", "Identity Crisis - Rags Morales Sketches")] + [InlineData("Green Lantern - Circle of Fire Special - Adam Strange (2000)", "Green Lantern - Circle of Fire Special - Adam Strange")] + [InlineData("Identity Crisis Extra - Rags Morales Sketches (2005)", "Identity Crisis Extra - Rags Morales Sketches")] [InlineData("Daredevil - t6 - 10 - (2019)", "Daredevil")] [InlineData("Batgirl T2000 #57", "Batgirl")] [InlineData("Teen Titans t1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "Teen Titans")] [InlineData("Conquistador_-Tome_2", "Conquistador")] [InlineData("Max_l_explorateur-_Tome_0", "Max l explorateur")] [InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "Chevaliers d'Héliopolis")] - [InlineData("Bd Fr-Aldebaran-Antares-t6", "Aldebaran-Antares")] + [InlineData("Bd Fr-Aldebaran-Antares-t6", "Bd Fr-Aldebaran-Antares")] [InlineData("Tintin - T22 Vol 714 pour Sydney", "Tintin")] [InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")] [InlineData("Kebab Том 1 Глава 1", "Kebab")] diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index 852eedb9e..7efc34254 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -139,7 +139,6 @@ public class MangaParsingTests [InlineData("Vagabond_v03", "Vagabond")] [InlineData("[AN] Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1", "Mahoutsukai to Deshi no Futekisetsu na Kankei")] [InlineData("Beelzebub_Side_Story_02_RHS.zip", "Beelzebub Side Story")] - [InlineData("[BAA]_Darker_than_Black_Omake-1.zip", "Darker than Black")] [InlineData("Baketeriya ch01-05.zip", "Baketeriya")] [InlineData("[PROzess]Kimi_ha_midara_na_Boku_no_Joou_-_Ch01", "Kimi ha midara na Boku no Joou")] [InlineData("[SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar", "NEEDLESS")] diff --git a/API.Tests/Parsing/ParsingTests.cs b/API.Tests/Parsing/ParsingTests.cs index e30bf6554..4fd2f1350 100644 --- a/API.Tests/Parsing/ParsingTests.cs +++ b/API.Tests/Parsing/ParsingTests.cs @@ -83,7 +83,8 @@ public class ParsingTests [InlineData("-The Title", false, "The Title")] [InlineData("- The Title", false, "The Title")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")] - [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")] + [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", + true, "Batman - Detective Comics - Rebirth Deluxe Edition Book 04")] [InlineData("Something - Full Color Edition", false, "Something - Full Color Edition")] [InlineData("Witchblade 089 (2005) (Bittertek-DCP) (Top Cow (Image Comics))", true, "Witchblade 089")] [InlineData("(C99) Kami-sama Hiroimashita. (SSSS.GRIDMAN)", false, "Kami-sama Hiroimashita.")] diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 7aea0519e..57b2d68f3 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -187,6 +187,36 @@ public class ScannerServiceTests : AbstractDbTest } + /// + /// Special Keywords shouldn't be removed from the series name and thus these 2 should group + /// + [Fact] + public async Task ScanLibrary_ExtraShouldNotAffect() + { + const string testcase = "Series with Extra - Manga.json"; + + // Get the first file and generate a ComicInfo + var infos = new Dictionary(); + infos.Add("Vol.01.cbz", new ComicInfo() + { + Series = "The Novel's Extra", + }); + + var library = await GenerateScannerData(testcase, infos); + + + var scanner = CreateServices(); + await scanner.ScanLibrary(library.Id); + var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + Assert.NotNull(postLib); + Assert.Single(postLib.Series); + var s = postLib.Series.First(); + Assert.Equal("The Novel's Extra", s.Name); + Assert.Equal(2, s.Volumes.Count); + } + + /// /// Files under a folder with a SP marker should group into one issue /// diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/API.Tests/Services/TachiyomiServiceTests.cs index 9c50f572c..1e5127865 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/API.Tests/Services/TachiyomiServiceTests.cs @@ -52,7 +52,7 @@ public class TachiyomiServiceTests Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); - _tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For>(), _readerService); + _tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For>(), _readerService); } diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json new file mode 100644 index 000000000..7ddcaecf4 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json @@ -0,0 +1,4 @@ +[ + "The Novel's Extra (Remake)/Vol.01.cbz", + "The Novel's Extra (Remake)/The Novel's Extra Chapter 100.cbz" +] \ No newline at end of file diff --git a/API/Controllers/CBLController.cs b/API/Controllers/CBLController.cs index b4e674a1d..fe274dacb 100644 --- a/API/Controllers/CBLController.cs +++ b/API/Controllers/CBLController.cs @@ -44,6 +44,7 @@ public class CblController : BaseApiController var cblReadingList = await SaveAndLoadCblFile(cbl); var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching); importSummary.FileName = cbl.FileName; + return Ok(importSummary); } catch (ArgumentNullException) diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index ccae1a835..188bf839c 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -124,7 +124,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// String separated libraryIds or null for all publication status /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] [HttpGet("publication-status")] public ActionResult> GetAllPublicationStatus(string? libraryIds) { @@ -148,7 +148,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc /// String separated libraryIds or null for all ratings /// [HttpGet("languages")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new []{"libraryIds"})] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] public async Task>> GetAllLanguages(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); @@ -171,20 +171,21 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc }).Where(l => !string.IsNullOrEmpty(l.IsoCode)); } - /// - /// Returns summary for the chapter + /// Given a language code returns the display name /// - /// + /// /// - [HttpGet("chapter-summary")] - public async Task> GetChapterSummary(int chapterId) + [HttpGet("language-title")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["code"])] + public ActionResult GetLanguageTitle(string code) { - // TODO: This doesn't seem used anywhere - if (chapterId <= 0) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); - var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - if (chapter == null) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); - return Ok(chapter.Summary); + if (string.IsNullOrEmpty(code)) return BadRequest("Code must be provided"); + + return CultureInfo.GetCultures(CultureTypes.AllCultures) + .Where(l => code.Equals(l.IetfLanguageTag)) + .Select(c => c.DisplayName) + .FirstOrDefault(); } /// diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index fe96e3841..6efeb04de 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -68,11 +68,11 @@ public class ReaderController : BaseApiController /// /// [HttpGet("pdf")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "apiKey"})] - public async Task GetPdf(int chapterId, string apiKey) + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "apiKey"])] + public async Task GetPdf(int chapterId, string apiKey, bool extractPdf = false) { if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); - var chapter = await _cacheService.Ensure(chapterId); + var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); // Validate the user has access to the PDF diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index c5acd4bf1..d7194bf40 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -344,7 +344,7 @@ public class SeriesController : BaseApiController /// /// [HttpPost("all")] - [Obsolete("User all-v2")] + [Obsolete("Use all-v2")] public async Task>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { var userId = User.GetUserId(); diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 4f5dadfe0..241fd68ca 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -171,7 +171,7 @@ public class CacheService : ICacheService var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); var extractPath = GetCachePath(chapterId); - SemaphoreSlim extractLock = ExtractLocks.GetOrAdd(chapterId, id => new SemaphoreSlim(1,1)); + var extractLock = ExtractLocks.GetOrAdd(chapterId, id => new SemaphoreSlim(1,1)); await extractLock.WaitAsync(); try { diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index f56c268dd..ae3c26a89 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -609,10 +609,6 @@ public class ReadingListService : IReadingListService private static List GetUniqueSeries(CblReadingList cblReading, bool useComicLibraryMatching) { - if (useComicLibraryMatching) - { - return cblReading.Books.Book.Select(b => Parser.Normalize(GetSeriesFormatting(b, useComicLibraryMatching))).Distinct().ToList(); - } return cblReading.Books.Book.Select(b => Parser.Normalize(GetSeriesFormatting(b, useComicLibraryMatching))).Distinct().ToList(); } diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index f65bb00c4..7cba28695 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -30,12 +30,12 @@ public class TachiyomiService : ITachiyomiService { private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IReaderService _readerService; private static readonly CultureInfo EnglishCulture = CultureInfo.CreateSpecificCulture("en-US"); - public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger logger, IReaderService readerService) + public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger logger, IReaderService readerService) { _unitOfWork = unitOfWork; _readerService = readerService; diff --git a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs index 4c1e6a9ae..b68596245 100644 --- a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs +++ b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs @@ -64,7 +64,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser // When there was at least one directory and we failed to parse the series, this is the final fallback if (string.IsNullOrEmpty(info.Series)) { - info.Series = Parser.CleanTitle(directories[0], true, true); + info.Series = Parser.CleanTitle(directories[0], true); } } else @@ -85,7 +85,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (string.IsNullOrEmpty(info.Series)) { - info.Series = Parser.CleanTitle(directoryName, true, false); + info.Series = Parser.CleanTitle(directoryName, true); } diff --git a/API/Services/Tasks/Scanner/Parser/ImageParser.cs b/API/Services/Tasks/Scanner/Parser/ImageParser.cs index e61b79aea..415533631 100644 --- a/API/Services/Tasks/Scanner/Parser/ImageParser.cs +++ b/API/Services/Tasks/Scanner/Parser/ImageParser.cs @@ -35,7 +35,7 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir // Override the series name, as fallback folders needs it to try and parse folder name if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName)) { - ret.Series = Parser.CleanTitle(directoryName, replaceSpecials: false); + ret.Series = Parser.CleanTitle(directoryName); } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index ebeaaa6f1..1ce3bf9bf 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -959,25 +959,25 @@ public static class Parser /// /// - public static string CleanTitle(string title, bool isComic = false, bool replaceSpecials = true) + public static string CleanTitle(string title, bool isComic = false) { title = ReplaceUnderscores(title); title = RemoveEditionTagHolders(title); - if (replaceSpecials) - { - if (isComic) - { - title = RemoveComicSpecialTags(title); - title = RemoveEuropeanTags(title); - } - else - { - title = RemoveMangaSpecialTags(title); - } - } + // if (replaceSpecials) + // { + // if (isComic) + // { + // title = RemoveComicSpecialTags(title); + // title = RemoveEuropeanTags(title); + // } + // else + // { + // title = RemoveMangaSpecialTags(title); + // } + // } title = title.Trim(SpacesAndSeparators); diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 1b7efb96e..6ebea3431 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -464,6 +464,7 @@ "version": "18.2.9", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.9.tgz", "integrity": "sha512-4iMoRvyMmq/fdI/4Gob9HKjL/jvTlCjbS4kouAYHuGO9w9dmUhi1pY1z+mALtCEl9/Q8CzU2W8e5cU2xtV4nVg==", + "dev": true, "dependencies": { "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -491,6 +492,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -505,6 +507,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, "engines": { "node": ">= 14.16.0" }, @@ -1912,18 +1915,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -3730,14 +3721,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/cacache": { "version": "18.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", @@ -4018,14 +4001,6 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4035,21 +4010,8 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/copy-anything": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", - "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "is-what": "^3.14.1" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/cosmiconfig": { "version": "8.3.6", @@ -4556,6 +4518,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -4565,6 +4528,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -4612,20 +4576,6 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5491,20 +5441,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/immutable": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", @@ -5700,14 +5636,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-what": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5958,71 +5886,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/less": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", - "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "copy-anything": "^2.0.1", - "parse-node-version": "^1.0.1", - "tslib": "^2.3.0" - }, - "bin": { - "lessc": "bin/lessc" - }, - "engines": { - "node": ">=6" - }, - "optionalDependencies": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^3.1.0", - "source-map": "~0.6.0" - } - }, - "node_modules/less/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/less/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/less/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6479,20 +6342,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6775,38 +6624,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/needle": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", - "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -7378,17 +7195,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -7507,17 +7313,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/piscina": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", @@ -7598,14 +7393,6 @@ "node": ">=10" } }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -7683,7 +7470,8 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true }, "node_modules/replace-in-file": { "version": "7.1.0", @@ -7954,7 +7742,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "dev": true }, "node_modules/sass": { "version": "1.77.6", @@ -7973,14 +7761,6 @@ "node": ">=14.0.0" } }, - "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/screenfull": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz", @@ -7996,6 +7776,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -8165,29 +7946,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -8418,26 +8176,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/terser": { - "version": "5.31.6", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", - "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8593,6 +8331,7 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/UI/Web/src/app/_models/metadata/person.ts b/UI/Web/src/app/_models/metadata/person.ts index 47c7590b3..c8a4c566e 100644 --- a/UI/Web/src/app/_models/metadata/person.ts +++ b/UI/Web/src/app/_models/metadata/person.ts @@ -1,3 +1,5 @@ +import {IHasCover} from "../common/i-has-cover"; + export enum PersonRole { Other = 1, Artist = 2, @@ -16,7 +18,7 @@ export enum PersonRole { Location = 15 } -export interface Person { +export interface Person extends IHasCover { id: number; name: string; description: string; @@ -26,6 +28,6 @@ export interface Person { aniListId?: number; hardcoverId?: string; asin?: string; - primaryColor?: string; - secondaryColor?: string; + primaryColor: string; + secondaryColor: string; } diff --git a/UI/Web/src/app/_pipes/language-name.pipe.ts b/UI/Web/src/app/_pipes/language-name.pipe.ts index be6d954b0..697554bd3 100644 --- a/UI/Web/src/app/_pipes/language-name.pipe.ts +++ b/UI/Web/src/app/_pipes/language-name.pipe.ts @@ -9,16 +9,10 @@ import {shareReplay} from "rxjs/operators"; }) export class LanguageNamePipe implements PipeTransform { - constructor(private metadataService: MetadataService) { - } + constructor(private metadataService: MetadataService) {} transform(isoCode: string): Observable { - // TODO: See if we can speed this up. It rarely changes and is quite heavy to download on each page - return this.metadataService.getAllValidLanguages().pipe(map(lang => { - const l = lang.filter(l => l.isoCode === isoCode); - if (l.length > 0) return l[0].title; - return ''; - }), shareReplay()); + return this.metadataService.getLanguageNameForCode(isoCode).pipe(shareReplay()); } } diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index d6280900d..a87ad6844 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -18,6 +18,7 @@ import {FilterStatement} from "../_models/metadata/v2/filter-statement"; import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus"; import {LibraryType} from "../_models/library/library"; import {IHasCast} from "../_models/common/i-has-cast"; +import {TextResonse} from "../_types/text-response"; @Injectable({ providedIn: 'root' @@ -77,6 +78,10 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + method); } + getLanguageNameForCode(code: string) { + return this.httpClient.get(`${this.baseUrl}metadata/language-title?code=${code}`, TextResonse); + } + /** * All the potential language tags there can be 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 c3e6c2dff..0aa808939 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 @@ -1,5 +1,43 @@
+ + @if (readingTime) { +
+

{{t('read-time-title')}}

+
+ {{readingTime | readTime}} +
+
+ } + + @if (releaseYear) { +
+

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

+
+ {{releaseYear}} +
+
+ } + + @if (language) { +
+

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

+
+ {{language | languageName | async}} +
+
+ } + +
+

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

+
+ {{format | mangaFormat }} +
+
+ + + +

{{t('genres-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 dd9d644d6..48697b89c 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 @@ -3,18 +3,25 @@ import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/ca import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component"; import {TranslocoDirective} from "@jsverse/transloco"; import {IHasCast} from "../../_models/common/i-has-cast"; -import {Person, PersonRole} from "../../_models/metadata/person"; -import {Router} from "@angular/router"; +import {PersonRole} from "../../_models/metadata/person"; import {FilterField} from "../../_models/metadata/v2/filter-field"; import {FilterComparison} from "../../_models/metadata/v2/filter-comparison"; import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service"; import {Genre} from "../../_models/metadata/genre"; import {Tag} from "../../_models/tag"; -import {TagBadgeComponent, TagBadgeCursor} from "../../shared/tag-badge/tag-badge.component"; +import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component"; import {ImageComponent} from "../../shared/image/image.component"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {ImageService} from "../../_services/image.service"; import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component"; +import {IHasReadingTime} from "../../_models/common/i-has-reading-time"; +import {ReadTimePipe} from "../../_pipes/read-time.pipe"; +import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe"; +import {MangaFormat} from "../../_models/manga-format"; +import {SeriesFormatComponent} from "../../shared/series-format/series-format.component"; +import {MangaFormatPipe} from "../../_pipes/manga-format.pipe"; +import {LanguageNamePipe} from "../../_pipes/language-name.pipe"; +import {AsyncPipe} from "@angular/common"; @Component({ selector: 'app-details-tab', @@ -26,7 +33,13 @@ import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander TagBadgeComponent, ImageComponent, SafeHtmlPipe, - BadgeExpanderComponent + BadgeExpanderComponent, + ReadTimePipe, + SentenceCasePipe, + SeriesFormatComponent, + MangaFormatPipe, + LanguageNamePipe, + AsyncPipe ], templateUrl: './details-tab.component.html', styleUrl: './details-tab.component.scss', @@ -41,6 +54,10 @@ export class DetailsTabComponent { protected readonly FilterField = FilterField; @Input({required: true}) metadata!: IHasCast; + @Input() readingTime: IHasReadingTime | undefined; + @Input() language: string | undefined; + @Input() format: MangaFormat = MangaFormat.UNKNOWN; + @Input() releaseYear: number | undefined; @Input() genres: Array = []; @Input() tags: Array = []; @Input() webLinks: Array = []; @@ -50,4 +67,6 @@ export class DetailsTabComponent { if (queryParamName === FilterField.None) return; this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe(); } + + protected readonly MangaFormat = MangaFormat; } diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts index 25980865d..8c27878ce 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts @@ -493,7 +493,7 @@ export class EditChapterModalComponent implements OnInit { }; personSettings.addTransformFn = ((title: string) => { - return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false }; + return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' }; }); return personSettings; diff --git a/UI/Web/src/app/admin/license/license.component.html b/UI/Web/src/app/admin/license/license.component.html index 1a8746635..e028d5b05 100644 --- a/UI/Web/src/app/admin/license/license.component.html +++ b/UI/Web/src/app/admin/license/license.component.html @@ -52,7 +52,7 @@
- Help + {{t('help-label')}} @if (formGroup.dirty || formGroup.touched) {
diff --git a/UI/Web/src/app/all-filters/all-filters.component.html b/UI/Web/src/app/all-filters/all-filters.component.html index 04656f821..2f487574a 100644 --- a/UI/Web/src/app/all-filters/all-filters.component.html +++ b/UI/Web/src/app/all-filters/all-filters.component.html @@ -14,6 +14,6 @@ - +
diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html index 2445e351c..14067952b 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html @@ -198,7 +198,7 @@
diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index 219099f02..9adb23be6 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -515,7 +515,7 @@ export class EditSeriesModalComponent implements OnInit { }; personSettings.addTransformFn = ((title: string) => { - return {id: 0, name: title, description: '', coverImageLocked: false }; + return {id: 0, name: title, description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' }; }); return personSettings; @@ -551,6 +551,7 @@ export class EditSeriesModalComponent implements OnInit { // We only need to call updateSeries if we changed name, sort name, or localized name or reset a cover image const nameFieldsDirty = this.editSeriesForm.get('name')?.dirty || this.editSeriesForm.get('sortName')?.dirty || this.editSeriesForm.get('localizedName')?.dirty; const nameFieldLockChanged = this.series.nameLocked !== this.initSeries.nameLocked || this.series.sortNameLocked !== this.initSeries.sortNameLocked || this.series.localizedNameLocked !== this.initSeries.localizedNameLocked; + if (nameFieldsDirty || nameFieldLockChanged || this.coverImageReset) { model.nameLocked = this.series.nameLocked; model.sortNameLocked = this.series.sortNameLocked; diff --git a/UI/Web/src/app/cards/chapter-card/chapter-card.component.html b/UI/Web/src/app/cards/chapter-card/chapter-card.component.html index 9268309e3..1c2646981 100644 --- a/UI/Web/src/app/cards/chapter-card/chapter-card.component.html +++ b/UI/Web/src/app/cards/chapter-card/chapter-card.component.html @@ -46,12 +46,12 @@
- - -
- -
-
+ + +
+ +
+
@@ -85,10 +85,10 @@ - @if (actions && actions.length > 0) { - - } - + @if (actions && actions.length > 0) { + + } +
diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html index 76e957545..ea55a9af7 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html @@ -1,32 +1,31 @@ -
+
-
-
-
- -
+ @if (mode === 'all') { +
+ - - - + } @else if (mode === 'url') {
@@ -42,9 +41,7 @@ {{t('back')}}
- - - + } @@ -54,28 +51,32 @@
-
- - -
- -
+ @if (showReset) { +
+ + @if (showApplyButton) { +
+ + } +
+ } + + @for (url of imageUrls; track url; let idx = $index) { +
+ + @if (showApplyButton) { +
+ + } +
+ } + -
-
- - -
- -
-
diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts index d0f6806c1..5217bac09 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts @@ -17,7 +17,7 @@ import { ToastrService } from 'ngx-toastr'; import { ImageService } from 'src/app/_services/image.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { UploadService } from 'src/app/_services/upload.service'; -import {CommonModule, DOCUMENT} from '@angular/common'; +import {DOCUMENT, NgClass} from '@angular/common'; import {ImageComponent} from "../../shared/image/image.component"; import {translate, TranslocoModule} from "@jsverse/transloco"; @@ -27,9 +27,9 @@ import {translate, TranslocoModule} from "@jsverse/transloco"; imports: [ ReactiveFormsModule, NgxFileDropModule, - CommonModule, ImageComponent, - TranslocoModule + TranslocoModule, + NgClass ], templateUrl: './cover-image-chooser.component.html', styleUrls: ['./cover-image-chooser.component.scss'], diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.html b/UI/Web/src/app/cards/entity-title/entity-title.component.html index 7817d870c..92b30eaa2 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.html +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.html @@ -1,87 +1,94 @@ - @switch (libraryType) { - @case (LibraryType.Comic) { - @if (titleName !== '' && prioritizeTitleName) { - @if (isChapter && includeChapter) { - {{t('issue-num') + ' ' + number + ' - ' }} - } + {{renderText | defaultValue}} + + + + + + - {{titleName}} - } @else { - @if (includeVolume && volumeTitle !== '') { - {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}} - } - {{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}} - } - } + + + + + + + + - @case (LibraryType.ComicVine) { - @if (titleName !== '' && prioritizeTitleName) { - @if (isChapter && includeChapter) { - {{t('issue-num') + ' ' + number + ' - ' }} - } + + + + + - {{titleName}} - } @else { - @if (includeVolume && volumeTitle !== '') { - {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}} - } - {{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}} - } - } + + + + + + + + + + + + - @case (LibraryType.Manga) { - @if (titleName !== '' && prioritizeTitleName) { - @if (isChapter && includeChapter) { - @if (number === LooseLeafOrSpecial) { - {{t('chapter') + ' - ' }} - } @else { - {{t('chapter') + ' ' + number + ' - ' }} - } + + + + + + + + - } - {{titleName}} - } @else { - @if (includeVolume && volumeTitle !== '') { - @if (number !== LooseLeafOrSpecial && isChapter && includeVolume) { - {{volumeTitle}} - } - } + + + + + + + + - @if (number !== LooseLeafOrSpecial) { - @if (isChapter) { - {{t('chapter') + ' ' + number}} - } @else { - {{volumeTitle}} - } - } @else { - {{t('special')}} - } - } - } + + + + + + + + + + + + + - @case (LibraryType.Book) { - @if (titleName !== '' && prioritizeTitleName) { - {{titleName}} - } @else if (number === LooseLeafOrSpecial) { - {{null | defaultValue}} - } @else { - {{t('book-num', {num: volumeTitle})}} - } - } + + + + + + + + + - @case (LibraryType.LightNovel) { - @if (titleName !== '' && prioritizeTitleName) { - {{titleName}} - } @else if (number === LooseLeafOrSpecial) { - {{null | defaultValue}} - } @else { - {{t('book-num', {num: (isChapter ? number : volumeTitle)})}} - } - } + + + + + + + + + - @case (LibraryType.Images) { - {{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}} - } - } + + + + diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.ts b/UI/Web/src/app/cards/entity-title/entity-title.component.ts index eafdfdef4..8e9ea924c 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.ts +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.ts @@ -1,9 +1,9 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; import { UtilityService } from 'src/app/shared/_services/utility.service'; import { Chapter, LooseLeafOrDefaultNumber } from 'src/app/_models/chapter'; import { LibraryType } from 'src/app/_models/library/library'; import { Volume } from 'src/app/_models/volume'; -import {TranslocoModule} from "@jsverse/transloco"; +import {translate, TranslocoModule} from "@jsverse/transloco"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; /** @@ -22,6 +22,9 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; }) export class EntityTitleComponent implements OnInit { + private readonly utilityService = inject(UtilityService); + private readonly cdRef = inject(ChangeDetectorRef); + protected readonly LooseLeafOrSpecial = LooseLeafOrDefaultNumber + ""; protected readonly LibraryType = LibraryType; @@ -42,16 +45,18 @@ export class EntityTitleComponent implements OnInit { * When a titleName (aka a title) is available on the entity, show it over Volume X Chapter Y */ @Input() prioritizeTitleName: boolean = true; + /** + * When there is no meaningful title to display and the chapter is just a single volume, show the volume number + */ + @Input() fallbackToVolume: boolean = true; isChapter = false; titleName: string = ''; volumeTitle: string = ''; - number: string = ''; + renderText: string = ''; - constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {} - ngOnInit(): void { this.isChapter = this.utilityService.isChapter(this.entity); @@ -60,7 +65,6 @@ export class EntityTitleComponent implements OnInit { this.volumeTitle = c.volumeTitle || ''; this.titleName = c.titleName || ''; this.number = c.range; - } else { const v = this.utilityService.asVolume(this.entity); this.volumeTitle = v.name || ''; @@ -70,6 +74,125 @@ export class EntityTitleComponent implements OnInit { } this.number = v.name; } + + this.calculateRenderText(); + this.cdRef.markForCheck(); } + + private calculateRenderText() { + switch (this.libraryType) { + case LibraryType.Manga: + this.renderText = this.calculateMangaRenderText(); + break; + case LibraryType.Comic: + this.renderText = this.calculateComicRenderText(); + break; + case LibraryType.Book: + this.renderText = this.calculateBookRenderText(); + break; + case LibraryType.Images: + this.renderText = this.calculateImageRenderText(); + break; + case LibraryType.LightNovel: + this.renderText = this.calculateLightNovelRenderText(); + break; + case LibraryType.ComicVine: + this.renderText = this.calculateComicRenderText(); + break; + } + this.cdRef.markForCheck(); + } + + private calculateBookRenderText() { + let renderText = ''; + if (this.titleName !== '' && this.prioritizeTitleName) { + renderText = this.titleName; + } else if (this.number === this.LooseLeafOrSpecial) { + renderText = ''; + } else { + renderText = translate('entity-title.book-num', {num: this.volumeTitle}); + } + return renderText; + } + + private calculateLightNovelRenderText() { + let renderText = ''; + if (this.titleName !== '' && this.prioritizeTitleName) { + renderText = this.titleName; + } else if (this.number === this.LooseLeafOrSpecial) { + renderText = ''; + } else { + const bookNum = this.isChapter ? this.number : this.volumeTitle; + renderText = translate('entity-title.book-num', {num: bookNum}); + } + return renderText; + } + + private calculateMangaRenderText() { + let renderText = ''; + + if (this.titleName !== '' && this.prioritizeTitleName) { + if (this.isChapter && this.includeChapter) { + if (this.number === this.LooseLeafOrSpecial) { + renderText = translate('entity-title.chapter') + ' - '; + } else { + renderText = translate('entity-title.chapter') + ' ' + this.number + ' - '; + } + } + + renderText += this.titleName; + } else { + if (this.includeVolume && this.volumeTitle !== '') { + if (this.number !== this.LooseLeafOrSpecial && this.isChapter && this.includeVolume) { + renderText = this.volumeTitle; + } + } + + if (this.number !== this.LooseLeafOrSpecial) { + if (this.isChapter) { + renderText = translate('entity-title.chapter') + ' ' + this.number; + } else { + renderText = this.volumeTitle; + } + } else if (this.fallbackToVolume && this.isChapter && this.volumeTitle) { + renderText = translate('entity-title.vol-num', {num: this.volumeTitle}); + } else if (this.fallbackToVolume && this.isChapter) { // this.volumeTitle === '' (this is a single volume on volume detail page) + renderText = translate('entity-title.single-volume'); + } else { + renderText = translate('entity-title.special'); + } + } + + + return renderText; + } + + private calculateImageRenderText() { + let renderText = ''; + + if (this.number !== this.LooseLeafOrSpecial) { + if (this.isChapter) { + renderText = translate('entity-title.chapter') + ' ' + this.number; + } else { + renderText = this.volumeTitle; + } + } else { + renderText = translate('entity-title.special'); + } + + return renderText; + } + + + private calculateComicRenderText() { + let renderText = ''; + if (this.titleName !== '' && this.prioritizeTitleName) { + if (this.isChapter && this.includeChapter) { + renderText = translate('entity-title.issue-num') + ' ' + this.number + ' - '; + } + renderText += this.titleName; + } + return renderText; + } } diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.html b/UI/Web/src/app/chapter-detail/chapter-detail.component.html index 1a52fe313..1b5b9d0ef 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.html +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.html @@ -163,7 +163,13 @@ {{t('details-tab')}} @defer (when activeTabId === TabID.Details; prefetch on idle) { - + } diff --git a/UI/Web/src/app/person-detail/person-detail.component.html b/UI/Web/src/app/person-detail/person-detail.component.html index e3677e1c9..219bf32f2 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.html +++ b/UI/Web/src/app/person-detail/person-detail.component.html @@ -59,7 +59,9 @@
- + + +
diff --git a/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html b/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html index a34b63a5b..bc3e9320c 100644 --- a/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html +++ b/UI/Web/src/app/reading-list/_components/import-cbl/import-cbl.component.html @@ -165,7 +165,7 @@
diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index e0037204e..15513790f 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -187,7 +187,7 @@
- @for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) { + @for(item of scroll.viewPortItems; let idx = $index; track item) { @if (item.isChapter) { } @else { @@ -214,7 +214,7 @@ @defer (when activeTabId === TabID.Volumes; prefetch on idle) {
- @for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) { + @for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + + '_volumes') { } @@ -235,7 +235,7 @@ @defer (when activeTabId === TabID.Chapters; prefetch on idle) {
- @for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) { + @for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_chapters') { } @@ -256,7 +256,7 @@ @defer (when activeTabId === TabID.Specials; prefetch on idle) {
- @for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) { + @for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_specials') { }
@@ -338,7 +338,15 @@ {{t(TabID.Details)}} @defer (when activeTabId === TabID.Details; prefetch on idle) { - + + } diff --git a/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html b/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html index 5b02fea2f..136c9d096 100644 --- a/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html +++ b/UI/Web/src/app/sidenav/_components/manage-smart-filters/manage-smart-filters.component.html @@ -19,7 +19,7 @@ {{t('errored')}} } - {{f.name}} + {{f.name}}