Round 3 of Bugfixing (#3318)

This commit is contained in:
Joe Milazzo 2024-10-28 18:13:48 -05:00 committed by GitHub
parent 727fbd353b
commit abdf15b895
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 482 additions and 489 deletions

View File

@ -51,15 +51,15 @@ public class ComicParsingTests
[InlineData("Demon 012 (Sep 1973) c2c", "Demon")] [InlineData("Demon 012 (Sep 1973) c2c", "Demon")]
[InlineData("Dragon Age - Until We Sleep 01 (of 03)", "Dragon Age - Until We Sleep")] [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 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("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 - Rags Morales Sketches")] [InlineData("Identity Crisis Extra - Rags Morales Sketches (2005)", "Identity Crisis Extra - Rags Morales Sketches")]
[InlineData("Daredevil - t6 - 10 - (2019)", "Daredevil")] [InlineData("Daredevil - t6 - 10 - (2019)", "Daredevil")]
[InlineData("Batgirl T2000 #57", "Batgirl")] [InlineData("Batgirl T2000 #57", "Batgirl")]
[InlineData("Teen Titans t1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "Teen Titans")] [InlineData("Teen Titans t1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "Teen Titans")]
[InlineData("Conquistador_-Tome_2", "Conquistador")] [InlineData("Conquistador_-Tome_2", "Conquistador")]
[InlineData("Max_l_explorateur-_Tome_0", "Max l explorateur")] [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("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("Tintin - T22 Vol 714 pour Sydney", "Tintin")]
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")] [InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
[InlineData("Kebab Том 1 Глава 1", "Kebab")] [InlineData("Kebab Том 1 Глава 1", "Kebab")]

View File

@ -139,7 +139,6 @@ public class MangaParsingTests
[InlineData("Vagabond_v03", "Vagabond")] [InlineData("Vagabond_v03", "Vagabond")]
[InlineData("[AN] Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1", "Mahoutsukai to Deshi no Futekisetsu na Kankei")] [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("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("Baketeriya ch01-05.zip", "Baketeriya")]
[InlineData("[PROzess]Kimi_ha_midara_na_Boku_no_Joou_-_Ch01", "Kimi ha midara na Boku no Joou")] [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")] [InlineData("[SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar", "NEEDLESS")]

View File

@ -83,7 +83,8 @@ public class ParsingTests
[InlineData("-The Title", false, "The Title")] [InlineData("-The Title", false, "The Title")]
[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("[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("Something - Full Color Edition", false, "Something - Full Color Edition")]
[InlineData("Witchblade 089 (2005) (Bittertek-DCP) (Top Cow (Image Comics))", true, "Witchblade 089")] [InlineData("Witchblade 089 (2005) (Bittertek-DCP) (Top Cow (Image Comics))", true, "Witchblade 089")]
[InlineData("(C99) Kami-sama Hiroimashita. (SSSS.GRIDMAN)", false, "Kami-sama Hiroimashita.")] [InlineData("(C99) Kami-sama Hiroimashita. (SSSS.GRIDMAN)", false, "Kami-sama Hiroimashita.")]

View File

@ -187,6 +187,36 @@ public class ScannerServiceTests : AbstractDbTest
} }
/// <summary>
/// Special Keywords shouldn't be removed from the series name and thus these 2 should group
/// </summary>
[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<string, ComicInfo>();
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);
}
/// <summary> /// <summary>
/// Files under a folder with a SP marker should group into one issue /// Files under a folder with a SP marker should group into one issue
/// </summary> /// </summary>

View File

@ -52,7 +52,7 @@ public class TachiyomiServiceTests
Substitute.For<IEventHub>(), Substitute.For<IImageService>(), Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()), new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()),
Substitute.For<IScrobblingService>()); Substitute.For<IScrobblingService>());
_tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), _readerService); _tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<TachiyomiService>>(), _readerService);
} }

View File

@ -0,0 +1,4 @@
[
"The Novel's Extra (Remake)/Vol.01.cbz",
"The Novel's Extra (Remake)/The Novel's Extra Chapter 100.cbz"
]

View File

@ -44,6 +44,7 @@ public class CblController : BaseApiController
var cblReadingList = await SaveAndLoadCblFile(cbl); var cblReadingList = await SaveAndLoadCblFile(cbl);
var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching); var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching);
importSummary.FileName = cbl.FileName; importSummary.FileName = cbl.FileName;
return Ok(importSummary); return Ok(importSummary);
} }
catch (ArgumentNullException) catch (ArgumentNullException)

View File

