From 47a9ac58a60eb455c4070fa6d9b74f3177623f54 Mon Sep 17 00:00:00 2001 From: Jon Crall Date: Tue, 30 Dec 2025 16:00:48 -0500 Subject: [PATCH] Add configurable pagination defaults and settings --- .../Commands/UpdatePaginationSettings.cs | 6 ++ .../UpdatePaginationSettingsHandler.cs | 29 ++++++++ .../PaginationSettingsViewModel.cs | 6 ++ .../Queries/GetPaginationSettings.cs | 3 + .../Queries/GetPaginationSettingsHandler.cs | 28 ++++++++ .../Queries/GetPagedFillerPresetsHandler.cs | 9 ++- .../Queries/GetRecentLogEntriesHandler.cs | 6 +- .../Queries/GetMusicVideoCardsHandler.cs | 5 +- .../GetTelevisionEpisodeCardsHandler.cs | 5 +- .../GetTelevisionSeasonCardsHandler.cs | 7 +- .../Queries/GetPagedCollectionsHandler.cs | 9 ++- .../GetPagedMultiCollectionsHandler.cs | 9 ++- .../GetPagedRerunCollectionsHandler.cs | 9 ++- .../GetPagedSmartCollectionsHandler.cs | 9 ++- .../Queries/GetPagedTraktListsHandler.cs | 9 ++- .../Queries/GetBlockPlayoutHistoryHandler.cs | 7 +- .../GetFuturePlayoutItemsByIdHandler.cs | 7 +- .../Queries/GetPagedPlayoutsHandler.cs | 9 ++- .../GetPagedProgramSchedulesHandler.cs | 9 ++- .../Queries/QuerySearchIndexArtistsHandler.cs | 7 +- .../QuerySearchIndexEpisodesHandler.cs | 7 +- .../Queries/QuerySearchIndexImagesHandler.cs | 7 +- .../Queries/QuerySearchIndexMoviesHandler.cs | 7 +- .../QuerySearchIndexMusicVideosHandler.cs | 7 +- .../QuerySearchIndexOtherVideosHandler.cs | 7 +- .../QuerySearchIndexRemoteStreamsHandler.cs | 7 +- .../Queries/QuerySearchIndexSeasonsHandler.cs | 7 +- .../Queries/QuerySearchIndexShowsHandler.cs | 7 +- .../Queries/QuerySearchIndexSongsHandler.cs | 7 +- ErsatzTV.Core.Tests/PaginationOptionsTests.cs | 34 +++++++++ ErsatzTV.Core/Domain/ConfigElementKey.cs | 1 + ErsatzTV.Core/PaginationOptions.cs | 22 ++++++ ErsatzTV/Extensions/PaginationExtensions.cs | 14 ++++ ErsatzTV/Pages/Artist.razor | 4 +- ErsatzTV/Pages/ArtistList.razor | 3 +- ErsatzTV/Pages/EpisodeList.razor | 3 +- ErsatzTV/Pages/ImageList.razor | 3 +- ErsatzTV/Pages/MovieList.razor | 4 +- ErsatzTV/Pages/MultiSelectBase.cs | 5 ++ ErsatzTV/Pages/MusicVideoList.razor | 3 +- ErsatzTV/Pages/OtherVideoList.razor | 3 +- ErsatzTV/Pages/RemoteStreamList.razor | 3 +- .../Pages/Settings/PaginationSettings.razor | 70 +++++++++++++++++++ ErsatzTV/Pages/SongList.razor | 3 +- ErsatzTV/Pages/TelevisionEpisodeList.razor | 5 +- ErsatzTV/Pages/TelevisionSeasonList.razor | 6 +- .../Pages/TelevisionSeasonSearchResults.razor | 3 +- ErsatzTV/Pages/TelevisionShowList.razor | 3 +- ErsatzTV/Shared/MainLayout.razor | 1 + 49 files changed, 374 insertions(+), 70 deletions(-) create mode 100644 ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettings.cs create mode 100644 ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettingsHandler.cs create mode 100644 ErsatzTV.Application/Configuration/PaginationSettingsViewModel.cs create mode 100644 ErsatzTV.Application/Configuration/Queries/GetPaginationSettings.cs create mode 100644 ErsatzTV.Application/Configuration/Queries/GetPaginationSettingsHandler.cs create mode 100644 ErsatzTV.Core.Tests/PaginationOptionsTests.cs create mode 100644 ErsatzTV.Core/PaginationOptions.cs create mode 100644 ErsatzTV/Extensions/PaginationExtensions.cs create mode 100644 ErsatzTV/Pages/Settings/PaginationSettings.razor diff --git a/ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettings.cs b/ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettings.cs new file mode 100644 index 000000000..33bcf133c --- /dev/null +++ b/ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettings.cs @@ -0,0 +1,6 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.Configuration; + +public record UpdatePaginationSettings(PaginationSettingsViewModel Settings) : IRequest>; diff --git a/ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettingsHandler.cs b/ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettingsHandler.cs new file mode 100644 index 000000000..ded69ed5c --- /dev/null +++ b/ErsatzTV.Application/Configuration/Commands/UpdatePaginationSettingsHandler.cs @@ -0,0 +1,29 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using System.Globalization; +using LanguageExt; + +namespace ErsatzTV.Application.Configuration; + +public class UpdatePaginationSettingsHandler : IRequestHandler> +{ + private readonly IConfigElementRepository _configElementRepository; + + public UpdatePaginationSettingsHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; + + public async Task> Handle( + UpdatePaginationSettings request, + CancellationToken cancellationToken) + { + int pageSize = PaginationOptions.NormalizePageSize(request.Settings.DefaultPageSize); + + await _configElementRepository.Upsert( + ConfigElementKey.PagesDefaultPageSize, + pageSize.ToString(CultureInfo.InvariantCulture), + cancellationToken); + + return Unit.Default; + } +} diff --git a/ErsatzTV.Application/Configuration/PaginationSettingsViewModel.cs b/ErsatzTV.Application/Configuration/PaginationSettingsViewModel.cs new file mode 100644 index 000000000..f90db083f --- /dev/null +++ b/ErsatzTV.Application/Configuration/PaginationSettingsViewModel.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Application.Configuration; + +public class PaginationSettingsViewModel +{ + public int DefaultPageSize { get; set; } +} diff --git a/ErsatzTV.Application/Configuration/Queries/GetPaginationSettings.cs b/ErsatzTV.Application/Configuration/Queries/GetPaginationSettings.cs new file mode 100644 index 000000000..a2207208d --- /dev/null +++ b/ErsatzTV.Application/Configuration/Queries/GetPaginationSettings.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Configuration; + +public record GetPaginationSettings : IRequest; diff --git a/ErsatzTV.Application/Configuration/Queries/GetPaginationSettingsHandler.cs b/ErsatzTV.Application/Configuration/Queries/GetPaginationSettingsHandler.cs new file mode 100644 index 000000000..abf26ae7f --- /dev/null +++ b/ErsatzTV.Application/Configuration/Queries/GetPaginationSettingsHandler.cs @@ -0,0 +1,28 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.Configuration; + +public class GetPaginationSettingsHandler : IRequestHandler +{ + private readonly IConfigElementRepository _configElementRepository; + + public GetPaginationSettingsHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; + + public async Task Handle( + GetPaginationSettings request, + CancellationToken cancellationToken) + { + Option maybeElement = await _configElementRepository.GetConfigElement( + ConfigElementKey.PagesDefaultPageSize, + cancellationToken); + + int defaultPageSize = PaginationOptions.NormalizePageSize( + maybeElement.Bind(element => int.TryParse(element.Value, out int value) ? value : null)); + + return new PaginationSettingsViewModel { DefaultPageSize = defaultPageSize }; + } +} diff --git a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs index 444e133da..cf50ef796 100644 --- a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs +++ b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Core; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Filler.Mapper; @@ -15,13 +16,15 @@ public class GetPagedFillerPresetsHandler : IRequestHandler page = await dbContext.FillerPresets .AsNoTracking() .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken) .Map(list => list.Map(ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs index b83e4b4fd..8f605c2ea 100644 --- a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs +++ b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs @@ -14,6 +14,8 @@ public class GetRecentLogEntriesHandler : IRequestHandler entries.OrderByDescending(le => le.Timestamp)); var page = ordered - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToList(); return new PagedLogEntriesViewModel(count, page).AsTask(); diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs index d5661e3fa..81db26a35 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.MediaCards; @@ -31,10 +32,12 @@ public class GetMusicVideoCardsHandler : IRequestHandler musicVideos = await _musicVideoRepository - .GetPagedMusicVideos(request.ArtistId, request.PageNumber, request.PageSize); + .GetPagedMusicVideos(request.ArtistId, request.PageNumber, pageSize); var results = new List(); diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs index 900ac97e5..38a84e89a 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.MediaCards; @@ -35,6 +36,8 @@ public class GetTelevisionEpisodeCards request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + int count = await _televisionRepository.GetEpisodeCount(request.TelevisionSeasonId); Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin(cancellationToken) @@ -44,7 +47,7 @@ public class .Map(list => list.HeadOrNone()); List episodes = await _televisionRepository - .GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize); + .GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, pageSize); var results = new List(); foreach (EpisodeMetadata episodeMetadata in episodes) diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs index 559c1c36b..3d79a01f4 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.MediaCards.Mapper; @@ -23,6 +24,8 @@ public class GetTelevisionSeasonCards request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + int count = await _televisionRepository.GetSeasonCount(request.TelevisionShowId); Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin(cancellationToken) @@ -32,7 +35,7 @@ public class .Map(list => list.HeadOrNone()); List results = await _televisionRepository - .GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize, cancellationToken) + .GetPagedSeasons(request.TelevisionShowId, request.PageNumber, pageSize, cancellationToken) .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); return new TelevisionSeasonCardResultsViewModel(count, results, null); diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs index 10ad04976..23d875cb2 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; @@ -12,6 +13,8 @@ public class GetPagedCollectionsHandler(IDbContextFactory dbContextFa GetPagedCollections request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.Collections.CountAsync(cancellationToken); @@ -26,8 +29,8 @@ public class GetPagedCollectionsHandler(IDbContextFactory dbContextFa List page = await query .OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation)) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken) .Map(list => list.Map(ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs index ab5d73e82..d755aca58 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; @@ -12,6 +13,8 @@ public class GetPagedMultiCollectionsHandler(IDbContextFactory dbCont GetPagedMultiCollections request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.MultiCollections.CountAsync(cancellationToken); @@ -26,8 +29,8 @@ public class GetPagedMultiCollectionsHandler(IDbContextFactory dbCont List page = await query .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .Include(mc => mc.MultiCollectionItems) .ThenInclude(i => i.Collection) .ToListAsync(cancellationToken) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs index 5b63fab42..07469f4cb 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; @@ -12,6 +13,8 @@ public class GetPagedRerunCollectionsHandler(IDbContextFactory dbCont GetPagedRerunCollections request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.RerunCollections.CountAsync(cancellationToken); @@ -26,8 +29,8 @@ public class GetPagedRerunCollectionsHandler(IDbContextFactory dbCont List page = await query .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken) .Map(list => list.Map(ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs index cabed9b96..bb5bf9aa0 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; @@ -12,6 +13,8 @@ public class GetPagedSmartCollectionsHandler(IDbContextFactory dbCont GetPagedSmartCollections request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.SmartCollections.CountAsync(cancellationToken); @@ -26,8 +29,8 @@ public class GetPagedSmartCollectionsHandler(IDbContextFactory dbCont List page = await query .OrderBy(s => EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation)) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken) .Map(list => list.Map(ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs index e1de26911..f063a9c2d 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Core; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; @@ -15,13 +16,15 @@ public class GetPagedTraktListsHandler : IRequestHandler page = await dbContext.TraktLists .AsNoTracking() .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .Include(l => l.Items) .ToListAsync(cancellationToken) .Map(list => list.Map(ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistoryHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistoryHandler.cs index 2f95ac284..b6794ce08 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistoryHandler.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetBlockPlayoutHistoryHandler.cs @@ -1,3 +1,4 @@ +using ErsatzTV.Core; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; @@ -11,6 +12,8 @@ public class GetBlockPlayoutHistoryHandler(IDbContextFactory dbContex GetBlockPlayoutHistory request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); IQueryable query = dbContext.PlayoutHistory @@ -21,8 +24,8 @@ public class GetBlockPlayoutHistoryHandler(IDbContextFactory dbContex List allHistory = await query .OrderBy(ph => ph.Id) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken); return new PagedPlayoutHistoryViewModel(totalCount, allHistory.Map(Mapper.ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs index 6e26de7ae..ea14f7524 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs @@ -1,4 +1,5 @@ using System.Globalization; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; @@ -14,6 +15,8 @@ public class GetFuturePlayoutItemsByIdHandler(IDbContextFactory dbCon GetFuturePlayoutItemsById request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); DateTime now = DateTimeOffset.Now.UtcDateTime; @@ -39,8 +42,8 @@ public class GetFuturePlayoutItemsByIdHandler(IDbContextFactory dbCon List page = await combined .OrderBy(c => c.Start) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken) .Bind(async pageOfCombined => { diff --git a/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs index 3e11aaffd..ba8df3685 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Playouts.Mapper; @@ -12,6 +13,8 @@ public class GetPagedPlayoutsHandler(IDbContextFactory dbContextFacto GetPagedPlayouts request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.Playouts.CountAsync(cancellationToken); @@ -31,8 +34,8 @@ public class GetPagedPlayoutsHandler(IDbContextFactory dbContextFacto List page = await query .OrderBy(p => p.Channel.SortNumber) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken) .Map(list => list.Map(ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs index 8a1f6e573..71ba4f7a0 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.ProgramSchedules.Mapper; @@ -12,6 +13,8 @@ public class GetPagedProgramSchedulesHandler(IDbContextFactory dbCont GetPagedProgramSchedules request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.ProgramSchedules.CountAsync(cancellationToken); @@ -26,8 +29,8 @@ public class GetPagedProgramSchedulesHandler(IDbContextFactory dbCont List page = await query .OrderBy(s => EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation)) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) + .Skip(request.PageNum * pageSize) + .Take(pageSize) .ToListAsync(cancellationToken) .Map(list => list.Map(ProjectToViewModel).ToList()); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs index ffc1ff3c8..506378c84 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -18,12 +19,14 @@ public class QuerySearchIndexArtistsHandler( QuerySearchIndexArtists request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs index 62898b26b..aa42d8579 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs @@ -11,6 +11,7 @@ using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -30,12 +31,14 @@ public class QuerySearchIndexEpisodes request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexImagesHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexImagesHandler.cs index 6beb3fc41..f92185584 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexImagesHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexImagesHandler.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -18,12 +19,14 @@ public class QuerySearchIndexImagesHandler( QuerySearchIndexImages request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs index 5c590cc82..c9593e3ec 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs @@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -19,12 +20,14 @@ public class QuerySearchIndexMoviesHandler( QuerySearchIndexMovies request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs index bca2e8a4e..e7fc98afe 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs @@ -9,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -27,12 +28,14 @@ public class QuerySearchIndexMusicVideos request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs index 74f5b39db..8d0d1581e 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -19,12 +20,14 @@ public class QuerySearchIndexOtherVideos request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreamsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreamsHandler.cs index 275ad7a2a..5eeb51f2a 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreamsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreamsHandler.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -18,12 +19,14 @@ public class QuerySearchIndexRemoteStreamsHandler( QuerySearchIndexRemoteStreams request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs index 70fad06fe..19b89d191 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs @@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -20,12 +21,14 @@ public class QuerySearchIndexSeasons request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs index 883e27199..31538cae2 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs @@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -20,12 +21,14 @@ public class QuerySearchIndexShows request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs index c20226982..024d77fbf 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs @@ -4,6 +4,7 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using ErsatzTV.Core; using static ErsatzTV.Application.MediaCards.Mapper; namespace ErsatzTV.Application.Search; @@ -15,12 +16,14 @@ public class QuerySearchIndexSongsHandler(IClient client, ISearchIndex searchInd QuerySearchIndexSongs request, CancellationToken cancellationToken) { + int pageSize = PaginationOptions.NormalizePageSize(request.PageSize); + SearchResult searchResult = await searchIndex.Search( client, request.Query, string.Empty, - (request.PageNumber - 1) * request.PageSize, - request.PageSize, + (request.PageNumber - 1) * pageSize, + pageSize, cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Core.Tests/PaginationOptionsTests.cs b/ErsatzTV.Core.Tests/PaginationOptionsTests.cs new file mode 100644 index 000000000..10827f066 --- /dev/null +++ b/ErsatzTV.Core.Tests/PaginationOptionsTests.cs @@ -0,0 +1,34 @@ +using ErsatzTV.Core; +using FluentAssertions; +using NUnit.Framework; + +namespace ErsatzTV.Core.Tests; + +[TestFixture] +public class PaginationOptionsTests +{ + [Test] + public void NormalizePageSize_Should_Default_For_Null() + { + PaginationOptions.NormalizePageSize(null).Should().Be(PaginationOptions.DefaultPageSize); + } + + [TestCase(0)] + [TestCase(-1)] + public void NormalizePageSize_Should_Default_For_NonPositive(int input) + { + PaginationOptions.NormalizePageSize(input).Should().Be(PaginationOptions.DefaultPageSize); + } + + [Test] + public void NormalizePageSize_Should_Accept_Positive() + { + PaginationOptions.NormalizePageSize(50).Should().Be(50); + } + + [Test] + public void NormalizePageSize_Should_Clamp_To_Max() + { + PaginationOptions.NormalizePageSize(1000).Should().Be(PaginationOptions.MaxPageSize); + } +} diff --git a/ErsatzTV.Core/Domain/ConfigElementKey.cs b/ErsatzTV.Core/Domain/ConfigElementKey.cs index c1f89cdbf..0bc2c383b 100644 --- a/ErsatzTV.Core/Domain/ConfigElementKey.cs +++ b/ErsatzTV.Core/Domain/ConfigElementKey.cs @@ -32,6 +32,7 @@ public class ConfigElementKey public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count"); public static ConfigElementKey HDHRUUID => new("hdhr.uuid"); public static ConfigElementKey PagesIsDarkMode => new("pages.is_dark_mode"); + public static ConfigElementKey PagesDefaultPageSize => new("pages.default_page_size"); public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size"); public static ConfigElementKey ChannelsShowDisabled => new("pages.channels.show_disabled"); public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size"); diff --git a/ErsatzTV.Core/PaginationOptions.cs b/ErsatzTV.Core/PaginationOptions.cs new file mode 100644 index 000000000..54ca8aaa5 --- /dev/null +++ b/ErsatzTV.Core/PaginationOptions.cs @@ -0,0 +1,22 @@ +using System; + +namespace ErsatzTV.Core; + +public static class PaginationOptions +{ + public const int DefaultPageSize = 100; + public const int MaxPageSize = 500; + + public static int NormalizePageSize(int? requested) => + NormalizePageSize(requested, DefaultPageSize, MaxPageSize); + + public static int NormalizePageSize(int? requested, int defaultSize, int maxSize) + { + if (requested is null || requested <= 0) + { + return defaultSize; + } + + return Math.Clamp(requested.Value, 1, maxSize); + } +} diff --git a/ErsatzTV/Extensions/PaginationExtensions.cs b/ErsatzTV/Extensions/PaginationExtensions.cs new file mode 100644 index 000000000..a0582d1a3 --- /dev/null +++ b/ErsatzTV/Extensions/PaginationExtensions.cs @@ -0,0 +1,14 @@ +using ErsatzTV.Application.Configuration; +using ErsatzTV.Core; +using MediatR; + +namespace ErsatzTV.Extensions; + +public static class PaginationExtensions +{ + public static async Task GetDefaultPageSize(this IMediator mediator, CancellationToken cancellationToken) + { + PaginationSettingsViewModel settings = await mediator.Send(new GetPaginationSettings(), cancellationToken); + return PaginationOptions.NormalizePageSize(settings.DefaultPageSize); + } +} diff --git a/ErsatzTV/Pages/Artist.razor b/ErsatzTV/Pages/Artist.razor index be6674555..52562f228 100644 --- a/ErsatzTV/Pages/Artist.razor +++ b/ErsatzTV/Pages/Artist.razor @@ -253,6 +253,7 @@ private List _sortedGenres = []; private List _sortedStyles = []; private List _sortedMoods = []; + private int _pageSize = PaginationOptions.DefaultPageSize; private MusicVideoCardResultsViewModel _musicVideos = new(0, [], new SearchPageMap([])); public void Dispose() @@ -270,6 +271,7 @@ try { + _pageSize = await Mediator.GetDefaultPageSize(token); await Mediator.Send(new GetArtistById(ArtistId), token).IfSomeAsync(vm => { _artist = vm; @@ -279,7 +281,7 @@ _sortedMoods = _artist.Moods.OrderBy(m => m).ToList(); }); - _musicVideos = await Mediator.Send(new GetMusicVideoCards(ArtistId, 1, 100), token); + _musicVideos = await Mediator.Send(new GetMusicVideoCards(ArtistId, 1, _pageSize), token); } catch (OperationCanceledException) { diff --git a/ErsatzTV/Pages/ArtistList.razor b/ErsatzTV/Pages/ArtistList.razor index c3e29ce36..609d7f0db 100644 --- a/ErsatzTV/Pages/ArtistList.razor +++ b/ErsatzTV/Pages/ArtistList.razor @@ -45,7 +45,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -55,6 +54,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/EpisodeList.razor b/ErsatzTV/Pages/EpisodeList.razor index d2d7e18db..672a6fb58 100644 --- a/ErsatzTV/Pages/EpisodeList.razor +++ b/ErsatzTV/Pages/EpisodeList.razor @@ -45,7 +45,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -55,6 +54,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/ImageList.razor b/ErsatzTV/Pages/ImageList.razor index c9b1f1646..82bb239c5 100644 --- a/ErsatzTV/Pages/ImageList.razor +++ b/ErsatzTV/Pages/ImageList.razor @@ -45,7 +45,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -55,6 +54,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/MovieList.razor b/ErsatzTV/Pages/MovieList.razor index 9c4cadf9b..76eaf6ca0 100644 --- a/ErsatzTV/Pages/MovieList.razor +++ b/ErsatzTV/Pages/MovieList.razor @@ -47,8 +47,6 @@ } @code { - private static int PageSize => 100; - [Parameter] public int PageNumber { get; set; } @@ -66,6 +64,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/MultiSelectBase.cs b/ErsatzTV/Pages/MultiSelectBase.cs index bb2ed5608..d38ca8ab6 100644 --- a/ErsatzTV/Pages/MultiSelectBase.cs +++ b/ErsatzTV/Pages/MultiSelectBase.cs @@ -2,6 +2,7 @@ using ErsatzTV.Application.MediaCards; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Core; +using ErsatzTV.Extensions; using ErsatzTV.Shared; using LanguageExt.UnsafeValueAccess; using MediatR; @@ -14,6 +15,7 @@ namespace ErsatzTV.Pages; public class MultiSelectBase : FragmentNavigationBase { private Option _recentlySelected = None; + protected int PageSize { get; private set; } = PaginationOptions.DefaultPageSize; [Inject] protected IDialogService Dialog { get; set; } @@ -29,6 +31,9 @@ public class MultiSelectBase : FragmentNavigationBase protected System.Collections.Generic.HashSet SelectedItems { get; } = []; + protected async Task InitializePageSize(CancellationToken cancellationToken) => + PageSize = await Mediator.GetDefaultPageSize(cancellationToken); + protected bool IsSelected(MediaCardViewModel card) => SelectedItems.Contains(card); diff --git a/ErsatzTV/Pages/MusicVideoList.razor b/ErsatzTV/Pages/MusicVideoList.razor index 4a7a1dc98..412172217 100644 --- a/ErsatzTV/Pages/MusicVideoList.razor +++ b/ErsatzTV/Pages/MusicVideoList.razor @@ -45,7 +45,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -55,6 +54,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/OtherVideoList.razor b/ErsatzTV/Pages/OtherVideoList.razor index bb4509216..5d6e9ca4b 100644 --- a/ErsatzTV/Pages/OtherVideoList.razor +++ b/ErsatzTV/Pages/OtherVideoList.razor @@ -45,7 +45,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -55,6 +54,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/RemoteStreamList.razor b/ErsatzTV/Pages/RemoteStreamList.razor index d2d78bd1f..2de66864d 100644 --- a/ErsatzTV/Pages/RemoteStreamList.razor +++ b/ErsatzTV/Pages/RemoteStreamList.razor @@ -45,7 +45,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -55,6 +54,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/Settings/PaginationSettings.razor b/ErsatzTV/Pages/Settings/PaginationSettings.razor new file mode 100644 index 000000000..d12cb84de --- /dev/null +++ b/ErsatzTV/Pages/Settings/PaginationSettings.razor @@ -0,0 +1,70 @@ +@page "/settings/pagination" +@using ErsatzTV.Application.Configuration +@inject IMediator Mediator +@inject ISnackbar Snackbar +@inject ILogger Logger +@implements IDisposable + + + + Save Settings + +
+ + Pagination + + +
+ Items Per Page +
+ + 25 + 50 + 100 + 200 + 500 + +
+
+
+
+ +@code { + private CancellationTokenSource _cts; + private PaginationSettingsViewModel _settings = new(); + + public void Dispose() + { + _cts?.Cancel(); + _cts?.Dispose(); + } + + protected override async Task OnParametersSetAsync() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + try + { + _settings = await Mediator.Send(new GetPaginationSettings(), token); + } + catch (OperationCanceledException) + { + // do nothing + } + } + + private async Task SavePaginationSettings() + { + Either result = await Mediator.Send(new UpdatePaginationSettings(_settings), _cts.Token); + result.Match( + Left: error => + { + Snackbar.Add(error.Value, Severity.Error); + Logger.LogError("Unexpected error saving pagination settings: {Error}", error.Value); + }, + Right: _ => Snackbar.Add("Successfully saved pagination settings", Severity.Success)); + } +} diff --git a/ErsatzTV/Pages/SongList.razor b/ErsatzTV/Pages/SongList.razor index b7d2b071e..612cffd0e 100644 --- a/ErsatzTV/Pages/SongList.razor +++ b/ErsatzTV/Pages/SongList.razor @@ -45,7 +45,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -55,6 +54,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor index 6c71191d6..f8184d4a6 100644 --- a/ErsatzTV/Pages/TelevisionEpisodeList.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -188,7 +188,7 @@ private TelevisionSeasonViewModel _season; - private static int PageSize => 100; + private int _pageSize = PaginationOptions.DefaultPageSize; private const int PageNumber = 1; private TelevisionEpisodeCardResultsViewModel _data = new(0, new List(), null); @@ -223,8 +223,9 @@ try { + _pageSize = await Mediator.GetDefaultPageSize(token); await Mediator.Send(new GetTelevisionSeasonById(SeasonId), token).IfSomeAsync(vm => _season = vm); - _data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, PageNumber, PageSize), token); + _data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, PageNumber, _pageSize), token); } catch (OperationCanceledException) { diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index 40016caf6..41a03791d 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -189,7 +189,7 @@ private List _sortedGenres = []; private List _sortedTags = []; - private static int PageSize => 100; + private int _pageSize = PaginationOptions.DefaultPageSize; private const int PageNumber = 1; private TelevisionSeasonCardResultsViewModel _data = new(0, [], null); @@ -209,6 +209,8 @@ try { + _pageSize = await Mediator.GetDefaultPageSize(token); + Option maybeShow = await Mediator.Send(new GetTelevisionShowById(ShowId), token); foreach (TelevisionShowViewModel show in maybeShow) { @@ -221,7 +223,7 @@ _sortedNetworks = _show.Networks.OrderBy(n => n).ToList(); } - _data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, PageNumber, PageSize), token); + _data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, PageNumber, _pageSize), token); } catch (OperationCanceledException) { diff --git a/ErsatzTV/Pages/TelevisionSeasonSearchResults.razor b/ErsatzTV/Pages/TelevisionSeasonSearchResults.razor index dfa486109..b3c77cc9c 100644 --- a/ErsatzTV/Pages/TelevisionSeasonSearchResults.razor +++ b/ErsatzTV/Pages/TelevisionSeasonSearchResults.razor @@ -69,7 +69,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -79,6 +78,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor index 45c73b94e..e83596cb4 100644 --- a/ErsatzTV/Pages/TelevisionShowList.razor +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -46,7 +46,6 @@ } @code { - private static int PageSize => 100; [Parameter] public int PageNumber { get; set; } @@ -65,6 +64,8 @@ protected override async Task OnParametersSetAsync() { + await InitializePageSize(CancellationToken); + if (PageNumber == 0) { PageNumber = 1; diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index f5870fc16..2e2a7bf87 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -189,6 +189,7 @@ HDHomeRun Scanner Playout + Pagination XMLTV