Last Batch before Release (#2899)

This commit is contained in:
Joe Milazzo 2024-04-21 10:53:40 -05:00 committed by GitHub
parent 8d77b398b2
commit 32bedb4e06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 3302 additions and 124 deletions

View File

@ -1,4 +1,5 @@
using Xunit; using API.Entities.Enums;
using Xunit;
namespace API.Tests.Parsing; namespace API.Tests.Parsing;
@ -10,7 +11,7 @@ public class BookParsingTests
[InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "Faust")] [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "Faust")]
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Book));
} }
[Theory] [Theory]
@ -18,7 +19,7 @@ public class BookParsingTests
[InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "1")] [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "1")]
public void ParseVolumeTest(string filename, string expected) public void ParseVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Book));
} }
// [Theory] // [Theory]

View File

@ -1,4 +1,5 @@
using System.IO.Abstractions.TestingHelpers; using System.IO.Abstractions.TestingHelpers;
using API.Entities.Enums;
using API.Services; using API.Services;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -10,16 +11,6 @@ namespace API.Tests.Parsing;
public class ComicParsingTests public class ComicParsingTests
{ {
private readonly ITestOutputHelper _testOutputHelper;
private readonly IDefaultParser _basicParser;
public ComicParsingTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
_basicParser = new BasicParser(directoryService, new ImageParser(directoryService));
}
[Theory] [Theory]
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")] [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")]
[InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "The First Asterix Frieze")] [InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "The First Asterix Frieze")]
@ -188,7 +179,7 @@ public class ComicParsingTests
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")] [InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
public void ParseComicChapterTest(string filename, string expected) public void ParseComicChapterTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Comic));
} }
@ -219,6 +210,6 @@ public class ComicParsingTests
[InlineData("Blood Syndicate Annual #001", true)] [InlineData("Blood Syndicate Annual #001", true)]
public void IsComicSpecialTest(string input, bool expected) public void IsComicSpecialTest(string input, bool expected)
{ {
Assert.Equal(expected, Parser.IsComicSpecial(input)); Assert.Equal(expected, Parser.IsSpecial(input, LibraryType.Comic));
} }
} }

View File

@ -86,7 +86,7 @@ public class MangaParsingTests
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")] [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
public void ParseVolumeTest(string filename, string expected) public void ParseVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga));
} }
[Theory] [Theory]
@ -212,7 +212,7 @@ public class MangaParsingTests
[InlineData("หนึ่งความคิด นิจนิรันดร์ เล่ม 2", "หนึ่งความคิด นิจนิรันดร์")] [InlineData("หนึ่งความคิด นิจนิรันดร์ เล่ม 2", "หนึ่งความคิด นิจนิรันดร์")]
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga));
} }
[Theory] [Theory]
@ -304,7 +304,7 @@ public class MangaParsingTests
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")] [InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga));
} }
@ -342,7 +342,7 @@ public class MangaParsingTests
[InlineData("Hajime no Ippo - Artbook", false)] [InlineData("Hajime no Ippo - Artbook", false)]
public void IsMangaSpecialTest(string input, bool expected) public void IsMangaSpecialTest(string input, bool expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsMangaSpecial(input)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsSpecial(input, LibraryType.Manga));
} }
[Theory] [Theory]

View File

@ -961,7 +961,7 @@ public class OpdsController : BaseApiController
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey)); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
if (!await _accountService.HasDownloadPermission(user)) if (!await _accountService.HasDownloadPermission(user))
{ {
return BadRequest("User does not have download permissions"); return Forbid("User does not have download permissions");
} }
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ChapterSortOrderLock : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PdfLayoutMode",
table: "AppUserPreferences");
migrationBuilder.AddColumn<bool>(
name: "SortOrderLocked",
table: "Chapter",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SortOrderLocked",
table: "Chapter");
migrationBuilder.AddColumn<int>(
name: "PdfLayoutMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
}
}

