Lots of Bugfixes (#3308)

This commit is contained in:
Joe Milazzo 2024-10-25 09:22:12 -07:00 committed by GitHub
parent ed7e9d4a6e
commit fc269d3dd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 331 additions and 588 deletions

View File

@ -7,7 +7,10 @@ using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using API.Data;
using API.Data.Metadata;
using API.Data.Repositories;
using API.Entities;
using API.Entities.Enums;
@ -35,6 +38,7 @@ public class ScannerServiceTests : AbstractDbTest
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests");
private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases");
private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png");
private static readonly string[] ComicInfoExtensions = new[] { ".cbz", ".cbr", ".zip", ".rar" };
public ScannerServiceTests(ITestOutputHelper testOutputHelper)
{
@ -125,9 +129,61 @@ public class ScannerServiceTests : AbstractDbTest
Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null));
}
private async Task<Library> GenerateScannerData(string testcase)
/// <summary>
/// This is testing that if the first file is named A and has a localized name of B if all other files are named B, it should still group and name the series A
/// </summary>
[Fact]
public async Task ScanLibrary_LocalizedSeries()
{
var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase));
const string testcase = "Series with Localized - Manga.json";
// Get the first file and generate a ComicInfo
var infos = new Dictionary<string, ComicInfo>();
infos.Add("My Dress-Up Darling v01.cbz", new ComicInfo()
{
Series = "My Dress-Up Darling",
LocalizedSeries = "Sono Bisque Doll wa Koi wo Suru"
});
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);
Assert.Equal(3, postLib.Series.First().Volumes.Count);
}
/// <summary>
/// Files under a folder with a SP marker should group into one issue
/// </summary>
/// <remarks>https://github.com/Kareadita/Kavita/issues/3299</remarks>
[Fact]
public async Task ScanLibrary_ImageSeries_SpecialGrouping()
{
const string testcase = "Image Series with SP Folder - Manga.json";
var library = await GenerateScannerData(testcase);
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);
Assert.Equal(3, postLib.Series.First().Volumes.Count);
}
#region Setup
private async Task<Library> GenerateScannerData(string testcase, Dictionary<string, ComicInfo> comicInfos = null)
{
var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase), comicInfos);
var (publisher, type) = SplitPublisherAndLibraryType(Path.GetFileNameWithoutExtension(testcase));
@ -148,11 +204,17 @@ public class ScannerServiceTests : AbstractDbTest
private ScannerService CreateServices()
{
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem());
var mockReadingService = new MockReadingItemService(ds, Substitute.For<IBookService>());
var fs = new FileSystem();
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var archiveService = new ArchiveService(Substitute.For<ILogger<ArchiveService>>(), ds,
Substitute.For<IImageService>(), Substitute.For<IMediaErrorService>());
var readingItemService = new ReadingItemService(archiveService, Substitute.For<IBookService>(),
Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>());
var processSeries = new ProcessSeries(_unitOfWork, Substitute.For<ILogger<ProcessSeries>>(),
Substitute.For<IEventHub>(),
ds, Substitute.For<ICacheHelper>(), mockReadingService, Substitute.For<IFileService>(),
ds, Substitute.For<ICacheHelper>(), readingItemService, new FileService(fs),
Substitute.For<IMetadataService>(),
Substitute.For<IWordCountAnalyzerService>(),
Substitute.For<IReadingListService>(),
@ -161,7 +223,7 @@ public class ScannerServiceTests : AbstractDbTest
var scanner = new ScannerService(_unitOfWork, Substitute.For<ILogger<ScannerService>>(),
Substitute.For<IMetadataService>(),
Substitute.For<ICacheService>(), Substitute.For<IEventHub>(), ds,
mockReadingService, processSeries, Substitute.For<IWordCountAnalyzerService>());
readingItemService, processSeries, Substitute.For<IWordCountAnalyzerService>());
return scanner;
}
@ -189,7 +251,7 @@ public class ScannerServiceTests : AbstractDbTest
private async Task<string> GenerateTestDirectory(string mapPath)
private async Task<string> GenerateTestDirectory(string mapPath, Dictionary<string, ComicInfo> comicInfos = null)
{
// Read the map file
var mapContent = await File.ReadAllTextAsync(mapPath);
@ -206,7 +268,7 @@ public class ScannerServiceTests : AbstractDbTest
Directory.CreateDirectory(testDirectory);
// Generate the files and folders
await Scaffold(testDirectory, filePaths);
await Scaffold(testDirectory, filePaths, comicInfos);
_testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}");
@ -214,7 +276,7 @@ public class ScannerServiceTests : AbstractDbTest
}
private async Task Scaffold(string testDirectory, List<string> filePaths)
private async Task Scaffold(string testDirectory, List<string> filePaths, Dictionary<string, ComicInfo> comicInfos = null)
{
foreach (var relativePath in filePaths)
{
@ -229,9 +291,9 @@ public class ScannerServiceTests : AbstractDbTest
}
var ext = Path.GetExtension(fullPath).ToLower();
if (new[] { ".cbz", ".cbr", ".zip", ".rar" }.Contains(ext))
if (ComicInfoExtensions.Contains(ext) && comicInfos != null && comicInfos.TryGetValue(Path.GetFileName(relativePath), out var info))
{
CreateMinimalCbz(fullPath, includeMetadata: true);
CreateMinimalCbz(fullPath, info);
}
else
{
@ -242,54 +304,44 @@ public class ScannerServiceTests : AbstractDbTest
}
}
private void CreateMinimalCbz(string filePath, bool includeMetadata)
private void CreateMinimalCbz(string filePath, ComicInfo? comicInfo = null)
{
var tempImagePath = _imagePath; // Assuming _imagePath is a valid path to the 1x1 image
using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Create))
{
// Add the 1x1 image to the archive
archive.CreateEntryFromFile(tempImagePath, "1x1.png");
archive.CreateEntryFromFile(_imagePath, "1x1.png");
if (includeMetadata)
if (comicInfo != null)
{
var comicInfo = GenerateComicInfo();
// Serialize ComicInfo object to XML
var comicInfoXml = SerializeComicInfoToXml(comicInfo);
// Create an entry for ComicInfo.xml in the archive
var entry = archive.CreateEntry("ComicInfo.xml");
using var entryStream = entry.Open();
using var writer = new StreamWriter(entryStream, Encoding.UTF8);
writer.Write(comicInfo);
// Write the XML to the archive
writer.Write(comicInfoXml);
}
}
Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(includeMetadata ? "" : "out")} metadata.");
Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(comicInfo != null ? "" : "out")} metadata.");
}
private string GenerateComicInfo()
private static string SerializeComicInfoToXml(ComicInfo comicInfo)
{
var comicInfo = new StringBuilder();
comicInfo.AppendLine("<?xml version='1.0' encoding='utf-8'?>");
comicInfo.AppendLine("<ComicInfo>");
// People Tags
string[] people = { "Joe Shmo", "Tommy Two Hands"};
string[] genres = { /* Your list of genres here */ };
void AddRandomTag(string tagName, string[] choices)
var xmlSerializer = new XmlSerializer(typeof(ComicInfo));
using var stringWriter = new StringWriter();
using (var xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false), OmitXmlDeclaration = false}))
{
if (new Random().Next(0, 2) == 1) // 50% chance to include the tag
{
var selected = choices.OrderBy(x => Guid.NewGuid()).Take(new Random().Next(1, 5)).ToArray();
comicInfo.AppendLine($" <{tagName}>{string.Join(", ", selected)}</{tagName}>");
}
xmlSerializer.Serialize(xmlWriter, comicInfo);
}
foreach (var tag in new[] { "Writer", "Penciller", "Inker", "CoverArtist", "Publisher", "Character", "Imprint", "Colorist", "Letterer", "Editor", "Translator", "Team", "Location" })
{
AddRandomTag(tag, people);
}
AddRandomTag("Genre", genres);
comicInfo.AppendLine("</ComicInfo>");
return comicInfo.ToString();
// For the love of god, I spent 2 hours trying to get utf-8 with no BOM
return stringWriter.ToString().Replace("""<?xml version="1.0" encoding="utf-16"?>""",
@"<?xml version='1.0' encoding='utf-8'?>");
}
#endregion
}