@ -124,7 +124,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param> /// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks> /// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
/// <returns></returns> /// <returns></returns>
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})] [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])]
[HttpGet("publication-status")] [HttpGet("publication-status")]
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds) public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
{ {
@ -148,7 +148,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param> /// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
/// <returns></returns> /// <returns></returns>
[HttpGet("languages")] [HttpGet("languages")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new []{"libraryIds"})] [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])]
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds) public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
{ {
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); 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)); }).Where(l => !string.IsNullOrEmpty(l.IsoCode));
} }
/// <summary> /// <summary>
/// Returns summary for the chapter /// Given a language code returns the display name
/// </summary> /// </summary>
/// <param name="chapterId"></param> /// <param name="code"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("chapter-summary")] [HttpGet("language-title")]
public async Task<ActionResult<string>> GetChapterSummary(int chapterId) [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["code"])]
public ActionResult<string?> GetLanguageTitle(string code)
{ {
// TODO: This doesn't seem used anywhere if (string.IsNullOrEmpty(code)) return BadRequest("Code must be provided");
if (chapterId <= 0) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId); return CultureInfo.GetCultures(CultureTypes.AllCultures)
if (chapter == null) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); .Where(l => code.Equals(l.IetfLanguageTag))
return Ok(chapter.Summary); .Select(c => c.DisplayName)
.FirstOrDefault();
} }
/// <summary> /// <summary>

View File

@ -68,11 +68,11 @@ public class ReaderController : BaseApiController
/// <param name="chapterId"></param> /// <param name="chapterId"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("pdf")] [HttpGet("pdf")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "apiKey"})] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "apiKey"])]
public async Task<ActionResult> GetPdf(int chapterId, string apiKey) public async Task<ActionResult> GetPdf(int chapterId, string apiKey, bool extractPdf = false)
{ {
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest(); 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(); if (chapter == null) return NoContent();
// Validate the user has access to the PDF // Validate the user has access to the PDF

View File

@ -344,7 +344,7 @@ public class SeriesController : BaseApiController
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost("all")] [HttpPost("all")]
[Obsolete("User all-v2")] [Obsolete("Use all-v2")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{ {
var userId = User.GetUserId(); var userId = User.GetUserId();

View File

@ -171,7 +171,7 @@ public class CacheService : ICacheService
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
var extractPath = GetCachePath(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(); await extractLock.WaitAsync();
try { try {

View File

@ -609,10 +609,6 @@ public class ReadingListService : IReadingListService
private static List<string> GetUniqueSeries(CblReadingList cblReading, bool useComicLibraryMatching) private static List<string> 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(); return cblReading.Books.Book.Select(b => Parser.Normalize(GetSeriesFormatting(b, useComicLibraryMatching))).Distinct().ToList();
} }

View File

@ -30,12 +30,12 @@ public class TachiyomiService : ITachiyomiService
{ {
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ILogger<ReaderService> _logger; private readonly ILogger<TachiyomiService> _logger;
private readonly IReaderService _readerService; private readonly IReaderService _readerService;
private static readonly CultureInfo EnglishCulture = CultureInfo.CreateSpecificCulture("en-US"); private static readonly CultureInfo EnglishCulture = CultureInfo.CreateSpecificCulture("en-US");
public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger<ReaderService> logger, IReaderService readerService) public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger<TachiyomiService> logger, IReaderService readerService)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_readerService = readerService; _readerService = readerService;

View File

@ -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 // When there was at least one directory and we failed to parse the series, this is the final fallback
if (string.IsNullOrEmpty(info.Series)) if (string.IsNullOrEmpty(info.Series))
{ {
info.Series = Parser.CleanTitle(directories[0], true, true); info.Series = Parser.CleanTitle(directories[0], true);
} }
} }
else else
@ -85,7 +85,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
if (string.IsNullOrEmpty(info.Series)) if (string.IsNullOrEmpty(info.Series))
{ {
info.Series = Parser.CleanTitle(directoryName, true, false); info.Series = Parser.CleanTitle(directoryName, true);
} }

View File

@ -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 // Override the series name, as fallback folders needs it to try and parse folder name
if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName)) if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName))
{ {
ret.Series = Parser.CleanTitle(directoryName, replaceSpecials: false); ret.Series = Parser.CleanTitle(directoryName);
} }

View File

@ -959,25 +959,25 @@ public static class Parser
/// <param name="isComic"></param> /// <param name="isComic"></param>
/// <returns></returns> /// <returns></returns>
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 = ReplaceUnderscores(title);
title = RemoveEditionTagHolders(title); title = RemoveEditionTagHolders(title);
if (replaceSpecials) // if (replaceSpecials)
{ // {
if (isComic) // if (isComic)
{ // {
title = RemoveComicSpecialTags(title); // title = RemoveComicSpecialTags(title);
title = RemoveEuropeanTags(title); // title = RemoveEuropeanTags(title);
} // }
else // else
{ // {
title = RemoveMangaSpecialTags(title); // title = RemoveMangaSpecialTags(title);
} // }
} // }
title = title.Trim(SpacesAndSeparators); title = title.Trim(SpacesAndSeparators);

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