View File

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.3"); modelBuilder.HasAnnotation("ProductVersion", "8.0.4");
modelBuilder.Entity("API.Entities.AppRole", b => modelBuilder.Entity("API.Entities.AppRole", b =>
{ {
@ -415,9 +415,6 @@ namespace API.Data.Migrations
b.Property<int>("PageSplitOption") b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("PdfLayoutMode")
.HasColumnType("INTEGER");
b.Property<int>("PdfScrollMode") b.Property<int>("PdfScrollMode")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -784,6 +781,9 @@ namespace API.Data.Migrations
b.Property<float>("SortOrder") b.Property<float>("SortOrder")
.HasColumnType("REAL"); .HasColumnType("REAL");
b.Property<bool>("SortOrderLocked")
.HasColumnType("INTEGER");
b.Property<string>("StoryArc") b.Property<string>("StoryArc")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@ -116,11 +116,6 @@ public class AppUserPreferences
/// </summary> /// </summary>
public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical;
/// <summary> /// <summary>
/// PDF Reader: Layout Mode of the reader
/// </summary>
/// Book mode is too buggy to include
//public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple;
/// <summary>
/// PDF Reader: Spread Mode of the reader /// PDF Reader: Spread Mode of the reader
/// </summary> /// </summary>
public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Interfaces; using API.Entities.Interfaces;
@ -33,6 +34,10 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
/// </summary> /// </summary>
public float SortOrder { get; set; } public float SortOrder { get; set; }
/// <summary> /// <summary>
/// Can the sort order be updated on scan or is it locked from UI
/// </summary>
public bool SortOrderLocked { get; set; }
/// <summary>
/// The files that represent this Chapter /// The files that represent this Chapter
/// </summary> /// </summary>
public ICollection<MangaFile> Files { get; set; } = null!; public ICollection<MangaFile> Files { get; set; } = null!;
@ -171,7 +176,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
return Parser.RemoveExtensionIfSupported(Title); return Parser.RemoveExtensionIfSupported(Title);
} }
if (MinNumber.Is(0) && !float.TryParse(Range, out _)) if (MinNumber.Is(0) && !float.TryParse(Range, CultureInfo.InvariantCulture, out _))
{ {
return $"{Range}"; return $"{Range}";
} }

View File

@ -38,7 +38,7 @@ public static class ChapterListExtensions
fakeChapter.UpdateFrom(info); fakeChapter.UpdateFrom(info);
return specialTreatment return specialTreatment
? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath)) ? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath))
: chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle()); : chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle()); // BUG: TODO: On non-english locales, for floats, the range will be 20,5 but the NumberTitle will return 20.5
} }
/// <summary> /// <summary>

View File

@ -86,7 +86,7 @@ public static class FilterFieldValueConverter
.Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x)) .Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x))
.ToList(), .ToList(),
FilterField.ReadTime => int.Parse(value), FilterField.ReadTime => int.Parse(value),
FilterField.AverageRating => float.Parse(value), FilterField.AverageRating => value.AsFloat(),
_ => throw new ArgumentException("Invalid field type") _ => throw new ArgumentException("Invalid field type")
}; };
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -37,6 +38,9 @@ public class Program
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
Console.OutputEncoding = System.Text.Encoding.UTF8; Console.OutputEncoding = System.Text.Encoding.UTF8;
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.Console() .WriteTo.Console()

View File

