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": {