@ -464,6 +464,7 @@
"version": "18.2.9", "version": "18.2.9",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.9.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.9.tgz",
"integrity": "sha512-4iMoRvyMmq/fdI/4Gob9HKjL/jvTlCjbS4kouAYHuGO9w9dmUhi1pY1z+mALtCEl9/Q8CzU2W8e5cU2xtV4nVg==", "integrity": "sha512-4iMoRvyMmq/fdI/4Gob9HKjL/jvTlCjbS4kouAYHuGO9w9dmUhi1pY1z+mALtCEl9/Q8CzU2W8e5cU2xtV4nVg==",
"dev": true,
"dependencies": { "dependencies": {
"@babel/core": "7.25.2", "@babel/core": "7.25.2",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
@ -491,6 +492,7 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"dependencies": { "dependencies": {
"readdirp": "^4.0.1" "readdirp": "^4.0.1"
}, },
@ -505,6 +507,7 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"engines": { "engines": {
"node": ">= 14.16.0" "node": ">= 14.16.0"
}, },
@ -1912,18 +1915,6 @@
"node": ">=6.0.0" "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": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@ -3730,14 +3721,6 @@
"ieee754": "^1.1.13" "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": { "node_modules/cacache": {
"version": "18.0.4", "version": "18.0.4",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz",
@ -4018,14 +4001,6 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -4035,21 +4010,8 @@
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
}, "dev": true
"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"
}
}, },
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "8.3.6", "version": "8.3.6",
@ -4556,6 +4518,7 @@
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"iconv-lite": "^0.6.2" "iconv-lite": "^0.6.2"
@ -4565,6 +4528,7 @@
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
@ -4612,20 +4576,6 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"dev": true "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": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "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": "^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": { "node_modules/immutable": {
"version": "4.3.5", "version": "4.3.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
@ -5700,14 +5636,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -5958,71 +5886,6 @@
"json-buffer": "3.0.1" "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": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -6479,20 +6342,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/mimic-fn": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@ -6775,38 +6624,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "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": { "node_modules/negotiator": {
"version": "0.6.4", "version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "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", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "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": { "node_modules/parse5": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@ -7507,17 +7313,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/piscina": {
"version": "4.6.1", "version": "4.6.1",
"resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz",
@ -7598,14 +7393,6 @@
"node": ">=10" "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": { "node_modules/psl": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@ -7683,7 +7470,8 @@
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "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": { "node_modules/replace-in-file": {
"version": "7.1.0", "version": "7.1.0",
@ -7954,7 +7742,7 @@
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true "dev": true
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.77.6", "version": "1.77.6",
@ -7973,14 +7761,6 @@
"node": ">=14.0.0" "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": { "node_modules/screenfull": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz", "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz",
@ -7996,6 +7776,7 @@
"version": "7.6.3", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@ -8165,29 +7946,6 @@
"node": ">=0.10.0" "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": { "node_modules/spdx-correct": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
@ -8418,26 +8176,6 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "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": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -8593,6 +8331,7 @@
"version": "5.5.4", "version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@ -1,3 +1,5 @@
import {IHasCover} from "../common/i-has-cover";
export enum PersonRole { export enum PersonRole {
Other = 1, Other = 1,
Artist = 2, Artist = 2,
@ -16,7 +18,7 @@ export enum PersonRole {
Location = 15 Location = 15
} }
export interface Person { export interface Person extends IHasCover {
id: number; id: number;
name: string; name: string;
description: string; description: string;
@ -26,6 +28,6 @@ export interface Person {
aniListId?: number; aniListId?: number;
hardcoverId?: string; hardcoverId?: string;
asin?: string; asin?: string;
primaryColor?: string; primaryColor: string;
secondaryColor?: string; secondaryColor: string;
} }

View File

@ -9,16 +9,10 @@ import {shareReplay} from "rxjs/operators";
}) })
export class LanguageNamePipe implements PipeTransform { export class LanguageNamePipe implements PipeTransform {
constructor(private metadataService: MetadataService) { constructor(private metadataService: MetadataService) {}
}
transform(isoCode: string): Observable<string> { transform(isoCode: string): Observable<string> {
// TODO: See if we can speed this up. It rarely changes and is quite heavy to download on each page return this.metadataService.getLanguageNameForCode(isoCode).pipe(shareReplay());
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());
} }
} }

View File

@ -18,6 +18,7 @@ import {FilterStatement} from "../_models/metadata/v2/filter-statement";
import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus"; import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus";
import {LibraryType} from "../_models/library/library"; import {LibraryType} from "../_models/library/library";
import {IHasCast} from "../_models/common/i-has-cast"; import {IHasCast} from "../_models/common/i-has-cast";
import {TextResonse} from "../_types/text-response";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -77,6 +78,10 @@ export class MetadataService {
return this.httpClient.get<Array<Language>>(this.baseUrl + method); return this.httpClient.get<Array<Language>>(this.baseUrl + method);
} }
getLanguageNameForCode(code: string) {
return this.httpClient.get<string>(`${this.baseUrl}metadata/language-title?code=${code}`, TextResonse);
}
/** /**
* All the potential language tags there can be * All the potential language tags there can be

View File

@ -1,5 +1,43 @@
<ng-container *transloco="let t; read: 'details-tab'"> <ng-container *transloco="let t; read: 'details-tab'">
<div class="details pb-3"> <div class="details pb-3">
@if (readingTime) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('read-time-title')}}</h4>
<div class="ms-3">
{{readingTime | readTime}}
</div>
</div>
}
@if (releaseYear) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('release-title')}}</h4>
<div class="ms-3">
{{releaseYear}}
</div>
</div>
}
@if (language) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('language-title')}}</h4>
<div class="ms-3">
{{language | languageName | async}}
</div>
</div>
}
<div class="mb-3 ms-1">
<h4 class="header">{{t('format-title')}}</h4>
<div class="ms-3">
<app-series-format [format]="format"></app-series-format> {{format | mangaFormat }}
</div>
</div>
<div class="setting-section-break" aria-hidden="true"></div>
<div class="mb-3 ms-1"> <div class="mb-3 ms-1">
<h4 class="header">{{t('genres-title')}}</h4> <h4 class="header">{{t('genres-title')}}</h4>
<div class="ms-3"> <div class="ms-3">

View File

@ -3,18 +3,25 @@ import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/ca
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component"; import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
import {TranslocoDirective} from "@jsverse/transloco"; import {TranslocoDirective} from "@jsverse/transloco";
import {IHasCast} from "../../_models/common/i-has-cast"; import {IHasCast} from "../../_models/common/i-has-cast";
import {Person, PersonRole} from "../../_models/metadata/person"; import {PersonRole} from "../../_models/metadata/person";
import {Router} from "@angular/router";
import {FilterField} from "../../_models/metadata/v2/filter-field"; import {FilterField} from "../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison"; import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service"; import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
import {Genre} from "../../_models/metadata/genre"; import {Genre} from "../../_models/metadata/genre";
import {Tag} from "../../_models/tag"; 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 {ImageComponent} from "../../shared/image/image.component";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {ImageService} from "../../_services/image.service"; import {ImageService} from "../../_services/image.service";
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component"; 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({ @Component({
selector: 'app-details-tab', selector: 'app-details-tab',
@ -26,7 +33,13 @@ import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander
TagBadgeComponent, TagBadgeComponent,
ImageComponent, ImageComponent,
SafeHtmlPipe, SafeHtmlPipe,
BadgeExpanderComponent BadgeExpanderComponent,
ReadTimePipe,
SentenceCasePipe,
SeriesFormatComponent,
MangaFormatPipe,
LanguageNamePipe,
AsyncPipe
], ],
templateUrl: './details-tab.component.html', templateUrl: './details-tab.component.html',
styleUrl: './details-tab.component.scss', styleUrl: './details-tab.component.scss',
@ -41,6 +54,10 @@ export class DetailsTabComponent {
protected readonly FilterField = FilterField; protected readonly FilterField = FilterField;
@Input({required: true}) metadata!: IHasCast; @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<Genre> = []; @Input() genres: Array<Genre> = [];
@Input() tags: Array<Tag> = []; @Input() tags: Array<Tag> = [];
@Input() webLinks: Array<string> = []; @Input() webLinks: Array<string> = [];
@ -50,4 +67,6 @@ export class DetailsTabComponent {
if (queryParamName === FilterField.None) return; if (queryParamName === FilterField.None) return;
this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe(); this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe();
} }
protected readonly MangaFormat = MangaFormat;
} }

View File

@ -493,7 +493,7 @@ export class EditChapterModalComponent implements OnInit {
}; };
personSettings.addTransformFn = ((title: string) => { 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; return personSettings;

View File

@ -52,7 +52,7 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="discordId">{{t('activate-discordId-label')}}</label> <label for="discordId">{{t('activate-discordId-label')}}</label>
<i class="fa fa-circle-info ms-1" aria-hidden="true" [ngbTooltip]="t('activate-discordId-tooltip')"></i> <i class="fa fa-circle-info ms-1" aria-hidden="true" [ngbTooltip]="t('activate-discordId-tooltip')"></i>
<a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">Help</a> <a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
<input id="discordId" type="text" class="form-control" formControlName="discordId" autocomplete="off" [class.is-invalid]="formGroup.get('discordId')?.invalid && formGroup.get('discordId')?.touched"/> <input id="discordId" type="text" class="form-control" formControlName="discordId" autocomplete="off" [class.is-invalid]="formGroup.get('discordId')?.invalid && formGroup.get('discordId')?.touched"/>
@if (formGroup.dirty || formGroup.touched) { @if (formGroup.dirty || formGroup.touched) {
<div id="inviteForm-validations" class="invalid-feedback"> <div id="inviteForm-validations" class="invalid-feedback">

View File

@ -14,6 +14,6 @@
</app-side-nav-companion-bar> </app-side-nav-companion-bar>
<app-manage-smart-filters></app-manage-smart-filters> <app-manage-smart-filters [target]="'_self'"></app-manage-smart-filters>
</div> </div>
</ng-container> </ng-container>

View File

@ -198,7 +198,7 @@
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'publicationStatusLocked' }"></ng-container> <ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'publicationStatusLocked' }"></ng-container>
<select class="form-select" id="publication-status" formControlName="publicationStatus"> <select class="form-select" id="publication-status" formControlName="publicationStatus">
@for (opt of publicationStatuses; track opt.value) { @for (opt of publicationStatuses; track opt.value) {
<option [value]="opt.value">{{opt.title | titlecase}}</option> <option [value]="opt.value">{{opt.value | publicationStatus}}</option>
} }
</select> </select>
</div> </div>

View File

@ -515,7 +515,7 @@ export class EditSeriesModalComponent implements OnInit {
}; };
personSettings.addTransformFn = ((title: string) => { personSettings.addTransformFn = ((title: string) => {
return {id: 0, name: title, description: '', coverImageLocked: false }; return {id: 0, name: title, description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
}); });
return personSettings; 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 // 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 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; 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) { if (nameFieldsDirty || nameFieldLockChanged || this.coverImageReset) {
model.nameLocked = this.series.nameLocked; model.nameLocked = this.series.nameLocked;
model.sortNameLocked = this.series.sortNameLocked; model.sortNameLocked = this.series.sortNameLocked;

View File

@ -1,11 +1,12 @@
<ng-container *transloco="let t; read: 'cover-image-chooser'"> <ng-container *transloco="let t; read: 'cover-image-chooser'">
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px"> <div class="container-fluid" style="padding-left: 0; padding-right: 0">
<form [formGroup]="form"> <form [formGroup]="form">
<ngx-file-drop (onFileDrop)="dropped($event)" <ngx-file-drop (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false" (onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false"
dropZoneClassName="file-upload" contentClassName="file-upload-zone"> dropZoneClassName="file-upload" contentClassName="file-upload-zone">
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector"> <ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'"> @if (mode === 'all') {
<div class="row g-0 mt-3 pb-3">
<div class="mx-auto"> <div class="mx-auto">
<div class="row g-0 mb-3"> <div class="row g-0 mb-3">
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i> <i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
@ -24,9 +25,7 @@
</div> </div>
</div> </div>
</div> </div>
} @else if (mode === 'url') {
<ng-container *ngIf="mode === 'url'">
<div class="row g-0 mt-3 pb-3 ms-md-2 me-md-2"> <div class="row g-0 mt-3 pb-3 ms-md-2 me-md-2">
<div class="input-group col-auto me-md-2" style="width: 83%"> <div class="input-group col-auto me-md-2" style="width: 83%">
<label class="input-group-text" for="load-image">{{t('url-label')}}</label> <label class="input-group-text" for="load-image">{{t('url-label')}}</label>
@ -42,9 +41,7 @@
<span class="phone-hidden">{{t('back')}}</span> <span class="phone-hidden">{{t('back')}}</span>
</button> </button>
</div> </div>
}
</ng-container>
</ng-template> </ng-template>
</ngx-file-drop> </ngx-file-drop>
@ -54,28 +51,32 @@
</form> </form>
<div class="row g-0 chooser" style="padding-top: 10px"> <div class="row g-0 chooser" style="padding-top: 10px">
<div class="clickable col-auto" @if (showReset) {
*ngIf="showReset" tabindex="0" (click)="reset()" <div class="clickable col-auto" tabindex="0" (click)="reset()"
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}"> [ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="232.91px" width="160px" [imageUrl]="imageService.resetCoverImage"></app-image> <app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="232.91px" width="160px" [imageUrl]="imageService.resetCoverImage"></app-image>
<ng-container *ngIf="showApplyButton"> @if (showApplyButton) {
<br> <br>
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button> <button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
</ng-container> }
</div> </div>
<div class="clickable col-auto" }
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
@for (url of imageUrls; track url; let idx = $index) {
<div class="clickable col-auto" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}"> [ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
<app-image class="card-img-top" height="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image> <app-image class="card-img-top" height="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
<ng-container *ngIf="showApplyButton"> @if (showApplyButton) {
<br> <br>
<button class="btn btn-primary" style="width: 100%;" <button class="btn btn-primary" style="width: 100%;"
(click)="applyImage(idx)"> (click)="applyImage(idx)">
{{appliedIndex === idx ? t('applied') : t('apply')}} {{appliedIndex === idx ? t('applied') : t('apply')}}
</button> </button>
</ng-container> }
</div> </div>
}
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@ import { ToastrService } from 'ngx-toastr';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { UploadService } from 'src/app/_services/upload.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 {ImageComponent} from "../../shared/image/image.component";
import {translate, TranslocoModule} from "@jsverse/transloco"; import {translate, TranslocoModule} from "@jsverse/transloco";
@ -27,9 +27,9 @@ import {translate, TranslocoModule} from "@jsverse/transloco";
imports: [ imports: [
ReactiveFormsModule, ReactiveFormsModule,
NgxFileDropModule, NgxFileDropModule,
CommonModule,
ImageComponent, ImageComponent,
TranslocoModule TranslocoModule,
NgClass
], ],
templateUrl: './cover-image-chooser.component.html', templateUrl: './cover-image-chooser.component.html',
styleUrls: ['./cover-image-chooser.component.scss'], styleUrls: ['./cover-image-chooser.component.scss'],

View File

@ -1,87 +1,94 @@
<ng-container *transloco="let t; read: 'entity-title'"> <ng-container *transloco="let t; read: 'entity-title'">
@switch (libraryType) { {{renderText | defaultValue}}
@case (LibraryType.Comic) { <!-- @switch (libraryType) {-->
@if (titleName !== '' && prioritizeTitleName) { <!-- @case (LibraryType.Comic) {-->
@if (isChapter && includeChapter) { <!-- @if (titleName !== '' && prioritizeTitleName) {-->
{{t('issue-num') + ' ' + number + ' - ' }} <!-- @if (isChapter && includeChapter) {-->
} <!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
<!-- }-->
{{titleName}} <!-- {{titleName}}-->
} @else { <!-- } @else {-->
@if (includeVolume && volumeTitle !== '') { <!-- @if (includeVolume && volumeTitle !== '') {-->
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}} <!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
} <!-- }-->
{{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}} <!-- {{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}-->
} <!-- }-->
} <!-- }-->
@case (LibraryType.ComicVine) { <!-- @case (LibraryType.ComicVine) {-->
@if (titleName !== '' && prioritizeTitleName) { <!-- @if (titleName !== '' && prioritizeTitleName) {-->
@if (isChapter && includeChapter) { <!-- @if (isChapter && includeChapter) {-->
{{t('issue-num') + ' ' + number + ' - ' }} <!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
} <!-- }-->
{{titleName}} <!-- {{titleName}}-->
} @else { <!-- } @else {-->
@if (includeVolume && volumeTitle !== '') { <!-- @if (includeVolume && volumeTitle !== '') {-->
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}} <!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
} <!-- }-->
{{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}} <!-- @if (number !== LooseLeafOrSpecial) {-->
} <!-- {{isChapter ? t('issue-num') + number : volumeTitle}}-->
} <!-- } @else {-->
<!-- {{t('special')}}-->
<!-- }-->
<!-- }-->
<!-- }-->
@case (LibraryType.Manga) { <!-- @case (LibraryType.Manga) {-->
@if (titleName !== '' && prioritizeTitleName) { <!-- @if (titleName !== '' && prioritizeTitleName) {-->
@if (isChapter && includeChapter) { <!-- @if (isChapter && includeChapter) {-->
@if (number === LooseLeafOrSpecial) { <!-- @if (number === LooseLeafOrSpecial) {-->
{{t('chapter') + ' - ' }} <!-- {{t('chapter') + ' - ' }}-->
} @else { <!-- } @else {-->
{{t('chapter') + ' ' + number + ' - ' }} <!-- {{t('chapter') + ' ' + number + ' - ' }}-->
} <!-- }-->
} <!-- }-->
{{titleName}} <!-- {{titleName}}-->
} @else { <!-- } @else {-->
@if (includeVolume && volumeTitle !== '') { <!-- @if (includeVolume && volumeTitle !== '') {-->
@if (number !== LooseLeafOrSpecial && isChapter && includeVolume) { <!-- @if (number !== LooseLeafOrSpecial && isChapter && includeVolume) {-->
{{volumeTitle}} <!-- {{volumeTitle}}-->
} <!-- }-->
} <!-- }-->
@if (number !== LooseLeafOrSpecial) { <!-- @if (number !== LooseLeafOrSpecial) {-->
@if (isChapter) { <!-- @if (isChapter) {-->
{{t('chapter') + ' ' + number}} <!-- {{t('chapter') + ' ' + number}}-->
} @else { <!-- } @else {-->
{{volumeTitle}} <!-- {{volumeTitle}}-->
} <!-- }-->
} @else { <!-- } @else if (fallbackToVolume && isChapter && volumeTitle) {-->
{{t('special')}} <!-- {{t('vol-num', {num: volumeTitle})}}-->
} <!-- } @else {-->
} <!-- {{t('special')}}-->
} <!-- }-->
<!-- }-->
<!-- }-->
@case (LibraryType.Book) { <!-- @case (LibraryType.Book) {-->
@if (titleName !== '' && prioritizeTitleName) { <!-- @if (titleName !== '' && prioritizeTitleName) {-->
{{titleName}} <!-- {{titleName}}-->
} @else if (number === LooseLeafOrSpecial) { <!-- } @else if (number === LooseLeafOrSpecial) {-->
{{null | defaultValue}} <!-- {{null | defaultValue}}-->
} @else { <!-- } @else {-->
{{t('book-num', {num: volumeTitle})}} <!-- {{t('book-num', {num: volumeTitle})}}-->
} <!-- }-->
} <!-- }-->
@case (LibraryType.LightNovel) { <!-- @case (LibraryType.LightNovel) {-->
@if (titleName !== '' && prioritizeTitleName) { <!-- @if (titleName !== '' && prioritizeTitleName) {-->
{{titleName}} <!-- {{titleName}}-->
} @else if (number === LooseLeafOrSpecial) { <!-- } @else if (number === LooseLeafOrSpecial) {-->
{{null | defaultValue}} <!-- {{null | defaultValue}}-->
} @else { <!-- } @else {-->
{{t('book-num', {num: (isChapter ? number : volumeTitle)})}} <!-- {{t('book-num', {num: (isChapter ? number : volumeTitle)})}}-->
} <!-- }-->
} <!-- }-->
@case (LibraryType.Images) { <!-- @case (LibraryType.Images) {-->
{{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}} <!-- {{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}-->
} <!-- }-->
} <!-- }-->
</ng-container> </ng-container>

View File

@ -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 { UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter, LooseLeafOrDefaultNumber } from 'src/app/_models/chapter'; import { Chapter, LooseLeafOrDefaultNumber } from 'src/app/_models/chapter';
import { LibraryType } from 'src/app/_models/library/library'; import { LibraryType } from 'src/app/_models/library/library';
import { Volume } from 'src/app/_models/volume'; 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"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
/** /**
@ -22,6 +22,9 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
}) })
export class EntityTitleComponent implements OnInit { export class EntityTitleComponent implements OnInit {
private readonly utilityService = inject(UtilityService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly LooseLeafOrSpecial = LooseLeafOrDefaultNumber + ""; protected readonly LooseLeafOrSpecial = LooseLeafOrDefaultNumber + "";
protected readonly LibraryType = LibraryType; 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 * When a titleName (aka a title) is available on the entity, show it over Volume X Chapter Y
*/ */
@Input() prioritizeTitleName: boolean = true; @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; isChapter = false;
titleName: string = ''; titleName: string = '';
volumeTitle: string = ''; volumeTitle: string = '';
number: string = ''; number: string = '';
renderText: string = '';
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void { ngOnInit(): void {
this.isChapter = this.utilityService.isChapter(this.entity); this.isChapter = this.utilityService.isChapter(this.entity);
@ -60,7 +65,6 @@ export class EntityTitleComponent implements OnInit {
this.volumeTitle = c.volumeTitle || ''; this.volumeTitle = c.volumeTitle || '';
this.titleName = c.titleName || ''; this.titleName = c.titleName || '';
this.number = c.range; this.number = c.range;
} else { } else {
const v = this.utilityService.asVolume(this.entity); const v = this.utilityService.asVolume(this.entity);
this.volumeTitle = v.name || ''; this.volumeTitle = v.name || '';
@ -70,6 +74,125 @@ export class EntityTitleComponent implements OnInit {
} }
this.number = v.name; this.number = v.name;
} }
this.calculateRenderText();
this.cdRef.markForCheck(); 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;
}
} }

View File

@ -163,7 +163,13 @@
<a ngbNavLink>{{t('details-tab')}}</a> <a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) { @defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="chapter" [genres]="chapter.genres" [tags]="chapter.tags" [webLinks]="weblinks"></app-details-tab> <app-details-tab [metadata]="chapter"
[genres]="chapter.genres"
[tags]="chapter.tags"
[webLinks]="weblinks"
[readingTime]="chapter"
[language]="chapter.language"
[format]="series.format"></app-details-tab>
} }
</ng-template> </ng-template>
</li> </li>