@ -551,7 +551,7 @@ public class BookService : IBookService
} }
// If this is a single book and not a collection, set publication status to Completed // If this is a single book and not a collection, set publication status to Completed
if (string.IsNullOrEmpty(info.Volume) && Parser.ParseVolume(filePath).Equals(Parser.LooseLeafVolume)) if (string.IsNullOrEmpty(info.Volume) && Parser.ParseVolume(filePath, LibraryType.Manga).Equals(Parser.LooseLeafVolume))
{ {
info.Count = 1; info.Count = 1;
} }
@ -560,14 +560,14 @@ public class BookService : IBookService
info.Writer = string.Join(",", info.Writer = string.Join(",",
epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.CleanAuthor(c.Creator))); epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.CleanAuthor(c.Creator)));
var hasVolumeInSeries = !Parser.ParseVolume(info.Title) var hasVolumeInSeries = !Parser.ParseVolume(info.Title, LibraryType.Manga)
.Equals(Parser.LooseLeafVolume); .Equals(Parser.LooseLeafVolume);
if (string.IsNullOrEmpty(info.Volume) && hasVolumeInSeries && (!info.Series.Equals(info.Title) || string.IsNullOrEmpty(info.Series))) if (string.IsNullOrEmpty(info.Volume) && hasVolumeInSeries && (!info.Series.Equals(info.Title) || string.IsNullOrEmpty(info.Series)))
{ {
// This is likely a light novel for which we can set series from parsed title // This is likely a light novel for which we can set series from parsed title
info.Series = Parser.ParseSeries(info.Title); info.Series = Parser.ParseSeries(info.Title, LibraryType.Manga);
info.Volume = Parser.ParseVolume(info.Title); info.Volume = Parser.ParseVolume(info.Title, LibraryType.Manga);
} }
return info; return info;
@ -608,7 +608,6 @@ public class BookService : IBookService
item.Property == "display-seq" && item.Refines == metadataItem.Refines); item.Property == "display-seq" && item.Refines == metadataItem.Refines);
if (count == null || count.Content == "0") if (count == null || count.Content == "0")
{ {
// TODO: Rewrite this to use a StringBuilder
// Treat this as a Collection // Treat this as a Collection
info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") + info.SeriesGroup += (string.IsNullOrEmpty(info.StoryArc) ? string.Empty : ",") +
readingListElem.Title.Replace(',', '_'); readingListElem.Title.Replace(',', '_');
@ -740,8 +739,6 @@ public class BookService : IBookService
private static string EscapeTags(string content) private static string EscapeTags(string content)
{ {
// content = StartingScriptTag().Replace(content, "<script$1></script>");
// content = StartingTitleTag().Replace(content, "<title$1></title>");
content = Regex.Replace(content, @"<script(.*)(/>)", "<script$1></script>", RegexOptions.None, Parser.RegexTimeout); content = Regex.Replace(content, @"<script(.*)(/>)", "<script$1></script>", RegexOptions.None, Parser.RegexTimeout);
content = Regex.Replace(content, @"<title(.*)(/>)", "<title$1></title>", RegexOptions.None, Parser.RegexTimeout); content = Regex.Replace(content, @"<title(.*)(/>)", "<title$1></title>", RegexOptions.None, Parser.RegexTimeout);
return content; return content;
@ -1043,8 +1040,6 @@ public class BookService : IBookService
// TODO: We may want to check if there is a toc.ncs file to better handle nested toc // TODO: We may want to check if there is a toc.ncs file to better handle nested toc
// We could do a fallback first with ol/lis // We could do a fallback first with ol/lis
//var sections = doc.DocumentNode.SelectNodes("//ol");
//if (sections == null)
@ -1239,7 +1234,7 @@ public class BookService : IBookService
{ {
_logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); _logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath);
_mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, _mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService,
"There was a critical error and prevented thumbnail generation", ex); // TODO: Localize this "There was a critical error and prevented thumbnail generation", ex);
} }
return string.Empty; return string.Empty;

View File

@ -489,7 +489,7 @@ public class ReaderService : IReaderService
currentChapter.SortOrder, currentChapter.SortOrder,
dto => dto.SortOrder); dto => dto.SortOrder);
if (chapterId > 0) return chapterId; if (chapterId > 0) return chapterId;
currentVolume = volumes.FirstOrDefault(v => v.IsLooseLeaf()); currentVolume = volumes.Find(v => v.IsLooseLeaf());
} }
if (currentVolume != null && currentVolume.IsLooseLeaf()) if (currentVolume != null && currentVolume.IsLooseLeaf())
@ -506,7 +506,7 @@ public class ReaderService : IReaderService
// When we started as a special and there was no loose leafs, reset the currentVolume // When we started as a special and there was no loose leafs, reset the currentVolume
if (currentVolume == null) if (currentVolume == null)
{ {
currentVolume = volumes.FirstOrDefault(v => !v.IsLooseLeaf() && !v.IsSpecial()); currentVolume = volumes.Find(v => !v.IsLooseLeaf() && !v.IsSpecial());
if (currentVolume == null) return -1; if (currentVolume == null) return -1;
return currentVolume.Chapters.OrderBy(x => x.SortOrder).Last()?.Id ?? -1; return currentVolume.Chapters.OrderBy(x => x.SortOrder).Last()?.Id ?? -1;
} }
@ -786,7 +786,7 @@ public class ReaderService : IReaderService
} }
var files = _directoryService.GetFilesWithExtension(outputDirectory, var files = _directoryService.GetFilesWithExtension(outputDirectory,
Tasks.Scanner.Parser.Parser.ImageFileExtensions); Parser.ImageFileExtensions);
return CacheService.GetPageFromFiles(files, pageNum); return CacheService.GetPageFromFiles(files, pageNum);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -421,6 +422,7 @@ public class ParseScannedFiles
_logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder); _logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder);
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", library.Name, ProgressEventType.Updated)); MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", library.Name, ProgressEventType.Updated));
if (files.Count == 0) if (files.Count == 0)
{ {
_logger.LogInformation("[ScannerService] {Folder} is empty, no longer in this location, or has no file types that match Library File Types", folder); _logger.LogInformation("[ScannerService] {Folder} is empty, no longer in this location, or has no file types that match Library File Types", folder);
@ -483,17 +485,17 @@ public class ParseScannedFiles
} }
chapters = infos chapters = infos
.OrderByNatural(info => info.Chapters) .OrderByNatural(info => info.Chapters, StringComparer.InvariantCulture)
.ToList(); .ToList();
counter = 0f; counter = 0f;
var prevIssue = string.Empty; var prevIssue = string.Empty;
foreach (var chapter in chapters) foreach (var chapter in chapters)
{ {
if (float.TryParse(chapter.Chapters, out var parsedChapter)) if (float.TryParse(chapter.Chapters, CultureInfo.InvariantCulture, out var parsedChapter))
{ {
counter = parsedChapter; counter = parsedChapter;
if (!string.IsNullOrEmpty(prevIssue) && float.TryParse(prevIssue, out var prevIssueFloat) && parsedChapter.Is(prevIssueFloat)) if (!string.IsNullOrEmpty(prevIssue) && float.TryParse(prevIssue, CultureInfo.InvariantCulture, out var prevIssueFloat) && parsedChapter.Is(prevIssueFloat))
{ {
// Bump by 0.1 // Bump by 0.1
counter += 0.1f; counter += 0.1f;
@ -565,7 +567,10 @@ public class ParseScannedFiles
// Normalize this as many of the cases is a capitalization difference // Normalize this as many of the cases is a capitalization difference
var nonLocalizedSeriesFound = infos var nonLocalizedSeriesFound = infos
.Where(i => !i.IsSpecial) .Where(i => !i.IsSpecial)
.Select(i => i.Series).DistinctBy(Parser.Parser.Normalize).ToList(); .Select(i => i.Series)
.DistinctBy(Parser.Parser.Normalize)
.ToList();
if (nonLocalizedSeriesFound.Count == 1) if (nonLocalizedSeriesFound.Count == 1)
{ {
nonLocalizedSeries = nonLocalizedSeriesFound[0]; nonLocalizedSeries = nonLocalizedSeriesFound[0];

View File

@ -35,17 +35,15 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag
// This will be called if the epub is already parsed once then we call and merge the information, if the // This will be called if the epub is already parsed once then we call and merge the information, if the
if (Parser.IsEpub(filePath)) if (Parser.IsEpub(filePath))
{ {
ret.Chapters = Parser.ParseChapter(fileName); ret.Chapters = Parser.ParseChapter(fileName, type);
ret.Series = Parser.ParseSeries(fileName); ret.Series = Parser.ParseSeries(fileName, type);
ret.Volumes = Parser.ParseVolume(fileName); ret.Volumes = Parser.ParseVolume(fileName, type);
} }
else else
{ {
ret.Chapters = type == LibraryType.Comic ret.Chapters = Parser.ParseChapter(fileName, type);
? Parser.ParseComicChapter(fileName) ret.Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName, type);
: Parser.ParseChapter(fileName); ret.Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName, type);
ret.Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName);
ret.Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName);
} }
if (ret.Series == string.Empty || Parser.IsImage(filePath)) if (ret.Series == string.Empty || Parser.IsImage(filePath))
@ -61,7 +59,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag
ret.Edition = edition; ret.Edition = edition;
} }
var isSpecial = type == LibraryType.Comic ? Parser.IsComicSpecial(fileName) : Parser.IsMangaSpecial(fileName); var isSpecial = Parser.IsSpecial(fileName, type);
// We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that
// could cause a problem as Omake is a special term, but there is valid volume/chapter information. // could cause a problem as Omake is a special term, but there is valid volume/chapter information.
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial) if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial)

