diff --git a/.editorconfig b/.editorconfig index c24677846..c82009e40 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,7 @@ # Editor configuration, see https://editorconfig.org root = true + [*] charset = utf-8 indent_style = space @@ -22,3 +23,7 @@ indent_size = 2 [*.csproj] indent_size = 2 + +[*.cs] +# Disable SonarLint warning S1075 (Don't use hardcoded url) +dotnet_diagnostic.S1075.severity = none diff --git a/API.Tests/AbstractDbTest.cs b/API.Tests/AbstractDbTest.cs index ade0cceab..77f978e7f 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/API.Tests/AbstractDbTest.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; using System.Data.Common; -using System.IO; -using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -13,7 +10,6 @@ using API.Helpers.Builders; using API.Services; using AutoMapper; using Hangfire; -using Microsoft.AspNetCore.Identity; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/API.Tests/Converters/CronConverterTests.cs b/API.Tests/Converters/CronConverterTests.cs index 4e214e8f1..5568c89d0 100644 --- a/API.Tests/Converters/CronConverterTests.cs +++ b/API.Tests/Converters/CronConverterTests.cs @@ -1,5 +1,4 @@ using API.Helpers.Converters; -using Hangfire; using Xunit; namespace API.Tests.Converters; diff --git a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs index 325b19c5d..227dd2b32 100644 --- a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs +++ b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs @@ -7,7 +7,6 @@ using API.Extensions; using API.Helpers.Builders; using API.Services; using API.Services.Tasks.Scanner.Parser; -using API.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index 4ea9a5a4b..866e0202c 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; using System.Linq; -using API.Data; using API.Data.Misc; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; +using API.Entities.Person; using API.Extensions.QueryExtensions; using API.Helpers.Builders; using Xunit; diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/API.Tests/Extensions/VolumeListExtensionsTests.cs index b8b734c51..bbb8f215c 100644 --- a/API.Tests/Extensions/VolumeListExtensionsTests.cs +++ b/API.Tests/Extensions/VolumeListExtensionsTests.cs @@ -3,7 +3,6 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers.Builders; -using API.Tests.Helpers; using Xunit; namespace API.Tests.Extensions; diff --git a/API.Tests/Helpers/CacheHelperTests.cs b/API.Tests/Helpers/CacheHelperTests.cs index 93dae98d8..3962ba2df 100644 --- a/API.Tests/Helpers/CacheHelperTests.cs +++ b/API.Tests/Helpers/CacheHelperTests.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.IO; using System.IO.Abstractions.TestingHelpers; -using System.Threading; -using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Helpers.Builders; diff --git a/API.Tests/Helpers/ParserInfoHelperTests.cs b/API.Tests/Helpers/ParserInfoHelperTests.cs index 70ce3aa69..0bb7efb9b 100644 --- a/API.Tests/Helpers/ParserInfoHelperTests.cs +++ b/API.Tests/Helpers/ParserInfoHelperTests.cs @@ -1,8 +1,5 @@ using System.Collections.Generic; -using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services.Tasks.Scanner; diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/API.Tests/Helpers/PersonHelperTests.cs index a25af7a07..1a38ccdac 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/API.Tests/Helpers/PersonHelperTests.cs @@ -1,15 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Xunit; namespace API.Tests.Helpers; diff --git a/API.Tests/Helpers/ScannerHelper.cs b/API.Tests/Helpers/ScannerHelper.cs index 6abe5b01b..653efebb1 100644 --- a/API.Tests/Helpers/ScannerHelper.cs +++ b/API.Tests/Helpers/ScannerHelper.cs @@ -26,6 +26,7 @@ using NSubstitute; using Xunit.Abstractions; namespace API.Tests.Helpers; +#nullable enable public class ScannerHelper { diff --git a/API.Tests/Helpers/SeriesHelperTests.cs b/API.Tests/Helpers/SeriesHelperTests.cs index a5b5a063b..22b4a3cd1 100644 --- a/API.Tests/Helpers/SeriesHelperTests.cs +++ b/API.Tests/Helpers/SeriesHelperTests.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using API.Data; using API.Entities; using API.Entities.Enums; using API.Extensions; diff --git a/API.Tests/Helpers/StringHelperTests.cs b/API.Tests/Helpers/StringHelperTests.cs index 6ae079c3e..1eac0a6ed 100644 --- a/API.Tests/Helpers/StringHelperTests.cs +++ b/API.Tests/Helpers/StringHelperTests.cs @@ -1,5 +1,4 @@ -using System; -using API.Helpers; +using API.Helpers; using Xunit; namespace API.Tests.Helpers; diff --git a/API.Tests/Parsers/BookParserTests.cs b/API.Tests/Parsers/BookParserTests.cs index 6be0fe386..90147ac6b 100644 --- a/API.Tests/Parsers/BookParserTests.cs +++ b/API.Tests/Parsers/BookParserTests.cs @@ -1,5 +1,4 @@ using System.IO.Abstractions.TestingHelpers; -using API.Data.Metadata; using API.Entities.Enums; using API.Services; using API.Services.Tasks.Scanner.Parser; diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index a975cc7ee..82d9e51e7 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -1,18 +1,10 @@ using API.Entities.Enums; using Xunit; -using Xunit.Abstractions; namespace API.Tests.Parsing; public class MangaParsingTests { - private readonly ITestOutputHelper _testOutputHelper; - - public MangaParsingTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - [Theory] [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")] [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "1")] @@ -84,6 +76,7 @@ public class MangaParsingTests [InlineData("Accel World Chapter 001 Volume 002", "2")] [InlineData("Accel World Volume 2", "2")] [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")] + [InlineData("Zom 100 - Bucket List of the Dead v01", "1")] public void ParseVolumeTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga)); @@ -212,6 +205,8 @@ public class MangaParsingTests [InlineData("不安の種\uff0b - 01", "不安の種\uff0b")] [InlineData("Giant Ojou-sama - Ch. 33.5 - Volume 04 Bonus Chapter", "Giant Ojou-sama")] [InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")] + [InlineData("Monster #8 Ch. 001", "Monster #8")] + [InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga)); @@ -304,6 +299,7 @@ public class MangaParsingTests [InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")] [InlineData("Max Level Returner ตอนที่ 5", "5")] [InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")] + [InlineData("Monster #8 Ch. 001", "1")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga)); diff --git a/API.Tests/Parsing/ParsingTests.cs b/API.Tests/Parsing/ParsingTests.cs index 85ea1a858..7d5da4f9c 100644 --- a/API.Tests/Parsing/ParsingTests.cs +++ b/API.Tests/Parsing/ParsingTests.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Linq; -using System.Runtime.InteropServices; using Xunit; using static API.Services.Tasks.Scanner.Parser.Parser; diff --git a/API.Tests/Repository/CollectionTagRepositoryTests.cs b/API.Tests/Repository/CollectionTagRepositoryTests.cs index 6abf3f7e7..5318260be 100644 --- a/API.Tests/Repository/CollectionTagRepositoryTests.cs +++ b/API.Tests/Repository/CollectionTagRepositoryTests.cs @@ -15,7 +15,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; namespace API.Tests.Repository; diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/API.Tests/Repository/SeriesRepositoryTests.cs index 73ed58a5a..5705e1bc0 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/API.Tests/Repository/SeriesRepositoryTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using API.Data; using API.Entities; using API.Entities.Enums; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index 260676843..8cf93df37 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -7,7 +7,6 @@ using System.Linq; using API.Archive; using API.Entities.Enums; using API.Services; -using EasyCaching.Core; using Microsoft.Extensions.Logging; using NetVips; using NSubstitute; diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs index 4a34ec3d3..aac5724f7 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/API.Tests/Services/BackupServiceTests.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using System.Data.Common; -using System.IO; +using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; -using API.Entities; using API.Entities.Enums; using API.Helpers.Builders; using API.Services; diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index de87b9b6a..af5805cc5 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -1,7 +1,6 @@ using System.IO; using System.IO.Abstractions; using API.Services; -using EasyCaching.Core; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -92,18 +91,17 @@ public class BookServiceTests Assert.Equal("Georges Bizet \\(1838-1875\\)", comicInfo.Writer); } - // TODO: Get the file from microtherion - // [Fact] - // public void ShouldUsePdfInfoDict() - // { - // var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs"); - // var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf"); - // var comicInfo = _bookService.GetComicInfo(document); - // Assert.NotNull(comicInfo); - // Assert.Equal("Rollo at Work", comicInfo.Title); - // Assert.Equal("Jacob Abbott", comicInfo.Writer); - // Assert.Equal(2008, comicInfo.Year); - // } + //[Fact] + public void ShouldUsePdfInfoDict() + { + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs"); + var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf"); + var comicInfo = _bookService.GetComicInfo(document); + Assert.NotNull(comicInfo); + Assert.Equal("Rollo at Work", comicInfo.Title); + Assert.Equal("Jacob Abbott", comicInfo.Writer); + Assert.Equal(2008, comicInfo.Year); + } [Fact] public void ShouldHandleIndirectPdfObjects() diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 9c7d87737..596fbbc4d 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -9,12 +9,9 @@ using API.Data.Repositories; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; -using API.SignalR; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 72b3015b8..5c1752cd8 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; -using System.Data.Common; +using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Metadata; -using API.Entities; using API.Entities.Enums; using API.Helpers.Builders; using API.Services; diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index d355378a7..0f1e9e9da 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; -using API.Data; using API.Data.Repositories; using API.DTOs.Filtering; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Extensions; using API.Helpers; using API.Helpers.Builders; diff --git a/API.Tests/Services/CollectionTagServiceTests.cs b/API.Tests/Services/CollectionTagServiceTests.cs index f2fe14a81..14ce131d8 100644 --- a/API.Tests/Services/CollectionTagServiceTests.cs +++ b/API.Tests/Services/CollectionTagServiceTests.cs @@ -13,7 +13,6 @@ using API.Services; using API.Services.Plus; using API.SignalR; using Kavita.Common; -using Microsoft.EntityFrameworkCore; using NSubstitute; using Xunit; diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 27e40c3e9..127bceb7a 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using API.Constants; @@ -12,6 +11,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Entities.MetadataMatching; +using API.Entities.Person; using API.Helpers.Builders; using API.Services.Plus; using API.Services.Tasks.Metadata; @@ -21,8 +21,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -using Xunit.Abstractions; -using YamlDotNet.Serialization; namespace API.Tests.Services; @@ -31,17 +29,14 @@ namespace API.Tests.Services; /// public class ExternalMetadataServiceTests : AbstractDbTest { - private readonly ITestOutputHelper _testOutputHelper; private readonly ExternalMetadataService _externalMetadataService; private readonly Dictionary _genreLookup = new Dictionary(); private readonly Dictionary _tagLookup = new Dictionary(); private readonly Dictionary _personLookup = new Dictionary(); - public ExternalMetadataServiceTests(ITestOutputHelper testOutputHelper) + public ExternalMetadataServiceTests() { - _testOutputHelper = testOutputHelper; - // Set up Hangfire to use in-memory storage for testing GlobalConfiguration.Configuration.UseInMemoryStorage(); diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index ac3c3157f..a1073a55b 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -1,14 +1,9 @@ -using System.Drawing; -using System.IO; -using System.IO.Abstractions; +using System.IO; using System.Linq; using System.Text; using API.Entities.Enums; using API.Services; -using EasyCaching.Core; -using Microsoft.Extensions.Logging; using NetVips; -using NSubstitute; using Xunit; using Image = NetVips.Image; @@ -28,6 +23,7 @@ public class ImageServiceTests public void GenerateBaseline() { GenerateFiles(BaselinePattern); + Assert.True(true); } /// @@ -38,6 +34,7 @@ public class ImageServiceTests { GenerateFiles(OutputPattern); GenerateHtmlFile(); + Assert.True(true); } private void GenerateFiles(string outputExtension) @@ -159,7 +156,7 @@ public class ImageServiceTests // Step 4: Generate HTML file GenerateHtmlFileForColorScape(); - + Assert.True(true); } private static void GenerateColorImage(string hexColor, string outputPath) diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index f34711305..f81ebd3c4 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -1,29 +1,19 @@ using System; using System.Collections.Generic; -using System.Data.Common; using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using API.Data; using API.Data.Metadata; using API.Data.Repositories; -using API.Entities; using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; using API.Services; using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; using API.SignalR; using API.Tests.Helpers; -using AutoMapper; using Hangfire; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -391,7 +381,7 @@ public class ParseScannedFilesTests : AbstractDbTest var executionerAndHerWayOfLife = postLib.Series.First(x => x.Name == "The Executioner and Her Way of Life"); Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Count); - Thread.Sleep(1100); // Ensure at least one second has passed since library scan + await Task.Delay(1100); // Ensure at least one second has passed since library scan // Add a new chapter to a volume of the series, and scan. Validate that only, and all directories of this // series are marked as HasChanged @@ -440,7 +430,7 @@ public class ParseScannedFilesTests : AbstractDbTest var frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End"); Assert.Equal(2, frieren.Volumes.Count); - Thread.Sleep(1100); // Ensure at least one second has passed since library scan + await Task.Delay(1100); // Ensure at least one second has passed since library scan // Add a volume to a series, and scan. Ensure only this series is marked as HasChanged var executionerCopyDir = Path.Join(Path.Join(testDirectoryPath, "YenPress"), "The Executioner and Her Way of Life"); @@ -483,7 +473,7 @@ public class ParseScannedFilesTests : AbstractDbTest // Needs to be actual time as the write time is now, so if we set LastFolderChecked in the past // it'll always a scan as it was changed since the last scan. - Thread.Sleep(1100); // Ensure at least one second has passed since library scan + await Task.Delay(1100); // Ensure at least one second has passed since library scan var res = await psf.ScanFiles(testDirectoryPath, true, await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); diff --git a/API.Tests/Services/ProcessSeriesTests.cs b/API.Tests/Services/ProcessSeriesTests.cs index 0fbe5db12..119e1bc10 100644 --- a/API.Tests/Services/ProcessSeriesTests.cs +++ b/API.Tests/Services/ProcessSeriesTests.cs @@ -1,19 +1,4 @@ -using System.IO; -using API.Data; -using API.Data.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.SignalR; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; +namespace API.Tests.Services; public class ProcessSeriesTests { diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 3dd929a4b..102ea3b81 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1,25 +1,20 @@ using System.Collections.Generic; using System.Data.Common; -using System.Globalization; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; -using API.DTOs; using API.DTOs.Progress; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; -using API.Services.Tasks; using API.SignalR; -using API.Tests.Helpers; using AutoMapper; using Hangfire; using Hangfire.InMemory; diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index 7157aa90f..7a6ed3e0b 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -11,15 +11,11 @@ using API.DTOs.ReadingLists; using API.DTOs.ReadingLists.CBL; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; -using API.Services.Tasks; using API.SignalR; -using API.Tests.Helpers; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index aea254e51..5addc767d 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -1,34 +1,16 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.Abstractions; -using System.IO.Compression; using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading; 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; using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using API.Tests.Helpers; using Hangfire; -using Microsoft.Extensions.Logging; -using NSubstitute; using Xunit; using Xunit.Abstractions; diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 66f89713d..11ad9fa1b 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -12,6 +12,7 @@ using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers.Builders; using API.Services; @@ -809,6 +810,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g1 => g1.Title).All(g2 => g2 == "New Genre".SentenceCase())); Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked @@ -847,6 +849,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked @@ -887,6 +890,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); Assert.True(series.Metadata.PublisherLocked); @@ -976,6 +980,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.False(series.Metadata.People.Any()); } @@ -1010,6 +1015,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "Existing Genre".SentenceCase())); Assert.True(series.Metadata.GenresLocked); @@ -1039,6 +1045,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Equal(0, series.Metadata.ReleaseYear); Assert.False(series.Metadata.ReleaseYearLocked); @@ -1071,6 +1078,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); Assert.False(series.Metadata.GenresLocked); // Ensure the lock is not activated unless specified. @@ -1104,6 +1112,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.DoesNotContain("Existing Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); @@ -1137,6 +1146,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Empty(series.Metadata.Genres); } @@ -1168,6 +1178,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); } @@ -1200,6 +1211,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.DoesNotContain("Existing Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); @@ -1233,6 +1245,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Empty(series.Metadata.Tags); } @@ -1363,7 +1376,7 @@ public class SeriesServiceTests : AbstractDbTest #endregion - #region SeriesRelation + #region Series Relation [Fact] public async Task UpdateRelatedSeries_ShouldAddAllRelations() { @@ -1431,6 +1444,7 @@ public class SeriesServiceTests : AbstractDbTest addRelationDto.Sequels.Add(2); await _seriesService.UpdateRelatedSeries(addRelationDto); Assert.NotNull(series1); + Assert.NotNull(series2); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); Assert.Equal(1, series2.Relations.Single(s => s.TargetSeriesId == 1).TargetSeriesId); } @@ -1473,8 +1487,9 @@ public class SeriesServiceTests : AbstractDbTest // Remove relations var removeRelationDto = CreateRelationsDto(series1); await _seriesService.UpdateRelatedSeries(removeRelationDto); - Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 1)); - Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 2)); + Assert.NotNull(series1); + Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 1); + Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 2); } @@ -1507,6 +1522,8 @@ public class SeriesServiceTests : AbstractDbTest var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); await _seriesService.UpdateRelatedSeries(addRelationDto); + + Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); _context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/API.Tests/Services/TachiyomiServiceTests.cs index 1e5127865..17e26139c 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/API.Tests/Services/TachiyomiServiceTests.cs @@ -1,7 +1,5 @@ -using API.Extensions; -using API.Helpers.Builders; +using API.Helpers.Builders; using API.Services.Plus; -using API.Services.Tasks; namespace API.Tests.Services; using System.Collections.Generic; @@ -16,7 +14,6 @@ using API.Entities.Enums; using API.Helpers; using API.Services; using SignalR; -using Helpers; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf b/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf new file mode 100644 index 000000000..0e0ffa8c7 Binary files /dev/null and b/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf differ diff --git a/API.Tests/Services/VersionUpdaterServiceTests.cs b/API.Tests/Services/VersionUpdaterServiceTests.cs index 5bdc25c31..c7a8a14d8 100644 --- a/API.Tests/Services/VersionUpdaterServiceTests.cs +++ b/API.Tests/Services/VersionUpdaterServiceTests.cs @@ -1,16 +1,11 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net.Http; -using System.Reflection; using System.Threading.Tasks; using API.DTOs.Update; -using API.Extensions; using API.Services; using API.Services.Tasks; using API.SignalR; -using Flurl.Http; using Flurl.Http.Testing; using Kavita.Common.EnvironmentInfo; using Microsoft.Extensions.Logging; diff --git a/API.Tests/Services/WordCountAnalysisTests.cs b/API.Tests/Services/WordCountAnalysisTests.cs index ae17172b2..8c8c4193c 100644 --- a/API.Tests/Services/WordCountAnalysisTests.cs +++ b/API.Tests/Services/WordCountAnalysisTests.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; @@ -11,10 +10,8 @@ using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; -using API.Services.Tasks; using API.Services.Tasks.Metadata; using API.SignalR; -using API.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API/API.csproj b/API/API.csproj index 71397c49c..512174a56 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -98,10 +98,10 @@ - + - + diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index e98edfb6a..31983cda2 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -8,6 +8,7 @@ using API.Data.Repositories; using API.DTOs; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Services; diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 9604592c7..5405513d6 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -351,27 +351,6 @@ public class LibraryController : BaseApiController return Ok(); } - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("analyze")] - public ActionResult Analyze(int libraryId) - { - _taskScheduler.AnalyzeFilesForLibrary(libraryId, true); - return Ok(); - } - - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("analyze-multiple")] - public ActionResult AnalyzeMultiple(BulkActionDto dto) - { - foreach (var libraryId in dto.Ids) - { - _taskScheduler.AnalyzeFilesForLibrary(libraryId, dto.Force ?? false); - } - - return Ok(); - } - - /// /// Copy the library settings (adv tab + optional type) to a set of other libraries. /// diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index e4233e36f..6ec781758 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -9,7 +9,6 @@ using API.DTOs.ReadingLists; using API.Extensions; using API.Helpers; using API.Services; -using API.SignalR; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -39,7 +38,7 @@ public class ReadingListController : BaseApiController /// /// [HttpGet] - public async Task> GetList(int readingListId) + public async Task> GetList(int readingListId) { var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId()); if (readingList == null) @@ -268,7 +267,7 @@ public class ReadingListController : BaseApiController var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist")); var chapterIdsForSeries = - await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId}); + await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]); // If there are adds, tell tracking this has been modified if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList)) diff --git a/API/Controllers/ScrobblingController.cs b/API/Controllers/ScrobblingController.cs index 19e9d36b2..ba4b09c2d 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/API/Controllers/ScrobblingController.cs @@ -54,7 +54,7 @@ public class ScrobblingController : BaseApiController } /// - /// Get the current user's MAL token & username + /// Get the current user's MAL token and username /// /// [HttpGet("mal-token")] diff --git a/API/DTOs/Account/UpdateUserDto.cs b/API/DTOs/Account/UpdateUserDto.cs index ef19973f5..c40124b7b 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/API/DTOs/Account/UpdateUserDto.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Account; +#nullable enable public record UpdateUserDto { diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 634ced4e9..70c77e92d 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -5,6 +5,7 @@ using API.Entities.Enums; using API.Entities.Interfaces; namespace API.DTOs; +#nullable enable /// /// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying @@ -188,8 +189,8 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage #endregion public string CoverImage { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string PrimaryColor { get; set; } = string.Empty; + public string SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/DTOs/Collection/MalStackDto.cs b/API/DTOs/Collection/MalStackDto.cs index 3144f6c72..d9d902e88 100644 --- a/API/DTOs/Collection/MalStackDto.cs +++ b/API/DTOs/Collection/MalStackDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.Collection; +#nullable enable /// /// Represents an Interest Stack from MAL diff --git a/API/DTOs/ColorScape.cs b/API/DTOs/ColorScape.cs index 39d1446dd..d95346af7 100644 --- a/API/DTOs/ColorScape.cs +++ b/API/DTOs/ColorScape.cs @@ -1,4 +1,5 @@ namespace API.DTOs; +#nullable enable /// /// A primary and secondary color diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index 99d4c619d..547bb63a8 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs @@ -1,6 +1,7 @@ using API.DTOs.Scrobbling; namespace API.DTOs.KavitaPlus.ExternalMetadata; +#nullable enable /// /// Used for matching and fetching metadata on a series diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index 00806aef8..f63fe5a9e 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -2,6 +2,7 @@ using API.DTOs.Scrobbling; namespace API.DTOs.KavitaPlus.ExternalMetadata; +#nullable enable internal class MatchSeriesRequestDto { diff --git a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs b/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs index 140c41e4c..eedbed2ef 100644 --- a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs +++ b/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.KavitaPlus.License; +#nullable enable public class EncryptLicenseDto { diff --git a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs b/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs index d5d6847ba..4621810f0 100644 --- a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs +++ b/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.KavitaPlus.License; +#nullable enable public class UpdateLicenseDto { diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs b/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs index 4cb8a54ee..bb5a3f20a 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs +++ b/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs @@ -1,4 +1,5 @@ namespace API.DTOs.KavitaPlus.Metadata; +#nullable enable public enum CharacterRole { diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 3f0983067..9f2f19a42 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -2,6 +2,7 @@ using API.Entities.Enums; namespace API.DTOs; +#nullable enable public class MangaFileDto { diff --git a/API/DTOs/Person/PersonDto.cs b/API/DTOs/Person/PersonDto.cs index aa0f0680c..511317f2a 100644 --- a/API/DTOs/Person/PersonDto.cs +++ b/API/DTOs/Person/PersonDto.cs @@ -1,4 +1,7 @@ +using System.Runtime.Serialization; + namespace API.DTOs; +#nullable enable public class PersonDto { @@ -6,12 +9,12 @@ public class PersonDto public required string Name { get; set; } public bool CoverImageLocked { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string? PrimaryColor { get; set; } + public string? SecondaryColor { get; set; } public string? CoverImage { get; set; } - public string Description { get; set; } + public string? Description { get; set; } /// /// ASIN for person /// diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/API/DTOs/Person/UpdatePersonDto.cs index 78eb54aaf..d21fb7350 100644 --- a/API/DTOs/Person/UpdatePersonDto.cs +++ b/API/DTOs/Person/UpdatePersonDto.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs; +#nullable enable public class UpdatePersonDto { diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/API/DTOs/Reader/CreatePersonalToCDto.cs index 25526b490..3b80ece4a 100644 --- a/API/DTOs/Reader/CreatePersonalToCDto.cs +++ b/API/DTOs/Reader/CreatePersonalToCDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.Reader; +#nullable enable public class CreatePersonalToCDto { diff --git a/API/DTOs/Scrobbling/MediaRecommendationDto.cs b/API/DTOs/Scrobbling/MediaRecommendationDto.cs index c83694b2b..3f565296b 100644 --- a/API/DTOs/Scrobbling/MediaRecommendationDto.cs +++ b/API/DTOs/Scrobbling/MediaRecommendationDto.cs @@ -2,6 +2,7 @@ using API.Services.Plus; namespace API.DTOs.Scrobbling; +#nullable enable public record MediaRecommendationDto { diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/API/DTOs/Scrobbling/PlusSeriesDto.cs index 75e443d2e..c36516837 100644 --- a/API/DTOs/Scrobbling/PlusSeriesDto.cs +++ b/API/DTOs/Scrobbling/PlusSeriesDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs.Scrobbling; +#nullable enable /// /// Represents information about a potential Series for Kavita+ diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index 298e32180..b62c87866 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -1,6 +1,7 @@ using System; namespace API.DTOs.Scrobbling; +#nullable enable public class ScrobbleEventDto { diff --git a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs index afebbaca4..76e77ae2c 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs +++ b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs @@ -2,6 +2,7 @@ using API.DTOs.Recommendation; namespace API.DTOs.SeriesDetail; +#nullable enable /// /// All the data from Kavita+ for Series Detail diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 214a357b4..6aa1ecefd 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -79,8 +79,8 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage #endregion public string? CoverImage { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string PrimaryColor { get; set; } = string.Empty; + public string SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 45abcc528..2c5e604da 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -3,6 +3,7 @@ using API.Entities.Enums; using API.Services; namespace API.DTOs.Settings; +#nullable enable public class ServerSettingDto { diff --git a/API/DTOs/SideNav/SideNavStreamDto.cs b/API/DTOs/SideNav/SideNavStreamDto.cs index 1f3453611..fdef82a08 100644 --- a/API/DTOs/SideNav/SideNavStreamDto.cs +++ b/API/DTOs/SideNav/SideNavStreamDto.cs @@ -2,6 +2,7 @@ using API.Entities.Enums; namespace API.DTOs.SideNav; +#nullable enable public class SideNavStreamDto { diff --git a/API/DTOs/StandaloneChapterDto.cs b/API/DTOs/StandaloneChapterDto.cs index 6d8b5423d..2f4cd2ee1 100644 --- a/API/DTOs/StandaloneChapterDto.cs +++ b/API/DTOs/StandaloneChapterDto.cs @@ -1,6 +1,7 @@ using API.Entities.Enums; namespace API.DTOs; +#nullable enable /// /// Used on Person Profile page diff --git a/API/DTOs/Statistics/ReadHistoryEvent.cs b/API/DTOs/Statistics/ReadHistoryEvent.cs index 9d3a9436e..496148789 100644 --- a/API/DTOs/Statistics/ReadHistoryEvent.cs +++ b/API/DTOs/Statistics/ReadHistoryEvent.cs @@ -1,6 +1,7 @@ using System; namespace API.DTOs.Statistics; +#nullable enable /// /// Represents a single User's reading event diff --git a/API/DTOs/Statistics/UserReadStatistics.cs b/API/DTOs/Statistics/UserReadStatistics.cs index 5e3f5aa5d..5da4b491e 100644 --- a/API/DTOs/Statistics/UserReadStatistics.cs +++ b/API/DTOs/Statistics/UserReadStatistics.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; namespace API.DTOs.Statistics; +#nullable enable public class UserReadStatistics { diff --git a/API/DTOs/Stats/ServerInfoSlimDto.cs b/API/DTOs/Stats/ServerInfoSlimDto.cs index ef44bb408..0b47fa2f3 100644 --- a/API/DTOs/Stats/ServerInfoSlimDto.cs +++ b/API/DTOs/Stats/ServerInfoSlimDto.cs @@ -1,6 +1,7 @@ using System; namespace API.DTOs.Stats; +#nullable enable /// /// This is just for the Server tab on UI diff --git a/API/DTOs/TachiyomiChapterDto.cs b/API/DTOs/TachiyomiChapterDto.cs index 03e242dfa..ecdd5115c 100644 --- a/API/DTOs/TachiyomiChapterDto.cs +++ b/API/DTOs/TachiyomiChapterDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs; +#nullable enable /// /// This is explicitly for Tachiyomi. Number field was removed in v0.8.0, but Tachiyomi needs it for the hacks. diff --git a/API/DTOs/UpdateSeriesDto.cs b/API/DTOs/UpdateSeriesDto.cs index 52826f9d1..ab4ffcb22 100644 --- a/API/DTOs/UpdateSeriesDto.cs +++ b/API/DTOs/UpdateSeriesDto.cs @@ -1,4 +1,5 @@ namespace API.DTOs; +#nullable enable public class UpdateSeriesDto { diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 577253ada..92b1b6be5 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -5,6 +5,7 @@ using API.Entities.Enums; using API.Entities.Enums.UserPreferences; namespace API.DTOs; +#nullable enable public class UserPreferencesDto { diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index f6413ff6f..8ef22a93b 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -66,8 +66,8 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage public string CoverImage { get; set; } private bool CoverImageLocked { get; set; } - public string PrimaryColor { get; set; } - public string SecondaryColor { get; set; } + public string PrimaryColor { get; set; } = string.Empty; + public string SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 0937be22f..babdada4a 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -12,6 +12,7 @@ using API.Entities.History; using API.Entities.Interfaces; using API.Entities.Metadata; using API.Entities.MetadataMatching; +using API.Entities.Person; using API.Entities.Scrobble; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index 388ca5b7e..78e2dc6c0 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -16,6 +16,7 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; #nullable enable + public interface IAppUserProgressRepository { void Update(AppUserProgress userProgress); @@ -41,7 +42,7 @@ public interface IAppUserProgressRepository Task UpdateAllProgressThatAreMoreThanChapterPages(); Task> GetUserProgressForChapter(int chapterId, int userId = 0); } -#nullable disable + public class AppUserProgressRepository : IAppUserProgressRepository { private readonly DataContext _context; diff --git a/API/Data/Repositories/CoverDbRepository.cs b/API/Data/Repositories/CoverDbRepository.cs index 3563f9357..ed13493ab 100644 --- a/API/Data/Repositories/CoverDbRepository.cs +++ b/API/Data/Repositories/CoverDbRepository.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using API.DTOs.CoverDb; using API.Entities; +using API.Entities.Person; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index 7492ba5dd..ef9dfa7ec 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IGenreRepository { diff --git a/API/Data/Repositories/MediaErrorRepository.cs b/API/Data/Repositories/MediaErrorRepository.cs index c2e932d32..0d3cae2ed 100644 --- a/API/Data/Repositories/MediaErrorRepository.cs +++ b/API/Data/Repositories/MediaErrorRepository.cs @@ -9,6 +9,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IMediaErrorRepository { diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index a100a5046..0fee41557 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using API.DTOs; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Extensions; using API.Extensions.QueryExtensions; using API.Helpers; @@ -14,6 +15,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IPersonRepository { diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 3d43c533e..2296a03cc 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum ReadingListIncludes diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index 0addd7473..177ddfb96 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IScrobbleRepository { diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index d80d479f4..31ddc22f1 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -39,6 +39,7 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum SeriesIncludes diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs index 433ab6edb..90246e75f 100644 --- a/API/Data/Repositories/SettingsRepository.cs +++ b/API/Data/Repositories/SettingsRepository.cs @@ -13,6 +13,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface ISettingsRepository { diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs index 2498dfa60..33517e846 100644 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ b/API/Data/Repositories/SiteThemeRepository.cs @@ -8,6 +8,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface ISiteThemeRepository { diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index 4a7fbf4ab..c4f189957 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -11,6 +11,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface ITagRepository { diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 0e90b617f..3a46ad2a1 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum AppUserIncludes diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index 0dfbd6393..cb0783a89 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -15,6 +15,7 @@ using Kavita.Common; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum VolumeIncludes diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index a00f315c3..fc709961b 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using API.Entities.Enums; using API.Entities.Interfaces; +using API.Entities.Person; using API.Extensions; using API.Services.Tasks.Scanner.Parser; diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index 40d1b10a8..a8d943b2d 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -12,7 +12,7 @@ public enum LibraryType /// /// Uses Comic regex for filename parsing /// - [Description("Comic")] + [Description("Comic (Legacy)")] Comic = 1, /// /// Uses Manga regex for filename parsing also uses epub metadata @@ -30,8 +30,8 @@ public enum LibraryType [Description("Light Novel")] LightNovel = 4, /// - /// Uses Comic regex for filename parsing, uses Comic Vine type of Parsing. Will replace Comic type in future + /// Uses Comic regex for filename parsing, uses Comic Vine type of Parsing /// - [Description("Comic (Comic Vine)")] + [Description("Comic")] ComicVine = 5, } diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index b3e543315..046c07efa 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using API.Entities.Enums; using API.Entities.Interfaces; +using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Entities.Metadata; diff --git a/API/Entities/Person/ChapterPeople.cs b/API/Entities/Person/ChapterPeople.cs index cc0802782..15da3994d 100644 --- a/API/Entities/Person/ChapterPeople.cs +++ b/API/Entities/Person/ChapterPeople.cs @@ -1,6 +1,6 @@ using API.Entities.Enums; -namespace API.Entities; +namespace API.Entities.Person; public class ChapterPeople { diff --git a/API/Entities/Person/Person.cs b/API/Entities/Person/Person.cs index 13f8a9b77..8eed08f5c 100644 --- a/API/Entities/Person/Person.cs +++ b/API/Entities/Person/Person.cs @@ -1,10 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Services.Plus; -namespace API.Entities; +namespace API.Entities.Person; public class Person : IHasCoverImage { diff --git a/API/Entities/Person/SeriesMetadataPeople.cs b/API/Entities/Person/SeriesMetadataPeople.cs index 1f5dd2f5b..caea10cd6 100644 --- a/API/Entities/Person/SeriesMetadataPeople.cs +++ b/API/Entities/Person/SeriesMetadataPeople.cs @@ -1,8 +1,7 @@ using API.Entities.Enums; using API.Entities.Metadata; -using API.Services.Plus; -namespace API.Entities; +namespace API.Entities.Person; public class SeriesMetadataPeople { diff --git a/API/Extensions/FlurlExtensions.cs b/API/Extensions/FlurlExtensions.cs index efd805045..62d8543b6 100644 --- a/API/Extensions/FlurlExtensions.cs +++ b/API/Extensions/FlurlExtensions.cs @@ -4,6 +4,7 @@ using Kavita.Common; using Kavita.Common.EnvironmentInfo; namespace API.Extensions; +#nullable enable public static class FlurlExtensions { diff --git a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs index f6606026b..cc40491d0 100644 --- a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs @@ -4,7 +4,7 @@ using API.Data.Misc; using API.Data.Repositories; using API.Entities; using API.Entities.Metadata; -using AutoMapper.QueryableExtensions; +using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions.Filtering; diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index ebc233056..fc3314f58 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using API.Data.Misc; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; namespace API.Extensions.QueryExtensions; #nullable enable diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 3d9de9ea7..69ed884fd 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -30,6 +30,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Entities.MetadataMatching; +using API.Entities.Person; using API.Entities.Scrobble; using API.Extensions.QueryExtensions.Filtering; using API.Helpers.Converters; diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index 4d09a7abf..348696820 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Services.Tasks.Scanner.Parser; namespace API.Helpers.Builders; diff --git a/API/Helpers/Builders/PersonBuilder.cs b/API/Helpers/Builders/PersonBuilder.cs index 2bbdfa744..492d79e17 100644 --- a/API/Helpers/Builders/PersonBuilder.cs +++ b/API/Helpers/Builders/PersonBuilder.cs @@ -2,6 +2,7 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; namespace API.Helpers.Builders; diff --git a/API/Helpers/Builders/SeriesMetadataBuilder.cs b/API/Helpers/Builders/SeriesMetadataBuilder.cs index b94e3e4c3..8ceb16d95 100644 --- a/API/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/API/Helpers/Builders/SeriesMetadataBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; namespace API.Helpers.Builders; diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index 193513453..fe437daa8 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -7,6 +6,7 @@ using API.DTOs; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers.Builders; diff --git a/API/Program.cs b/API/Program.cs index 425b60654..77fac9e49 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -56,9 +56,6 @@ public class Program Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); } - Configuration.KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development - ? "http://localhost:5020" : "https://plus.kavitareader.com"; - try { var host = CreateHostBuilder(args).Build(); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 30d40b72b..f8f1ef222 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -73,12 +73,27 @@ public class BookService : IBookService private const string BookApiUrl = "book-resources?file="; private readonly PdfComicInfoExtractor _pdfComicInfoExtractor; + /// + /// Setup the most lenient book parsing options possible as people have some really bad epubs + /// public static readonly EpubReaderOptions BookReaderOptions = new() { PackageReaderOptions = new PackageReaderOptions { IgnoreMissingToc = true, - SkipInvalidManifestItems = true + SkipInvalidManifestItems = true, + }, + Epub2NcxReaderOptions = new Epub2NcxReaderOptions + { + IgnoreMissingContentForNavigationPoints = true + }, + SpineReaderOptions = new SpineReaderOptions + { + IgnoreMissingManifestItems = true + }, + BookCoverReaderOptions = new BookCoverReaderOptions + { + Epub2MetadataIgnoreMissingManifestItem = true } }; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 22bc7ff1b..5fd6e68d4 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -11,6 +11,7 @@ using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Helpers.Builders; @@ -73,7 +74,7 @@ public class SeriesService : ISeriesService } /// - /// Returns the first chapter for a series to extract metadata from (ie Summary, etc) + /// Returns the first chapter for a series to extract metadata from (ie Summary, etc.) /// /// The full series with all volumes and chapters on it /// @@ -324,7 +325,7 @@ public class SeriesService : ISeriesService await _unitOfWork.CommitAsync(); - // Trigger code to cleanup tags, collections, people, etc + // Trigger code to clean up tags, collections, people, etc try { await _taskScheduler.CleanupDbEntries(); @@ -915,19 +916,19 @@ public class SeriesService : ISeriesService // Calculate the time differences between consecutive chapters var timeDifferences = new List(); DateTime? previousChapterTime = null; - foreach (var chapter in chapters) + foreach (var chapterCreatedUtc in chapters.Select(c => c.CreatedUtc)) { - if (previousChapterTime.HasValue && (chapter.CreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1)) + if (previousChapterTime.HasValue && (chapterCreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1)) { continue; // Skip this chapter if it's within an hour of the previous one } - if ((chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero) != TimeSpan.Zero) + if ((chapterCreatedUtc - previousChapterTime ?? TimeSpan.Zero) != TimeSpan.Zero) { - timeDifferences.Add(chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero); + timeDifferences.Add(chapterCreatedUtc - previousChapterTime ?? TimeSpan.Zero); } - previousChapterTime = chapter.CreatedUtc; + previousChapterTime = chapterCreatedUtc; } if (timeDifferences.Count < minimumTimeDeltas) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 46ba18abf..c3f1a00e5 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -34,7 +34,6 @@ public interface ITaskScheduler void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); - void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false); void CancelStatsTasks(); Task RunStatCollection(); void CovertAllCoversToEncoding(); @@ -267,11 +266,6 @@ public class TaskScheduler : ITaskScheduler RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), RecurringJobOptions); } - public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false) - { - _logger.LogInformation("Enqueuing library file analysis for: {LibraryId}", libraryId); - BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, forceUpdate)); - } /// /// Upon cancelling stat, we do report to the Stat service that we are no longer going to be reporting diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index 6dfb414df..59214e116 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -8,6 +8,7 @@ using API.Data; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Extensions; using API.SignalR; using EasyCaching.Core; diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 992dcf108..163954ba7 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -708,7 +708,7 @@ public static partial class Parser return HasSpecialMarker(filePath); } - public static string ParseMangaSeries(string filename) + private static string ParseMangaSeries(string filename) { foreach (var regex in MangaSeriesRegex) { @@ -716,6 +716,7 @@ public static partial class Parser var group = matches .Select(match => match.Groups["Series"]) .FirstOrDefault(group => group.Success && group != Match.Empty); + if (group != null) { return CleanTitle(group.Value); diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 5e1a81906..61199d106 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -11,6 +11,7 @@ using API.Data.Repositories; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Entities.Person; using API.Extensions; using API.Helpers; using API.Helpers.Builders; diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index ab5f4ae2b..3dca14ab9 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -302,7 +302,8 @@ public class ThemeService : IThemeService var existingThemes = _directoryService.ScanFiles(_directoryService.SiteThemeDirectory, string.Empty); if (existingThemes.Any(f => Path.GetFileName(f) == dto.CssFile)) { - throw new KavitaException("Cannot download file, file already on disk"); + // This can happen if you delete then immediately download (to refresh). We should just delete the old file and download. Users can always rollback their version with github directly + _directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile)); } var finalLocation = await DownloadSiteTheme(dto); diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 43b3d9c6a..119571ffc 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -346,7 +346,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc <= CacheDuration) { var cachedData = await File.ReadAllTextAsync(_cacheFilePath); - return System.Text.Json.JsonSerializer.Deserialize>(cachedData); + return JsonSerializer.Deserialize>(cachedData); } return null; diff --git a/API/Startup.cs b/API/Startup.cs index 262d3c95a..c650e018a 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -41,6 +41,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using Serilog; +using Swashbuckle.AspNetCore.SwaggerGen; using TaskScheduler = API.Services.TaskScheduler; namespace API; @@ -138,8 +139,8 @@ public class Startup c.SwaggerDoc("v1", new OpenApiInfo { Version = "3.1.0", - Title = "Kavita", - Description = $"Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v{BuildInfo.Version.ToString()}", + Title = $"Kavita (v{BuildInfo.Version})", + Description = $"Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v{BuildInfo.Version}", License = new OpenApiLicense { Name = "GPL-3.0", diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index 00ec84d06..f2d64cde6 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -16,8 +16,9 @@ public static class Configuration public const long DefaultCacheMemory = 75; private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); - public static string KavitaPlusApiUrl = "https://plus.kavitareader.com"; - public static string StatsApiUrl = "https://stats.kavitareader.com"; + public static readonly string KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development + ? "http://localhost:5020" : "https://plus.kavitareader.com"; + public static readonly string StatsApiUrl = "https://stats.kavitareader.com"; public static int Port { @@ -315,6 +316,7 @@ public static class Configuration { public string TokenKey { get; set; } // ReSharper disable once MemberHidesStaticFromOuterClass +#pragma warning disable S3218 public int Port { get; set; } = DefaultHttpPort; // ReSharper disable once MemberHidesStaticFromOuterClass public string IpAddresses { get; set; } = string.Empty; @@ -323,6 +325,7 @@ public static class Configuration // ReSharper disable once MemberHidesStaticFromOuterClass public long Cache { get; set; } = DefaultCacheMemory; // ReSharper disable once MemberHidesStaticFromOuterClass - public bool AllowIFraming { get; set; } = false; + public bool AllowIFraming { get; init; } = false; +#pragma warning restore S3218 } } diff --git a/Kavita.Common/Helpers/CronHelper.cs b/Kavita.Common/Helpers/CronHelper.cs index 77a4e934e..0b40113ce 100644 --- a/Kavita.Common/Helpers/CronHelper.cs +++ b/Kavita.Common/Helpers/CronHelper.cs @@ -13,7 +13,7 @@ public static class CronHelper CronExpression.Parse(cronExpression); return true; } - catch (Exception ex) + catch (Exception) { /* Swallow */ return false; diff --git a/Kavita.Common/Helpers/FlurlConfiguration.cs b/Kavita.Common/Helpers/FlurlConfiguration.cs index 0003546d4..b80dff8d9 100644 --- a/Kavita.Common/Helpers/FlurlConfiguration.cs +++ b/Kavita.Common/Helpers/FlurlConfiguration.cs @@ -28,7 +28,9 @@ public static class FlurlConfiguration if (ConfiguredClients.Contains(host)) return; FlurlHttp.ConfigureClientForUrl(url).ConfigureInnerHandler(cli => +#pragma warning disable S4830 cli.ServerCertificateCustomValidationCallback = (_, _, _, _) => true); +#pragma warning restore S4830 ConfiguredClients.Add(host); } diff --git a/TestData b/TestData deleted file mode 160000 index 4f5750025..000000000 --- a/TestData +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4f5750025a1c0b48cd72eaa6f1b61642c41f147f diff --git a/UI/Web/src/app/_helpers/browser.ts b/UI/Web/src/app/_helpers/browser.ts new file mode 100644 index 000000000..4d92e207c --- /dev/null +++ b/UI/Web/src/app/_helpers/browser.ts @@ -0,0 +1,62 @@ +export const isSafari = [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod' + ].includes(navigator.platform) + // iPad on iOS 13 detection + || (navigator.userAgent.includes("Mac") && "ontouchend" in document); + +/** + * Represents a Version for a browser + */ +export class Version { + major: number; + minor: number; + patch: number; + + constructor(major: number, minor: number, patch: number) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + isLessThan(other: Version): boolean { + if (this.major < other.major) return true; + if (this.major > other.major) return false; + if (this.minor < other.minor) return true; + if (this.minor > other.minor) return false; + return this.patch < other.patch; + } + + isGreaterThan(other: Version): boolean { + if (this.major > other.major) return true; + if (this.major < other.major) return false; + if (this.minor > other.minor) return true; + if (this.minor < other.minor) return false; + return this.patch > other.patch; + } + + isEqualTo(other: Version): boolean { + return ( + this.major === other.major && + this.minor === other.minor && + this.patch === other.patch + ); + } +} + + +export const getIosVersion = () => { + const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + const patch = parseInt(match[3] || '0', 10); + + return new Version(major, minor, patch); + } + return null; +} diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 87ffb56c4..74cabc658 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -1,5 +1,4 @@ import {FileTypeGroup} from "./file-type-group.enum"; -import {IHasCover} from "../common/i-has-cover"; export enum LibraryType { Manga = 0, @@ -10,6 +9,8 @@ export enum LibraryType { ComicVine = 5 } +export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images]; + export interface Library { id: number; name: string; diff --git a/UI/Web/src/app/_pipes/library-type.pipe.ts b/UI/Web/src/app/_pipes/library-type.pipe.ts index 74a62647f..1881b64d5 100644 --- a/UI/Web/src/app/_pipes/library-type.pipe.ts +++ b/UI/Web/src/app/_pipes/library-type.pipe.ts @@ -11,7 +11,7 @@ import {TranslocoService} from "@jsverse/transloco"; }) export class LibraryTypePipe implements PipeTransform { - translocoService = inject(TranslocoService); + private readonly translocoService = inject(TranslocoService); transform(libraryType: LibraryType): string { switch (libraryType) { case LibraryType.Book: diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 9afa400a0..025e5cf29 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -23,6 +23,7 @@ import {Volume} from "../_models/volume"; import {UtilityService} from "../shared/_services/utility.service"; import {translate} from "@jsverse/transloco"; import {ToastrService} from "ngx-toastr"; +import {getIosVersion, isSafari, Version} from "../_helpers/browser"; export const CHAPTER_ID_DOESNT_EXIST = -1; @@ -46,7 +47,8 @@ export class ReaderService { // Override background color for reader and restore it onDestroy private originalBodyColor!: string; - private noSleep = new NoSleep(); + + private noSleep: NoSleep = new NoSleep(); constructor(private httpClient: HttpClient, @Inject(DOCUMENT) private document: Document) { this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => { @@ -56,17 +58,18 @@ export class ReaderService { }); } + enableWakeLock(element?: Element | Document) { // Enable wake lock. // (must be wrapped in a user input event handler e.g. a mouse or touch handler) if (!element) element = this.document; - const enableNoSleepHandler = () => { + const enableNoSleepHandler = async () => { element!.removeEventListener('click', enableNoSleepHandler, false); element!.removeEventListener('touchmove', enableNoSleepHandler, false); element!.removeEventListener('mousemove', enableNoSleepHandler, false); - this.noSleep!.enable(); + await this.noSleep.enable(); }; // Enable wake lock. diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 6ee0cd8b7..4a71e836e 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -41,10 +41,6 @@ export class ServerService { return this.http.post(this.baseUrl + 'server/backup-db', {}); } - analyzeFiles() { - return this.http.post(this.baseUrl + 'server/analyze-files', {}); - } - syncThemes() { return this.http.post(this.baseUrl + 'server/sync-themes', {}); } @@ -58,10 +54,6 @@ export class ServerService { .pipe(map(r => parseInt(r, 10))); } - checkForUpdates() { - return this.http.get(this.baseUrl + 'server/check-for-updates', {}); - } - getChangelog(count: number = 0) { return this.http.get(this.baseUrl + 'server/changelog?count=' + count, {}); } diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html index 7ea7ccf15..711141195 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html @@ -41,7 +41,7 @@

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