View File

@ -59,7 +59,9 @@
<div class="row mt-2"> <div class="row mt-2">
<app-carousel-reel [items]="(chaptersByRole[role] | async)!" [title]="t('individual-role-title', {role: (role | personRole)})" (sectionClick)="loadFilterByRole(role)"> <app-carousel-reel [items]="(chaptersByRole[role] | async)!" [title]="t('individual-role-title', {role: (role | personRole)})" (sectionClick)="loadFilterByRole(role)">
<ng-template #carouselItem let-item> <ng-template #carouselItem let-item>
<app-chapter-card [chapter]="item" [libraryId]="item.libraryId" [libraryType]="item.libraryType" [seriesId]="item.seriesId"></app-chapter-card> <app-chapter-card [chapter]="item" [libraryId]="item.libraryId" [libraryType]="item.libraryType" [seriesId]="item.seriesId">
</app-chapter-card>
</ng-template> </ng-template>
</app-carousel-reel> </app-carousel-reel>
</div> </div>

View File

@ -165,7 +165,7 @@
<!-- Spacer --> <!-- Spacer -->
<div class="col" aria-hidden="true"></div> <div class="col" aria-hidden="true"></div>
<div class="col-auto ms-1"> <div class="col-auto ms-1">
<a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">Help</a> <a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
</div> </div>
<div class="col-auto ms-1"> <div class="col-auto ms-1">
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button> <button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>