View File

@ -13,25 +13,25 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer
info.ComicInfo = comicInfo; info.ComicInfo = comicInfo;
// This catches when original library type is Manga/Comic and when parsing with non // This catches when original library type is Manga/Comic and when parsing with non
if (Parser.ParseVolume(info.Series) != Parser.LooseLeafVolume) // Shouldn't this be info.Volume != DefaultVolume? if (Parser.ParseVolume(info.Series, type) != Parser.LooseLeafVolume) // Shouldn't this be info.Volume != DefaultVolume?
{ {
var hasVolumeInTitle = !Parser.ParseVolume(info.Title) var hasVolumeInTitle = !Parser.ParseVolume(info.Title, type)
.Equals(Parser.LooseLeafVolume); .Equals(Parser.LooseLeafVolume);
var hasVolumeInSeries = !Parser.ParseVolume(info.Series) var hasVolumeInSeries = !Parser.ParseVolume(info.Series, type)
.Equals(Parser.LooseLeafVolume); .Equals(Parser.LooseLeafVolume);
if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series))) if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series)))
{ {
// NOTE: I'm not sure the comment is true. I've never seen this triggered // NOTE: I'm not sure the comment is true. I've never seen this triggered
// This is likely a light novel for which we can set series from parsed title // This is likely a light novel for which we can set series from parsed title
info.Series = Parser.ParseSeries(info.Title); info.Series = Parser.ParseSeries(info.Title, type);
info.Volumes = Parser.ParseVolume(info.Title); info.Volumes = Parser.ParseVolume(info.Title, type);
} }
else else
{ {
var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, comicInfo); var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, comicInfo);
info.Merge(info2); info.Merge(info2);
if (hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series) if (hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series, type)
.Equals(Parser.LooseLeafVolume)) .Equals(Parser.LooseLeafVolume))
{ {
// Override the Series name so it groups appropriately // Override the Series name so it groups appropriately

View File

@ -37,8 +37,8 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
FullFilePath = Parser.NormalizePath(filePath), FullFilePath = Parser.NormalizePath(filePath),
Series = string.Empty, Series = string.Empty,
ComicInfo = comicInfo, ComicInfo = comicInfo,
Chapters = Parser.ParseComicChapter(fileName), Chapters = Parser.ParseChapter(fileName, type),
Volumes = Parser.ParseComicVolume(fileName) Volumes = Parser.ParseVolume(fileName, type)
}; };
// See if we can formulate the name from the ComicInfo // See if we can formulate the name from the ComicInfo
@ -78,7 +78,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
} }
// Check if this is a Special/Annual // Check if this is a Special/Annual
info.IsSpecial = Parser.IsComicSpecial(info.Filename) || Parser.IsComicSpecial(info.ComicInfo?.Format); info.IsSpecial = Parser.IsSpecial(info.Filename, type) || Parser.IsSpecial(info.ComicInfo?.Format, type);
// Patch in other information from ComicInfo // Patch in other information from ComicInfo
UpdateFromComicInfo(info); UpdateFromComicInfo(info);