View File

@ -0,0 +1,6 @@
[
"My Dress-Up Darling/My Dress-Up Darling vol 1/0001.png",
"My Dress-Up Darling/My Dress-Up Darling vol 1/0002.png",
"My Dress-Up Darling/My Dress-Up Darling vol 2/0001.png",
"My Dress-Up Darling/Specials/My Dress-Up Darling SP01/0001.png"
]

View File

@ -0,0 +1,5 @@
[
"My Dress-Up Darling/My Dress-Up Darling v01.cbz",
"My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru v02.cbz",
"My Dress-Up Darling/Sono Bisque Doll wa Koi wo Suru ch 10.cbz"
]

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Nager.ArticleNumber;
namespace API.Controllers;
#nullable enable
public class PersonController : BaseApiController
{
@ -39,11 +40,11 @@ public class PersonController : BaseApiController
}
/// <summary>
/// Returns a list of authors for browsing
/// Returns a list of authors & artists for browsing
/// </summary>
/// <param name="userParams"></param>
/// <returns></returns>
[HttpPost("authors")]
[HttpPost("all")]
public async Task<ActionResult<PagedList<BrowsePersonDto>>> GetAuthorsForBrowse([FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;

View File

@ -171,6 +171,7 @@ public class PersonRepository : IPersonRepository
Id = p.Id,
Name = p.Name,
Description = p.Description,
CoverImage = p.CoverImage,
SeriesCount = p.SeriesMetadataPeople
.Where(smp => roles.Contains(smp.Role))
.Select(smp => smp.SeriesMetadata.SeriesId)

View File

@ -424,7 +424,7 @@ public static class SeriesFilter
public static IQueryable<Series> HasTags(this IQueryable<Series> queryable, bool condition,
FilterComparison comparison, IList<int> tags)
{
if (!condition || tags.Count == 0) return queryable;
if (!condition || (comparison != FilterComparison.IsEmpty && tags.Count == 0)) return queryable;
switch (comparison)
{
@ -547,7 +547,7 @@ public static class SeriesFilter
public static IQueryable<Series> HasGenre(this IQueryable<Series> queryable, bool condition,
FilterComparison comparison, IList<int> genres)
{
if (!condition || genres.Count == 0) return queryable;
if (!condition || (comparison != FilterComparison.IsEmpty && genres.Count == 0)) return queryable;
switch (comparison)
{
@ -620,7 +620,7 @@ public static class SeriesFilter
public static IQueryable<Series> HasCollectionTags(this IQueryable<Series> queryable, bool condition,
FilterComparison comparison, IList<int> collectionTags, IList<int> collectionSeries)
{
if (!condition || collectionTags.Count == 0) return queryable;
if (!condition || (comparison != FilterComparison.IsEmpty && collectionTags.Count == 0)) return queryable;
switch (comparison)

View File

@ -9,7 +9,6 @@ namespace API.Services;
public interface IReadingItemService
{
ComicInfo? GetComicInfo(string filePath);
int GetNumberOfPages(string filePath, MangaFormat format);
string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
@ -51,7 +50,7 @@ public class ReadingItemService : IReadingItemService
/// </summary>
/// <param name="filePath">Fully qualified path of file</param>
/// <returns></returns>
public ComicInfo? GetComicInfo(string filePath)
private ComicInfo? GetComicInfo(string filePath)
{
if (Parser.IsEpub(filePath))
{

View File

@ -110,7 +110,7 @@ public class ProcessSeries : IProcessSeries
try
{
_logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName);
_logger.LogInformation("[ScannerService] Processing series {SeriesName} with {Count} files", series.OriginalName, parsedInfos.Count);
// parsedInfos[0] is not the first volume or chapter. We need to find it using a ComicInfo check (as it uses firstParsedInfo for series sort)
var firstParsedInfo = parsedInfos.FirstOrDefault(p => p.ComicInfo != null, firstInfo);
@ -423,7 +423,7 @@ public class ProcessSeries : IProcessSeries
var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(AppUserIncludes.Collections);
if (defaultAdmin == null) return;
_logger.LogDebug("Collection tag(s) found for {SeriesName}, updating collections", series.Name);
_logger.LogInformation("Collection tag(s) found for {SeriesName}, updating collections", series.Name);
var sw = Stopwatch.StartNew();
foreach (var collection in firstChapter.SeriesGroup.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
@ -593,7 +593,6 @@ public class ProcessSeries : IProcessSeries
{
// Add new volumes and update chapters per volume
var distinctVolumes = parsedInfos.DistinctVolumes();
_logger.LogTrace("[ScannerService] Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name);
foreach (var volumeNumber in distinctVolumes)
{
Volume? volume;
@ -621,7 +620,6 @@ public class ProcessSeries : IProcessSeries
volume.LookupName = volumeNumber;
volume.Name = volume.GetNumberTitle();
_logger.LogTrace("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name);
var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray();
await UpdateChapters(series, volume, infos, forceUpdate);
@ -641,7 +639,7 @@ public class ProcessSeries : IProcessSeries
if (series.Volumes.Count == nonDeletedVolumes.Count) return;
_logger.LogTrace("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name",
_logger.LogDebug("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name",
(series.Volumes.Count - nonDeletedVolumes.Count), series.Name);
var deletedVolumes = series.Volumes.Except(nonDeletedVolumes);
foreach (var volume in deletedVolumes)
@ -655,7 +653,7 @@ public class ProcessSeries : IProcessSeries
file);
}
_logger.LogTrace("[ScannerService] Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file);
_logger.LogDebug("[ScannerService] Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file);
}
series.Volumes = nonDeletedVolumes;
@ -681,7 +679,7 @@ public class ProcessSeries : IProcessSeries
if (chapter == null)
{
_logger.LogTrace(
_logger.LogDebug(
"[ScannerService] Adding new chapter, {Series} - Vol {Volume} Ch {Chapter}", info.Series, info.Volumes, info.Chapters);
chapter = ChapterBuilder.FromParserInfo(info).Build();
volume.Chapters.Add(chapter);
@ -778,7 +776,7 @@ public class ProcessSeries : IProcessSeries
// If no files remain after filtering, remove the chapter
if (existingChapter.Files.Count != 0) continue;
_logger.LogTrace("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}",
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}",
existingChapter.Range, volume.Name, parsedInfos[0].Series);
volume.Chapters.Remove(existingChapter);
}
@ -789,7 +787,7 @@ public class ProcessSeries : IProcessSeries
// If no files exist, remove the chapter
if (filesExist) continue;
_logger.LogTrace("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName} as no files exist",
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName} as no files exist",
existingChapter.Range, volume.Name, parsedInfos[0].Series);
volume.Chapters.Remove(existingChapter);
}

View File

@ -504,6 +504,7 @@
"version": "17.3.4",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.4.tgz",
"integrity": "sha512-TVWjpZSI/GIXTYsmVgEKYjBckcW8Aj62DcxLNehRFR+c7UB95OY3ZFjU8U4jL0XvWPgTkkVWQVq+P6N4KCBsyw==",
"dev": true,
"dependencies": {
"@babel/core": "7.23.9",
"@jridgewell/sourcemap-codec": "^1.4.14",
@ -531,6 +532,7 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
"integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
@ -559,12 +561,14 @@
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@ -745,6 +749,7 @@
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
"integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
@ -773,12 +778,14 @@
"node_modules/@babel/core/node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@ -5622,6 +5629,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@ -5634,6 +5642,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
@ -5905,6 +5914,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
@ -6216,6 +6226,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@ -6507,7 +6518,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=="
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true
},
"node_modules/cookie": {
"version": "0.6.0",
@ -7409,6 +7421,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"
@ -7418,6 +7431,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"
@ -8526,6 +8540,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@ -9207,6 +9222,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@ -11047,6 +11063,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -12436,6 +12453,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@ -12447,6 +12465,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
@ -12457,7 +12476,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/regenerate": {
"version": "1.4.2",
@ -12925,7 +12945,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.71.1",
@ -13044,6 +13064,7 @@
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@ -13058,6 +13079,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@ -13068,7 +13090,8 @@
"node_modules/semver/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/send": {
"version": "0.18.0",
@ -14199,6 +14222,7 @@
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core';
import {TranslocoService} from "@jsverse/transloco";
import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range";
import {DecimalPipe} from "@angular/common";
@Pipe({
name: 'readTimeLeft',
@ -8,9 +9,31 @@ import {HourEstimateRange} from "../_models/series-detail/hour-estimate-range";
})
export class ReadTimeLeftPipe implements PipeTransform {
constructor(private translocoService: TranslocoService) {}
constructor(private readonly translocoService: TranslocoService) {}
transform(readingTimeLeft: HourEstimateRange): string {
return `~${readingTimeLeft.avgHours} ${readingTimeLeft.avgHours > 1 ? this.translocoService.translate('read-time-pipe.hours') : this.translocoService.translate('read-time-pipe.hour')}`;
const hoursLabel = readingTimeLeft.avgHours > 1
? this.translocoService.translate('read-time-pipe.hours')
: this.translocoService.translate('read-time-pipe.hour');
const formattedHours = this.customRound(readingTimeLeft.avgHours);
return `~${formattedHours} ${hoursLabel}`;
}
private customRound(value: number): string {
const integerPart = Math.floor(value);
const decimalPart = value - integerPart;
if (decimalPart < 0.5) {
// Round down to the nearest whole number
return integerPart.toString();
} else if (decimalPart >= 0.5 && decimalPart < 0.9) {
// Return with 1 decimal place
return value.toFixed(1);
} else {
// Round up to the nearest whole number
return Math.ceil(value).toString();
}
}
}

View File

@ -490,7 +490,7 @@ export class ActionService {
this.readingListModalRef.componentInstance.seriesId = seriesId;
this.readingListModalRef.componentInstance.volumeIds = volumes.map(v => v.id);
this.readingListModalRef.componentInstance.chapterIds = chapters?.map(c => c.id);
this.readingListModalRef.componentInstance.title = translate('action.multiple-selections');
this.readingListModalRef.componentInstance.title = translate('actionable.multiple-selections');
this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple;
@ -530,7 +530,7 @@ export class ActionService {
if (this.readingListModalRef != null) { return; }
this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' });
this.readingListModalRef.componentInstance.seriesIds = series.map(v => v.id);
this.readingListModalRef.componentInstance.title = translate('action.multiple-selections');
this.readingListModalRef.componentInstance.title = translate('actionable.multiple-selections');
this.readingListModalRef.componentInstance.type = ADD_FLOW.Multiple_Series;

View File

@ -44,7 +44,7 @@ export class PersonService {
let params = new HttpParams();
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
return this.httpClient.post<PaginatedResult<BrowsePerson[]>>(this.baseUrl + 'person/authors', {}, {observe: 'response', params}).pipe(
return this.httpClient.post<PaginatedResult<BrowsePerson[]>>(this.baseUrl + 'person/all', {}, {observe: 'response', params}).pipe(
map((response: any) => {
return this.utilityService.createPaginatedResult(response) as PaginatedResult<BrowsePerson[]>;
})

View File

@ -2,21 +2,23 @@
<div class="modal-container">
<div class="modal-header">
<h4 class="modal-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="modal.close()"></button>
<button type="button" class="btn-close" aria-label="close" (click)="modal.close()"></button>
</div>
<div class="modal-body scrollable-modal">
@if (currentLevel.length > 0) {
<button class="btn btn-secondary w-100 mb-3 text-start" (click)="handleBack()">
← {{t('back-to', {action: t(currentLevel[currentLevel.length - 1])})}}
← {{t('back-to', {action: currentLevel[currentLevel.length - 1]})}}
</button>
}
<div class="d-grid gap-2">
@for (action of currentItems; track action.title) {
@if (willRenderAction(action)) {
<button class="btn btn-outline-primary text-start d-flex justify-content-between align-items-center w-100"
(click)="handleItemClick(action)">
{{t(action.title)}}
{{action.title}}
@if (action.children.length > 0 || action.dynamicList) {
<span class="ms-1"></span>
}

View File

@ -9,7 +9,7 @@ import {
Output
} from '@angular/core';
import {NgClass} from "@angular/common";
import {TranslocoDirective} from "@jsverse/transloco";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {Action, ActionItem} from "../../_services/action-factory.service";
@ -48,7 +48,8 @@ export class ActionableModalComponent implements OnInit {
user!: User | undefined;
ngOnInit() {
this.currentItems = this.actions;
this.currentItems = this.translateOptions(this.actions);
this.accountService.currentUser$.pipe(tap(user => {
this.user = user;
this.cdRef.markForCheck();
@ -58,17 +59,21 @@ export class ActionableModalComponent implements OnInit {
handleItemClick(item: ActionItem<any>) {
if (item.children && item.children.length > 0) {
this.currentLevel.push(item.title);
this.currentItems = item.children;
} else if (item.dynamicList) {
item.dynamicList.subscribe(dynamicItems => {
this.currentLevel.push(item.title);
this.currentItems = dynamicItems.map(di => ({
...item,
title: di.title,
_extra: di
}));
});
} else {
if (item.children.length === 1 && item.children[0].dynamicList) {
item.children[0].dynamicList.subscribe(dynamicItems => {
this.currentItems = dynamicItems.map(di => ({
...item,
children: [], // Required as dynamic list is only one deep
title: di.title,
_extra: di
}));
});
} else {
this.currentItems = this.translateOptions(item.children);
}
}
else {
this.actionPerformed.emit(item);
this.modal.close(item);
}
@ -84,15 +89,15 @@ export class ActionableModalComponent implements OnInit {
items = items.find(item => item.title === level)?.children || [];
}
this.currentItems = items;
this.currentItems = this.translateOptions(items);
this.cdRef.markForCheck();
}
}
// willRenderAction(action: ActionItem<any>) {
// if (this.user === undefined) return false;
//
// return this.accountService.canInvokeAction(this.user, action.action);
// }
translateOptions(opts: Array<ActionItem<any>>) {
return opts.map(a => {
return {...a, title: translate('actionable.' + a.title)};
})
}
}

View File

@ -124,7 +124,7 @@ export class CardActionablesComponent implements OnInit {
openMobileActionableMenu(event: any) {
this.preventEvent(event);
const ref = this.modalService.open(ActionableModalComponent, {fullscreen: 'sm'});
const ref = this.modalService.open(ActionableModalComponent, {fullscreen: true, centered: true});
ref.componentInstance.actions = this.actions;
ref.componentInstance.willRenderAction = this.willRenderAction.bind(this);
ref.componentInstance.shouldRenderSubMenu = this.shouldRenderSubMenu.bind(this);

View File

@ -1,31 +1,32 @@
<ng-container *transloco="let t; read: 'related-tab'">
<div style="padding-bottom: 1rem;">
@if (relations.length > 0) {
<app-carousel-reel [items]="relations" [title]="t('relations-title')">
<ng-template #carouselItem let-item>
<app-series-card class="col-auto mt-2 mb-2" [series]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
</ng-template>
</app-carousel-reel>
}
@if (relations.length > 0) {
<app-carousel-reel [items]="relations" [title]="t('relations-title')">
<ng-template #carouselItem let-item>
<app-series-card class="col-auto mt-2 mb-2" [series]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
</ng-template>
</app-carousel-reel>
}
@if (collections.length > 0) {
<app-carousel-reel [items]="collections" [title]="t('collections-title')">
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[suppressLibraryLink]="true" [imageUrl]="imageService.getCollectionCoverImage(item.id)"
(clicked)="openCollection(item)" [linkUrl]="'/collections/' + item.id" [showFormat]="false"></app-card-item>
</ng-template>
</app-carousel-reel>
}
@if (collections.length > 0) {
<app-carousel-reel [items]="collections" [title]="t('collections-title')">
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[suppressLibraryLink]="true" [imageUrl]="imageService.getCollectionCoverImage(item.id)"
(clicked)="openCollection(item)" [linkUrl]="'/collections/' + item.id" [showFormat]="false"></app-card-item>
</ng-template>
</app-carousel-reel>
}
@if (readingLists.length > 0) {
<app-carousel-reel [items]="readingLists" [title]="t('reading-lists-title')">
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
(clicked)="openReadingList(item)" [linkUrl]="'/lists/' + item.id" [showFormat]="false"></app-card-item>
</ng-template>
</app-carousel-reel>
}
@if (readingLists.length > 0) {
<app-carousel-reel [items]="readingLists" [title]="t('reading-lists-title')">
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
(clicked)="openReadingList(item)" [linkUrl]="'/lists/' + item.id" [showFormat]="false"></app-card-item>
</ng-template>
</app-carousel-reel>
}
</div>
</ng-container>

View File

@ -33,6 +33,8 @@ import {filter, map} from "rxjs/operators";
import {UserProgressUpdateEvent} from "../../_models/events/user-progress-update-event";
import {ReaderService} from "../../_services/reader.service";
import {LibraryType} from "../../_models/library/library";
import {Device} from "../../_models/device/device";
import {ActionService} from "../../_services/action.service";
@Component({
selector: 'app-chapter-card',
@ -59,6 +61,7 @@ export class ChapterCardComponent implements OnInit {
public readonly imageService = inject(ImageService);
public readonly bulkSelectionService = inject(BulkSelectionService);
private readonly downloadService = inject(DownloadService);
private readonly actionService = inject(ActionService);
private readonly messageHub = inject(MessageHubService);
private readonly accountService = inject(AccountService);
private readonly scrollService = inject(ScrollService);
@ -183,6 +186,12 @@ export class ChapterCardComponent implements OnInit {
return; // Don't propagate the download from a card
}
if (action.action == Action.SendTo) {
const device = (action._extra!.data as Device);
this.actionService.sendToDevice([this.chapter.id], device);
return;
}
if (typeof action.callback === 'function') {
action.callback(action, this.chapter);
}

View File

@ -30,8 +30,10 @@
<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>
<input type="text" autofocus autocomplete="off" class="form-control" formControlName="coverImageUrl" placeholder="https://" id="load-image" class="form-control">
<button class="btn btn-outline-secondary" type="button" id="load-image-addon" (click)="loadImageFromUrl(); mode='all';" [disabled]="(form.get('coverImageUrl')?.value).length === 0">
<input type="text" autofocus autocomplete="off" class="form-control" formControlName="coverImageUrl" placeholder="https://" id="load-image">
<button class="btn btn-outline-secondary" type="button" id="load-image-addon"
(click)="loadImageFromUrl(); mode='all';"
[disabled]="(form.get('coverImageUrl')?.value).length === 0">
{{t('load')}}
</button>
</div>

View File

@ -1,8 +1,11 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef,
EventEmitter, HostListener, inject,
Component,
DestroyRef,
EventEmitter,
HostListener,
inject,
Input,
OnChanges,
OnInit,
@ -18,7 +21,7 @@ import {SeriesService} from 'src/app/_services/series.service';
import {ActionService} from 'src/app/_services/action.service';
import {EditSeriesModalComponent} from '../_modals/edit-series-modal/edit-series-modal.component';
import {RelationKind} from 'src/app/_models/series-detail/relation-kind';
import {CommonModule} from "@angular/common";
import {DecimalPipe} from "@angular/common";
import {CardItemComponent} from "../card-item/card-item.component";
import {RelationshipPipe} from "../../_pipes/relationship.pipe";
import {Device} from "../../_models/device/device";
@ -68,7 +71,9 @@ function deepClone(obj: any): any {
@Component({
selector: 'app-series-card',
standalone: true,
imports: [CommonModule, CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent, EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective, SeriesFormatComponent],
imports: [CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent,
EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective,
SeriesFormatComponent, DecimalPipe],
templateUrl: './series-card.component.html',
styleUrls: ['./series-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@ -264,6 +269,9 @@ export class SeriesCardComponent implements OnInit, OnChanges {
case Action.RemoveFromOnDeck:
this.seriesService.removeFromOnDeck(series.id).subscribe(() => this.reload.emit(series.id));
break;
case Action.Download:
this.downloadService.download('series', this.series);
break;
default:
break;
}

View File

@ -35,6 +35,8 @@ import {Volume} from "../../_models/volume";
import {UtilityService} from "../../shared/_services/utility.service";
import {LibraryType} from "../../_models/library/library";
import {RelationshipPipe} from "../../_pipes/relationship.pipe";
import {Device} from "../../_models/device/device";
import {ActionService} from "../../_services/action.service";
@Component({
selector: 'app-volume-card',
@ -63,11 +65,11 @@ export class VolumeCardComponent implements OnInit {
public readonly imageService = inject(ImageService);
public readonly bulkSelectionService = inject(BulkSelectionService);
private readonly downloadService = inject(DownloadService);
private readonly actionService = inject(ActionService);
private readonly messageHub = inject(MessageHubService);
private readonly accountService = inject(AccountService);
private readonly scrollService = inject(ScrollService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly actionFactoryService = inject(ActionFactoryService);
private readonly router = inject(Router);
private readonly readerService = inject(ReaderService);
protected readonly utilityService = inject(UtilityService);
@ -196,6 +198,12 @@ export class VolumeCardComponent implements OnInit {
return; // Don't propagate the download from a card
}
if (action.action == Action.SendTo) {
const device = (action._extra!.data as Device);
this.actionService.sendToDevice(this.volume.chapters.map(c => c.id), device);
return;
}
if (typeof action.callback === 'function') {
action.callback(action, this.volume);
}

View File

@ -163,7 +163,7 @@
<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"></app-details-tab>
<app-details-tab [metadata]="chapter" [genres]="chapter.genres" [tags]="chapter.tags" [webLinks]="weblinks"></app-details-tab>
}
</ng-template>
</li>

View File

@ -190,6 +190,7 @@ export class ChapterDetailComponent implements OnInit {
series: Series | null = null;
libraryType: LibraryType | null = null;
hasReadingProgress = false;
weblinks: Array<string> = [];
activeTabId = TabID.Details;
/**
* This is the download we get from download service.
@ -259,6 +260,7 @@ export class ChapterDetailComponent implements OnInit {
this.series = results.series;
this.chapter = results.chapter;
this.weblinks = this.chapter.webLinks.split(',');
this.libraryType = results.libraryType;
this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor);
@ -281,7 +283,8 @@ export class ChapterDetailComponent implements OnInit {
}
}), takeUntilDestroyed(this.destroyRef)).subscribe();
this.showDetailsTab = hasAnyCast(this.chapter) || (this.chapter.genres || []).length > 0 || (this.chapter.tags || []).length > 0;
this.showDetailsTab = hasAnyCast(this.chapter) || (this.chapter.genres || []).length > 0 ||
(this.chapter.tags || []).length > 0 || this.chapter.webLinks.length > 0;
this.isLoading = false;
this.cdRef.markForCheck();
});

View File

@ -171,18 +171,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
}
}
get ScrollingBlockHeight() {
if (this.scrollingBlock === undefined) return 'calc(var(--vh)*100)';
const navbar = this.document.querySelector('.navbar') as HTMLElement;
if (navbar === null) return 'calc(var(--vh)*100)';
const companionHeight = this.companionBar!.nativeElement.offsetHeight;
const navbarHeight = navbar.offsetHeight;
const totalHeight = companionHeight + navbarHeight + 21; //21px to account for padding
return 'calc(var(--vh)*100 - ' + totalHeight + 'px)';
}
constructor(@Inject(DOCUMENT) private document: Document) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
@ -299,10 +287,16 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
}
switch (action.action) {
case Action.Promote:
this.collectionService.promoteMultipleCollections([this.collectionTag.id], true).subscribe();
this.collectionService.promoteMultipleCollections([this.collectionTag.id], true).subscribe(() => {
this.collectionTag.promoted = true;
this.cdRef.markForCheck();
});
break;
case Action.UnPromote:
this.collectionService.promoteMultipleCollections([this.collectionTag.id], false).subscribe();
this.collectionService.promoteMultipleCollections([this.collectionTag.id], false).subscribe(() => {
this.collectionTag.promoted = false;
this.cdRef.markForCheck();
});
break;
case(Action.Edit):
this.openEditCollectionTagModal(this.collectionTag);

View File

@ -16,7 +16,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {CardItemComponent} from '../../cards/card-item/card-item.component';
import {SeriesCardComponent} from '../../cards/series-card/series-card.component';
import {CarouselReelComponent} from '../../carousel/_components/carousel-reel/carousel-reel.component';
import {AsyncPipe, NgForOf, NgTemplateOutlet} from '@angular/common';
import {AsyncPipe, NgTemplateOutlet} from '@angular/common';
import {
SideNavCompanionBarComponent
} from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
@ -51,7 +51,7 @@ enum StreamId {
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [SideNavCompanionBarComponent, RouterLink, CarouselReelComponent, SeriesCardComponent,
CardItemComponent, AsyncPipe, TranslocoDirective, NgForOf, NgTemplateOutlet, LoadingComponent],
CardItemComponent, AsyncPipe, TranslocoDirective, NgTemplateOutlet, LoadingComponent],
})
export class DashboardComponent implements OnInit {

View File

@ -102,6 +102,7 @@
{{t('cover-image-description')}}
</p>
<app-cover-image-chooser [(imageUrls)]="imageUrls"
(imageUrlsChange)="handleUploadByUrl($event)"
(imageSelected)="updateSelectedIndex($event)"
(selectedBase64Url)="updateSelectedImage($event)"
[showReset]="person.coverImageLocked"

View File

@ -102,7 +102,9 @@ export class EditPersonModalComponent implements OnInit {
save() {
const apis = [];
if (this.touchedCoverImage || this.coverImageReset) {
const hasCoverChanges = this.touchedCoverImage || this.coverImageReset;
if (hasCoverChanges) {
apis.push(this.uploadService.updatePersonCoverImage(this.person.id, this.selectedCover, !this.coverImageReset));
}
@ -121,10 +123,16 @@ export class EditPersonModalComponent implements OnInit {
apis.push(this.personService.updatePerson(person));
forkJoin(apis).subscribe(_ => {
this.modal.close({success: true, coverImageUpdate: false, person: person});
this.modal.close({success: true, coverImageUpdate: hasCoverChanges, person: person});
});
}
handleUploadByUrl(urls: Array<string>) {
this.selectedCover = urls[0];
this.touchedCoverImage = true;
this.cdRef.markForCheck();
}
updateSelectedIndex(index: number) {
this.editForm.patchValue({
coverImageIndex: index

View File

@ -28,7 +28,7 @@ import {BadgeExpanderComponent} from '../../../shared/badge-expander/badge-expan
import {ReadMoreComponent} from '../../../shared/read-more/read-more.component';
import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle} from '@ng-bootstrap/ng-bootstrap';
import {ImageComponent} from '../../../shared/image/image.component';
import {AsyncPipe, DatePipe, DecimalPipe, NgClass, NgIf} from '@angular/common';
import {AsyncPipe, DatePipe, DecimalPipe, NgClass} from '@angular/common';
import {
SideNavCompanionBarComponent
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
@ -174,7 +174,23 @@ export class ReadingListDetailComponent implements OnInit {
this.readingList = rl;
this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '<br>');
this.cdRef.markForCheck();
})
});
});
break;
case Action.Promote:
this.actionService.promoteMultipleReadingLists([this.readingList!], true, () => {
if (this.readingList) {
this.readingList.promoted = true;
this.cdRef.markForCheck();
}
});
break;
case Action.UnPromote:
this.actionService.promoteMultipleReadingLists([this.readingList!], false, () => {
if (this.readingList) {
this.readingList.promoted = false;
this.cdRef.markForCheck();
}
});
break;
}

View File

@ -438,8 +438,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
if (this.currentlyReadingChapter.minNumber === LooseLeafOrDefaultNumber) {
return translate(chapterLocaleKey, {num: vol[0].minNumber});
return translate(volumeLocaleKey, {num: vol[0].minNumber});
}
return translate(volumeLocaleKey, {num: vol[0].minNumber})
+ ' ' + translate(chapterLocaleKey, {num: this.currentlyReadingChapter.minNumber});
}
@ -872,7 +873,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.showVolumeTab = this.shouldShowVolumeTab();
this.showStorylineTab = this.shouldShowStorylineTab();
this.showChapterTab = this.shouldShowChaptersTab();
this.showDetailsTab = hasAnyCast(this.seriesMetadata) || (this.seriesMetadata?.genres || []).length > 0 || (this.seriesMetadata?.tags || []).length > 0;
this.showDetailsTab = hasAnyCast(this.seriesMetadata) || (this.seriesMetadata?.genres || []).length > 0
|| (this.seriesMetadata?.tags || []).length > 0 || (this.seriesMetadata?.webLinks || []).length > 0;
this.cdRef.markForCheck();
}

View File

@ -3,7 +3,7 @@
<div class="row g-0">
<div class="col-10">
<h6 class="section-title">
@if(labelId) {
@if (labelId) {
<label class="reset-label" [for]="labelId">{{title}}</label>
} @else {
{{title}}

View File

@ -85,6 +85,7 @@ export class SettingItemComponent {
toggleEditMode() {
if (!this.toggleOnViewClick) return;
if (!this.canEdit) return;
this.isEditMode = !this.isEditMode;
this.editMode.emit(this.isEditMode);

View File

@ -1,8 +1,8 @@
<ng-container *transloco="let t; read:'change-age-restriction'">
@if (user) {
<app-setting-item [title]="t('age-restriction-label')">
<app-setting-item [title]="t('age-restriction-label')" [canEdit]="accountService.hasChangeAgeRestrictionRole(user) || accountService.hasAdminRole(user)">
<ng-template #view>
<span class="col-12">{{user.ageRestriction.ageRating | ageRating }}
<span class="col-12" [ngClass]="{'disabled': !accountService.hasChangeAgeRestrictionRole(user) && !accountService.hasAdminRole(user)}">{{user.ageRestriction.ageRating | ageRating }}
@if (user.ageRestriction.ageRating !== AgeRating.NotApplicable && user.ageRestriction.includeUnknowns) {
<span class="ms-1 me-1">+</span> {{t('unknowns')}}
}

View File

@ -0,0 +1,3 @@
.disabled {
color: var(--btn-disabled-text-color);
}

View File

@ -16,7 +16,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { AgeRatingPipe } from '../../_pipes/age-rating.pipe';
import { RestrictionSelectorComponent } from '../restriction-selector/restriction-selector.component';
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import {AsyncPipe, NgForOf, NgIf} from '@angular/common';
import {AsyncPipe, NgClass, NgForOf, NgIf} from '@angular/common';
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {SettingTitleComponent} from "../../settings/_components/setting-title/setting-title.component";
import {ReactiveFormsModule} from "@angular/forms";
@ -29,11 +29,12 @@ import {SettingItemComponent} from "../../settings/_components/setting-item/sett
styleUrls: ['./change-age-restriction.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgbCollapse, RestrictionSelectorComponent, AsyncPipe, AgeRatingPipe, TranslocoDirective, SettingTitleComponent, NgForOf, NgIf, ReactiveFormsModule, Select2Module, SettingItemComponent]
imports: [NgbCollapse, RestrictionSelectorComponent, AsyncPipe, AgeRatingPipe, TranslocoDirective, SettingTitleComponent,
ReactiveFormsModule, SettingItemComponent, NgClass]
})
export class ChangeAgeRestrictionComponent implements OnInit {
private readonly accountService = inject(AccountService);
protected readonly accountService = inject(AccountService);
private readonly toastr = inject(ToastrService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
@ -64,6 +65,8 @@ export class ChangeAgeRestrictionComponent implements OnInit {
updateRestrictionSelection(restriction: AgeRestriction) {
this.selectedRestriction = restriction;
this.saveForm();
}
resetForm() {

View File

@ -53,7 +53,6 @@
--default-state-scrollbar: transparent;
--text-muted-color: hsla(0,0%,100%,.45);
/* Theming colors that performs a gradient for background. Can be disabled else automatically applied based on cover image colors.
* --colorscape-primary-color and the alpha variants will be updated in real time. the default variant is fixed and represents the default state and should
* match the non-default/alpha on launch.

View File

@ -1,188 +0,0 @@
:root .bg-e-ink {
--color-scheme: light;
--primary-color: black;
--primary-color-dark-shade: #3B9E76;
--primary-color-darker-shade: #338A67;
--primary-color-darkest-shade: #25624A;
--error-color: #ff4136;
--bs-body-bg: #fff;
--body-text-color: black;
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
/* Navbar */
--navbar-bg-color: black;
--navbar-text-color: white;
--navbar-fa-icon-color: white;
--navbar-btn-hover-outline-color: rgba(255, 255, 255, 1);
/* Inputs */
--input-bg-color: #fff;
--input-focused-border-color: #ccc;
--input-bg-readonly-color: rgba(0,0,0,0.2);
--input-placeholder-color: #aeaeae;
--input-border-color: #ccc;
--input-range-color: var(--primary-color);
--input-range-active-color: var(--primary-color-darker-shade);
/* Buttons */
--btn-primary-text-color: black;
--btn-primary-bg-color: white;
--btn-primary-border-color: black;
--btn-primary-hover-text-color: white;
--btn-primary-hover-bg-color: black;
--btn-primary-hover-border-color: black;
--btn-alt-bg-color: #424c72;
--btn-alt-border-color: #444f75;
--btn-alt-hover-bg-color: #3b4466;
--btn-alt-focus-bg-color: #343c59;
--btn-alt-focus-boxshadow-color: rgb(68 79 117 / 50%);
--btn-fa-icon-color: black;
--btn-disabled-bg-color: #020202;
--btn-disabled-text-color: white;
--btn-disabled-border-color: #6c757d;
--btn-fa-icon-color: black;
/* Nav */
--nav-link-active-text-color: white;
--nav-link-bg-color: black;
--nav-link-text-color: black;
--nav-link-hover-text-color: var(--primary-color);
--nav-tab-border-hover-color: transparent;
--nav-tab-active-text-color: white;
--nav-tab-text-color: var(--body-text-color);
--nav-tab-bg-color: black;
--nav-tab-hover-border-color: black;
/* Checkboxes */
--checkbox-checked-bg-color: var(--primary-color);
--checkbox-bg-color: white;
--checkbox-border-color: var(--primary-color);
--checkbox-focus-border-color: var(--input-border-color);
/* Tagbadge */
--tagbadge-bg-color: #c9c9c9;
/* Side Nav */
--side-nav-bg-color: rgba(255,255,255,0.6);
--side-nav-mobile-bg-color: rgb(255,255,255);
--side-nav-openclose-transition: 1ms;
--side-nav-box-shadow: none;
--side-nav-mobile-box-shadow: 3px 0em 5px 10em rgb(0 0 0 / 50%);
--side-nav-hover-text-color: white;
--side-nav-hover-bg-color: black;
--side-nav-text-color: black;
--side-nav-border-radius: 5px;
--side-nav-border: none;
--side-nav-border-closed: none;
--side-nav-border-transition: 0s;
--side-nav-bg-color-transition: 0s;
--side-nav-companion-bar-transistion: 0s;
--side-nav-closed-bg-color: transparent;
--side-nav-item-active-color: var(--primary-color);
--side-nav-active-bg-color: rgba(0,0,0,0.5);
--side-nav-overlay-color: rgba(0,0,0,1);
--side-nav-item-active-text-color: white;
/* Toasts */
--toast-success-bg-color: rgba(74, 198, 148, 0.9);
--toast-error-bg-color: #BD362F;
--toast-info-bg-color: #2F96B4;
--toast-warning-bg-color: #F89406;
/* Rating star */
--ratingstar-star-empty: #b0c4de;
--ratingstar-star-filled: var(--primary-color);
/* Global */
--accent-bg-color: rgba(206, 206, 206, 0.5);
--accent-text-color: black;
--hr-color: rgba(239, 239, 239, 0.125);
--grid-breakpoints-xs: $grid-breakpoint-xs;
--grid-breakpoints-sm: $grid-breakpoint-sm;
--grid-breakpoints-md: $grid-breakpoint-md;
--grid-breakpoints-lg: $grid-breakpoint-lg;
--grid-breakpoints-xl: $grid-breakpoint-xl;
--body-font-family: "EBGaramond", "Helvetica Neue", sans-serif;
--brand-font-family: "Spartan", sans-serif;
/* Breadcrumb */
--breadcrumb-bg-color: #eaeaea;
--breadcrumb-item-text-color: var(--body-text-color);
/* Card */
--card-text-color: #000;
--card-border-width: 0 1px 1px 1px;
--card-border-style: solid;
--card-border-color: #ccc;
--card-progress-bar-color: var(--primary-color);
--card-overlay-hover-bg-color: rgba(0, 0, 0, 0.2);
/* List items */
--list-group-item-text-color: var(--body-text-color);
--list-group-item-bg-color: white;
--list-group-hover-text-color: black;
--list-group-hover-bg-color: #eaeaea;
--list-group-item-border-color: rgba(239, 239, 239, 0.125);
--list-group-active-border-color: none;
/* Dropdown */
--dropdown-item-hover-text-color: white;
--dropdown-item-hover-bg-color: var(--primary-color);
--dropdown-overlay-color: rgba(0,0,0,0.5);
--dropdown-item-bg-color: white;
/* Manga Reader */
--manga-reader-overlay-filter: none;
--manga-reader-overlay-bg-color: rgba(0,0,0,0.5);
--manga-reader-overlay-text-color: white;
--manga-reader-bg-color: black;
--manga-reader-next-highlight-bg-color: transparent;
--manga-reader-prev-highlight-bg-color: transparent;
/* Radios */
--radio-accent-color: var(--primary-color);
--radio-hover-accent-color: var(--primary-color-dark-shade);
/* Carousel */
--carousel-header-text-color: black;
--carousel-header-text-decoration: none;
--carousel-hover-header-text-decoration: none;
/** Drawer */
--drawer-bg-color: white;
--drawer-text-color: black;
/* Pagination */
--pagination-active-link-border-color: var(--primary-color);
--pagination-active-link-bg-color: var(--primary-color);
--pagination-active-link-text-color: white;
--pagination-link-border-color: rgba(239, 239, 239, 1);
--pagination-link-text-color: black;
--pagination-link-bg-color: white;
--pagination-focus-border-color: var(--primary-color);
--pagination-link-hover-color: var(--primary-color);
/** Event Widget */
--event-widget-bg-color: white;
--event-widget-item-bg-color: lightgrey;
--event-widget-text-color: black;
--event-widget-item-border-color: lightgrey;
--event-widget-border-color: lightgrey;
/* Popover */
--popover-body-bg-color: var(--navbar-bg-color);
--popover-body-text-color: var(--navbar-text-color);
--popover-outerarrow-color: lightgrey;
--popover-arrow-color: lightgrey;
--popover-bg-color: lightgrey;
--popover-border-color: lightgrey;
/* Search */
--search-result-text-lite-color: rgba(0,0,0,1);
/* Bulk Selection */
--bulk-selection-text-color: white;
--bulk-selection-highlight-text-color: white;
}

View File

@ -1,243 +0,0 @@
/* Default styles for Kavita */
:root {
--color-scheme: dark;
--primary-color: #4ac694;
--primary-color-dark-shade: #3B9E76;
--primary-color-darker-shade: #338A67;
--primary-color-darkest-shade: #25624A;
--error-color: #BD362F;
--bs-body-bg: #343a40;
--body-text-color: #efefef;
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
--primary-color-scrollbar: rgba(74,198,148,0.75);
/* Navbar */
--navbar-bg-color: black;
--navbar-text-color: white;
--navbar-fa-icon-color: white;
--navbar-btn-hover-outline-color: rgba(255, 255, 255, 1);
/* Inputs */
--input-bg-color: #343a40;
--input-bg-readonly-color: #434648;
--input-focused-border-color: #ccc;
--input-text-color: #fff;
--input-placeholder-color: #aeaeae;
--input-border-color: #ccc;
--input-focus-boxshadow-color: rgb(255 255 255 / 50%);
/* Buttons */
--btn-focus-boxshadow-color: rgb(255 255 255 / 50%);
--btn-primary-text-color: white;
--btn-primary-bg-color: var(--primary-color);
--btn-primary-border-color: var(--primary-color);
--btn-primary-hover-text-color: white;
--btn-primary-hover-bg-color: var(--primary-color-darker-shade);
--btn-primary-hover-border-color: var(--primary-color-darker-shade);
--btn-alt-bg-color: #424c72;
--btn-alt-border-color: #444f75;
--btn-alt-hover-bg-color: #3b4466;
--btn-alt-focus-bg-color: #343c59;
--btn-alt-focus-boxshadow-color: rgb(255 255 255 / 50%);
--btn-fa-icon-color: white;
--btn-disabled-bg-color: #343a40;
--btn-disabled-text-color: white;
--btn-disabled-border-color: #6c757d;
--bs-btn-disabled-border-color: transparent;
/* Nav (Tabs) */
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
--nav-tab-text-color: var(--body-text-color);
--nav-tab-bg-color: var(--primary-color);
--nav-tab-hover-border-color: var(--primary-color);
--nav-tab-active-text-color: white;
--nav-tab-border-hover-color: transparent;
--nav-tab-hover-text-color: var(--body-text-color);
--nav-tab-hover-bg-color: transparent;
--nav-tab-border-top: rgba(44, 118, 88, 0.7);
--nav-tab-border-left: rgba(44, 118, 88, 0.7);
--nav-tab-border-bottom: rgba(44, 118, 88, 0.7);
--nav-tab-border-right: rgba(44, 118, 88, 0.7);
--nav-tab-hover-border-top: rgba(44, 118, 88, 0.7);
--nav-tab-hover-border-left: rgba(44, 118, 88, 0.7);
--nav-tab-hover-border-bottom: var(--bs-body-bg);
--nav-tab-hover-border-right: rgba(44, 118, 88, 0.7);
--nav-tab-active-hover-bg-color: var(--primary-color);
--nav-link-bg-color: var(--primary-color);
--nav-link-active-text-color: white;
--nav-link-text-color: white;
/* Header */
--nav-header-text-color: white;
--nav-header-bg-color: rgb(22, 27, 34);
/* Toasts */
--toast-success-bg-color: rgba(59, 158, 118, 0.9);
--toast-error-bg-color: #BD362F;
--toast-info-bg-color: #2F96B4;
--toast-warning-bg-color: #F89406;
/* Checkboxes/Switch */
--checkbox-checked-bg-color: var(--primary-color);
--checkbox-border-color: var(--input-focused-border-color);
--checkbox-focus-border-color: var(--primary-color);
--checkbox-focus-boxshadow-color: rgb(255 255 255 / 50%);
/* Tag Badge */
--tagbadge-border-color: rgba(239, 239, 239, 0.125);
--tagbadge-text-color: var(--body-text-color);
--tagbadge-bg-color: var(--nav-tab-hover-bg-color);
--tagbadge-filled-border-color: rgba(239, 239, 239, 0.125);
--tagbadge-filled-text-color: var(--body-text-color);
--tagbadge-filled-bg-color: var(--primary-color);
/* Side Nav */
--side-nav-bg-color: rgba(0,0,0,0.2);
--side-nav-mobile-bg-color: rgb(25,26,28);
--side-nav-openclose-transition: 0.15s ease-in-out;
--side-nav-box-shadow: rgba(0,0,0,0.5);
--side-nav-mobile-box-shadow: 3px 0em 5px 10em rgb(0 0 0 / 50%);
--side-nav-hover-text-color: white;
--side-nav-hover-bg-color: black;
--side-nav-text-color: white;
--side-nav-border-radius: 5px;
--side-nav-border: none;
--side-nav-border-closed: none;
--side-nav-border-transition: 0.5s ease-in-out;
--side-nav-companion-bar-transistion: 0.15s linear;
--side-nav-bg-color-transition: 0.5s ease-in-out;
--side-nav-closed-bg-color: transparent;
--side-nav-item-active-color: var(--primary-color);
--side-nav-item-active-text-color: white;
--side-nav-active-bg-color: rgba(0,0,0,0.5);
--side-nav-overlay-color: rgba(0,0,0,0.5);
/* List items */
--list-group-item-text-color: var(--body-text-color); /*rgba(74, 198, 148, 0.9)*/
--list-group-item-bg-color: #343a40;
--list-group-item-border-color: rgba(239, 239, 239, 0.125);
--list-group-hover-text-color: white;
--list-group-hover-bg-color: rgb(22, 27, 34);
--list-group-active-border-color: none;
/* Popover */
--popover-body-bg-color: var(--navbar-bg-color);
--popover-body-text-color: var(--navbar-text-color);
--popover-outerarrow-color: transparent;
--popover-arrow-color: transparent;
--popover-bg-color: black;
--popover-border-color: black;
/* Pagination */
--pagination-active-link-border-color: var(--primary-color);
--pagination-active-link-bg-color: var(--primary-color);
--pagination-active-link-text-color: white;
--pagination-link-border-color: rgba(239, 239, 239, 0.125);
--pagination-link-text-color: white;
--pagination-link-bg-color: rgba(1, 4, 9, 0.5);
--pagination-focus-border-color: var(--primary-color);
--pagination-link-hover-color: var(--primary-color);
/* Progress Bar */
--progress-striped-animated-color: linear-gradient(45deg, rgba(74,198,148, 0.75) 25%, rgba(51, 138, 103, 0.75) 25%, rgba(51, 138, 103, 0.75) 50%, rgba(74,198,148, 0.75) 50%, rgba(74,198,148, 0.75) 75%, rgba(51, 138, 103, 0.75) 75%, rgba(51, 138, 103, 0.75));
--progress-bg-color: var(--nav-header-bg-color);
--progress-bar-color: var(--primary-color-dark-shade);
/* Dropdown */
--dropdown-item-hover-text-color: white;
--dropdown-item-hover-bg-color: var(--primary-color-dark-shade);
--dropdown-item-text-color: var(--navbar-text-color);
--dropdown-item-bg-color: var(--navbar-bg-color);
--dropdown-overlay-color: rgba(0,0,0,0.5);
/* Accordion */
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
--accordion-header-bg-color: rgba(52, 60, 70, 0.5);
--accordion-body-bg-color: #292929;
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
--accordion-body-text-color: var(--body-text-color);
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
--accordion-header-collapsed-bg-color: #292929;
--accordion-button-focus-border-color: unset;
--accordion-button-focus-box-shadow: unset;
--accordion-active-body-bg-color: #292929;
--accordion-body-box-shadow: none;
/* Breadcrumb */
--breadcrumb-bg-color: #292d32;
--breadcrumb-item-text-color: var(--body-text-color);
/* Rating star */
--ratingstar-color: white;
--ratingstar-star-empty: #b0c4de;
--ratingstar-star-filled: var(--primary-color);
/* Global */
--hr-color: rgba(239, 239, 239, 0.125);
--accent-bg-color: rgba(1, 4, 9, 0.5);
--accent-text-color: lightgrey;
--grid-breakpoints-xs: $grid-breakpoint-xs;
--grid-breakpoints-sm: $grid-breakpoint-sm;
--grid-breakpoints-md: $grid-breakpoint-md;
--grid-breakpoints-lg: $grid-breakpoint-lg;
--grid-breakpoints-xl: $grid-breakpoint-xl;
--body-font-family: "EBGaramond", "Helvetica Neue", sans-serif;
--brand-font-family: "Spartan", sans-serif;
/* Card */
--card-bg-color: rgba(22,27,34,0.5);
--card-text-color: var(--body-text-color);
--card-border-width: 0 1px 1px 1px;
--card-border-style: solid;
--card-border-color: transparent;
--card-progress-bar-color: var(--primary-color);
--card-overlay-bg-color: rgba(0, 0, 0, 0);
--card-overlay-hover-bg-color: rgba(0, 0, 0, 0.2);
/* Slider */
--slider-text-color: white;
--input-range-color: var(--primary-color);
--input-range-active-color: var(--primary-color-darker-shade);
/* Manga Reader */
--manga-reader-overlay-filter: blur(10px);
--manga-reader-overlay-bg-color: rgba(0,0,0,0.5);
--manga-reader-overlay-text-color: white;
--manga-reader-bg-color: black;
--manga-reader-next-highlight-bg-color: rgba(65, 225, 100, 0.5);
--manga-reader-prev-highlight-bg-color: rgba(65, 105, 225, 0.5);
/* Radios */
--radio-accent-color: var(--primary-color);
--radio-hover-accent-color: var(--primary-color);
--radio-focus-boxshadow-color: rgb(255 255 255 / 50%);
/* Carousel */
--carousel-header-text-color: var(--body-text-color);
--carousel-header-text-decoration: none;
--carousel-hover-header-text-decoration: none;
/** Drawer */
--drawer-background-color: black; // TODO: Remove this for bg
--drawer-bg-color: #292929;
--drawer-text-color: white;
/** Event Widget */
--event-widget-bg-color: rgb(1, 4, 9);
--event-widget-item-bg-color: rgb(1, 4, 9);
--event-widget-text-color: var(--body-text-color);
--event-widget-item-border-color: rgba(53, 53, 53, 0.5);
--event-widget-border-color: rgba(1, 4, 9, 0.5);
/* Search */
--search-result-text-lite-color: initial;
/* Bulk Selection */
--bulk-selection-text-color: var(--navbar-text-color);
--bulk-selection-highlight-text-color: var(--primary-color);
/* List Card Item */
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
}