mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Round 3 of Bugfixing (#3318)
This commit is contained in:
parent
727fbd353b
commit
abdf15b895
@ -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")]
|
||||
|
@ -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")]
|
||||
|
@ -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.")]
|
||||
|
@ -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>
|
||||
/// Files under a folder with a SP marker should group into one issue
|
||||
/// </summary>
|
||||
|
@ -52,7 +52,7 @@ public class TachiyomiServiceTests
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()),
|
||||
Substitute.For<IScrobblingService>());
|
||||
_tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), _readerService);
|
||||
_tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<TachiyomiService>>(), _readerService);
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
[
|
||||
"The Novel's Extra (Remake)/Vol.01.cbz",
|
||||
"The Novel's Extra (Remake)/The Novel's Extra Chapter 100.cbz"
|
||||
]
|
@ -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)
|
||||
|
@ -124,7 +124,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
||||
/// <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>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])]
|
||||
[HttpGet("publication-status")]
|
||||
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>
|
||||
/// <returns></returns>
|
||||
[HttpGet("languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])]
|
||||
public async Task<ActionResult<IList<LanguageDto>>> 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));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns summary for the chapter
|
||||
/// Given a language code returns the display name
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-summary")]
|
||||
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
|
||||
[HttpGet("language-title")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["code"])]
|
||||
public ActionResult<string?> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -68,11 +68,11 @@ public class ReaderController : BaseApiController
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("pdf")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
|
||||
public async Task<ActionResult> GetPdf(int chapterId, string apiKey)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "apiKey"])]
|
||||
public async Task<ActionResult> 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
|
||||
|
@ -344,7 +344,7 @@ public class SeriesController : BaseApiController
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[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)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
@ -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 {
|
||||
|
@ -609,10 +609,6 @@ public class ReadingListService : IReadingListService
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -30,12 +30,12 @@ public class TachiyomiService : ITachiyomiService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<ReaderService> _logger;
|
||||
private readonly ILogger<TachiyomiService> _logger;
|
||||
private readonly IReaderService _readerService;
|
||||
|
||||
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;
|
||||
_readerService = readerService;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -959,25 +959,25 @@ public static class Parser
|
||||
/// <param name="isComic"></param>
|
||||
/// <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 = 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);
|
||||
|
285
UI/Web/package-lock.json
generated
285
UI/Web/package-lock.json
generated
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<string> {
|
||||
// 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<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
|
||||
|
@ -1,5 +1,43 @@
|
||||
<ng-container *transloco="let t; read: 'details-tab'">
|
||||
<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">
|
||||
<h4 class="header">{{t('genres-title')}}</h4>
|
||||
<div class="ms-3">
|
||||
|
@ -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<Genre> = [];
|
||||
@Input() tags: Array<Tag> = [];
|
||||
@Input() webLinks: Array<string> = [];
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -52,7 +52,7 @@
|
||||
<div class="form-group mb-3">
|
||||
<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>
|
||||
<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"/>
|
||||
@if (formGroup.dirty || formGroup.touched) {
|
||||
<div id="inviteForm-validations" class="invalid-feedback">
|
||||
|
@ -14,6 +14,6 @@
|
||||
|
||||
</app-side-nav-companion-bar>
|
||||
|
||||
<app-manage-smart-filters></app-manage-smart-filters>
|
||||
<app-manage-smart-filters [target]="'_self'"></app-manage-smart-filters>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -198,7 +198,7 @@
|
||||
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'publicationStatusLocked' }"></ng-container>
|
||||
<select class="form-select" id="publication-status" formControlName="publicationStatus">
|
||||
@for (opt of publicationStatuses; track opt.value) {
|
||||
<option [value]="opt.value">{{opt.title | titlecase}}</option>
|
||||
<option [value]="opt.value">{{opt.value | publicationStatus}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -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;
|
||||
|
@ -46,12 +46,12 @@
|
||||
<div class="card-overlay"></div>
|
||||
<div class="chapter overlay-information">
|
||||
<div class="overlay-information--centered">
|
||||
<span class="card-title library mx-auto" style="width: auto;" (click)="read($event)">
|
||||
<!-- Card Image -->
|
||||
<div>
|
||||
<i class="fa-solid fa-book" aria-hidden="true"></i>
|
||||
</div>
|
||||
</span>
|
||||
<span class="card-title library mx-auto" style="width: auto;" (click)="read($event)">
|
||||
<!-- Card Image -->
|
||||
<div>
|
||||
<i class="fa-solid fa-book" aria-hidden="true"></i>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,10 +85,10 @@
|
||||
</a>
|
||||
</span>
|
||||
<span class="card-actions">
|
||||
@if (actions && actions.length > 0) {
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
|
||||
}
|
||||
</span>
|
||||
@if (actions && actions.length > 0) {
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -1,32 +1,31 @@
|
||||
<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">
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false"
|
||||
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
|
||||
<div class="mx-auto">
|
||||
<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>
|
||||
</div>
|
||||
@if (mode === 'all') {
|
||||
<div class="row g-0 mt-3 pb-3">
|
||||
<div class="mx-auto">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<a class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
|
||||
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
|
||||
</a>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<a class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
|
||||
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
|
||||
</a>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container *ngIf="mode === 'url'">
|
||||
} @else if (mode === 'url') {
|
||||
<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%">
|
||||
<label class="input-group-text" for="load-image">{{t('url-label')}}</label>
|
||||
@ -42,9 +41,7 @@
|
||||
<span class="phone-hidden">{{t('back')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
}
|
||||
</ng-template>
|
||||
</ngx-file-drop>
|
||||
|
||||
@ -54,28 +51,32 @@
|
||||
</form>
|
||||
|
||||
<div class="row g-0 chooser" style="padding-top: 10px">
|
||||
<div class="clickable col-auto"
|
||||
*ngIf="showReset" tabindex="0" (click)="reset()"
|
||||
[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>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
|
||||
</ng-container>
|
||||
@if (showReset) {
|
||||
<div class="clickable col-auto" tabindex="0" (click)="reset()"
|
||||
[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>
|
||||
@if (showApplyButton) {
|
||||
<br>
|
||||
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@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}">
|
||||
<app-image class="card-img-top" height="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
|
||||
@if (showApplyButton) {
|
||||
<br>
|
||||
<button class="btn btn-primary" style="width: 100%;"
|
||||
(click)="applyImage(idx)">
|
||||
{{appliedIndex === idx ? t('applied') : t('apply')}}
|
||||
</button>
|
||||
}
|
||||
</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)"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
||||
<app-image class="card-img-top" height="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button class="btn btn-primary" style="width: 100%;"
|
||||
(click)="applyImage(idx)">
|
||||
{{appliedIndex === idx ? t('applied') : t('apply')}}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -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'],
|
||||
|
@ -1,87 +1,94 @@
|
||||
<ng-container *transloco="let t; read: 'entity-title'">
|
||||
@switch (libraryType) {
|
||||
@case (LibraryType.Comic) {
|
||||
@if (titleName !== '' && prioritizeTitleName) {
|
||||
@if (isChapter && includeChapter) {
|
||||
{{t('issue-num') + ' ' + number + ' - ' }}
|
||||
}
|
||||
{{renderText | defaultValue}}
|
||||
<!-- @switch (libraryType) {-->
|
||||
<!-- @case (LibraryType.Comic) {-->
|
||||
<!-- @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')}}
|
||||
}
|
||||
}
|
||||
<!-- {{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 + ' - ' }}
|
||||
}
|
||||
<!-- @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')}}
|
||||
}
|
||||
}
|
||||
<!-- {{titleName}}-->
|
||||
<!-- } @else {-->
|
||||
<!-- @if (includeVolume && volumeTitle !== '') {-->
|
||||
<!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
|
||||
<!-- }-->
|
||||
<!-- @if (number !== LooseLeafOrSpecial) {-->
|
||||
<!-- {{isChapter ? t('issue-num') + number : volumeTitle}}-->
|
||||
<!-- } @else {-->
|
||||
<!-- {{t('special')}}-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
|
||||
@case (LibraryType.Manga) {
|
||||
@if (titleName !== '' && prioritizeTitleName) {
|
||||
@if (isChapter && includeChapter) {
|
||||
@if (number === LooseLeafOrSpecial) {
|
||||
{{t('chapter') + ' - ' }}
|
||||
} @else {
|
||||
{{t('chapter') + ' ' + number + ' - ' }}
|
||||
}
|
||||
<!-- @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}}
|
||||
}
|
||||
}
|
||||
<!-- }-->
|
||||
<!-- {{titleName}}-->
|
||||
<!-- } @else {-->
|
||||
<!-- @if (includeVolume && volumeTitle !== '') {-->
|
||||
<!-- @if (number !== LooseLeafOrSpecial && isChapter && includeVolume) {-->
|
||||
<!-- {{volumeTitle}}-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
|
||||
@if (number !== LooseLeafOrSpecial) {
|
||||
@if (isChapter) {
|
||||
{{t('chapter') + ' ' + number}}
|
||||
} @else {
|
||||
{{volumeTitle}}
|
||||
}
|
||||
} @else {
|
||||
{{t('special')}}
|
||||
}
|
||||
}
|
||||
}
|
||||
<!-- @if (number !== LooseLeafOrSpecial) {-->
|
||||
<!-- @if (isChapter) {-->
|
||||
<!-- {{t('chapter') + ' ' + number}}-->
|
||||
<!-- } @else {-->
|
||||
<!-- {{volumeTitle}}-->
|
||||
<!-- }-->
|
||||
<!-- } @else if (fallbackToVolume && isChapter && volumeTitle) {-->
|
||||
<!-- {{t('vol-num', {num: 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.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.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')}}
|
||||
}
|
||||
}
|
||||
<!-- @case (LibraryType.Images) {-->
|
||||
<!-- {{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
</ng-container>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +163,13 @@
|
||||
<a ngbNavLink>{{t('details-tab')}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@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>
|
||||
</li>
|
||||
|
@ -59,7 +59,9 @@
|
||||
<div class="row mt-2">
|
||||
<app-carousel-reel [items]="(chaptersByRole[role] | async)!" [title]="t('individual-role-title', {role: (role | personRole)})" (sectionClick)="loadFilterByRole(role)">
|
||||
<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>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -165,7 +165,7 @@
|
||||
<!-- Spacer -->
|
||||
<div class="col" aria-hidden="true"></div>
|
||||
<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 class="col-auto ms-1">
|
||||
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>
|
||||
|
@ -187,7 +187,7 @@
|
||||
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
|
||||
<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) {
|
||||
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container>
|
||||
} @else {
|
||||
@ -214,7 +214,7 @@
|
||||
@defer (when activeTabId === TabID.Volumes; prefetch on idle) {
|
||||
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
<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]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container>
|
||||
@ -235,7 +235,7 @@
|
||||
@defer (when activeTabId === TabID.Chapters; prefetch on idle) {
|
||||
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
<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]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container>
|
||||
@ -256,7 +256,7 @@
|
||||
@defer (when activeTabId === TabID.Specials; prefetch on idle) {
|
||||
<virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock" [childHeight]="1">
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
@ -338,7 +338,15 @@
|
||||
<a ngbNavLink>{{t(TabID.Details)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@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>
|
||||
</li>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<i class="fa-solid fa-triangle-exclamation red me-2" [ngbTooltip]="t('errored')"></i>
|
||||
<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>
|
||||
<button class="btn btn-danger float-end" (click)="deleteFilter(f)">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
|
@ -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 {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
@ -24,6 +24,8 @@ export class ManageSmartFiltersComponent {
|
||||
private readonly actionService = inject(ActionService);
|
||||
protected readonly baseUrl = inject(APP_BASE_HREF);
|
||||
|
||||
@Input() target: '_self' | '_blank' = '_blank';
|
||||
|
||||
filters: Array<SmartFilter> = [];
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterQuery': new FormControl('', [])
|
||||
|
@ -196,7 +196,12 @@
|
||||
<a ngbNavLink>{{t('details-tab')}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@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>
|
||||
</li>
|
||||
|
@ -668,6 +668,7 @@
|
||||
"license-valid": "License is Valid",
|
||||
"license-not-valid": "License Not Valid",
|
||||
"loading": "{{common.loading}}",
|
||||
"help-label": "{{common.help}}",
|
||||
|
||||
"activate-description": "Enter the License Key and Email used to register with Stripe",
|
||||
"activate-license-label": "License Key",
|
||||
@ -1071,7 +1072,12 @@
|
||||
"imprints-title": "{{metadata-fields.imprints-title}}",
|
||||
"genres-title": "{{metadata-fields.genres-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": {
|
||||
@ -1126,7 +1132,9 @@
|
||||
"special": "Special",
|
||||
"issue-num": "{{common.issue-hash-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": {
|
||||
@ -1691,6 +1699,7 @@
|
||||
"import-cbl-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"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.",
|
||||
"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.",
|
||||
|
Loading…
x
Reference in New Issue
Block a user