View File

@ -39,13 +39,13 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret) public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret)
{ {
var fallbackFolders = directoryService.GetFoldersTillRoot(rootPath, filePath) var fallbackFolders = directoryService.GetFoldersTillRoot(rootPath, filePath)
.Where(f => !Parser.IsMangaSpecial(f)) .Where(f => !Parser.IsSpecial(f, type))
.ToList(); .ToList();
if (fallbackFolders.Count == 0) if (fallbackFolders.Count == 0)
{ {
var rootFolderName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; var rootFolderName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
var series = Parser.ParseSeries(rootFolderName); var series = Parser.ParseSeries(rootFolderName, type);
if (string.IsNullOrEmpty(series)) if (string.IsNullOrEmpty(series))
{ {
@ -64,16 +64,18 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
{ {
var folder = fallbackFolders[i]; var folder = fallbackFolders[i];
var parsedVolume = type is LibraryType.Manga ? Parser.ParseVolume(folder) : Parser.ParseComicVolume(folder); var parsedVolume = Parser.ParseVolume(folder, type);
var parsedChapter = type is LibraryType.Manga ? Parser.ParseChapter(folder) : Parser.ParseComicChapter(folder); var parsedChapter = Parser.ParseChapter(folder, type);
if (!parsedVolume.Equals(Parser.LooseLeafVolume) || !parsedChapter.Equals(Parser.DefaultChapter)) if (!parsedVolume.Equals(Parser.LooseLeafVolume) || !parsedChapter.Equals(Parser.DefaultChapter))
{ {
if ((string.IsNullOrEmpty(ret.Volumes) || ret.Volumes.Equals(Parser.LooseLeafVolume)) && !string.IsNullOrEmpty(parsedVolume) && !parsedVolume.Equals(Parser.LooseLeafVolume)) if ((string.IsNullOrEmpty(ret.Volumes) || ret.Volumes.Equals(Parser.LooseLeafVolume))
&& !string.IsNullOrEmpty(parsedVolume) && !parsedVolume.Equals(Parser.LooseLeafVolume))
{ {
ret.Volumes = parsedVolume; ret.Volumes = parsedVolume;
} }
if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) && !string.IsNullOrEmpty(parsedChapter) && !parsedChapter.Equals(Parser.DefaultChapter)) if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter))
&& !string.IsNullOrEmpty(parsedChapter) && !parsedChapter.Equals(Parser.DefaultChapter))
{ {
ret.Chapters = parsedChapter; ret.Chapters = parsedChapter;
} }
@ -82,7 +84,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
// Generally users group in series folders. Let's try to parse series from the top folder // Generally users group in series folders. Let's try to parse series from the top folder
if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1) if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1)
{ {
var series = Parser.ParseSeries(folder); var series = Parser.ParseSeries(folder, type);
if (string.IsNullOrEmpty(series)) if (string.IsNullOrEmpty(series))
{ {

View File

@ -1,6 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -722,20 +722,37 @@ public static class Parser
return int.Parse(match); return int.Parse(match);
} }
public static bool IsMangaSpecial(string filePath) public static bool IsSpecial(string? filePath, LibraryType type)
{ {
filePath = ReplaceUnderscores(filePath); return type switch
return MangaSpecialRegex.IsMatch(filePath); {
LibraryType.Manga => IsMangaSpecial(filePath),
LibraryType.Comic => IsComicSpecial(filePath),
LibraryType.Book => IsMangaSpecial(filePath),
LibraryType.Image => IsMangaSpecial(filePath),
LibraryType.LightNovel => IsMangaSpecial(filePath),
LibraryType.ComicVine => IsComicSpecial(filePath),
_ => false
};
} }
public static bool IsComicSpecial(string? filePath) private static bool IsMangaSpecial(string? filePath)
{
if (string.IsNullOrEmpty(filePath)) return false;
filePath = ReplaceUnderscores(filePath);
return MangaSpecialRegex.IsMatch(filePath);
}
private static bool IsComicSpecial(string? filePath)
{ {
if (string.IsNullOrEmpty(filePath)) return false; if (string.IsNullOrEmpty(filePath)) return false;
filePath = ReplaceUnderscores(filePath); filePath = ReplaceUnderscores(filePath);
return ComicSpecialRegex.IsMatch(filePath); return ComicSpecialRegex.IsMatch(filePath);
} }
public static string ParseSeries(string filename)
public static string ParseMangaSeries(string filename)
{ {
foreach (var regex in MangaSeriesRegex) foreach (var regex in MangaSeriesRegex)
{ {
@ -762,7 +779,7 @@ public static class Parser
return string.Empty; return string.Empty;
} }
public static string ParseVolume(string filename) public static string ParseMangaVolume(string filename)
{ {
foreach (var regex in MangaVolumeRegex) foreach (var regex in MangaVolumeRegex)
{ {
@ -798,6 +815,7 @@ public static class Parser
return LooseLeafVolume; return LooseLeafVolume;
} }
private static string FormatValue(string value, bool hasPart) private static string FormatValue(string value, bool hasPart)
{ {
if (!value.Contains('-')) if (!value.Contains('-'))
@ -807,6 +825,7 @@ public static class Parser
var tokens = value.Split("-"); var tokens = value.Split("-");
var from = RemoveLeadingZeroes(tokens[0]); var from = RemoveLeadingZeroes(tokens[0]);
if (tokens.Length != 2) return from; if (tokens.Length != 2) return from;
// Occasionally users will use c01-c02 instead of c01-02, clean any leftover c // Occasionally users will use c01-c02 instead of c01-02, clean any leftover c
@ -818,7 +837,49 @@ public static class Parser
return $"{from}-{to}"; return $"{from}-{to}";
} }
public static string ParseChapter(string filename) public static string ParseSeries(string filename, LibraryType type)
{
return type switch
{
LibraryType.Manga => ParseMangaSeries(filename),
LibraryType.Comic => ParseComicSeries(filename),
LibraryType.Book => ParseMangaSeries(filename),
LibraryType.Image => ParseMangaSeries(filename),
LibraryType.LightNovel => ParseMangaSeries(filename),
LibraryType.ComicVine => ParseComicSeries(filename),
_ => string.Empty
};
}
public static string ParseVolume(string filename, LibraryType type)
{
return type switch
{
LibraryType.Manga => ParseMangaVolume(filename),
LibraryType.Comic => ParseComicVolume(filename),
LibraryType.Book => ParseMangaVolume(filename),
LibraryType.Image => ParseMangaVolume(filename),
LibraryType.LightNovel => ParseMangaVolume(filename),
LibraryType.ComicVine => ParseComicVolume(filename),
_ => LooseLeafVolume
};
}
public static string ParseChapter(string filename, LibraryType type)
{
return type switch
{
LibraryType.Manga => ParseMangaChapter(filename),
LibraryType.Comic => ParseComicChapter(filename),
LibraryType.Book => ParseMangaChapter(filename),
LibraryType.Image => ParseMangaChapter(filename),
LibraryType.LightNovel => ParseMangaChapter(filename),
LibraryType.ComicVine => ParseComicChapter(filename),
_ => DefaultChapter
};
}
private static string ParseMangaChapter(string filename)
{ {
foreach (var regex in MangaChapterRegex) foreach (var regex in MangaChapterRegex)
{ {
@ -847,7 +908,7 @@ public static class Parser
return $"{value}.5"; return $"{value}.5";
} }
public static string ParseComicChapter(string filename) private static string ParseComicChapter(string filename)
{ {
foreach (var regex in ComicChapterRegex) foreach (var regex in ComicChapterRegex)
{ {
@ -1003,7 +1064,7 @@ public static class Parser
return tokens.Min(t => t.AsFloat()); return tokens.Min(t => t.AsFloat());
} }
return float.Parse(range); return range.AsFloat();
} }
catch (Exception) catch (Exception)
{ {
@ -1030,7 +1091,7 @@ public static class Parser
return tokens.Max(t => t.AsFloat()); return tokens.Max(t => t.AsFloat());
} }
return float.Parse(range); return range.AsFloat();
} }
catch (Exception) catch (Exception)
{ {

View File

@ -17,9 +17,7 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc
FullFilePath = Parser.NormalizePath(filePath), FullFilePath = Parser.NormalizePath(filePath),
Series = string.Empty, Series = string.Empty,
ComicInfo = comicInfo, ComicInfo = comicInfo,
Chapters = type == LibraryType.Comic Chapters = Parser.ParseChapter(fileName, type)
? Parser.ParseComicChapter(fileName)
: Parser.ParseChapter(fileName)
}; };
if (type == LibraryType.Book) if (type == LibraryType.Book)
@ -27,8 +25,8 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc
ret.Chapters = Parser.DefaultChapter; ret.Chapters = Parser.DefaultChapter;
} }
ret.Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName); ret.Series = Parser.ParseSeries(fileName, type);
ret.Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName); ret.Volumes = Parser.ParseVolume(fileName, type);
if (ret.Series == string.Empty) if (ret.Series == string.Empty)
{ {
@ -43,7 +41,7 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc
ret.Edition = edition; ret.Edition = edition;
} }
var isSpecial = type == LibraryType.Comic ? Parser.IsComicSpecial(fileName) : Parser.IsMangaSpecial(fileName); var isSpecial = Parser.IsSpecial(fileName, type);
// We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that
// could cause a problem as Omake is a special term, but there is valid volume/chapter information. // could cause a problem as Omake is a special term, but there is valid volume/chapter information.
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial) if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial)

View File

@ -705,7 +705,10 @@ public class ProcessSeries : IProcessSeries
chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture);
chapter.MinNumber = Parser.Parser.MinNumberFromRange(info.Chapters); chapter.MinNumber = Parser.Parser.MinNumberFromRange(info.Chapters);
chapter.MaxNumber = Parser.Parser.MaxNumberFromRange(info.Chapters); chapter.MaxNumber = Parser.Parser.MaxNumberFromRange(info.Chapters);
chapter.SortOrder = info.IssueOrder; if (!chapter.SortOrderLocked)
{
chapter.SortOrder = info.IssueOrder;
}
chapter.Range = chapter.GetNumberTitle(); chapter.Range = chapter.GetNumberTitle();
} }
@ -725,7 +728,8 @@ public class ProcessSeries : IProcessSeries
// Ensure we remove any files that no longer exist AND order // Ensure we remove any files that no longer exist AND order
existingChapter.Files = existingChapter.Files existingChapter.Files = existingChapter.Files
.Where(f => parsedInfos.Any(p => Parser.Parser.NormalizePath(p.FullFilePath) == Parser.Parser.NormalizePath(f.FilePath))) .Where(f => parsedInfos.Any(p => Parser.Parser.NormalizePath(p.FullFilePath) == Parser.Parser.NormalizePath(f.FilePath)))
.OrderByNatural(f => f.FilePath).ToList(); .OrderByNatural(f => f.FilePath)
.ToList();
existingChapter.Pages = existingChapter.Files.Sum(f => f.Pages); existingChapter.Pages = existingChapter.Files.Sum(f => f.Pages);
} }
} }