View File

@ -187,7 +187,7 @@
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@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) { @if (item.isChapter) {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container> <ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container>
} @else { } @else {
@ -214,7 +214,7 @@
@defer (when activeTabId === TabID.Volumes; prefetch on idle) { @defer (when activeTabId === TabID.Volumes; prefetch on idle) {
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@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') {
<ng-container [ngTemplateOutlet]="nonChapterVolumeCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: volumes.length}"></ng-container> <ng-container [ngTemplateOutlet]="nonChapterVolumeCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: volumes.length}"></ng-container>
} }
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container> <ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container>
@ -235,7 +235,7 @@
@defer (when activeTabId === TabID.Chapters; prefetch on idle) { @defer (when activeTabId === TabID.Chapters; prefetch on idle) {
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@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') {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: chapters.length}"></ng-container> <ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: chapters.length}"></ng-container>
} }
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container> <ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container>
@ -256,7 +256,7 @@
@defer (when activeTabId === TabID.Specials; prefetch on idle) { @defer (when activeTabId === TabID.Specials; prefetch on idle) {
<virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@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') {
<ng-container [ngTemplateOutlet]="specialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, chaptersLength: chapters.length}"></ng-container> <ng-container [ngTemplateOutlet]="specialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, chaptersLength: chapters.length}"></ng-container>
} }
</div> </div>
@ -338,7 +338,15 @@
<a ngbNavLink>{{t(TabID.Details)}}</a> <a ngbNavLink>{{t(TabID.Details)}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) { @defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="seriesMetadata" [genres]="seriesMetadata.genres" [tags]="seriesMetadata.tags" [webLinks]="WebLinks"></app-details-tab> <app-details-tab [metadata]="seriesMetadata"
[genres]="seriesMetadata.genres"
[tags]="seriesMetadata.tags"
[webLinks]="WebLinks"
[readingTime]="series"
[releaseYear]="seriesMetadata.releaseYear"
[language]="seriesMetadata.language"
[format]="series.format">
</app-details-tab>
} }
</ng-template> </ng-template>
</li> </li>

View File

@ -19,7 +19,7 @@
<i class="fa-solid fa-triangle-exclamation red me-2" [ngbTooltip]="t('errored')"></i> <i class="fa-solid fa-triangle-exclamation red me-2" [ngbTooltip]="t('errored')"></i>
<span class="visually-hidden">{{t('errored')}}</span> <span class="visually-hidden">{{t('errored')}}</span>
} }
<a [href]="baseUrl + 'all-series?' + f.filter" target="_blank">{{f.name}}</a> <a [href]="baseUrl + 'all-series?' + f.filter" [target]="target">{{f.name}}</a>
</span> </span>
<button class="btn btn-danger float-end" (click)="deleteFilter(f)"> <button class="btn btn-danger float-end" (click)="deleteFilter(f)">
<i class="fa-solid fa-trash" aria-hidden="true"></i> <i class="fa-solid fa-trash" aria-hidden="true"></i>