- + {{item.title}} @@ -52,7 +52,7 @@

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

- + {{item.title}} diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts index 6e0a8915b..08889ab3c 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts @@ -37,7 +37,7 @@ import {DownloadService} from "../../shared/_services/download.service"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; import {TypeaheadComponent} from "../../typeahead/_components/typeahead.component"; import {forkJoin, Observable, of, tap} from "rxjs"; -import {map} from "rxjs/operators"; +import {map, switchMap} from "rxjs/operators"; import {EntityTitleComponent} from "../../cards/entity-title/entity-title.component"; import {SettingButtonComponent} from "../../settings/_components/setting-button/setting-button.component"; import {CoverImageChooserComponent} from "../../cards/cover-image-chooser/cover-image-chooser.component"; @@ -212,11 +212,13 @@ export class EditChapterModalComponent implements OnInit { this.editForm.addControl('coverImageIndex', new FormControl(0, [])); this.editForm.addControl('coverImageLocked', new FormControl(this.chapter.coverImageLocked, [])); - this.metadataService.getAllValidLanguages().subscribe(validLanguages => { - this.validLanguages = validLanguages; - this.setupLanguageTypeahead(); - this.cdRef.markForCheck(); - }); + this.metadataService.getAllValidLanguages().pipe( + tap(validLanguages => { + this.validLanguages = validLanguages; + this.cdRef.markForCheck(); + }), + switchMap(_ => this.setupLanguageTypeahead()) + ).subscribe(); this.metadataService.getAllAgeRatings().subscribe(ratings => { this.ageRatings = ratings; diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html index 705f320bb..9255f2616 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html @@ -90,7 +90,7 @@
@if(settingsForm.get('enableSsl'); as formControl) { - +
diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.html b/UI/Web/src/app/admin/manage-system/manage-system.component.html index c0a613d8e..7792994c4 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.html +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.html @@ -1,77 +1,72 @@ -
- - @if (serverInfo) { -
-

{{t('title')}}

- -
-
-
{{t('version-title')}}
-
{{serverInfo.kavitaVersion}}
-
- -
-
{{t('installId-title')}}
-
{{serverInfo.installId}}
-
-
- -
-
-
{{t('first-install-version-title')}}
-
{{serverInfo.firstInstallVersion}}
-
- -
-
{{t('first-install-date-title')}}
-
{{serverInfo.firstInstallDate | date:'shortDate'}}
-
-
-
- -
- } - + @if (serverInfo) {
-

{{t('more-info-title')}}

+

{{t('title')}}

+
-
{{t('home-page-title')}}
- +
+
{{t('version-title')}}
+
{{serverInfo.kavitaVersion}}
+
+ +
+
{{t('installId-title')}}
+
{{serverInfo.installId}}
+
+
-
{{t('wiki-title')}}
- -
-
-
{{t('discord-title')}}
- -
-
-
{{t('donations-title')}}
- -
-
-
{{t('source-title')}}
- -
-
-
{{t('localization-title')}}
- -
-
-
{{t('feature-request-title')}}
- +
+
{{t('first-install-version-title')}}
+
{{serverInfo.firstInstallVersion}}
+
+ +
+
{{t('first-install-date-title')}}
+
{{serverInfo.firstInstallDate | date:'shortDate'}}
+
+ } -
-

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

- +
+

{{t('more-info-title')}}

+
+
{{t('home-page-title')}}
+ +
+
+
{{t('wiki-title')}}
+ +
+
+
{{t('discord-title')}}
+ +
+
+
{{t('donations-title')}}
+ +
+
+
{{t('source-title')}}
+ +
+
+
{{t('localization-title')}}
+ +
+
+
{{t('feature-request-title')}}
+
-
+
+ +
+

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

+ +
diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.ts b/UI/Web/src/app/admin/manage-system/manage-system.component.ts index a5042fb6d..e1f140266 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.ts +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.ts @@ -1,11 +1,9 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {ServerService} from 'src/app/_services/server.service'; import {ServerInfoSlim} from '../_models/server-info'; -import {DatePipe, NgIf} from '@angular/common'; +import {DatePipe} from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; import {ChangelogComponent} from "../../announcements/_components/changelog/changelog.component"; -import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; -import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; @Component({ selector: 'app-manage-system', @@ -13,7 +11,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; styleUrls: ['./manage-system.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, TranslocoDirective, ChangelogComponent, DefaultDatePipe, DefaultValuePipe, DatePipe] + imports: [TranslocoDirective, ChangelogComponent, DatePipe] }) export class ManageSystemComponent implements OnInit { diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html index 554ff34a1..443047c2f 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html @@ -1,194 +1,192 @@ -
- @if (serverSettings) { -
+ @if (serverSettings) { + -

{{t('title')}}

- -
- @if (settingsForm.get('taskScan'); as formControl) { - - - @if (formControl.value === customOption) { - {{t(formControl.value)}} ({{settingsForm.get('taskScanCustom')?.value}}) - } @else { - {{t(formControl.value)}} +

{{t('title')}}

+ +
+ @if (settingsForm.get('taskScan'); as formControl) { + + + @if (formControl.value === customOption) { + {{t(formControl.value)}} ({{settingsForm.get('taskScanCustom')?.value}}) + } @else { + {{t(formControl.value)}} + } + + + + - + + @if (settingsForm.dirty || !settingsForm.untouched) { +
+ @if(settingsForm.get('taskScanCustom')?.errors?.required) { +
{{t('required')}}
+ } + @if(settingsForm.get('taskScanCustom')?.errors?.invalidCron) { +
{{t('cron-notation')}}
+ } +
} - +
+ } +
+
+ } +
- @if (formControl.value === customOption) { -
- - +
+ @if (settingsForm.get('taskBackup'); as formControl) { + + + @if (formControl.value === customOption) { + {{t(formControl.value)}} ({{settingsForm.get('taskBackupCustom')?.value}}) + } @else { + {{t(formControl.value)}} + } + + - @if (settingsForm.dirty || !settingsForm.untouched) { -
- @if(settingsForm.get('taskScanCustom')?.errors?.required) { -
{{t('required')}}
- } - @if(settingsForm.get('taskScanCustom')?.errors?.invalidCron) { -
{{t('cron-notation')}}
- } -
- } -
+ -
- @if (settingsForm.get('taskBackup'); as formControl) { - - - @if (formControl.value === customOption) { - {{t(formControl.value)}} ({{settingsForm.get('taskBackupCustom')?.value}}) - } @else { - {{t(formControl.value)}} - } - - + @if (formControl.value === customOption) { +
+ + - +
+ } +
+
+ } +
- @if (formControl.value === customOption) { -
- - - @if (settingsForm.dirty || !settingsForm.untouched) { -
- @if(settingsForm.get('taskBackupCustom')?.errors?.required) { -
{{t('required')}}
- } - @if(settingsForm.get('taskBackupCustom')?.errors?.invalidCron) { -
{{t('cron-notation')}}
- } -
- } -
+
+ @if (settingsForm.get('taskCleanup'); as formControl) { + + + @if (formControl.value === customOption) { + {{t(formControl.value)}} ({{settingsForm.get('taskCleanupCustom')?.value}}) + } @else { + {{t(formControl.value)}} + } + + + + + @if (formControl.value === customOption) { +
+ + -
- @if (settingsForm.get('taskCleanup'); as formControl) { - - - @if (formControl.value === customOption) { - {{t(formControl.value)}} ({{settingsForm.get('taskCleanupCustom')?.value}}) - } @else { - {{t(formControl.value)}} - } - - - - +
+ } + + + } +
+ - @if (formControl.value === customOption) { -
- - +
- @if (settingsForm.get('taskCleanupCustom')?.invalid) { -
- @if(settingsForm.get('taskCleanupCustom')?.errors?.required) { -
{{t('required')}}
- } - @if(settingsForm.get('taskCleanupCustom')?.errors?.invalidCron) { -
{{t('cron-notation')}}
- } -
- } -
- } -
-
- } -
- +

{{t('adhoc-tasks-title')}}

-
+ @for(task of adhocTasks; track task.name; let idx = $index) { +
+ + + +
+ } -

{{t('adhoc-tasks-title')}}

+
- @for(task of adhocTasks; track task.name; let idx = $index) { -
- - - -
- } +

{{t('recurring-tasks-title')}}

+ -
- -

{{t('recurring-tasks-title')}}

- - - - - {{t('job-title-header')}} - - - {{item.title | titlecase}} - - + + + {{t('job-title-header')}} + + + {{item.title | titlecase}} + + - - - {{t('last-executed-header')}} - - - {{item.lastExecutionUtc | utcToLocalTime | defaultValue }} - - + + + {{t('last-executed-header')}} + + + {{item.lastExecutionUtc | utcToLocalTime | defaultValue }} + + - - - {{t('cron-header')}} - - - {{item.cron}} - - - - - } -
+ + + {{t('cron-header')}} + + + {{item.cron}} + + + + + }
diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts index 2a2fbf0f5..df11cad85 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts @@ -106,13 +106,6 @@ export class ManageTasksSettingsComponent implements OnInit { api: defer(() => of(this.downloadService.download('logs', undefined))), successMessage: '' }, - // TODO: Remove this in v0.9. Users should have all updated by then - { - name: 'analyze-files-task', - description: 'analyze-files-task-desc', - api: this.serverService.analyzeFiles(), - successMessage: 'analyze-files-task-success' - }, { name: 'sync-themes-task', description: 'sync-themes-task-desc', diff --git a/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html b/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html index 982a77e84..ad856dde6 100644 --- a/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html +++ b/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html @@ -1,52 +1,49 @@ -
-

{{t('description')}}

+

{{t('description')}}

- + - - - {{t('username-header')}} - - - {{item.username}} - - + + + {{t('username-header')}} + + + {{item.username}} + + - - - {{t('anilist-header')}} - - - @if (item.isAniListTokenSet) { - {{t('token-set-label')}} {{t('expires-label', {date: item.aniListValidUntilUtc | utcToLocalTime})}} - } @else { - {{null | defaultValue}} - } - - + + + {{t('anilist-header')}} + + + @if (item.isAniListTokenSet) { + {{t('token-set-label')}} {{t('expires-label', {date: item.aniListValidUntilUtc | utcToLocalTime})}} + } @else { + {{null | defaultValue}} + } + + - - - {{t('mal-header')}} - - - @if (item.isMalTokenSet) { - {{t('token-set-label')}} - } @else { - {{null | defaultValue}} - } - - - - -
+ + + {{t('mal-header')}} + + + @if (item.isMalTokenSet) { + {{t('token-set-label')}} + } @else { + {{null | defaultValue}} + } + + +
diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index f859b33d7..6c1f83952 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -18,7 +18,7 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import {forkJoin, Observable, of, tap} from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { TypeaheadSettings } from 'src/app/typeahead/_models/typeahead-settings'; import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter'; @@ -238,10 +238,7 @@ export class EditSeriesModalComponent implements OnInit { this.cdRef.markForCheck(); }); - this.metadataService.getAllValidLanguages().subscribe(validLanguages => { - this.validLanguages = validLanguages; - this.cdRef.markForCheck(); - }); + this.seriesService.getMetadata(this.series.id).subscribe(metadata => { if (metadata) { @@ -437,30 +434,41 @@ export class EditSeriesModalComponent implements OnInit { } setupLanguageTypeahead() { - this.languageSettings.minCharacters = 0; - this.languageSettings.multiple = false; - this.languageSettings.id = 'language'; - this.languageSettings.unique = true; - this.languageSettings.showLocked = true; - this.languageSettings.addIfNonExisting = false; - this.languageSettings.compareFn = (options: Language[], filter: string) => { - return options.filter(m => this.utilityService.filter(m.title, filter)); - } - this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => { - return options.filter(m => this.utilityService.filterMatches(m.title, filter)); - } - this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages) - .pipe(map(items => this.languageSettings.compareFn(items, filter))); - this.languageSettings.selectionCompareFn = (a: Language, b: Language) => { - return a.isoCode == b.isoCode; - } - const l = this.validLanguages.find(l => l.isoCode === this.metadata.language); - if (l !== undefined) { - this.languageSettings.savedData = l; - } - return of(true); + return this.metadataService.getAllValidLanguages() + .pipe( + tap(validLanguages => { + this.validLanguages = validLanguages; + + this.languageSettings.minCharacters = 0; + this.languageSettings.multiple = false; + this.languageSettings.id = 'language'; + this.languageSettings.unique = true; + this.languageSettings.showLocked = true; + this.languageSettings.addIfNonExisting = false; + this.languageSettings.compareFn = (options: Language[], filter: string) => { + return options.filter(m => this.utilityService.filter(m.title, filter)); + } + this.languageSettings.compareFnForAdd = (options: Language[], filter: string) => { + return options.filter(m => this.utilityService.filterMatches(m.title, filter)); + } + this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages) + .pipe(map(items => this.languageSettings.compareFn(items, filter))); + + this.languageSettings.selectionCompareFn = (a: Language, b: Language) => { + return a.isoCode == b.isoCode; + } + + const l = this.validLanguages.find(l => l.isoCode === this.metadata.language); + if (l !== undefined) { + this.languageSettings.savedData = l; + } + + this.cdRef.markForCheck(); + }), + switchMap(_ => of(true)) + ); } setupPersonTypeahead() { diff --git a/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html b/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html index abd2b7260..77b91b77c 100644 --- a/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html +++ b/UI/Web/src/app/collections/_components/import-mal-collection/import-mal-collection.component.html @@ -1,10 +1,6 @@

{{t('description')}}

- @if (stacks.length === 0) { -

{{t('nothing-found')}}

- } -
    @for(stack of stacks; track stack.url) {
  • @@ -21,6 +17,8 @@ } @empty { @if (isLoading) { + } @else { +

    {{t('nothing-found')}}

    } }
diff --git a/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts index 7257cd55a..87aae37b5 100644 --- a/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts +++ b/UI/Web/src/app/manga-reader/_components/canvas-renderer/canvas-renderer.component.ts @@ -22,6 +22,7 @@ import { MangaReaderService } from '../../_service/manga-reader.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import { SafeStylePipe } from '../../../_pipes/safe-style.pipe'; import { NgClass, AsyncPipe } from '@angular/common'; +import {isSafari} from "../../../_helpers/browser"; const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRightToLeft]; @@ -35,13 +36,21 @@ const ValidSplits = [PageSplitOption.SplitLeftToRight, PageSplitOption.SplitRigh }) export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRenderer { + protected readonly isSafari = isSafari; + + private readonly destroyRef = inject(DestroyRef); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly mangaReaderService = inject(MangaReaderService); + private readonly readerService = inject(ReaderService); + + @Input({required: true}) readerSettings$!: Observable; @Input({required: true}) image$!: Observable; @Input({required: true}) bookmark$!: Observable; @Input({required: true}) showClickOverlay$!: Observable; @Input() imageFit$!: Observable; @Output() imageHeight: EventEmitter = new EventEmitter(); - private readonly destroyRef = inject(DestroyRef); + @ViewChild('content') canvas: ElementRef | undefined; private ctx!: CanvasRenderingContext2D; @@ -67,7 +76,6 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRend - constructor(private readonly cdRef: ChangeDetectorRef, private mangaReaderService: MangaReaderService, private readerService: ReaderService) { } ngOnInit(): void { this.readerSettings$.pipe(takeUntilDestroyed(this.destroyRef), tap((value: ReaderSetting) => { @@ -250,21 +258,11 @@ export class CanvasRendererComponent implements OnInit, AfterViewInit, ImageRend setCanvasSize() { if (this.canvasImage == null) return; if (!this.ctx || !this.canvas) { return; } - const isSafari = [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod' - ].includes(navigator.platform) - // iPad on iOS 13 detection - || (navigator.userAgent.includes("Mac") && "ontouchend" in document); - const canvasLimit = isSafari ? 16_777_216 : 124_992_400; + const canvasLimit = this.isSafari ? 16_777_216 : 124_992_400; const needsScaling = this.canvasImage.width * this.canvasImage.height > canvasLimit; if (needsScaling) { - this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384; - this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384; + this.canvas.nativeElement.width = this.isSafari ? 4_096 : 16_384; + this.canvas.nativeElement.height = this.isSafari ? 4_096 : 16_384; } else { this.canvas.nativeElement.width = this.canvasImage.width; this.canvas.nativeElement.height = this.canvasImage.height; diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index bdb49ee89..8fabaa35e 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -1,11 +1,11 @@ - - -
- -
- - + @if (toggleService.toggleState$ | async; as isOpen) { + @if (utilityService.getActiveBreakpoint(); as activeBreakpoint) { + @if (activeBreakpoint >= Breakpoint.Tablet) { +
+ +
+ } @else {
@@ -17,60 +17,62 @@
- -
- + } + } + } -
-
- - -
-
-
-
-
- - + @if (fullyLoaded && filterV2) { +
+
+ + +
+ +
+
+
+ + +
-
-
+
-
-
- - - - - - - - - - - +
+
+ + +
+ + @if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) { + + }
- -
-
- -
- -
+ @if (utilityService.getActiveBreakpoint() <= Breakpoint.Tablet) { +
+ +
+ } + +
+ } + diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index a6469a0a3..3001f7175 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -11,7 +11,7 @@ import { Output } from '@angular/core'; import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {NgbCollapse, NgbModal, NgbRating, NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; +import {NgbCollapse} from '@ng-bootstrap/ng-bootstrap'; import {Breakpoint, UtilityService} from '../shared/_services/utility.service'; import {Library} from '../_models/library/library'; import {allSortFields, FilterEvent, FilterItem, SortField} from '../_models/metadata/series-filter'; @@ -19,28 +19,15 @@ import {ToggleService} from '../_services/toggle.service'; import {FilterSettings} from './filter-settings'; import {SeriesFilterV2} from '../_models/metadata/v2/series-filter-v2'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {TypeaheadComponent} from '../typeahead/_components/typeahead.component'; import {DrawerComponent} from '../shared/drawer/drawer.component'; -import {AsyncPipe, NgClass, NgForOf, NgIf, NgTemplateOutlet} from '@angular/common'; -import {translate, TranslocoModule} from "@jsverse/transloco"; +import {AsyncPipe, NgClass, NgTemplateOutlet} from '@angular/common'; +import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco"; import {SortFieldPipe} from "../_pipes/sort-field.pipe"; import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component"; import {allFields} from "../_models/metadata/v2/filter-field"; -import {MetadataService} from "../_services/metadata.service"; -import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; import {FilterService} from "../_services/filter.service"; import {ToastrService} from "ngx-toastr"; -import { - Select2AutoCreateEvent, - Select2Module, - Select2Option, - Select2UpdateEvent, - Select2UpdateValue -} from "ng-select2-component"; -import {SmartFilter} from "../_models/metadata/v2/smart-filter"; -import {animate, state, style, transition, trigger} from "@angular/animations"; - -const ANIMATION_SPEED = 750; +import {animate, style, transition, trigger} from "@angular/animations"; @Component({ selector: 'app-metadata-filter', @@ -71,65 +58,53 @@ const ANIMATION_SPEED = 750; ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgIf, NgbCollapse, NgTemplateOutlet, DrawerComponent, NgbTooltip, TypeaheadComponent, - ReactiveFormsModule, FormsModule, NgbRating, AsyncPipe, TranslocoModule, SortFieldPipe, - MetadataBuilderComponent, NgForOf, Select2Module, NgClass] + imports: [NgTemplateOutlet, DrawerComponent, + ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule, + MetadataBuilderComponent, NgClass] }) export class MetadataFilterComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + public readonly utilityService = inject(UtilityService); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly toastr = inject(ToastrService); + private readonly filterService = inject(FilterService); + protected readonly toggleService = inject(ToggleService); + protected readonly translocoService = inject(TranslocoService); + private readonly sortFieldPipe = new SortFieldPipe(this.translocoService); + /** * This toggles the opening/collapsing of the metadata filter code */ @Input() filterOpen: EventEmitter = new EventEmitter(); - /** * Should filtering be shown on the page */ @Input() filteringDisabled: boolean = false; - @Input({required: true}) filterSettings!: FilterSettings; - @Output() applyFilter: EventEmitter = new EventEmitter(); - @ContentChild('[ngbCollapse]') collapse!: NgbCollapse; - private readonly destroyRef = inject(DestroyRef); - public readonly utilityService = inject(UtilityService); - public readonly filterUtilitiesService = inject(FilterUtilitiesService); + /** * Controls the visibility of extended controls that sit below the main header. */ filteringCollapsed: boolean = true; - libraries: Array> = []; sortGroup!: FormGroup; isAscendingSort: boolean = true; - updateApplied: number = 0; fullyLoaded: boolean = false; filterV2: SeriesFilterV2 | undefined; - allSortFields = allSortFields; - allFilterFields = allFields; - smartFilters!: Array; + protected readonly allSortFields = allSortFields.map(f => { + return {title: this.sortFieldPipe.transform(f), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)); + protected readonly allFilterFields = allFields; - private readonly cdRef = inject(ChangeDetectorRef); - private readonly toastr = inject(ToastrService); - - - constructor(public toggleService: ToggleService, private filterService: FilterService) { - this.filterService.getAllFilters().subscribe(res => { - this.smartFilters = res.map(r => { - return { - value: r, - label: r.name, - } - }); - }); - } ngOnInit(): void { if (this.filterSettings === undefined) { diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index b957ab658..bffecb1e2 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -9,7 +9,7 @@
-
+

{{series.name}} diff --git a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html index c89257c0b..cf3639495 100644 --- a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html +++ b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.html @@ -15,7 +15,7 @@

@if (showEdit) { - } @@ -28,7 +28,7 @@ @if (isEditMode) { } @else { - + } diff --git a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss index 42e26490a..fdf0c0c95 100644 --- a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss +++ b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.scss @@ -12,23 +12,11 @@ cursor: pointer; } -//.setting-title.no-anim.edit:hover ~ .edit-btn { -// opacity: 1; -// transition: none; -//} -// -//.edit-btn { -// opacity: 0; -// transition: opacity 0.5s ease-out; -// transition-delay: 0.5s; -// -// &:hover { -// opacity: 1; -// transition: opacity 0.3s ease-out; -// } -//} -// -//.setting-title.no-anim + .edit-btn, -//.setting-title.no-anim.edit:hover ~ .edit-btn { -// transition: none !important; -//} +.non-selectable { + cursor: default; +} + +.btn-alignment { + padding-bottom: 0.5rem; // Align with h6 + padding-top: 0; +} diff --git a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts index 85d94cc2e..af720221a 100644 --- a/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts +++ b/UI/Web/src/app/settings/_components/setting-item/setting-item.component.ts @@ -8,7 +8,7 @@ import { TemplateRef } from '@angular/core'; import {TranslocoDirective} from "@jsverse/transloco"; -import {NgTemplateOutlet} from "@angular/common"; +import {NgClass, NgTemplateOutlet} from "@angular/common"; import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; import {filter, fromEvent, tap} from "rxjs"; import {AbstractControl} from "@angular/forms"; @@ -19,7 +19,8 @@ import {AbstractControl} from "@angular/forms"; imports: [ TranslocoDirective, NgTemplateOutlet, - SafeHtmlPipe + SafeHtmlPipe, + NgClass ], templateUrl: './setting-item.component.html', styleUrl: './setting-item.component.scss', diff --git a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html index eb4ffe331..24beb3329 100644 --- a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html +++ b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.html @@ -7,7 +7,13 @@ }
-
{{title}}
+
+ @if (labelId) { + + } @else { + {{title}} + } +
diff --git a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts index 6e3cf4d8c..9a7a24ec8 100644 --- a/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts +++ b/UI/Web/src/app/settings/_components/setting-switch/setting-switch.component.ts @@ -1,7 +1,8 @@ import { + AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, - Component, ContentChild, + Component, ContentChild, ElementRef, inject, Input, TemplateRef @@ -22,12 +23,39 @@ import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; styleUrl: './setting-switch.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class SettingSwitchComponent { +export class SettingSwitchComponent implements AfterContentInit { + private readonly cdRef = inject(ChangeDetectorRef); + private readonly elementRef = inject(ElementRef); @Input({required:true}) title: string = ''; @Input() subtitle: string | undefined = undefined; @Input() id: string | undefined = undefined; @ContentChild('switch') switchRef!: TemplateRef; + /** + * For wiring up with a real label + */ + labelId: string = ''; + + ngAfterContentInit(): void { + setTimeout(() => { + if (this.id) { + this.labelId = this.id; + this.cdRef.markForCheck(); + return; + } + + const element = this.elementRef.nativeElement; + const inputElement = element.querySelector('input'); + + if (inputElement && inputElement.id) { + this.labelId = inputElement.id; + this.cdRef.markForCheck(); + } else { + console.warn('No input with ID found in app-setting-switch. For accessibility, please ensure the input has an ID.'); + } + }); + } + } diff --git a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html index bb181e331..2dd12c0d0 100644 --- a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html +++ b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.html @@ -1,15 +1,21 @@ -
+
-
-
{{title}} +
+
+ @if (labelId) { + + } @else { + {{title}} + } @if (titleExtraRef) { }
-
- + +
+
diff --git a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss index e69de29bb..15a6d7ace 100644 --- a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss +++ b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.scss @@ -0,0 +1,4 @@ +.btn-alignment { + padding-bottom: 0.5rem; // Align with h6 + padding-top: 0; +} diff --git a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts index 3b6f03242..5e10137a7 100644 --- a/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts +++ b/UI/Web/src/app/settings/_components/setting-title/setting-title.component.ts @@ -26,6 +26,10 @@ export class SettingTitleComponent { private readonly cdRef = inject(ChangeDetectorRef); @Input({required:true}) title: string = ''; + /** + * If passed, will generate a proper label element. Requires `id` to be passed as well + */ + @Input() labelId: string | undefined = undefined; @Input() id: string | undefined = undefined; @Input() canEdit: boolean = true; @Input() isEditMode: boolean = false; diff --git a/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts b/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts index 1e7365629..8314307b1 100644 --- a/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts +++ b/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts @@ -28,6 +28,10 @@ export class BadgeExpanderComponent implements OnInit, OnChanges { @Input() itemsTillExpander: number = 4; @Input() allowToggle: boolean = true; @Input() includeComma: boolean = true; + /** + * If should be expanded by default. Defaults to false. + */ + @Input() defaultExpanded: boolean = false; /** * Invoked when the "and more" is clicked */ @@ -39,10 +43,20 @@ export class BadgeExpanderComponent implements OnInit, OnChanges { isCollapsed: boolean = false; get itemsLeft() { + if (this.defaultExpanded) return 0; + return Math.max(this.items.length - this.itemsTillExpander, 0); } ngOnInit(): void { + + if (this.defaultExpanded) { + this.isCollapsed = false; + this.visibleItems = this.items; + this.cdRef.markForCheck(); + return; + } + this.visibleItems = this.items.slice(0, this.itemsTillExpander); this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 69ba7c827..8cbac271a 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -20,8 +20,8 @@
@if (libraryForm.get('name'); as formControl) { - - @if (libraryForm.dirty || libraryForm.touched) { + + @if (libraryForm.dirty || !libraryForm.untouched) {
@if (formControl.errors?.required) {
{{t('required-field')}}
@@ -52,8 +52,8 @@ diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index c1b6871b7..15825a1f5 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -28,7 +28,7 @@ import { } from 'src/app/admin/_modals/directory-picker/directory-picker.component'; import {ConfirmService} from 'src/app/shared/confirm.service'; import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; -import {Library, LibraryType} from 'src/app/_models/library/library'; +import {allLibraryTypes, Library, LibraryType} from 'src/app/_models/library/library'; import {ImageService} from 'src/app/_services/image.service'; import {LibraryService} from 'src/app/_services/library.service'; import {UploadService} from 'src/app/_services/upload.service'; @@ -47,6 +47,7 @@ import {SettingSwitchComponent} from "../../../settings/_components/setting-swit import {SettingButtonComponent} from "../../../settings/_components/setting-button/setting-button.component"; import {Action, ActionFactoryService, ActionItem} from "../../../_services/action-factory.service"; import {ActionService} from "../../../_services/action.service"; +import {LibraryTypePipe} from "../../../_pipes/library-type.pipe"; enum TabID { General = 'general-tab', @@ -68,7 +69,7 @@ enum StepID { standalone: true, imports: [CommonModule, NgbModalModule, NgbNavLink, NgbNavItem, NgbNavContent, ReactiveFormsModule, NgbTooltip, SentenceCasePipe, NgbNav, NgbNavOutlet, CoverImageChooserComponent, TranslocoModule, DefaultDatePipe, - FileTypeGroupPipe, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent], + FileTypeGroupPipe, EditListComponent, SettingItemComponent, SettingSwitchComponent, SettingButtonComponent, LibraryTypePipe], templateUrl: './library-settings-modal.component.html', styleUrls: ['./library-settings-modal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -94,6 +95,7 @@ export class LibrarySettingsModalComponent implements OnInit { protected readonly TabID = TabID; protected readonly WikiLink = WikiLink; protected readonly Action = Action; + protected readonly libraryTypePipe = new LibraryTypePipe(); @Input({required: true}) library!: Library | undefined; @@ -119,7 +121,9 @@ export class LibrarySettingsModalComponent implements OnInit { selectedFolders: string[] = []; madeChanges = false; - libraryTypes: string[] = [] + libraryTypes = allLibraryTypes.map(f => { + return {title: this.libraryTypePipe.transform(f), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)); isAddLibrary = false; setupStep = StepID.General; @@ -134,11 +138,6 @@ export class LibrarySettingsModalComponent implements OnInit { } ngOnInit(): void { - this.settingService.getLibraryTypes().subscribe((types) => { - this.libraryTypes = types; - this.cdRef.markForCheck(); - }); - if (this.library === undefined) { this.isAddLibrary = true; this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html index f2ad3c572..a592d0a7e 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html @@ -1,117 +1,113 @@ -
-
- -
- - {{t('series-count', {num: stats.seriesCount | number})}} - -
-
-
- - -
- - {{t('volume-count', {num: stats.volumeCount | number})}} - -
-
-
- - -
- - {{t('file-count', {num: stats.totalFiles | number})}} - -
-
-
- - -
- - {{stats.totalSize | bytes}} - -
-
-
- - -
- - {{t('genre-count', {num: stats.totalGenres | compactNumber})}} - -
-
-
- - -
- - {{t('tag-count', {num: stats.totalTags | compactNumber})}} - -
-
-
- - -
- - {{t('people-count', {num: stats.totalPeople | compactNumber})}} - -
-
-
- - -
- - {{stats.totalReadingTime | timeDuration}} - -
-
-
- -
-
- +
+ +
+ + {{t('series-count', {num: stats.seriesCount | number})}} +
-
- +
+ + + +
+ + {{t('volume-count', {num: stats.volumeCount | number})}} +
-
- +
+ + + +
+ + {{t('file-count', {num: stats.totalFiles | number})}} +
-
- - +
+ + + +
+ + {{stats.totalSize | bytes}} +
-
- +
+ + + +
+ + {{t('genre-count', {num: stats.totalGenres | compactNumber})}} +
-
+
+
-
- -
+ +
+ + {{t('tag-count', {num: stats.totalTags | compactNumber})}} + +
+
+
-
- -
- -
- -
- -
- -
- -
- -
+ +
+ + {{t('people-count', {num: stats.totalPeople | compactNumber})}} + +
+
+
+ +
+ + {{stats.totalReadingTime | timeDuration}} + +
+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/UI/Web/src/app/user-settings/api-key/api-key.component.html b/UI/Web/src/app/user-settings/api-key/api-key.component.html index e84881ec1..0bc927229 100644 --- a/UI/Web/src/app/user-settings/api-key/api-key.component.html +++ b/UI/Web/src/app/user-settings/api-key/api-key.component.html @@ -2,7 +2,8 @@ - +
@if (hideData) { @@ -14,17 +15,10 @@ + @if (showRefresh) { + + }
- @if (showRefresh) { - - - - }
- - - {{t('regen-warning')}} - - diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 6a03ecec1..59dbe8d2c 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -337,7 +337,8 @@ "regen-warning": "Regenerating your API key will invalidate any existing clients.", "no-key": "ERROR - KEY NOT SET", "confirm-reset": "This will invalidate any OPDS configurations you have setup. Are you sure you want to continue?", - "key-reset": "API Key reset" + "key-reset": "API Key reset", + "reset": "Reset" }, "scrobbling-providers": { @@ -571,9 +572,9 @@ "library-type-pipe": { "book": "Book", - "comic": "Comic", + "comic": "Comic (Legacy)", "manga": "Manga", - "comicVine": "Comic Vine", + "comicVine": "Comic", "image": "Image", "lightNovel": "Light Novel" }, @@ -1576,10 +1577,6 @@ "download-logs-task": "Download Logs", "download-logs-task-desc": "Compiles all log files into a zip and downloads it.", - "analyze-files-task": "Analyze Files", - "analyze-files-task-desc": "Runs a long-running task which will analyze files to generate extension and size. This should only be ran once for the v0.7 release. Not needed if you installed post v0.7.", - "analyze-files-task-success": "File analysis has been queued", - "sync-themes-task": "Sync Themes", "sync-themes-task-desc": "Synchronize downloaded themes with upstream changes if version matches.", "sync-themes-success": "Synchronization of themes has been queued",