View File

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

View File

@ -34,7 +34,7 @@
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus"> <ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
<app-icon-and-title [label]="t('publication-status-title')" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}" <app-icon-and-title [label]="t('publication-status-title')" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}"
(click)="handleGoTo(FilterField.PublicationStatus, seriesMetadata.publicationStatus)" (click)="handleGoTo(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
[ngbTooltip]="t('publication-status-tooltip') + ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')'"> [ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
{{pubStatus}} {{pubStatus}}
</app-icon-and-title> </app-icon-and-title>
</ng-container> </ng-container>

View File

@ -16,7 +16,8 @@
</div> </div>
<div class="mb-3" style="width:100%"> <div class="mb-3" style="width:100%">
<label for="email" class="form-label">{{t('email-label')}}</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailTooltip" role="button" tabindex="0"></i> <label for="email" class="form-label">{{t('email-label')}}</label>
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="emailTooltip" role="button" tabindex="0"></i>
<ng-template #emailTooltip>{{t('email-tooltip')}}</ng-template> <ng-template #emailTooltip>{{t('email-tooltip')}}</ng-template>
<span class="visually-hidden" id="email-help"> <span class="visually-hidden" id="email-help">
<ng-container [ngTemplateOutlet]="emailTooltip"></ng-container> <ng-container [ngTemplateOutlet]="emailTooltip"></ng-container>
@ -34,21 +35,24 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Password</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i> <label for="password" class="form-label">Password</label>
<i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
<ng-template #passwordTooltip> <ng-template #passwordTooltip>
{{t('password-validation')}} {{t('password-validation')}}
</ng-template> </ng-template>
<span class="visually-hidden" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span> <span class="visually-hidden" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span>
<input id="password" class="form-control" maxlength="32" minlength="6" pattern="^.{6,32}$" formControlName="password" autocomplete="new-password" <input id="password" class="form-control" maxlength="32" minlength="6" pattern="^.{6,32}$" formControlName="password" autocomplete="new-password"
type="password" aria-describedby="password-help" [class.is-invalid]="registerForm.get('password')?.invalid && registerForm.get('password')?.touched"> type="password" aria-describedby="password-help" [class.is-invalid]="registerForm.get('password')?.invalid && registerForm.get('password')?.touched">
<div id="password-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched"> @if (registerForm.dirty || registerForm.touched) {
<div *ngIf="registerForm.get('password')?.errors?.required"> <div id="password-validations" class="invalid-feedback">
{{t('required-field')}} @if (registerForm.get('password')?.errors?.required) {
<div>{{t('required-field')}}</div>
}
@if (registerForm.get('password')?.errors?.minlength || registerForm.get('password')?.errors?.maxLength || registerForm.get('password')?.errors?.pattern) {
<div>{{t('password-validation')}}</div>
}
</div> </div>
<div *ngIf="registerForm.get('password')?.errors?.minlength || registerForm.get('password')?.errors?.maxLength || registerForm.get('password')?.errors?.pattern"> }
{{t('password-validation')}}
</div>
</div>
</div> </div>
<div class="float-end"> <div class="float-end">

View File

@ -27,7 +27,8 @@ export class RegisterComponent {
registerForm: FormGroup = new FormGroup({ registerForm: FormGroup = new FormGroup({
email: new FormControl('', [Validators.required]), email: new FormControl('', [Validators.required]),
username: new FormControl('', [Validators.required]), username: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6), Validators.pattern("^.{6,32}$")]), password: new FormControl('', [Validators.required, Validators.maxLength(32),
Validators.minLength(6), Validators.pattern("^.{6,32}$")]),
}); });
private readonly navService = inject(NavService); private readonly navService = inject(NavService);