View File

@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input} from '@angular/core';
import {FilterService} from "../../../_services/filter.service"; import {FilterService} from "../../../_services/filter.service";
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter"; import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
import {TranslocoDirective} from "@jsverse/transloco"; import {TranslocoDirective} from "@jsverse/transloco";
@ -24,6 +24,8 @@ export class ManageSmartFiltersComponent {
private readonly actionService = inject(ActionService); private readonly actionService = inject(ActionService);
protected readonly baseUrl = inject(APP_BASE_HREF); protected readonly baseUrl = inject(APP_BASE_HREF);
@Input() target: '_self' | '_blank' = '_blank';
filters: Array<SmartFilter> = []; filters: Array<SmartFilter> = [];
listForm: FormGroup = new FormGroup({ listForm: FormGroup = new FormGroup({
'filterQuery': new FormControl('', []) 'filterQuery': new FormControl('', [])

View File

@ -196,7 +196,12 @@
<a ngbNavLink>{{t('details-tab')}}</a> <a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) { @defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="volumeCast" [genres]="genres" [tags]="tags"></app-details-tab> <app-details-tab [metadata]="volumeCast"
[genres]="genres"
[tags]="tags"
[readingTime]="volume"
[language]="volume.chapters[0].language"
[format]="series.format"></app-details-tab>
} }
</ng-template> </ng-template>
</li> </li>

View File

@ -668,6 +668,7 @@
"license-valid": "License is Valid", "license-valid": "License is Valid",
"license-not-valid": "License Not Valid", "license-not-valid": "License Not Valid",
"loading": "{{common.loading}}", "loading": "{{common.loading}}",
"help-label": "{{common.help}}",
"activate-description": "Enter the License Key and Email used to register with Stripe", "activate-description": "Enter the License Key and Email used to register with Stripe",
"activate-license-label": "License Key", "activate-license-label": "License Key",
@ -1071,7 +1072,12 @@
"imprints-title": "{{metadata-fields.imprints-title}}", "imprints-title": "{{metadata-fields.imprints-title}}",
"genres-title": "{{metadata-fields.genres-title}}", "genres-title": "{{metadata-fields.genres-title}}",
"tags-title": "{{metadata-fields.tags-title}}", "tags-title": "{{metadata-fields.tags-title}}",
"weblinks-title": "{{tabs.weblink-tab}}" "weblinks-title": "{{tabs.weblink-tab}}",
"read-time-title": "{{edit-chapter-modal.reading-time-label}}",
"language-title": "{{edit-chapter-modal.language-label}}",
"release-title": "{{sort-field-pipe.release-year}}",
"format-title": "{{metadata-filter.format-label}}",
"length-title": "{{edit-chapter-modal.words-label}}"
}, },
"related-tab": { "related-tab": {
@ -1126,7 +1132,9 @@
"special": "Special", "special": "Special",
"issue-num": "{{common.issue-hash-num}}", "issue-num": "{{common.issue-hash-num}}",
"chapter": "{{common.chapter-num}}", "chapter": "{{common.chapter-num}}",
"book-num": "{{common.book-num-shorthand}}" "book-num": "{{common.book-num-shorthand}}",
"vol-num": "{{user-scrobble-history.volume-num}}",
"single-volume": "Single Volume"
}, },
"external-series-card": { "external-series-card": {
@ -1691,6 +1699,7 @@
"import-cbl-modal": { "import-cbl-modal": {
"close": "{{common.close}}", "close": "{{common.close}}",
"title": "CBL Import", "title": "CBL Import",
"help-label": "{{common.help}}",
"import-description": "To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.", "import-description": "To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.",
"validate-description": "All files have been validated to see if there are any operations to do on the list. Any lists that have failed will not move to the next step. Fix the CBL files and retry.", "validate-description": "All files have been validated to see if there are any operations to do on the list. Any lists that have failed will not move to the next step. Fix the CBL files and retry.",
"validate-warning": "There are issues with the CBL that will prevent an import. Correct these issues then try again.", "validate-warning": "There are issues with the CBL that will prevent an import. Correct these issues then try again.",