From 9894a2623cbe09a1bf67fe4c246b914694f2b311 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Thu, 2 Nov 2023 08:35:43 -0500 Subject: [PATCH] Smart Filter Encoding Fix (#2387) --- API.Benchmark/API.Benchmark.csproj | 4 +- API.Tests/API.Tests.csproj | 4 +- API.Tests/Helpers/SmartFilterHelperTests.cs | 68 ++++- API.Tests/Services/SiteThemeServiceTests.cs | 4 +- API/API.csproj | 22 +- API/Comparators/ChapterSortComparer.cs | 2 + API/Comparators/NumericComparer.cs | 2 + API/Comparators/StringLogicalComparer.cs | 1 + API/Controllers/AccountController.cs | 2 + API/Controllers/AdminController.cs | 2 + API/Controllers/BaseApiController.cs | 2 + API/Controllers/BookController.cs | 2 + API/Controllers/CBLController.cs | 2 + API/Controllers/CollectionController.cs | 2 + API/Controllers/DeviceController.cs | 2 + API/Controllers/DownloadController.cs | 2 + API/Controllers/FallbackController.cs | 2 + API/Controllers/FilterController.cs | 30 +- API/Controllers/HealthController.cs | 2 + API/Controllers/ImageController.cs | 2 + API/Controllers/LibraryController.cs | 2 + API/Controllers/LicenseController.cs | 2 + API/Controllers/LocaleController.cs | 2 + API/Controllers/MetadataController.cs | 1 + API/Controllers/PanelsController.cs | 2 + API/Controllers/PluginController.cs | 9 +- API/Controllers/RatingController.cs | 2 + API/Controllers/ReaderController.cs | 2 + API/Controllers/ReadingListController.cs | 2 + API/Controllers/RecommendedController.cs | 2 + API/Controllers/ReviewController.cs | 2 + API/Controllers/ScrobblingController.cs | 1 + API/Controllers/SearchController.cs | 2 + API/Controllers/SeriesController.cs | 2 + API/Controllers/ServerController.cs | 5 +- API/Controllers/SettingsController.cs | 2 + API/Controllers/StatsController.cs | 2 + API/Controllers/StreamController.cs | 2 + API/Controllers/TachiyomiController.cs | 2 + API/Controllers/ThemeController.cs | 2 + API/Controllers/UploadController.cs | 2 + API/Controllers/UsersController.cs | 2 + API/Controllers/WantToReadController.cs | 6 +- API/DTOs/Filtering/v2/DecodeFilterDto.cs | 9 + .../MigrateSmartFilterEncoding.cs | 61 ++++ API/Data/Repositories/UserRepository.cs | 1 - API/Extensions/AppUserExtensions.cs | 1 + .../ApplicationServiceExtensions.cs | 1 + API/Extensions/ChapterListExtensions.cs | 1 + API/Extensions/ClaimsPrincipalExtensions.cs | 1 + API/Extensions/DateTimeExtensions.cs | 1 + API/Extensions/EncodeFormatExtensions.cs | 1 + API/Extensions/EnumerableExtensions.cs | 1 + API/Extensions/FileInfoExtensions.cs | 1 + API/Extensions/FilterDtoExtensions.cs | 1 + API/Extensions/HttpExtensions.cs | 1 + API/Extensions/IdentityServiceExtensions.cs | 1 + API/Extensions/ParserInfoListExtensions.cs | 1 + API/Extensions/PathExtensions.cs | 1 + .../QueryExtensions/Filtering/BookmarkSort.cs | 6 +- .../QueryExtensions/Filtering/SeriesFilter.cs | 1 - .../QueryExtensions/Filtering/SeriesSort.cs | 4 +- .../QueryExtensions/IncludesExtensions.cs | 1 + .../QueryExtensions/QueryableExtensions.cs | 1 + .../RestrictByAgeExtensions.cs | 1 + API/Extensions/SeriesExtensions.cs | 4 +- API/Extensions/StringExtensions.cs | 1 + API/Extensions/VolumeListExtensions.cs | 1 + API/Extensions/ZipArchiveExtensions.cs | 1 + API/Helpers/Builders/SmartFilterBuilder.cs | 24 -- API/Helpers/CacheHelper.cs | 1 + API/Helpers/Converters/CronConverter.cs | 1 + .../Converters/FilterFieldValueConverter.cs | 1 + .../Converters/ServerSettingConverter.cs | 1 + API/Helpers/LibraryTypeHelper.cs | 1 + API/Helpers/NumberHelper.cs | 1 + API/Helpers/OrderableHelper.cs | 1 + API/Helpers/PagedList.cs | 3 +- API/Helpers/PaginationHeader.cs | 1 + API/Helpers/ParserInfoHelpers.cs | 1 + API/Helpers/PersonHelper.cs | 2 + API/Helpers/ReadingListHelper.cs | 1 - API/Helpers/SQLHelper.cs | 29 -- API/Helpers/SeriesHelper.cs | 1 + API/Helpers/SmartFilterHelper.cs | 68 +++-- API/Helpers/TagHelper.cs | 2 +- API/Helpers/UserParams.cs | 3 +- API/Services/AccountService.cs | 2 + API/Services/ArchiveService.cs | 2 + API/Services/BookService.cs | 1 + API/Services/BookmarkService.cs | 2 + .../StartupTasksHostedService.cs | 1 + API/Services/Plus/ExternalMetadataService.cs | 1 + API/Services/Plus/LicenseService.cs | 1 + API/Services/Plus/RatingService.cs | 1 + API/Services/Plus/RecommendationService.cs | 1 + API/Services/Plus/ScrobblingService.cs | 1 + API/Services/Tasks/BackupService.cs | 1 + API/Services/Tasks/CleanupService.cs | 1 + .../Metadata/WordCountAnalyzerService.cs | 1 + API/Services/Tasks/Scanner/LibraryWatcher.cs | 1 + .../Tasks/Scanner/ParseScannedFiles.cs | 1 + .../Tasks/Scanner/Parser/DefaultParser.cs | 1 + API/Services/Tasks/Scanner/Parser/Parser.cs | 1 + API/Services/Tasks/Scanner/ProcessSeries.cs | 1 - API/Services/Tasks/ScannerService.cs | 2 + API/Services/Tasks/SiteThemeService.cs | 1 + API/Services/Tasks/StatsService.cs | 2 + API/Services/Tasks/VersionUpdaterService.cs | 2 + API/Startup.cs | 3 + UI/Web/src/app/_models/pagination.ts | 9 +- UI/Web/src/app/_services/metadata.service.ts | 10 +- .../all-series/all-series.component.html | 2 +- .../all-series/all-series.component.ts | 32 +- .../bookmarks/bookmarks.component.html | 2 +- .../bookmarks/bookmarks.component.ts | 51 ++-- .../src/app/cards/bulk-selection.service.ts | 2 +- .../collection-detail.component.html | 4 +- .../collection-detail.component.ts | 34 ++- .../_components/dashboard.component.ts | 13 +- .../library-detail.component.html | 2 +- .../library-detail.component.ts | 43 +-- .../nav-header/nav-header.component.ts | 2 +- .../reading-list-detail.component.ts | 2 +- .../reading-lists/reading-lists.component.ts | 1 - .../metadata-detail.component.ts | 4 +- .../series-metadata-detail.component.ts | 2 +- .../_services/filter-utilities.service.ts | 289 +++++------------- .../server-stats/server-stats.component.ts | 4 +- .../want-to-read/want-to-read.component.html | 2 +- .../want-to-read/want-to-read.component.ts | 27 +- UI/Web/src/assets/langs/en.json | 3 +- openapi.json | 113 ++++++- 133 files changed, 677 insertions(+), 471 deletions(-) create mode 100644 API/DTOs/Filtering/v2/DecodeFilterDto.cs create mode 100644 API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs delete mode 100644 API/Helpers/Builders/SmartFilterBuilder.cs delete mode 100644 API/Helpers/ReadingListHelper.cs delete mode 100644 API/Helpers/SQLHelper.cs diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index 61f4f6d7b..29f16495d 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 35b0f80cf..a8b56591b 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -6,12 +6,12 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/API.Tests/Helpers/SmartFilterHelperTests.cs b/API.Tests/Helpers/SmartFilterHelperTests.cs index b557ecd27..731125b33 100644 --- a/API.Tests/Helpers/SmartFilterHelperTests.cs +++ b/API.Tests/Helpers/SmartFilterHelperTests.cs @@ -5,7 +5,6 @@ using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.Entities.Enums; using API.Helpers; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Xunit; namespace API.Tests.Helpers; @@ -15,25 +14,25 @@ public class SmartFilterHelperTests [Fact] public void Test_Decode() { - var encoded = """ - stmts=comparison%3D5%26field%3D18%26value%3D95%2Ccomparison%3D0%26field%3D4%26value%3D0%2Ccomparison%3D7%26field%3D1%26value%3Da&sortOptions=sortField=2&isAscending=false&limitTo=10&combination=1 - """; + const string encoded = """ + name=Test&stmts=comparison%253D0%25C2%25A6field%253D18%25C2%25A6value%253D95�comparison%253D0%25C2%25A6field%253D4%25C2%25A6value%253D0�comparison%253D7%25C2%25A6field%253D1%25C2%25A6value%253Da&sortOptions=sortField%3D2¦isAscending%3DFalse&limitTo=10&combination=1 + """; var filter = SmartFilterHelper.Decode(encoded); Assert.Equal(10, filter.LimitTo); Assert.Equal(SortField.CreatedDate, filter.SortOptions.SortField); Assert.False(filter.SortOptions.IsAscending); - Assert.Null(filter.Name); + Assert.Equal("Test" , filter.Name); var list = filter.Statements.ToList(); AssertStatementSame(list[2], FilterField.SeriesName, FilterComparison.Matches, "a"); AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + string.Empty); - AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Contains, "95"); + AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Equal, "95"); } [Fact] - public void Test_Encode() + public void Test_EncodeDecode() { var filter = new FilterV2Dto() { @@ -56,10 +55,61 @@ public class SmartFilterHelperTests }; var encodedFilter = SmartFilterHelper.Encode(filter); - Assert.Equal("name=Test&stmts=comparison%253D0%252Cfield%253D4%252Cvalue%253D0&sortOptions=sortField%3D2%2CisAscending%3DFalse&limitTo=10&combination=1", encodedFilter); + + var decoded = SmartFilterHelper.Decode(encodedFilter); + Assert.Single(decoded.Statements); + AssertStatementSame(decoded.Statements.First(), filter.Statements.First()); + Assert.Equal("Test", decoded.Name); + Assert.Equal(10, decoded.LimitTo); + Assert.Equal(SortField.CreatedDate, decoded.SortOptions.SortField); + Assert.False(decoded.SortOptions.IsAscending); } - private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value) + [Fact] + public void Test_EncodeDecode_MultipleValues_Contains() + { + var filter = new FilterV2Dto() + { + Name = "Test", + SortOptions = new SortOptions() { + IsAscending = false, + SortField = SortField.CreatedDate + }, + LimitTo = 10, + Combination = FilterCombination.And, + Statements = new List() + { + new FilterStatementDto() + { + Comparison = FilterComparison.Equal, + Field = FilterField.AgeRating, + Value = $"{(int) AgeRating.Unknown + string.Empty},{(int) AgeRating.G + string.Empty}" + } + } + }; + + var encodedFilter = SmartFilterHelper.Encode(filter); + var decoded = SmartFilterHelper.Decode(encodedFilter); + + Assert.Single(decoded.Statements); + AssertStatementSame(decoded.Statements.First(), filter.Statements.First()); + + Assert.Equal(2, decoded.Statements.First().Value.Split(",").Length); + + Assert.Equal("Test", decoded.Name); + Assert.Equal(10, decoded.LimitTo); + Assert.Equal(SortField.CreatedDate, decoded.SortOptions.SortField); + Assert.False(decoded.SortOptions.IsAscending); + } + + private static void AssertStatementSame(FilterStatementDto statement, FilterStatementDto statement2) + { + Assert.Equal(statement.Field, statement2.Field); + Assert.Equal(statement.Comparison, statement2.Comparison); + Assert.Equal(statement.Value, statement2.Value); + } + + private static void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value) { Assert.Equal(statement.Field, field); Assert.Equal(statement.Comparison, combination); diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/API.Tests/Services/SiteThemeServiceTests.cs index 8bf32a0c1..0ff6681dd 100644 --- a/API.Tests/Services/SiteThemeServiceTests.cs +++ b/API.Tests/Services/SiteThemeServiceTests.cs @@ -23,7 +23,7 @@ public abstract class SiteThemeServiceTest : AbstractDbTest private readonly IEventHub _messageHub = Substitute.For(); - protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper) : base() + protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } @@ -56,7 +56,7 @@ public abstract class SiteThemeServiceTest : AbstractDbTest }); await _context.SaveChangesAsync(); - var ex = await Assert.ThrowsAsync(async () => await siteThemeService.UpdateDefault(10)); + var ex = await Assert.ThrowsAsync(() => siteThemeService.UpdateDefault(10)); Assert.Equal("Theme file missing or invalid", ex.Message); } diff --git a/API/API.csproj b/API/API.csproj index e0884eb2a..0e494734e 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -55,27 +55,27 @@ - - + + - - - + + + - + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/API/Comparators/ChapterSortComparer.cs b/API/Comparators/ChapterSortComparer.cs index 55572aa7e..beac530fb 100644 --- a/API/Comparators/ChapterSortComparer.cs +++ b/API/Comparators/ChapterSortComparer.cs @@ -2,6 +2,8 @@ namespace API.Comparators; +#nullable enable + /// /// Sorts chapters based on their Number. Uses natural ordering of doubles. /// diff --git a/API/Comparators/NumericComparer.cs b/API/Comparators/NumericComparer.cs index 194d013ea..17eeee059 100644 --- a/API/Comparators/NumericComparer.cs +++ b/API/Comparators/NumericComparer.cs @@ -2,6 +2,8 @@ namespace API.Comparators; +#nullable enable + public class NumericComparer : IComparer { diff --git a/API/Comparators/StringLogicalComparer.cs b/API/Comparators/StringLogicalComparer.cs index 805f85623..6759454fb 100644 --- a/API/Comparators/StringLogicalComparer.cs +++ b/API/Comparators/StringLogicalComparer.cs @@ -6,6 +6,7 @@ using static System.Char; namespace API.Comparators; + public static class StringLogicalComparer { public static int Compare(string s1, string s2) diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 8dba67025..9442ff9d8 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -29,6 +29,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + /// /// All Account matters /// diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs index 25bde9ddb..7a7d5b06a 100644 --- a/API/Controllers/AdminController.cs +++ b/API/Controllers/AdminController.cs @@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + public class AdminController : BaseApiController { private readonly UserManager _userManager; diff --git a/API/Controllers/BaseApiController.cs b/API/Controllers/BaseApiController.cs index 2ac2b5cce..7806ef660 100644 --- a/API/Controllers/BaseApiController.cs +++ b/API/Controllers/BaseApiController.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + [ApiController] [Route("api/[controller]")] [Authorize] diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index f2b351a65..4f747b1f1 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -14,6 +14,8 @@ using VersOne.Epub; namespace API.Controllers; +#nullable enable + public class BookController : BaseApiController { private readonly IBookService _bookService; diff --git a/API/Controllers/CBLController.cs b/API/Controllers/CBLController.cs index 5ff82edb7..7952f3790 100644 --- a/API/Controllers/CBLController.cs +++ b/API/Controllers/CBLController.cs @@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// Responsible for the CBL import flow /// diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index 4f5f955be..a0158d3ab 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -13,6 +13,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// APIs for Collections /// diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs index fa5bc34fa..3e1b57fec 100644 --- a/API/Controllers/DeviceController.cs +++ b/API/Controllers/DeviceController.cs @@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// Responsible interacting and creating Devices /// diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index edfda64f6..b983a2d5c 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -16,6 +16,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + /// /// All APIs related to downloading entities from the system. Requires Download Role or Admin Role. /// diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs index 9902d28be..0c925476f 100644 --- a/API/Controllers/FallbackController.cs +++ b/API/Controllers/FallbackController.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + [AllowAnonymous] public class FallbackController : Controller { diff --git a/API/Controllers/FilterController.cs b/API/Controllers/FilterController.cs index eeffb10b7..e8cb71117 100644 --- a/API/Controllers/FilterController.cs +++ b/API/Controllers/FilterController.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs.Dashboard; @@ -10,23 +9,22 @@ using API.DTOs.Filtering.v2; using API.Entities; using API.Extensions; using API.Helpers; -using EasyCaching.Core; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// This is responsible for Filter caching /// public class FilterController : BaseApiController { private readonly IUnitOfWork _unitOfWork; - private readonly IEasyCachingProviderFactory _cacheFactory; - public FilterController(IUnitOfWork unitOfWork, IEasyCachingProviderFactory cacheFactory) + public FilterController(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; - _cacheFactory = cacheFactory; } /// @@ -93,4 +91,26 @@ public class FilterController : BaseApiController await _unitOfWork.CommitAsync(); return Ok(); } + + /// + /// Encode the Filter + /// + /// + /// + [HttpPost("encode")] + public ActionResult EncodeFilter(FilterV2Dto dto) + { + return Ok(SmartFilterHelper.Encode(dto)); + } + + /// + /// Decodes the Filter + /// + /// + /// + [HttpPost("decode")] + public ActionResult DecodeFilter(DecodeFilterDto dto) + { + return Ok(SmartFilterHelper.Decode(dto.EncodedFilter)); + } } diff --git a/API/Controllers/HealthController.cs b/API/Controllers/HealthController.cs index 27fe060ea..a1931f859 100644 --- a/API/Controllers/HealthController.cs +++ b/API/Controllers/HealthController.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + [AllowAnonymous] public class HealthController : BaseApiController { diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index da484981c..837ad999c 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -13,6 +13,8 @@ using MimeTypes; namespace API.Controllers; +#nullable enable + /// /// Responsible for servicing up images stored in Kavita for entities /// diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index ab8d46edd..c0706561b 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -27,6 +27,8 @@ using TaskScheduler = API.Services.TaskScheduler; namespace API.Controllers; +#nullable enable + [Authorize] public class LibraryController : BaseApiController { diff --git a/API/Controllers/LicenseController.cs b/API/Controllers/LicenseController.cs index e02a16b48..05886e77c 100644 --- a/API/Controllers/LicenseController.cs +++ b/API/Controllers/LicenseController.cs @@ -15,6 +15,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + public class LicenseController : BaseApiController { private readonly IUnitOfWork _unitOfWork; diff --git a/API/Controllers/LocaleController.cs b/API/Controllers/LocaleController.cs index de1c0d16c..d96419b0f 100644 --- a/API/Controllers/LocaleController.cs +++ b/API/Controllers/LocaleController.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + public class LocaleController : BaseApiController { private readonly ILocalizationService _localizationService; diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 0abf032af..b3dbb8a01 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable public class MetadataController : BaseApiController { diff --git a/API/Controllers/PanelsController.cs b/API/Controllers/PanelsController.cs index 511e8e6c5..2008b0c8d 100644 --- a/API/Controllers/PanelsController.cs +++ b/API/Controllers/PanelsController.cs @@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// For the Panels app explicitly /// diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index fd4150349..89a006f8c 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using API.Data; using API.DTOs; @@ -11,6 +12,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + public class PluginController : BaseApiController { private readonly IUnitOfWork _unitOfWork; @@ -43,7 +46,7 @@ public class PluginController : BaseApiController var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId <= 0) { - _logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {Information}", pluginName, new + _logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", Uri.EscapeDataString(pluginName), new { IpAddress = ipAddress, UserAgent = userAgent, @@ -52,7 +55,7 @@ public class PluginController : BaseApiController throw new KavitaUnauthenticatedUserException(); } var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - _logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user!.UserName, userId); + _logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", Uri.EscapeDataString(pluginName), user!.UserName, userId); return new UserDto { Username = user.UserName!, diff --git a/API/Controllers/RatingController.cs b/API/Controllers/RatingController.cs index 48e609d6b..e82cb1fbd 100644 --- a/API/Controllers/RatingController.cs +++ b/API/Controllers/RatingController.cs @@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + /// /// Responsible for providing external ratings for Series /// diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 39748325f..a5a2b6a7b 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -26,6 +26,8 @@ using MimeTypes; namespace API.Controllers; +#nullable enable + /// /// For all things regarding reading, mainly focusing on non-Book related entities /// diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 329fed1e2..11a50d614 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -16,6 +16,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + [Authorize] public class ReadingListController : BaseApiController { diff --git a/API/Controllers/RecommendedController.cs b/API/Controllers/RecommendedController.cs index 062b87bad..979584032 100644 --- a/API/Controllers/RecommendedController.cs +++ b/API/Controllers/RecommendedController.cs @@ -17,6 +17,8 @@ using Newtonsoft.Json; namespace API.Controllers; +#nullable enable + public class RecommendedController : BaseApiController { private readonly IUnitOfWork _unitOfWork; diff --git a/API/Controllers/ReviewController.cs b/API/Controllers/ReviewController.cs index 50bc55649..5eaedd6b2 100644 --- a/API/Controllers/ReviewController.cs +++ b/API/Controllers/ReviewController.cs @@ -18,6 +18,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + public class ReviewController : BaseApiController { private readonly ILogger _logger; diff --git a/API/Controllers/ScrobblingController.cs b/API/Controllers/ScrobblingController.cs index d81267936..fc32c3a46 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/API/Controllers/ScrobblingController.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable public class ScrobblingController : BaseApiController { diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index 98c969800..4ce7d282d 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// Responsible for the Search interface from the UI /// diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index eeb52e89f..893a6a9d8 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -28,6 +28,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + public class SeriesController : BaseApiController { private readonly ILogger _logger; diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index a770fbc6e..a303dcc58 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -20,13 +20,14 @@ using Hangfire.Storage; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using MimeTypes; using TaskScheduler = API.Services.TaskScheduler; namespace API.Controllers; +#nullable enable + [Authorize(Policy = "RequireAdminRole")] public class ServerController : BaseApiController { @@ -286,8 +287,6 @@ public class ServerController : BaseApiController if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null); return Ok(await _emailService.GetVersion(emailServiceUrl)); - } - } diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index c5b368fbc..6e930c90a 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -24,6 +24,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + public class SettingsController : BaseApiController { private readonly ILogger _logger; diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs index 625ff38ba..9654abef6 100644 --- a/API/Controllers/StatsController.cs +++ b/API/Controllers/StatsController.cs @@ -14,6 +14,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + public class StatsController : BaseApiController { private readonly IStatisticService _statService; diff --git a/API/Controllers/StreamController.cs b/API/Controllers/StreamController.cs index 11418e986..49ee1ed90 100644 --- a/API/Controllers/StreamController.cs +++ b/API/Controllers/StreamController.cs @@ -10,6 +10,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + /// /// Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream) /// diff --git a/API/Controllers/TachiyomiController.cs b/API/Controllers/TachiyomiController.cs index 900783097..e55dc3365 100644 --- a/API/Controllers/TachiyomiController.cs +++ b/API/Controllers/TachiyomiController.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any /// other purposes. diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs index 2b9284f27..814278bdd 100644 --- a/API/Controllers/ThemeController.cs +++ b/API/Controllers/ThemeController.cs @@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + public class ThemeController : BaseApiController { private readonly IUnitOfWork _unitOfWork; diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index ab01d7abb..81b3ea6fe 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging; namespace API.Controllers; +#nullable enable + /// /// /// diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 9358bd406..fdb6baa5d 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -13,6 +13,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + [Authorize] public class UsersController : BaseApiController { diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index 3fb33a822..116215a36 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -16,6 +16,8 @@ using Microsoft.AspNetCore.Mvc; namespace API.Controllers; +#nullable enable + /// /// Responsible for all things Want To Read /// @@ -42,7 +44,7 @@ public class WantToReadController : BaseApiController /// [HttpPost] [Obsolete("use v2 instead")] - public async Task>> GetWantToRead([FromQuery] UserParams userParams, FilterDto filterDto) + public async Task>> GetWantToRead([FromQuery] UserParams? userParams, FilterDto filterDto) { userParams ??= new UserParams(); var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(User.GetUserId(), userParams, filterDto); @@ -60,7 +62,7 @@ public class WantToReadController : BaseApiController /// /// [HttpPost("v2")] - public async Task>> GetWantToReadV2([FromQuery] UserParams userParams, FilterV2Dto filterDto) + public async Task>> GetWantToReadV2([FromQuery] UserParams? userParams, FilterV2Dto filterDto) { userParams ??= new UserParams(); var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(User.GetUserId(), userParams, filterDto); diff --git a/API/DTOs/Filtering/v2/DecodeFilterDto.cs b/API/DTOs/Filtering/v2/DecodeFilterDto.cs new file mode 100644 index 000000000..18dc166e7 --- /dev/null +++ b/API/DTOs/Filtering/v2/DecodeFilterDto.cs @@ -0,0 +1,9 @@ +namespace API.DTOs.Filtering.v2; + +/// +/// For requesting an encoded filter to be decoded +/// +public class DecodeFilterDto +{ + public string EncodedFilter { get; set; } +} diff --git a/API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs b/API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs new file mode 100644 index 000000000..fdabd72c3 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using API.DTOs.Filtering.v2; +using API.Helpers; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// v0.7.10.2 introduced a bad encoding, this will migrate those bad smart filters +/// +public static class MigrateSmartFilterEncoding +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + logger.LogCritical("Running MigrateSmartFilterEncoding migration - Please be patient, this may take some time. This is not an error"); + + var statementsRegex = new Regex("stmts=(?.*?)&"); + const string valueRegex = @"value=(?\d+)"; + const string fieldRegex = @"field=(?\d+)"; + const string comparisonRegex = @"comparison=(?\d+)"; + var smartFilters = dataContext.AppUserSmartFilter.ToList(); + foreach (var filter in smartFilters) + { + if (filter.Filter.Contains(SmartFilterHelper.StatementSeparator)) continue; + var statements = statementsRegex.Matches(filter.Filter) + .Select(match => match.Groups["Statements"]) + .FirstOrDefault(group => group.Success && group != Match.Empty)?.Value; + if (string.IsNullOrEmpty(statements)) continue; + + + // We have statements. Let's remove the statements and generate a filter dto + var noStmt = statementsRegex.Replace(filter.Filter, string.Empty).Replace("stmts=", string.Empty); + var filterDto = SmartFilterHelper.Decode(noStmt); + + // Now we just parse each individual stmt into the core components and add to statements + + var individualParts = Uri.UnescapeDataString(statements).Split(',').Select(Uri.UnescapeDataString); + foreach (var part in individualParts) + { + filterDto.Statements.Add(new FilterStatementDto() + { + Value = Regex.Match(part, valueRegex).Groups["value"].Value, + Field = Enum.Parse(Regex.Match(part, fieldRegex).Groups["value"].Value), + Comparison = Enum.Parse(Regex.Match(part, comparisonRegex).Groups["value"].Value), + }); + } + + filter.Filter = SmartFilterHelper.Encode(filterDto); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(); + } + + logger.LogCritical("Running MigrateSmartFilterEncoding migration - Completed. This is not an error"); + } +} diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index d172ef8ba..e230eba09 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -627,7 +627,6 @@ public class UserRepository : IUserRepository return await ApplyLimit(filterSeriesQuery .Sort(filter.SortOptions) .AsSplitQuery(), filter.LimitTo) - .Select(o => o.Bookmark) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } diff --git a/API/Extensions/AppUserExtensions.cs b/API/Extensions/AppUserExtensions.cs index 2fbf865d9..07b348c2d 100644 --- a/API/Extensions/AppUserExtensions.cs +++ b/API/Extensions/AppUserExtensions.cs @@ -4,6 +4,7 @@ using API.Entities; using API.Helpers; namespace API.Extensions; +#nullable enable public static class AppUserExtensions { diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 2ceeda942..d7a30415a 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection; namespace API.Extensions; + public static class ApplicationServiceExtensions { public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) diff --git a/API/Extensions/ChapterListExtensions.cs b/API/Extensions/ChapterListExtensions.cs index 9f14c22ee..4210b01b6 100644 --- a/API/Extensions/ChapterListExtensions.cs +++ b/API/Extensions/ChapterListExtensions.cs @@ -5,6 +5,7 @@ using API.Helpers; using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; +#nullable enable public static class ChapterListExtensions { diff --git a/API/Extensions/ClaimsPrincipalExtensions.cs b/API/Extensions/ClaimsPrincipalExtensions.cs index 07d94b23f..3355a7586 100644 --- a/API/Extensions/ClaimsPrincipalExtensions.cs +++ b/API/Extensions/ClaimsPrincipalExtensions.cs @@ -3,6 +3,7 @@ using Kavita.Common; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; namespace API.Extensions; +#nullable enable public static class ClaimsPrincipalExtensions { diff --git a/API/Extensions/DateTimeExtensions.cs b/API/Extensions/DateTimeExtensions.cs index 3967641ef..a5006261f 100644 --- a/API/Extensions/DateTimeExtensions.cs +++ b/API/Extensions/DateTimeExtensions.cs @@ -1,6 +1,7 @@ using System; namespace API.Extensions; +#nullable enable public static class DateTimeExtensions { diff --git a/API/Extensions/EncodeFormatExtensions.cs b/API/Extensions/EncodeFormatExtensions.cs index bede8e721..924ae8b89 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/API/Extensions/EncodeFormatExtensions.cs @@ -2,6 +2,7 @@ using API.Entities.Enums; namespace API.Extensions; +#nullable enable public static class EncodeFormatExtensions { diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs index 8dc2377df..4e84e2fa5 100644 --- a/API/Extensions/EnumerableExtensions.cs +++ b/API/Extensions/EnumerableExtensions.cs @@ -6,6 +6,7 @@ using API.Data.Misc; using API.Entities.Enums; namespace API.Extensions; +#nullable enable public static class EnumerableExtensions { diff --git a/API/Extensions/FileInfoExtensions.cs b/API/Extensions/FileInfoExtensions.cs index 1f4ea62e1..1403486dd 100644 --- a/API/Extensions/FileInfoExtensions.cs +++ b/API/Extensions/FileInfoExtensions.cs @@ -2,6 +2,7 @@ using System.IO; namespace API.Extensions; +#nullable enable public static class FileInfoExtensions { diff --git a/API/Extensions/FilterDtoExtensions.cs b/API/Extensions/FilterDtoExtensions.cs index bc5b4eb52..7a55f7db9 100644 --- a/API/Extensions/FilterDtoExtensions.cs +++ b/API/Extensions/FilterDtoExtensions.cs @@ -4,6 +4,7 @@ using API.DTOs.Filtering; using API.Entities.Enums; namespace API.Extensions; +#nullable enable public static class FilterDtoExtensions { diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 4a75dfece..12e491a34 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace API.Extensions; +#nullable enable public static class HttpExtensions { diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 5dc547362..9549e9a2c 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace API.Extensions; +#nullable enable public static class IdentityServiceExtensions { diff --git a/API/Extensions/ParserInfoListExtensions.cs b/API/Extensions/ParserInfoListExtensions.cs index 96e39176f..58fe6ba52 100644 --- a/API/Extensions/ParserInfoListExtensions.cs +++ b/API/Extensions/ParserInfoListExtensions.cs @@ -4,6 +4,7 @@ using API.Entities; using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; +#nullable enable public static class ParserInfoListExtensions { diff --git a/API/Extensions/PathExtensions.cs b/API/Extensions/PathExtensions.cs index f45787d1a..64c0616ab 100644 --- a/API/Extensions/PathExtensions.cs +++ b/API/Extensions/PathExtensions.cs @@ -1,6 +1,7 @@ using System.IO; namespace API.Extensions; +#nullable enable public static class PathExtensions { diff --git a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs index ed4f300a0..1ef2d5dd8 100644 --- a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs @@ -1,14 +1,14 @@ using System.Linq; using API.DTOs.Filtering; using API.Entities; -using API.Extensions.QueryExtensions; namespace API.Extensions.QueryExtensions.Filtering; +#nullable enable public class BookmarkSeriesPair { - public AppUserBookmark Bookmark { get; set; } - public Series Series { get; set; } + public AppUserBookmark Bookmark { get; init; } = null!; + public Series Series { get; init; } = null!; } public static class BookmarkSort diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index a2f8877fd..0f013b5a5 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -9,7 +9,6 @@ using Kavita.Common; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions.Filtering; - #nullable enable public static class SeriesFilter diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs index 52c41c4ee..e59e9e922 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs @@ -1,7 +1,9 @@ using System.Linq; using API.DTOs.Filtering; using API.Entities; -using API.Extensions.QueryExtensions; + +namespace API.Extensions.QueryExtensions.Filtering; +#nullable enable public static class SeriesSort { diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index 006364ffb..a7f89f96d 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -4,6 +4,7 @@ using API.Entities; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; +#nullable enable /// /// All extensions against IQueryable that enables the dynamic including based on bitwise flag pattern diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index eca302203..201c8dd28 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -12,6 +12,7 @@ using API.Entities.Scrobble; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; +#nullable enable public static class QueryableExtensions { diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index 866382587..8101c9d35 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -4,6 +4,7 @@ using API.Entities; using API.Entities.Enums; namespace API.Extensions.QueryExtensions; +#nullable enable /// /// Responsible for restricting Entities based on an AgeRestriction diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index 5223f3120..ba8bcc83e 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; using API.Comparators; @@ -7,6 +6,7 @@ using API.Entities; using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; +#nullable enable public static class SeriesExtensions { diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs index ae65ffe38..ee205dbb3 100644 --- a/API/Extensions/StringExtensions.cs +++ b/API/Extensions/StringExtensions.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; namespace API.Extensions; +#nullable enable public static class StringExtensions { diff --git a/API/Extensions/VolumeListExtensions.cs b/API/Extensions/VolumeListExtensions.cs index 0d42b15e8..51dc5cf8c 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/API/Extensions/VolumeListExtensions.cs @@ -5,6 +5,7 @@ using API.Entities; using API.Entities.Enums; namespace API.Extensions; +#nullable enable public static class VolumeListExtensions { diff --git a/API/Extensions/ZipArchiveExtensions.cs b/API/Extensions/ZipArchiveExtensions.cs index 89a083490..8ed338e57 100644 --- a/API/Extensions/ZipArchiveExtensions.cs +++ b/API/Extensions/ZipArchiveExtensions.cs @@ -3,6 +3,7 @@ using System.IO.Compression; using System.Linq; namespace API.Extensions; +#nullable enable public static class ZipArchiveExtensions { diff --git a/API/Helpers/Builders/SmartFilterBuilder.cs b/API/Helpers/Builders/SmartFilterBuilder.cs deleted file mode 100644 index 538d8a529..000000000 --- a/API/Helpers/Builders/SmartFilterBuilder.cs +++ /dev/null @@ -1,24 +0,0 @@ -using API.DTOs.Filtering.v2; -using API.Entities; - -namespace API.Helpers.Builders; - -public class SmartFilterBuilder : IEntityBuilder -{ - private AppUserSmartFilter _smartFilter; - public AppUserSmartFilter Build() => _smartFilter; - - public SmartFilterBuilder(FilterV2Dto filter) - { - _smartFilter = new AppUserSmartFilter() - { - Name = filter.Name, - Filter = SmartFilterHelper.Encode(filter) - }; - } - - // public SmartFilterBuilder WithName(string name) - // { - // - // } -} diff --git a/API/Helpers/CacheHelper.cs b/API/Helpers/CacheHelper.cs index d7e22c2e6..510ff5409 100644 --- a/API/Helpers/CacheHelper.cs +++ b/API/Helpers/CacheHelper.cs @@ -4,6 +4,7 @@ using API.Entities.Interfaces; using API.Services; namespace API.Helpers; +#nullable enable public interface ICacheHelper { diff --git a/API/Helpers/Converters/CronConverter.cs b/API/Helpers/Converters/CronConverter.cs index 4e9547c6c..a16ce4ef3 100644 --- a/API/Helpers/Converters/CronConverter.cs +++ b/API/Helpers/Converters/CronConverter.cs @@ -2,6 +2,7 @@ using Hangfire; namespace API.Helpers.Converters; +#nullable enable public static class CronConverter { diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/API/Helpers/Converters/FilterFieldValueConverter.cs index 7f4001f67..dd5630aeb 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/API/Helpers/Converters/FilterFieldValueConverter.cs @@ -7,6 +7,7 @@ using API.Entities.Enums; using API.Extensions; namespace API.Helpers.Converters; +#nullable enable public static class FilterFieldValueConverter { diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index a55e104a7..ffae4d5a8 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -6,6 +6,7 @@ using API.Entities.Enums; using AutoMapper; namespace API.Helpers.Converters; +#nullable enable public class ServerSettingConverter : ITypeConverter, ServerSettingDto> { diff --git a/API/Helpers/LibraryTypeHelper.cs b/API/Helpers/LibraryTypeHelper.cs index f2d320621..53f5e5b60 100644 --- a/API/Helpers/LibraryTypeHelper.cs +++ b/API/Helpers/LibraryTypeHelper.cs @@ -3,6 +3,7 @@ using API.DTOs.Scrobbling; using API.Entities.Enums; namespace API.Helpers; +#nullable enable public static class LibraryTypeHelper { diff --git a/API/Helpers/NumberHelper.cs b/API/Helpers/NumberHelper.cs index b15f7e680..906e405cc 100644 --- a/API/Helpers/NumberHelper.cs +++ b/API/Helpers/NumberHelper.cs @@ -1,4 +1,5 @@ namespace API.Helpers; +#nullable enable public static class NumberHelper { diff --git a/API/Helpers/OrderableHelper.cs b/API/Helpers/OrderableHelper.cs index 06a53f575..d936eb588 100644 --- a/API/Helpers/OrderableHelper.cs +++ b/API/Helpers/OrderableHelper.cs @@ -2,6 +2,7 @@ using API.Entities; namespace API.Helpers; +#nullable enable public static class OrderableHelper { diff --git a/API/Helpers/PagedList.cs b/API/Helpers/PagedList.cs index 0c666612d..44d8a5082 100644 --- a/API/Helpers/PagedList.cs +++ b/API/Helpers/PagedList.cs @@ -5,10 +5,11 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; namespace API.Helpers; +#nullable enable public class PagedList : List { - public PagedList(IEnumerable items, int count, int pageNumber, int pageSize) + private PagedList(IEnumerable items, int count, int pageNumber, int pageSize) { CurrentPage = pageNumber; TotalPages = (int) Math.Ceiling(count / (double) pageSize); diff --git a/API/Helpers/PaginationHeader.cs b/API/Helpers/PaginationHeader.cs index d3c582798..b11c5ecd4 100644 --- a/API/Helpers/PaginationHeader.cs +++ b/API/Helpers/PaginationHeader.cs @@ -1,4 +1,5 @@ namespace API.Helpers; +#nullable enable public class PaginationHeader { diff --git a/API/Helpers/ParserInfoHelpers.cs b/API/Helpers/ParserInfoHelpers.cs index dbd2f57da..18d2a8f82 100644 --- a/API/Helpers/ParserInfoHelpers.cs +++ b/API/Helpers/ParserInfoHelpers.cs @@ -6,6 +6,7 @@ using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; namespace API.Helpers; +#nullable enable public static class ParserInfoHelpers { diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index f8974a566..aa8e7bcd3 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -8,6 +8,7 @@ using API.Extensions; using API.Helpers.Builders; namespace API.Helpers; +#nullable enable public static class PersonHelper { @@ -29,6 +30,7 @@ public static class PersonHelper foreach (var name in names) { var normalizedName = name.ToNormalized(); + // BUG: Doesn't this create a duplicate entry because allPeopleTypeRoles is a different instance? var person = allPeopleTypeRole.Find(p => p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); if (person == null) diff --git a/API/Helpers/ReadingListHelper.cs b/API/Helpers/ReadingListHelper.cs deleted file mode 100644 index 5f282702b..000000000 --- a/API/Helpers/ReadingListHelper.cs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/API/Helpers/SQLHelper.cs b/API/Helpers/SQLHelper.cs deleted file mode 100644 index 575ba8c77..000000000 --- a/API/Helpers/SQLHelper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using Microsoft.EntityFrameworkCore; - -namespace API.Helpers; - -public static class SqlHelper -{ - public static List RawSqlQuery(DbContext context, string query, Func map) - { - using var command = context.Database.GetDbConnection().CreateCommand(); - command.CommandText = query; - command.CommandType = CommandType.Text; - - context.Database.OpenConnection(); - - using var result = command.ExecuteReader(); - var entities = new List(); - - while (result.Read()) - { - entities.Add(map(result)); - } - - return entities; - } -} diff --git a/API/Helpers/SeriesHelper.cs b/API/Helpers/SeriesHelper.cs index 2b520fb7e..231575b0e 100644 --- a/API/Helpers/SeriesHelper.cs +++ b/API/Helpers/SeriesHelper.cs @@ -6,6 +6,7 @@ using API.Extensions; using API.Services.Tasks.Scanner; namespace API.Helpers; +#nullable enable public static class SeriesHelper { diff --git a/API/Helpers/SmartFilterHelper.cs b/API/Helpers/SmartFilterHelper.cs index 740b8cd4e..0749cb29e 100644 --- a/API/Helpers/SmartFilterHelper.cs +++ b/API/Helpers/SmartFilterHelper.cs @@ -5,14 +5,24 @@ using System.Web; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; +#nullable enable + namespace API.Helpers; public static class SmartFilterHelper { private const string SortOptionsKey = "sortOptions="; + private const string NameKey = "name="; + private const string SortFieldKey = "sortField="; + private const string IsAscendingKey = "isAscending="; private const string StatementsKey = "stmts="; private const string LimitToKey = "limitTo="; private const string CombinationKey = "combination="; + private const string StatementComparisonKey = "comparison="; + private const string StatementFieldKey = "field="; + private const string StatementValueKey = "value="; + public const string StatementSeparator = "\ufffd"; + public const string InnerStatementSeparator = "¦"; public static FilterV2Dto Decode(string? encodedFilter) { @@ -21,7 +31,7 @@ public static class SmartFilterHelper return new FilterV2Dto(); // Create a default filter if the input is empty } - string[] parts = encodedFilter.Split('&'); + var parts = encodedFilter.Split('&'); var filter = new FilterV2Dto(); foreach (var part in parts) @@ -42,7 +52,7 @@ public static class SmartFilterHelper { filter.Statements = DecodeFilterStatementDtos(part.Substring(StatementsKey.Length)); } - else if (part.StartsWith("name=")) + else if (part.StartsWith(NameKey)) { filter.Name = HttpUtility.UrlDecode(part.Substring(5)); } @@ -51,7 +61,7 @@ public static class SmartFilterHelper return filter; } - public static string Encode(FilterV2Dto filter) + public static string Encode(FilterV2Dto? filter) { if (filter == null) return string.Empty; @@ -59,50 +69,50 @@ public static class SmartFilterHelper var encodedStatements = EncodeFilterStatementDtos(filter.Statements); var encodedSortOptions = filter.SortOptions != null ? $"{SortOptionsKey}{EncodeSortOptions(filter.SortOptions)}" - : ""; + : string.Empty; var encodedLimitTo = $"{LimitToKey}{filter.LimitTo}"; return $"{EncodeName(filter.Name)}{encodedStatements}&{encodedSortOptions}&{encodedLimitTo}&{CombinationKey}{(int) filter.Combination}"; } - private static string EncodeName(string name) + private static string EncodeName(string? name) { - return string.IsNullOrWhiteSpace(name) ? string.Empty : $"name={HttpUtility.UrlEncode(name)}&"; + return string.IsNullOrWhiteSpace(name) ? string.Empty : $"{NameKey}{Uri.EscapeDataString(name)}&"; } private static string EncodeSortOptions(SortOptions sortOptions) { - return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField},isAscending={sortOptions.IsAscending}"); + return Uri.EscapeDataString($"{SortFieldKey}{(int) sortOptions.SortField}{InnerStatementSeparator}{IsAscendingKey}{sortOptions.IsAscending}"); } - private static string EncodeFilterStatementDtos(ICollection statements) + private static string EncodeFilterStatementDtos(ICollection? statements) { if (statements == null || statements.Count == 0) return string.Empty; - var encodedStatements = StatementsKey + Uri.EscapeDataString(string.Join(",", statements.Select(EncodeFilterStatementDto))); + var encodedStatements = StatementsKey + Uri.EscapeDataString(string.Join(StatementSeparator, statements.Select(EncodeFilterStatementDto))); return encodedStatements; } private static string EncodeFilterStatementDto(FilterStatementDto statement) { - var encodedComparison = $"comparison={(int) statement.Comparison}"; - var encodedField = $"field={(int) statement.Field}"; - var encodedValue = $"value={Uri.EscapeDataString(statement.Value)}"; - return Uri.EscapeDataString($"{encodedComparison},{encodedField},{encodedValue}"); + var encodedComparison = $"{StatementComparisonKey}{(int) statement.Comparison}"; + var encodedField = $"{StatementFieldKey}{(int) statement.Field}"; + var encodedValue = $"{StatementValueKey}{Uri.EscapeDataString(statement.Value)}"; + + return Uri.EscapeDataString($"{encodedComparison}{InnerStatementSeparator}{encodedField}{InnerStatementSeparator}{encodedValue}"); } private static List DecodeFilterStatementDtos(string encodedStatements) { - encodedStatements = HttpUtility.UrlDecode(encodedStatements); - string[] statementStrings = encodedStatements.Split(','); + var statementStrings = Uri.UnescapeDataString(encodedStatements).Split(StatementSeparator); var statements = new List(); foreach (var statementString in statementStrings) { - var parts = statementString.Split('&'); + var parts = Uri.UnescapeDataString(statementString).Split(InnerStatementSeparator); if (parts.Length < 3) continue; @@ -110,7 +120,7 @@ public static class SmartFilterHelper { Comparison = Enum.Parse(parts[0].Split("=")[1]), Field = Enum.Parse(parts[1].Split("=")[1]), - Value = HttpUtility.UrlDecode(parts[2].Split("=")[1]) + Value = Uri.UnescapeDataString(parts[2].Split("=")[1]) }); } @@ -119,22 +129,22 @@ public static class SmartFilterHelper private static SortOptions DecodeSortOptions(string encodedSortOptions) { - string[] parts = encodedSortOptions.Split(','); - var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith("sortField=")); - var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith("isAscending=")); + var parts = Uri.UnescapeDataString(encodedSortOptions).Split(InnerStatementSeparator); + var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith(SortFieldKey)); + var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith(IsAscendingKey)); var isAscending = isAscendingPart?.Substring(11).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; - if (sortFieldPart != null) + if (sortFieldPart == null) { - var sortField = Enum.Parse(sortFieldPart.Split("=")[1]); - - return new SortOptions - { - SortField = sortField, - IsAscending = isAscending - }; + return new SortOptions(); } - return null; + var sortField = Enum.Parse(sortFieldPart.Split("=")[1]); + + return new SortOptions + { + SortField = sortField, + IsAscending = isAscending + }; } } diff --git a/API/Helpers/TagHelper.cs b/API/Helpers/TagHelper.cs index 492214a34..a69ed3c97 100644 --- a/API/Helpers/TagHelper.cs +++ b/API/Helpers/TagHelper.cs @@ -9,8 +9,8 @@ using API.Extensions; using API.Helpers.Builders; namespace API.Helpers; - #nullable enable + public static class TagHelper { /// diff --git a/API/Helpers/UserParams.cs b/API/Helpers/UserParams.cs index e5eb37802..525f9340c 100644 --- a/API/Helpers/UserParams.cs +++ b/API/Helpers/UserParams.cs @@ -1,4 +1,5 @@ namespace API.Helpers; +#nullable enable public class UserParams { @@ -15,7 +16,7 @@ public class UserParams init => _pageSize = (value == 0) ? MaxPageSize : value; } - public static readonly UserParams Default = new UserParams() + public static readonly UserParams Default = new() { PageSize = 20, PageNumber = 1 diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index 24fd69511..995604f17 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -16,6 +16,8 @@ using Microsoft.Extensions.Logging; namespace API.Services; +#nullable enable + public interface IAccountService { Task> ChangeUserPassword(AppUser user, string newPassword); diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index fd4349c90..8fe6207a4 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -18,6 +18,8 @@ using SharpCompress.Common; namespace API.Services; +#nullable enable + public interface IArchiveService { void ExtractArchive(string archivePath, string extractPath); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index fe7df8815..ffaf230fb 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -29,6 +29,7 @@ using VersOne.Epub.Options; using VersOne.Epub.Schema; namespace API.Services; + #nullable enable public interface IBookService diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index 7ff7cd0ad..f28ef9f74 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -12,6 +12,8 @@ using Microsoft.Extensions.Logging; namespace API.Services; +#nullable enable + public interface IBookmarkService { Task DeleteBookmarkFiles(IEnumerable bookmarks); diff --git a/API/Services/HostedServices/StartupTasksHostedService.cs b/API/Services/HostedServices/StartupTasksHostedService.cs index d7d74f77d..37b6effee 100644 --- a/API/Services/HostedServices/StartupTasksHostedService.cs +++ b/API/Services/HostedServices/StartupTasksHostedService.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace API.Services.HostedServices; +#nullable enable public class StartupTasksHostedService : IHostedService { diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 8b8d046c1..48103ef53 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -14,6 +14,7 @@ using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services.Plus; +#nullable enable /// /// Used for matching and fetching metadata on a series diff --git a/API/Services/Plus/LicenseService.cs b/API/Services/Plus/LicenseService.cs index 8bb845da3..2307520e4 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/API/Services/Plus/LicenseService.cs @@ -13,6 +13,7 @@ using Kavita.Common.EnvironmentInfo; using Microsoft.Extensions.Logging; namespace API.Services.Plus; +#nullable enable internal class RegisterLicenseResponseDto { diff --git a/API/Services/Plus/RatingService.cs b/API/Services/Plus/RatingService.cs index 0993948fd..7701b2326 100644 --- a/API/Services/Plus/RatingService.cs +++ b/API/Services/Plus/RatingService.cs @@ -17,6 +17,7 @@ using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services.Plus; +#nullable enable public interface IRatingService { diff --git a/API/Services/Plus/RecommendationService.cs b/API/Services/Plus/RecommendationService.cs index 87fbfabaa..d5dd67231 100644 --- a/API/Services/Plus/RecommendationService.cs +++ b/API/Services/Plus/RecommendationService.cs @@ -18,6 +18,7 @@ using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services.Plus; +#nullable enable public record PlusSeriesDto { diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index f9b206076..ead68227b 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -22,6 +22,7 @@ using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services.Plus; +#nullable enable /// /// Misleading name but is the source of data (like a review coming from AniList) diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index 3b1f7746c..99e921c50 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -12,6 +12,7 @@ using Hangfire; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; +#nullable enable public interface IBackupService { diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index 257103708..9dfb9c1cf 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -13,6 +13,7 @@ using Hangfire; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; +#nullable enable public interface ICleanupService { diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index 4ebbf57c6..a73a53b5b 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging; using VersOne.Epub; namespace API.Services.Tasks.Metadata; +#nullable enable public interface IWordCountAnalyzerService { diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index 6e844fbe3..2cbb24fb4 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks.Scanner; +#nullable enable public interface ILibraryWatcher { diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index f898d77cc..0604cb890 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -12,6 +12,7 @@ using Kavita.Common.Helpers; using Microsoft.Extensions.Logging; namespace API.Services.Tasks.Scanner; +#nullable enable public class ParsedSeries { diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 188afc9c1..5ea5a1a0a 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -3,6 +3,7 @@ using System.Linq; using API.Entities.Enums; namespace API.Services.Tasks.Scanner.Parser; +#nullable enable public interface IDefaultParser { diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 187c671ed..80461c12e 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -7,6 +7,7 @@ using API.Entities.Enums; using API.Extensions; namespace API.Services.Tasks.Scanner.Parser; +#nullable enable public static class Parser { diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index b42acafe7..bbcd87280 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -21,7 +21,6 @@ using Kavita.Common; using Microsoft.Extensions.Logging; namespace API.Services.Tasks.Scanner; - #nullable enable public interface IProcessSeries diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index e42ba42cc..6a08306df 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -19,6 +19,8 @@ using Hangfire; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; +#nullable enable + public interface IScannerService { /// diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index 40017f0ef..730900c16 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -10,6 +10,7 @@ using Kavita.Common; using Microsoft.AspNetCore.Authorization; namespace API.Services.Tasks; +#nullable enable public interface IThemeService { diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index bc14967a3..f635ff9af 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -19,6 +19,8 @@ using Microsoft.Extensions.Logging; namespace API.Services.Tasks; +#nullable enable + public interface IStatsService { Task Send(); diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 9bb7b86d5..d13461fee 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -14,6 +14,8 @@ using Microsoft.Extensions.Logging; namespace API.Services.Tasks; +#nullable enable + internal class GithubReleaseMetadata { /// diff --git a/API/Startup.cs b/API/Startup.cs index 4418fe270..2c9885280 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -241,6 +241,9 @@ public class Startup // v0.7.9 await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger); + // v0.7.11 + await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger); + // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); installVersion.Value = BuildInfo.Version.ToString(); diff --git a/UI/Web/src/app/_models/pagination.ts b/UI/Web/src/app/_models/pagination.ts index c007c528a..8d6a4a06a 100644 --- a/UI/Web/src/app/_models/pagination.ts +++ b/UI/Web/src/app/_models/pagination.ts @@ -1,8 +1,15 @@ -export interface Pagination { +export class Pagination { currentPage: number; itemsPerPage: number; totalItems: number; totalPages: number; + + constructor() { + this.currentPage = 0; + this.itemsPerPage = 0; + this.totalItems = 0; + this.totalPages = 0; + } } export class PaginatedResult { diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index 2e8214fb9..c3a21b8b5 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -1,16 +1,14 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {map, tap} from 'rxjs/operators'; -import {of, ReplaySubject, switchMap} from 'rxjs'; +import {tap} from 'rxjs/operators'; +import {of} from 'rxjs'; import {environment} from 'src/environments/environment'; import {Genre} from '../_models/metadata/genre'; -import {AgeRating} from '../_models/metadata/age-rating'; import {AgeRatingDto} from '../_models/metadata/age-rating-dto'; import {Language} from '../_models/metadata/language'; import {PublicationStatusDto} from '../_models/metadata/publication-status-dto'; import {Person, PersonRole} from '../_models/metadata/person'; import {Tag} from '../_models/tag'; -import {TextResonse} from '../_types/text-response'; import {FilterComparison} from '../_models/metadata/v2/filter-comparison'; import {FilterField} from '../_models/metadata/v2/filter-field'; import {Router} from "@angular/router"; @@ -93,10 +91,6 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + 'metadata/people-by-role?role=' + role); } - // getChapterSummary(chapterId: number) { - // return this.httpClient.get(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse); - // } - createDefaultFilterDto(): SeriesFilterV2 { return { statements: [] as FilterStatement[], diff --git a/UI/Web/src/app/all-series/_components/all-series/all-series.component.html b/UI/Web/src/app/all-series/_components/all-series/all-series.component.html index 707f62af7..49751aeb5 100644 --- a/UI/Web/src/app/all-series/_components/all-series/all-series.component.html +++ b/UI/Web/src/app/all-series/_components/all-series/all-series.component.html @@ -6,7 +6,7 @@
{{t('series-count', {num: pagination.totalItems | number})}}
- = new EventEmitter(); @@ -113,20 +113,18 @@ export class AllSeriesComponent implements OnInit { this.router.routeReuseStrategy.shouldReuseRoute = () => false; - this.title = this.route.snapshot.queryParamMap.get('title') || this.title; - this.titleService.setTitle('Kavita - ' + this.title); + this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { + this.filter = filter; - this.pagination = this.filterUtilityService.pagination(this.route.snapshot); + this.title = this.route.snapshot.queryParamMap.get('title') || this.filter.name || this.title; + this.titleService.setTitle('Kavita - ' + this.title); - this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot); - if (this.filter.statements.length === 0) { - this.filter!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); - } - this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); - this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); - this.filterSettings.presetsV2 = this.filter; + this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); + this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); + this.filterSettings.presetsV2 = this.filter; - this.cdRef.markForCheck(); + this.cdRef.markForCheck(); + }); } ngOnInit(): void { @@ -155,16 +153,20 @@ export class AllSeriesComponent implements OnInit { if (data.filterV2 === undefined) return; this.filter = data.filterV2; - if (!data.isFirst) { - this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter); + if (data.isFirst) { + this.loadPage(); + return; } - this.loadPage(); + this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => { + this.loadPage(); + }); } loadPage() { this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck); this.loadingSeries = true; + this.title = this.route.snapshot.queryParamMap.get('title') || this.filter?.name || translate('all-series.title'); this.cdRef.markForCheck(); this.seriesService.getAllSeriesV2(undefined, undefined, this.filter!).pipe(take(1)).subscribe(series => { this.series = series.result; diff --git a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html index 165e1b430..df44e5cc6 100644 --- a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html +++ b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html @@ -6,7 +6,7 @@
{{t('series-count', {num: series.length | number})}}
- [] = []; jumpbarKeys: Array = []; - pagination!: Pagination; + pagination: Pagination = new Pagination(); filter: SeriesFilterV2 | undefined = undefined; filterSettings: FilterSettings = new FilterSettings(); filterOpen: EventEmitter = new EventEmitter(); @@ -73,20 +73,23 @@ export class BookmarksComponent implements OnInit { private router: Router, private readonly cdRef: ChangeDetectorRef, private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute, private jumpbarService: JumpbarService, private titleService: Title) { - this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot); - if (this.filter.statements.length === 0) { - this.filter!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); - } - this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); - this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); - this.filterSettings.presetsV2 = this.filter; - this.filterSettings.statementLimit = 1; + + this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { + this.filter = filter; + + this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); + this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); + this.filterSettings.presetsV2 = this.filter; + this.filterSettings.statementLimit = 1; + + this.cdRef.markForCheck(); + }); + this.titleService.setTitle('Kavita - ' + translate('bookmarks.title')); } ngOnInit(): void { this.actions = this.actionFactoryService.getBookmarkActions(this.handleAction.bind(this)); - this.pagination = this.filterUtilityService.pagination(this.route.snapshot); } @@ -142,7 +145,7 @@ export class BookmarksComponent implements OnInit { this.readerService.clearMultipleBookmarks(seriesIds).subscribe(() => { this.toastr.success(this.translocoService.translate('bookmarks.delete-success')); this.bulkSelectionService.deselectAll(); - this.loadBookmarks(); + this.loadPage(); }); break; default: @@ -150,7 +153,7 @@ export class BookmarksComponent implements OnInit { } } - loadBookmarks() { + loadPage() { this.loadingBookmarks = true; this.cdRef.markForCheck(); @@ -210,11 +213,13 @@ export class BookmarksComponent implements OnInit { if (data.filterV2 === undefined) return; this.filter = data.filterV2; - if (!data.isFirst) { - this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter); + if (data.isFirst) { + this.loadPage(); + return; } - this.loadBookmarks(); + this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => { + this.loadPage(); + }); } - } diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index be8d1c9e2..279523eb8 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -170,7 +170,7 @@ export class BulkSelectionService { private applyFilter(action: ActionItem, allowedActions: Array) { - var ret = false; + let ret = false; if (action.action === Action.Submenu || allowedActions.includes(action.action)) { // Do something ret = true; diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html index fd5c5d564..ccf5aa0d9 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html @@ -22,8 +22,8 @@ - = []; - pagination!: Pagination; + pagination: Pagination = new Pagination(); collectionTagActions: ActionItem[] = []; filter: SeriesFilterV2 | undefined = undefined; filterSettings: FilterSettings = new FilterSettings(); @@ -168,19 +168,19 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked { } const tagId = parseInt(routeId, 10); - this.pagination = this.filterUtilityService.pagination(this.route.snapshot); + this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { + this.filter = filter; - this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot); - if (this.filter.statements.filter(stmt => stmt.field === FilterField.Libraries).length === 0) { - this.filter!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal}); - } - this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); - this.filterActiveCheck!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal}); - this.filterSettings.presetsV2 = this.filter; + if (this.filter.statements.filter(stmt => stmt.field === FilterField.CollectionTags).length === 0) { + this.filter!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal}); + } + this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); + this.filterActiveCheck!.statements.push({field: FilterField.CollectionTags, value: tagId + '', comparison: FilterComparison.Equal}); + this.filterSettings.presetsV2 = this.filter; + this.cdRef.markForCheck(); - this.cdRef.markForCheck(); - - this.updateTag(tagId); + this.updateTag(tagId); + }); } ngOnInit(): void { @@ -252,11 +252,14 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked { if (data.filterV2 === undefined) return; this.filter = data.filterV2; - if (!data.isFirst) { - this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter); + if (data.isFirst) { + this.loadPage(); + return; } - this.loadPage(); + this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => { + this.loadPage(); + }); } handleCollectionActionCallback(action: ActionItem, collectionTag: CollectionTag) { @@ -284,4 +287,5 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked { }); } + protected readonly undefined = undefined; } diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.ts b/UI/Web/src/app/dashboard/_components/dashboard.component.ts index 89c04ec6e..25259cacc 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.ts @@ -147,7 +147,10 @@ export class DashboardComponent implements OnInit { s.api = this.seriesService.getRecentlyUpdatedSeries(); break; case StreamType.SmartFilter: - s.api = this.seriesService.getAllSeriesV2(0, 20, this.filterUtilityService.decodeSeriesFilter(s.smartFilterEncoded!)) + s.api = this.filterUtilityService.decodeFilter(s.smartFilterEncoded!).pipe( + switchMap(filter => { + return this.seriesService.getAllSeriesV2(0, 20, filter); + })) .pipe(map(d => d.result), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true})); break; case StreamType.MoreInGenre: @@ -195,7 +198,7 @@ export class DashboardComponent implements OnInit { filter.sortOptions.sortField = SortField.LastChapterAdded; filter.sortOptions.isAscending = false; } - this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params) + this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); } else if (sectionTitle.toLowerCase() === 'on deck') { const params: any = {}; params['page'] = 1; @@ -208,7 +211,7 @@ export class DashboardComponent implements OnInit { filter.sortOptions.sortField = SortField.LastChapterAdded; filter.sortOptions.isAscending = false; } - this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params) + this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); } else if (sectionTitle.toLowerCase() === 'newly added series') { const params: any = {}; params['page'] = 1; @@ -218,14 +221,14 @@ export class DashboardComponent implements OnInit { filter.sortOptions.sortField = SortField.Created; filter.sortOptions.isAscending = false; } - this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params) + this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); } else if (sectionTitle.toLowerCase() === 'more in genre') { const params: any = {}; params['page'] = 1; params['title'] = translate('more-in-genre-title', {genre: this.genre?.title}); const filter = this.filterUtilityService.createSeriesV2Filter(); filter.statements.push({field: FilterField.Genres, value: this.genre?.id + '', comparison: FilterComparison.MustContains}); - this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params) + this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); } } diff --git a/UI/Web/src/app/library-detail/library-detail.component.html b/UI/Web/src/app/library-detail/library-detail.component.html index 3f29b4b5d..550a4049e 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.html +++ b/UI/Web/src/app/library-detail/library-detail.component.html @@ -7,7 +7,7 @@
{{t('common.series-count', {num: pagination.totalItems | number})}}
- [] = []; - filterV2: SeriesFilterV2 | undefined = undefined; + filter: SeriesFilterV2 | undefined = undefined; filterSettings: FilterSettings = new FilterSettings(); filterOpen: EventEmitter = new EventEmitter(); filterActive: boolean = false; @@ -158,19 +158,20 @@ export class LibraryDetailComponent implements OnInit { this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this)); - this.pagination = this.filterUtilityService.pagination(this.route.snapshot); - this.filterV2 = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot); + this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { + this.filter = filter; - if (this.filterV2.statements.filter(stmt => stmt.field === FilterField.Libraries).length === 0) { - this.filterV2!.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal}); - } + if (this.filter.statements.filter(stmt => stmt.field === FilterField.Libraries).length === 0) { + this.filter!.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal}); + } - this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); - this.filterActiveCheck.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal}); + this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); + this.filterActiveCheck.statements.push({field: FilterField.Libraries, value: this.libraryId + '', comparison: FilterComparison.Equal}); - this.filterSettings.presetsV2 = this.filterV2; + this.filterSettings.presetsV2 = this.filter; - this.cdRef.markForCheck(); + this.cdRef.markForCheck(); + }); } @@ -179,7 +180,7 @@ export class LibraryDetailComponent implements OnInit { if (event.event === EVENTS.SeriesAdded) { const seriesAdded = event.payload as SeriesAddedEvent; if (seriesAdded.libraryId !== this.libraryId) return; - if (!this.utilityService.deepEqual(this.filterV2, this.filterActiveCheck)) { + if (!this.utilityService.deepEqual(this.filter, this.filterActiveCheck)) { this.loadPage(); return; } @@ -199,7 +200,7 @@ export class LibraryDetailComponent implements OnInit { } else if (event.event === EVENTS.SeriesRemoved) { const seriesRemoved = event.payload as SeriesRemovedEvent; if (seriesRemoved.libraryId !== this.libraryId) return; - if (!this.utilityService.deepEqual(this.filterV2, this.filterActiveCheck)) { + if (!this.utilityService.deepEqual(this.filter, this.filterActiveCheck)) { this.loadPage(); return; } @@ -257,21 +258,24 @@ export class LibraryDetailComponent implements OnInit { updateFilter(data: FilterEvent) { if (data.filterV2 === undefined) return; - this.filterV2 = data.filterV2; + this.filter = data.filterV2; - if (!data.isFirst) { - this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filterV2); + if (data.isFirst) { + this.loadPage(); + return; } - this.loadPage(); + this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => { + this.loadPage(); + }); } loadPage() { this.loadingSeries = true; - this.filterActive = !this.utilityService.deepEqual(this.filterV2, this.filterActiveCheck); + this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck); this.cdRef.markForCheck(); - this.seriesService.getSeriesForLibraryV2(undefined, undefined, this.filterV2) + this.seriesService.getSeriesForLibraryV2(undefined, undefined, this.filter) .subscribe(series => { this.series = series.result; this.pagination = series.pagination; @@ -282,4 +286,5 @@ export class LibraryDetailComponent implements OnInit { } trackByIdentity = (index: number, item: Series) => `${item.id}_${item.name}_${item.localizedName}_${item.pagesRead}`; + protected readonly undefined = undefined; } diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts index ac76fe609..b1dd8e7b7 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts @@ -136,7 +136,7 @@ export class NavHeaderComponent implements OnInit { filter.statements = [statement]; params['page'] = 1; this.clearSearch(); - this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params); + this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); } goToOther(field: FilterField, value: string) { diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index 1997bd102..62814c295 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -234,6 +234,6 @@ export class ReadingListDetailComponent implements OnInit { } goToCharacter(character: Person) { - this.filterUtilityService.applyFilter(['all-series'], FilterField.Characters, FilterComparison.Contains, character.id + ''); + this.filterUtilityService.applyFilter(['all-series'], FilterField.Characters, FilterComparison.Contains, character.id + '').subscribe(); } } diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts index 7185c6bea..c5dec74f2 100644 --- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts @@ -123,5 +123,4 @@ export class ReadingListsComponent implements OnInit { handleClick(list: ReadingList) { this.router.navigateByUrl('lists/' + list.id); } - } diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts index 1225c8ee7..b006775dc 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts @@ -24,12 +24,12 @@ export class MetadataDetailComponent { @ContentChild('titleTemplate') titleTemplate!: TemplateRef; @ContentChild('itemTemplate') itemTemplate?: TemplateRef; - private readonly filterUtilitiesService = inject(FilterUtilitiesService); + private readonly filterUtilityService = inject(FilterUtilitiesService); protected readonly TagBadgeCursor = TagBadgeCursor; goTo(queryParamName: FilterField, filter: any) { if (queryParamName === FilterField.None) return; - this.filterUtilitiesService.applyFilter(['library', this.libraryId], queryParamName, FilterComparison.Equal, filter); + this.filterUtilityService.applyFilter(['library', this.libraryId], queryParamName, FilterComparison.Equal, filter).subscribe(); } } diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts index d340db42a..77dde3292 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts @@ -102,7 +102,7 @@ export class SeriesMetadataDetailComponent implements OnChanges { goTo(queryParamName: FilterField, filter: any) { this.filterUtilityService.applyFilter(['library', this.series.libraryId], queryParamName, - FilterComparison.Equal, filter); + FilterComparison.Equal, filter).subscribe(); } navigate(basePage: string, id: number) { diff --git a/UI/Web/src/app/shared/_services/filter-utilities.service.ts b/UI/Web/src/app/shared/_services/filter-utilities.service.ts index 2b5bb647d..b266ec00c 100644 --- a/UI/Web/src/app/shared/_services/filter-utilities.service.ts +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -1,6 +1,5 @@ import {Injectable} from '@angular/core'; import {ActivatedRouteSnapshot, Params, Router} from '@angular/router'; -import {Pagination} from 'src/app/_models/pagination'; import {SortField, SortOptions} from 'src/app/_models/metadata/series-filter'; import {MetadataService} from "../../_services/metadata.service"; import {SeriesFilterV2} from "../../_models/metadata/v2/series-filter-v2"; @@ -8,245 +7,93 @@ import {FilterStatement} from "../../_models/metadata/v2/filter-statement"; import {FilterCombination} from "../../_models/metadata/v2/filter-combination"; import {FilterField} from "../../_models/metadata/v2/filter-field"; import {FilterComparison} from "../../_models/metadata/v2/filter-comparison"; +import {HttpClient} from "@angular/common/http"; +import {TextResonse} from "../../_types/text-response"; +import {environment} from "../../../environments/environment"; +import {map, tap} from "rxjs/operators"; +import {of, switchMap} from "rxjs"; -const sortOptionsKey = 'sortOptions='; -const statementsKey = 'stmts='; -const limitToKey = 'limitTo='; -const combinationKey = 'combination='; @Injectable({ providedIn: 'root' }) export class FilterUtilitiesService { - constructor(private metadataService: MetadataService, private router: Router) {} + private apiUrl = environment.apiUrl; - applyFilter(page: Array, filter: FilterField, comparison: FilterComparison, value: string) { - const dto: SeriesFilterV2 = { - statements: [this.metadataService.createDefaultFilterStatement(filter, comparison, value + '')], - combination: FilterCombination.Or, - limitTo: 0 - }; + constructor(private metadataService: MetadataService, private router: Router, private http: HttpClient) {} - const url = this.urlFromFilterV2(page.join('/') + '?', dto); - return this.router.navigateByUrl(url); - } - - applyFilterWithParams(page: Array, filter: SeriesFilterV2, extraParams: Params) { - let url = this.urlFromFilterV2(page.join('/') + '?', filter); - url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join(''); - return this.router.navigateByUrl(url, extraParams); - } - - /** - * Updates the window location with a custom url based on filter and pagination objects - * @param pagination - * @param filter - */ - updateUrlFromFilterV2(pagination: Pagination, filter: SeriesFilterV2 | undefined) { - const params = '?page=' + pagination.currentPage + '&'; - - const url = this.urlFromFilterV2(window.location.href.split('?')[0] + params, filter); - window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination)); - } - - - private replacePaginationOnUrl(url: string, pagination: Pagination) { - return url.replace(/page=\d+/i, 'page=' + pagination.currentPage); - } - - /** - * Will fetch current page from route if present - * @param snapshot to fetch page from. Must be from component else may get stale data - * @param itemsPerPage If you want pagination, pass non-zero number - * @returns A default pagination object - */ - pagination(snapshot: ActivatedRouteSnapshot, itemsPerPage: number = 0): Pagination { - return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage, totalItems: 0, totalPages: 1}; - } - - - /** - * Returns the current url with query params for the filter - * @param currentUrl Full url, with ?page=1 as a minimum - * @param filter Filter to build url off - * @returns current url with query params added - */ - urlFromFilterV2(currentUrl: string, filter: SeriesFilterV2 | undefined) { - if (filter === undefined) return currentUrl; - - return currentUrl + this.encodeSeriesFilter(filter); - } - - encodeSeriesFilter(filter: SeriesFilterV2) { - const encodedStatements = this.encodeFilterStatements(filter.statements); - const encodedSortOptions = filter.sortOptions ? `${sortOptionsKey}${this.encodeSortOptions(filter.sortOptions)}` : ''; - const encodedLimitTo = `${limitToKey}${filter.limitTo}`; - - return `${this.encodeName(filter.name)}${encodedStatements}&${encodedSortOptions}&${encodedLimitTo}&${combinationKey}${filter.combination}`; - } - - encodeName(name: string | undefined) { - if (name === undefined || name === '') return ''; - return `name=${encodeURIComponent(name)}&` - } - - - encodeSortOptions(sortOptions: SortOptions) { - return `sortField=${sortOptions.sortField},isAscending=${sortOptions.isAscending}`; - } - - encodeFilterStatements(statements: Array) { - if (statements.length === 0) return ''; - return statementsKey + encodeURIComponent(statements.map(statement => { - const encodedComparison = `comparison=${statement.comparison}`; - const encodedField = `field=${statement.field}`; - const encodedValue = `value=${encodeURIComponent(statement.value)}`; - - return `${encodedComparison}&${encodedField}&${encodedValue}`; - }).join(',')); - } - - decodeSeriesFilter(encodedFilter: string) { - const filter = this.metadataService.createDefaultFilterDto(); - - if (encodedFilter.includes('name=')) { - filter.name = decodeURIComponent(encodedFilter).split('name=')[1].split('&')[0]; - } - - const stmtsStartIndex = encodedFilter.indexOf(statementsKey); - let endIndex = encodedFilter.indexOf('&' + sortOptionsKey); - if (endIndex < 0) { - endIndex = encodedFilter.indexOf('&' + limitToKey); - } - - if (stmtsStartIndex !== -1 || endIndex !== -1) { - // +1 is for the = - const stmtsEncoded = encodedFilter.substring(stmtsStartIndex + statementsKey.length, endIndex); - filter.statements = this.decodeFilterStatements(stmtsEncoded); - } - - if (encodedFilter.includes(sortOptionsKey)) { - const optionsStartIndex = encodedFilter.indexOf('&' + sortOptionsKey); - const endIndex = encodedFilter.indexOf('&' + limitToKey); - const sortOptionsEncoded = encodedFilter.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex); - const sortOptions = this.decodeSortOptions(sortOptionsEncoded); - if (sortOptions) { - filter.sortOptions = sortOptions; - } - } - - if (encodedFilter.includes(limitToKey)) { - const limitTo = decodeURIComponent(encodedFilter).split(limitToKey)[1].split('&')[0]; - filter.limitTo = parseInt(limitTo, 10); - } - - if (encodedFilter.includes(combinationKey)) { - const combo = decodeURIComponent(encodedFilter).split(combinationKey)[1].split('&')[0];; - filter.combination = parseInt(combo, 10) as FilterCombination; - } - - return filter; + encodeFilter(filter: SeriesFilterV2 | undefined) { + return this.http.post(this.apiUrl + 'filter/encode', filter, TextResonse); } + decodeFilter(encodedFilter: string) { + return this.http.post(this.apiUrl + 'filter/decode', {encodedFilter}).pipe(map(filter => { + if (filter == null) { + filter = this.metadataService.createDefaultFilterDto(); + filter.statements.push(this.createSeriesV2DefaultStatement()); + } - filterPresetsFromUrlV2(snapshot: ActivatedRouteSnapshot): SeriesFilterV2 { - const filter = this.metadataService.createDefaultFilterDto(); - if (!window.location.href.includes('?')) return filter; + return filter; + })) + } - const queryParams = snapshot.queryParams; + updateUrlFromFilter(filter: SeriesFilterV2 | undefined) { + return this.encodeFilter(filter).pipe(tap(encodedFilter => { + window.history.replaceState(window.location.href, '', window.location.href.split('?')[0]+ '?' + encodedFilter); + })); + } - if (queryParams.name) { - filter.name = queryParams.name; - } + filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot) { + const filter = this.metadataService.createDefaultFilterDto(); + filter.statements.push(this.createSeriesV2DefaultStatement()); + if (!window.location.href.includes('?')) return of(filter); - const fullUrl = window.location.href.split('?')[1]; - const stmtsStartIndex = fullUrl.indexOf(statementsKey); - let endIndex = fullUrl.indexOf('&' + sortOptionsKey); - if (endIndex < 0) { - endIndex = fullUrl.indexOf('&' + limitToKey); - } + return this.decodeFilter(window.location.href.split('?')[1]); + } - if (stmtsStartIndex !== -1 || endIndex !== -1) { - // +1 is for the = - const stmtsEncoded = fullUrl.substring(stmtsStartIndex + statementsKey.length, endIndex); - filter.statements = this.decodeFilterStatements(stmtsEncoded); - } + /** + * Applies and redirects to the passed page with the filter encoded + * @param page + * @param filter + * @param comparison + * @param value + */ + applyFilter(page: Array, filter: FilterField, comparison: FilterComparison, value: string) { + const dto = this.createSeriesV2Filter(); + dto.statements.push(this.metadataService.createDefaultFilterStatement(filter, comparison, value + '')); - if (queryParams.sortOptions) { - const optionsStartIndex = fullUrl.indexOf('&' + sortOptionsKey); - const endIndex = fullUrl.indexOf('&' + limitToKey); - const sortOptionsEncoded = fullUrl.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex); - const sortOptions = this.decodeSortOptions(sortOptionsEncoded); - if (sortOptions) { - filter.sortOptions = sortOptions; - } - } + return this.encodeFilter(dto).pipe(switchMap(encodedFilter => { + return this.router.navigateByUrl(page.join('/') + '?' + encodedFilter); + })); + } - if (queryParams.limitTo) { - filter.limitTo = parseInt(queryParams.limitTo, 10); - } + applyFilterWithParams(page: Array, filter: SeriesFilterV2, extraParams: Params) { + return this.encodeFilter(filter).pipe(switchMap(encodedFilter => { + let url = page.join('/') + '?' + encodedFilter; + url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join(''); - if (queryParams.combination) { - filter.combination = parseInt(queryParams.combination, 10) as FilterCombination; - } + return this.router.navigateByUrl(url, extraParams); + })); + } - return filter; - } - - decodeSortOptions(encodedSortOptions: string): SortOptions | null { - const parts = decodeURIComponent(encodedSortOptions).split(','); - const sortFieldPart = parts.find(part => part.startsWith('sortField=')); - const isAscendingPart = parts.find(part => part.startsWith('isAscending=')); - - if (sortFieldPart && isAscendingPart) { - const sortField = parseInt(sortFieldPart.split('=')[1], 10) as SortField; - const isAscending = isAscendingPart.split('=')[1].toLowerCase() === 'true'; - return {sortField, isAscending}; - } - - return null; - } - - decodeFilterStatements(encodedStatements: string): FilterStatement[] { - const statementStrings = decodeURIComponent(encodedStatements).split(',').map(s => decodeURIComponent(s)); - return statementStrings.map(statementString => { - const parts = statementString.split(','); - if (parts === null || parts.length < 3) return null; - - const comparisonStartToken = parts.find(part => part.startsWith('comparison=')); - if (!comparisonStartToken) return null; - const comparison = parseInt(comparisonStartToken.split('=')[1], 10) as FilterComparison; - - const fieldStartToken = parts.find(part => part.startsWith('field=')); - if (!fieldStartToken) return null; - const field = parseInt(fieldStartToken.split('=')[1], 10) as FilterField; - - const valueStartToken = parts.find(part => part.startsWith('value=')); - if (!valueStartToken) return null; - const value = decodeURIComponent(valueStartToken.split('=')[1]); - return {comparison, field, value}; - }).filter(o => o != null) as FilterStatement[]; - } - - createSeriesV2Filter(): SeriesFilterV2 { - return { - combination: FilterCombination.And, - statements: [], - limitTo: 0, - sortOptions: { - isAscending: true, - sortField: SortField.SortName - }, - }; - } - - createSeriesV2DefaultStatement(): FilterStatement { - return { - comparison: FilterComparison.Equal, - value: '', - field: FilterField.SeriesName - } - } + createSeriesV2Filter(): SeriesFilterV2 { + return { + combination: FilterCombination.And, + statements: [], + limitTo: 0, + sortOptions: { + isAscending: true, + sortField: SortField.SortName + }, + }; + } + createSeriesV2DefaultStatement(): FilterStatement { + return { + comparison: FilterComparison.Equal, + value: '', + field: FilterField.SeriesName + } + } } diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts index fe5ab3f92..41ec86929 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts @@ -115,7 +115,7 @@ export class ServerStatsComponent { ref.componentInstance.items = genres.map(t => t.title); ref.componentInstance.title = translate('server-stats.genres'); ref.componentInstance.clicked = (item: string) => { - this.filterUtilityService.applyFilter(['all-series'], FilterField.Genres, FilterComparison.Contains, genres.filter(g => g.title === item)[0].id + ''); + this.filterUtilityService.applyFilter(['all-series'], FilterField.Genres, FilterComparison.Contains, genres.filter(g => g.title === item)[0].id + '').subscribe(); }; }); } @@ -126,7 +126,7 @@ export class ServerStatsComponent { ref.componentInstance.items = tags.map(t => t.title); ref.componentInstance.title = translate('server-stats.tags'); ref.componentInstance.clicked = (item: string) => { - this.filterUtilityService.applyFilter(['all-series'], FilterField.Tags, FilterComparison.Contains, tags.filter(g => g.title === item)[0].id + ''); + this.filterUtilityService.applyFilter(['all-series'], FilterField.Tags, FilterComparison.Contains, tags.filter(g => g.title === item)[0].id + '').subscribe(); }; }); } diff --git a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html index 7bf9832f8..1d96d18a6 100644 --- a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html +++ b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.html @@ -13,7 +13,7 @@
- = []; - pagination!: Pagination; + pagination: Pagination = new Pagination(); filter: SeriesFilterV2 | undefined = undefined; filterSettings: FilterSettings = new FilterSettings(); refresh: EventEmitter = new EventEmitter(); @@ -106,17 +106,15 @@ export class WantToReadComponent implements OnInit, AfterContentChecked { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.titleService.setTitle('Kavita - ' + translate('want-to-read.title')); - this.pagination = this.filterUtilityService.pagination(this.route.snapshot); + this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { + this.filter = filter; - this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot); - if (this.filter.statements.length === 0) { - this.filter!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); - } - this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); - this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); - this.filterSettings.presetsV2 = this.filter; + this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); + this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); + this.filterSettings.presetsV2 = this.filter; - this.cdRef.markForCheck(); + this.cdRef.markForCheck(); + }); this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { if (event.event === EVENTS.SeriesRemoved) { @@ -187,11 +185,14 @@ export class WantToReadComponent implements OnInit, AfterContentChecked { if (data.filterV2 === undefined) return; this.filter = data.filterV2; - if (!data.isFirst) { - this.filterUtilityService.updateUrlFromFilterV2(this.pagination, this.filter); + if (data.isFirst) { + this.loadPage(); + return; } - this.loadPage(); + this.filterUtilityService.updateUrlFromFilter(this.filter).subscribe((encodedFilter) => { + this.loadPage(); + }); } } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 7b68358b9..f3703da93 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1299,7 +1299,8 @@ "collection-detail": { "no-data": "There are no items. Try adding a series.", "no-data-filtered": "No items match your current filter.", - "title-alt": "Kavita - {{collectionName}} Collection" + "title-alt": "Kavita - {{collectionName}} Collection", + "series-header": "Series" }, "all-collections": { diff --git a/openapi.json b/openapi.json index dc3c4e353..cf20cb3bc 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.10.1" + "version": "0.7.10.2" }, "servers": [ { @@ -2023,6 +2023,106 @@ } } }, + "/api/Filter/encode": { + "post": { + "tags": [ + "Filter" + ], + "summary": "Encode the Filter", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterV2Dto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/FilterV2Dto" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/FilterV2Dto" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/Filter/decode": { + "post": { + "tags": [ + "Filter" + ], + "summary": "Decodes the Filter", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DecodeFilterDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DecodeFilterDto" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DecodeFilterDto" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/FilterV2Dto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterV2Dto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/FilterV2Dto" + } + } + } + } + } + } + }, "/api/Health": { "get": { "tags": [ @@ -14449,6 +14549,17 @@ }, "additionalProperties": false }, + "DecodeFilterDto": { + "type": "object", + "properties": { + "encodedFilter": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false, + "description": "For requesting an encoded filter to be decoded" + }, "DeleteSeriesDto": { "type": "object", "properties": {