From ecacf3960f47fb239741b68929c04faa4d74ac9b Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:44:11 -0500 Subject: [PATCH] add search fields to filter collections and schedules tables (#2439) --- CHANGELOG.md | 1 + .../Queries/GetPagedCollections.cs | 2 +- .../Queries/GetPagedCollectionsHandler.cs | 26 ++++++++------ .../Queries/GetPagedMultiCollections.cs | 3 +- .../GetPagedMultiCollectionsHandler.cs | 26 ++++++++------ .../Queries/GetPagedRerunCollections.cs | 3 +- .../GetPagedRerunCollectionsHandler.cs | 16 +++++++-- .../Queries/GetPagedSmartCollections.cs | 3 +- .../GetPagedSmartCollectionsHandler.cs | 18 +++++++--- .../PagedProgramSchedulesViewModel.cs | 3 ++ .../Queries/GetPagedProgramSchedules.cs | 4 +++ .../GetPagedProgramSchedulesHandler.cs | 36 +++++++++++++++++++ ErsatzTV/Pages/ManualCollections.razor | 22 +++++++++--- ErsatzTV/Pages/MultiCollections.razor | 22 +++++++++--- ErsatzTV/Pages/RerunCollections.razor | 22 +++++++++--- ErsatzTV/Pages/Schedules.razor | 36 +++++++++---------- ErsatzTV/Pages/SmartCollections.razor | 24 +++++++++---- ErsatzTV/wwwroot/css/site.css | 3 +- 18 files changed, 198 insertions(+), 72 deletions(-) create mode 100644 ErsatzTV.Application/ProgramSchedules/PagedProgramSchedulesViewModel.cs create mode 100644 ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedules.cs create mode 100644 ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index c59453364..59b51bf20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `loop` - loop forever - `hold` - hold last frame forever, or `hold_seconds` - Draw order (`z_index`) +- Add search fields to filter collections and schedules tables ### Fixed - Fix green output when libplacebo tonemapping is used with NVIDIA acceleration and 10-bit output in FFmpeg Profile diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs index 1b2dde5fc..138718c1b 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs @@ -1,3 +1,3 @@ namespace ErsatzTV.Application.MediaCollections; -public record GetPagedCollections(int PageNum, int PageSize) : IRequest; +public record GetPagedCollections(string Query, int PageNum, int PageSize) : IRequest; diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs index 2aeb2b129..10ad04976 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs @@ -1,24 +1,30 @@ -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; namespace ErsatzTV.Application.MediaCollections; -public class GetPagedCollectionsHandler : IRequestHandler +public class GetPagedCollectionsHandler(IDbContextFactory dbContextFactory) + : IRequestHandler { - private readonly IDbContextFactory _dbContextFactory; - - public GetPagedCollectionsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task Handle( GetPagedCollections request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.Collections.CountAsync(cancellationToken); - List page = await dbContext.Collections - .AsNoTracking() + + IQueryable query = dbContext.Collections.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Query)) + { + query = query.Where(s => EF.Functions.Like( + EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), + $"%{request.Query}%")); + } + + List page = await query .OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation)) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs index f7972d5e4..b500d5a72 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs @@ -1,3 +1,4 @@ namespace ErsatzTV.Application.MediaCollections; -public record GetPagedMultiCollections(int PageNum, int PageSize) : IRequest; +public record GetPagedMultiCollections(string Query, int PageNum, int PageSize) + : IRequest; diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs index bd80bd9fa..ab5d73e82 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs @@ -1,24 +1,30 @@ -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; namespace ErsatzTV.Application.MediaCollections; -public class GetPagedMultiCollectionsHandler : IRequestHandler +public class GetPagedMultiCollectionsHandler(IDbContextFactory dbContextFactory) + : IRequestHandler { - private readonly IDbContextFactory _dbContextFactory; - - public GetPagedMultiCollectionsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task Handle( GetPagedMultiCollections request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.MultiCollections.CountAsync(cancellationToken); - List page = await dbContext.MultiCollections - .AsNoTracking() + + IQueryable query = dbContext.MultiCollections.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Query)) + { + query = query.Where(s => EF.Functions.Like( + EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), + $"%{request.Query}%")); + } + + List page = await query .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollections.cs index 49f9031d4..38ec6b399 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollections.cs @@ -1,3 +1,4 @@ namespace ErsatzTV.Application.MediaCollections; -public record GetPagedRerunCollections(int PageNum, int PageSize) : IRequest; +public record GetPagedRerunCollections(string Query, int PageNum, int PageSize) + : IRequest; diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs index 07fa49ebc..5b63fab42 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; @@ -13,8 +14,17 @@ public class GetPagedRerunCollectionsHandler(IDbContextFactory dbCont { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.RerunCollections.CountAsync(cancellationToken); - List page = await dbContext.RerunCollections - .AsNoTracking() + + IQueryable query = dbContext.RerunCollections.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Query)) + { + query = query.Where(s => EF.Functions.Like( + EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), + $"%{request.Query}%")); + } + + List page = await query .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs index 482321c0a..8609a6f22 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs @@ -1,3 +1,4 @@ namespace ErsatzTV.Application.MediaCollections; -public record GetPagedSmartCollections(int PageNum, int PageSize) : IRequest; +public record GetPagedSmartCollections(string Query, int PageNum, int PageSize) + : IRequest; diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs index f204feaaf..cabed9b96 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; @@ -13,9 +14,18 @@ public class GetPagedSmartCollectionsHandler(IDbContextFactory dbCont { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.SmartCollections.CountAsync(cancellationToken); - List page = await dbContext.SmartCollections - .AsNoTracking() - .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) + + IQueryable query = dbContext.SmartCollections.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Query)) + { + query = query.Where(s => EF.Functions.Like( + EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), + $"%{request.Query}%")); + } + + List page = await query + .OrderBy(s => EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation)) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken) diff --git a/ErsatzTV.Application/ProgramSchedules/PagedProgramSchedulesViewModel.cs b/ErsatzTV.Application/ProgramSchedules/PagedProgramSchedulesViewModel.cs new file mode 100644 index 000000000..b30449dc5 --- /dev/null +++ b/ErsatzTV.Application/ProgramSchedules/PagedProgramSchedulesViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.ProgramSchedules; + +public record PagedProgramSchedulesViewModel(int TotalCount, List Page); diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedules.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedules.cs new file mode 100644 index 000000000..f4839200c --- /dev/null +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedules.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.ProgramSchedules; + +public record GetPagedProgramSchedules(string Query, int PageNum, int PageSize) + : IRequest; diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs new file mode 100644 index 000000000..8a1f6e573 --- /dev/null +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs @@ -0,0 +1,36 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.ProgramSchedules.Mapper; + +namespace ErsatzTV.Application.ProgramSchedules; + +public class GetPagedProgramSchedulesHandler(IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle( + GetPagedProgramSchedules request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + int count = await dbContext.ProgramSchedules.CountAsync(cancellationToken); + + IQueryable query = dbContext.ProgramSchedules.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(request.Query)) + { + query = query.Where(s => EF.Functions.Like( + EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), + $"%{request.Query}%")); + } + + List page = await query + .OrderBy(s => EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation)) + .Skip(request.PageNum * request.PageSize) + .Take(request.PageSize) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new PagedProgramSchedulesViewModel(count, page); + } +} diff --git a/ErsatzTV/Pages/ManualCollections.razor b/ErsatzTV/Pages/ManualCollections.razor index 685c870db..e8ad0d4b5 100644 --- a/ErsatzTV/Pages/ManualCollections.razor +++ b/ErsatzTV/Pages/ManualCollections.razor @@ -36,10 +36,15 @@ - - Name - - + + + + @context.Name @@ -72,6 +77,7 @@ private MudTable _collectionsTable; private int _collectionsRowsPerPage = 10; + private string _searchString; public void Dispose() { @@ -118,7 +124,13 @@ { await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.CollectionsPageSize, state.PageSize.ToString()), cancellationToken); - PagedMediaCollectionsViewModel data = await Mediator.Send(new GetPagedCollections(state.Page, state.PageSize), cancellationToken); + PagedMediaCollectionsViewModel data = await Mediator.Send(new GetPagedCollections(_searchString, state.Page, state.PageSize), cancellationToken); return new TableData { TotalItems = data.TotalCount, Items = data.Page }; } + + private void OnSearch(string query) + { + _searchString = query; + _collectionsTable.ReloadServerData(); + } } diff --git a/ErsatzTV/Pages/MultiCollections.razor b/ErsatzTV/Pages/MultiCollections.razor index af706e53c..c3ed343a4 100644 --- a/ErsatzTV/Pages/MultiCollections.razor +++ b/ErsatzTV/Pages/MultiCollections.razor @@ -36,10 +36,15 @@ - - Name - - + + + + @context.Name @@ -72,6 +77,7 @@ private MudTable _multiCollectionsTable; private int _multiCollectionsRowsPerPage = 10; + private string _searchString; public void Dispose() { @@ -118,7 +124,13 @@ { await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.MultiCollectionsPageSize, state.PageSize.ToString()), cancellationToken); - PagedMultiCollectionsViewModel data = await Mediator.Send(new GetPagedMultiCollections(state.Page, state.PageSize), cancellationToken); + PagedMultiCollectionsViewModel data = await Mediator.Send(new GetPagedMultiCollections(_searchString, state.Page, state.PageSize), cancellationToken); return new TableData { TotalItems = data.TotalCount, Items = data.Page }; } + + private void OnSearch(string query) + { + _searchString = query; + _multiCollectionsTable.ReloadServerData(); + } } diff --git a/ErsatzTV/Pages/RerunCollections.razor b/ErsatzTV/Pages/RerunCollections.razor index 1a0cf909e..9ae7e7bca 100644 --- a/ErsatzTV/Pages/RerunCollections.razor +++ b/ErsatzTV/Pages/RerunCollections.razor @@ -36,10 +36,15 @@ - - Name - - + + + + @context.Name @@ -72,6 +77,7 @@ private MudTable _rerunCollectionsTable; private int _rerunCollectionsRowsPerPage = 10; + private string _searchString; public void Dispose() { @@ -118,7 +124,13 @@ { await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.RerunCollectionsPageSize, state.PageSize.ToString()), cancellationToken); - PagedRerunCollectionsViewModel data = await Mediator.Send(new GetPagedRerunCollections(state.Page, state.PageSize), cancellationToken); + PagedRerunCollectionsViewModel data = await Mediator.Send(new GetPagedRerunCollections(_searchString, state.Page, state.PageSize), cancellationToken); return new TableData { TotalItems = data.TotalCount, Items = data.Page }; } + + private void OnSearch(string query) + { + _searchString = query; + _rerunCollectionsTable.ReloadServerData(); + } } diff --git a/ErsatzTV/Pages/Schedules.razor b/ErsatzTV/Pages/Schedules.razor index 4957d684c..baa3e2bea 100644 --- a/ErsatzTV/Pages/Schedules.razor +++ b/ErsatzTV/Pages/Schedules.razor @@ -1,7 +1,6 @@ @page "/schedules" @using ErsatzTV.Application.Configuration @using ErsatzTV.Application.ProgramSchedules -@using NaturalSort.Extension @implements IDisposable @inject IDialogService Dialog @inject IMediator Mediator @@ -30,14 +29,15 @@ - - - - Name - - - - + + + + @context.Name @@ -109,6 +109,7 @@ private MudTable _detailTable; private int _rowsPerPage = 10; private int _detailRowsPerPage = 10; + private string _searchString; private ProgramScheduleViewModel _selectedSchedule; public void Dispose() @@ -185,15 +186,8 @@ { await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SchedulesPageSize, state.PageSize.ToString()), cancellationToken); - List schedules = await Mediator.Send(new GetAllProgramSchedules(), cancellationToken); - IOrderedEnumerable sorted = schedules.OrderBy(s => s.Name, new NaturalSortComparer(StringComparison.CurrentCultureIgnoreCase)); - - // TODO: properly page this data - return new TableData - { - TotalItems = schedules.Count, - Items = sorted.Skip(state.Page * state.PageSize).Take(state.PageSize) - }; + PagedProgramSchedulesViewModel data = await Mediator.Send(new GetPagedProgramSchedules(_searchString, state.Page, state.PageSize), cancellationToken); + return new TableData { TotalItems = data.TotalCount, Items = data.Page }; } private async Task> DetailServerReload(TableState state, CancellationToken cancellationToken) @@ -211,4 +205,10 @@ }; } + private void OnSearch(string query) + { + _selectedSchedule = null; + _searchString = query; + _table.ReloadServerData(); + } } diff --git a/ErsatzTV/Pages/SmartCollections.razor b/ErsatzTV/Pages/SmartCollections.razor index ad2409265..b9a72fe8d 100644 --- a/ErsatzTV/Pages/SmartCollections.razor +++ b/ErsatzTV/Pages/SmartCollections.razor @@ -7,8 +7,6 @@ @inject IMediator Mediator - -
Smart Collections @@ -24,10 +22,15 @@ - - Name - - + + + + @context.Name @@ -65,6 +68,7 @@ private MudTable _smartCollectionsTable; private int _smartCollectionsRowsPerPage = 10; + private string _searchString; public void Dispose() { @@ -111,7 +115,13 @@ { await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.SmartCollectionsPageSize, state.PageSize.ToString()), cancellationToken); - PagedSmartCollectionsViewModel data = await Mediator.Send(new GetPagedSmartCollections(state.Page, state.PageSize), cancellationToken); + PagedSmartCollectionsViewModel data = await Mediator.Send(new GetPagedSmartCollections(_searchString, state.Page, state.PageSize), cancellationToken); return new TableData { TotalItems = data.TotalCount, Items = data.Page }; } + + private void OnSearch(string query) + { + _searchString = query; + _smartCollectionsTable.ReloadServerData(); + } } diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index 633e739f3..b786892b6 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -180,7 +180,8 @@ div.ersatztv-light { } .form-field-stack div.mud-input-control, -.form-field-stack ul.mud-treeview { +.form-field-stack ul.mud-treeview, +.mud-toolbar .mud-input-text { max-width: 500px; }