View File

@ -382,6 +382,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
// This is a lone chapter // This is a lone chapter
if (vol.length === 0) { if (vol.length === 0) {
if (this.currentlyReadingChapter.minNumber === LooseLeafOrDefaultNumber) {
return this.currentlyReadingChapter.titleName;
}
return 'Ch ' + this.currentlyReadingChapter.minNumber; // TODO: Refactor this to use DisplayTitle (or Range) and Localize it return 'Ch ' + this.currentlyReadingChapter.minNumber; // TODO: Refactor this to use DisplayTitle (or Range) and Localize it
} }
@ -748,7 +751,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
} else { } else {
if (this.libraryType == LibraryType.Comic || this.libraryType == LibraryType.ComicVine) { if (this.libraryType == LibraryType.Comic || this.libraryType == LibraryType.ComicVine) {
if (this.chapters.length === 0) { if (this.chapters.length === 0) {
this.activeTabId = TabID.Specials; if (this.specials.length > 0) {
this.activeTabId = TabID.Specials;
} else {
this.activeTabId = TabID.Volumes;
}
} else { } else {
this.activeTabId = TabID.Chapters; this.activeTabId = TabID.Chapters;
} }

View File

@ -64,16 +64,26 @@ export class EditListComponent implements OnInit {
} }
remove(index: number) { remove(index: number) {
const tokens = this.combinedItems.split(',');
const tokenToRemove = tokens[index];
this.combinedItems = tokens.filter(t => t != tokenToRemove).join(','); const initialControls = Object.keys(this.form.controls)
for (const [index, [key, value]] of Object.entries(Object.entries(this.form.controls))) { .filter(key => key.startsWith('link'));
if (key.startsWith('link') && this.form.get(key)?.value === tokenToRemove) {
this.form.removeControl('link' + index, {emitEvent: true}); if (index == 0 && initialControls.length === 1) {
} this.form.get(initialControls[0])?.setValue('', {emitEvent: true});
this.emit();
this.cdRef.markForCheck();
return;
} }
// Remove the form control explicitly then rebuild the combinedItems
this.form.removeControl('link' + index, {emitEvent: true});
this.combinedItems = Object.keys(this.form.controls)
.filter(key => key.startsWith('link'))
.map(key => this.form.get(key)?.value)
.join(',');
this.emit(); this.emit();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }

View File

@ -8,8 +8,8 @@
<div>{{t('no-data')}}</div> <div>{{t('no-data')}}</div>
} @else { } @else {
<ngb-progressbar-stacked> <ngb-progressbar-stacked>
<ngb-progressbar type="danger" [showValue]="true" [value]="errorPercent" [ngbTooltip]="t('errored-series-label') + ' ' + breakdown.erroredSeries"></ngb-progressbar> <ngb-progressbar class="progress-bar-danger" type="danger" [showValue]="true" [value]="20" [ngbTooltip]="t('errored-series-label') + ' ' + breakdown.erroredSeries"></ngb-progressbar>
<ngb-progressbar type="success" [showValue]="true" [value]="completedPercent" [ngbTooltip]="t('completed-series-label') + ' ' + breakdown.seriesCompleted"></ngb-progressbar> <ngb-progressbar type="success" [showValue]="true" [value]="80" [ngbTooltip]="t('completed-series-label') + ' ' + breakdown.seriesCompleted"></ngb-progressbar>
</ngb-progressbar-stacked> </ngb-progressbar-stacked>
@if (breakdown.seriesCompleted >= breakdown.totalSeries) { @if (breakdown.seriesCompleted >= breakdown.totalSeries) {
<p>{{t('complete') }} <p>{{t('complete') }}

View File

@ -11,8 +11,12 @@
} }
.error { .error {
color: red; color: var(--error-color);
} }
.completed { .completed {
color: var(--color-5); color: var(--color-5);
} }
.progress-bar-danger.progress-bar {
background-color: var(--error-color);
}

View File

@ -2,7 +2,7 @@
background-color: var(--progress-bg-color); background-color: var(--progress-bg-color);
} }
.progress-bar { .progress-bar.text-bg-success {
background-color: var(--progress-bar-color) !important; background-color: var(--progress-bar-color) !important;
} }
@ -10,3 +10,8 @@
background-image: var(--progress-striped-animated-color); background-image: var(--progress-striped-animated-color);
background-color: unset; background-color: unset;
} }
// Bootstrap sliders:
.form-range::-webkit-slider-thumb {
background-color: var(--primary-color);
}

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.8.0.4" "version": "0.8.0.7"
}, },
"servers": [ "servers": [
{ {
@ -8580,7 +8580,8 @@
} }
} }
} }
} },
"deprecated": true
} }
}, },
"/api/Series/update-rating": { "/api/Series/update-rating": {
@ -14364,6 +14365,10 @@
"description": "The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.", "description": "The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.",
"format": "float" "format": "float"
}, },
"sortOrderLocked": {
"type": "boolean",
"description": "Can the sort order be updated on scan or is it locked from UI"
},
"files": { "files": {
"type": "array", "type": "array",
"items": { "items": {