diff --git a/CHANGELOG.md b/CHANGELOG.md index a94427554..c1d970235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Add search field to filter blocks table + ### Fixed - Do not allow deleting ffmpeg profiles that are used by channels - Allow ffmpeg profiles using VAAPI accel to set h264 video profile - Fix HLS Direct playback, and make it accessible on separate streaming port - Fix playback troubleshooting when using multiple watermarks or multiple graphics elements +### Changed +- Use table instead of tree view on blocks page + ## [25.7.0] - 2025-09-14 ### Added - Add new collection type `Rerun Collection` diff --git a/ErsatzTV.Application/Scheduling/Queries/GetAllBlocks.cs b/ErsatzTV.Application/Scheduling/Queries/GetAllBlocks.cs new file mode 100644 index 000000000..edf8966eb --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetAllBlocks.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record GetAllBlocks : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Queries/GetAllBlocksHandler.cs b/ErsatzTV.Application/Scheduling/Queries/GetAllBlocksHandler.cs new file mode 100644 index 000000000..c9350612b --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetAllBlocksHandler.cs @@ -0,0 +1,54 @@ +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class GetAllBlocksHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle(GetAllBlocks request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + List blocks = await dbContext.Blocks + .AsNoTracking() + .ToListAsync(cancellationToken); + + List blockGroups = await dbContext.BlockGroups + .AsNoTracking() + .ToListAsync(cancellationToken); + + var unusedBlockGroups = blockGroups.ToList(); + + // match blocks to block groups + foreach (var block in blocks) + { + var maybeBlockGroup = blockGroups.FirstOrDefault(bg => bg.Id == block.BlockGroupId); + if (maybeBlockGroup != null) + { + unusedBlockGroups.Remove(maybeBlockGroup); + block.BlockGroup = maybeBlockGroup; + } + } + + // create dummy blocks for any groups that have no blocks yet + foreach (var unusedGroup in unusedBlockGroups) + { + var dummyBlock = new Block + { + Id = unusedGroup.Id * -1, + BlockGroupId = unusedGroup.Id, + BlockGroup = unusedGroup, + Name = "(none)" + }; + + blocks.Add(dummyBlock); + } + + return blocks.Map(Mapper.ProjectToViewModel) + .OrderBy(b => b.GroupName) + .ThenBy(b => b.Name) + .ToList(); + } +} diff --git a/ErsatzTV/Pages/Blocks.razor b/ErsatzTV/Pages/Blocks.razor index 1d0cd6ab8..13b74faa0 100644 --- a/ErsatzTV/Pages/Blocks.razor +++ b/ErsatzTV/Pages/Blocks.razor @@ -48,48 +48,82 @@ Add Block - -
- - - - -
- - - @item.Value.Text - - @if (!string.IsNullOrWhiteSpace(item.Value.EndText)) - { - - @item.Value.EndText - - } - -
- @foreach (int blockId in Optional(item.Value.BlockId)) - { - - } - -
-
-
-
-
-
-
+ + + + + + + + + + + + + + + @($"{context.Key}") + + +
+
+ + + + +
+
+
+ + @context.Name + +
+ @if (context.Id >= 0) + { + + + + + + + + + } + else + { +
+ } +
+
+
+
@code { private CancellationTokenSource _cts; - private readonly List> _treeItems = []; + private readonly List _blocks = []; private List _blockGroups = []; private BlockGroupViewModel _selectedBlockGroup; private string _blockGroupName; private string _blockName; + private string _searchString; public void Dispose() { @@ -108,12 +142,8 @@ { _blockGroups = await Mediator.Send(new GetAllBlockGroups(), token); - _treeItems.Clear(); - BlockTreeViewModel tree = await Mediator.Send(new GetBlockTree(), token); - foreach (BlockTreeBlockGroupViewModel group in tree.Groups) - { - _treeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(group) }); - } + _blocks.Clear(); + _blocks.AddRange(await Mediator.Send(new GetAllBlocks(), token)); } catch (OperationCanceledException) { @@ -121,6 +151,14 @@ } } + private readonly TableGroupDefinition _groupDefinition = new() + { + GroupName = "Group", + Indentation = false, + Expandable = true, + Selector = (e) => e.GroupName + }; + private async Task AddBlockGroup() { if (!string.IsNullOrWhiteSpace(_blockGroupName)) @@ -135,11 +173,13 @@ foreach (BlockGroupViewModel blockGroup in result.RightToSeq()) { - _treeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(blockGroup) }); - _treeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture)); _blockGroupName = null; - _blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token); + _selectedBlockGroup = _blockGroups.Find(bg => bg.Id == blockGroup.Id); + + _blocks.Clear(); + _blocks.AddRange(await Mediator.Send(new GetAllBlocks(), _cts.Token)); + await InvokeAsync(StateHasChanged); } } @@ -157,13 +197,10 @@ Logger.LogError("Unexpected error adding block: {Error}", error.Value); } - foreach (BlockViewModel block in result.RightToSeq()) + if (result.IsRight) { - foreach (BlockTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.BlockGroupId == _selectedBlockGroup.Id)) - { - item.TreeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(block) }); - item.TreeItems.Sort((x, y) => string.Compare(x.Value?.Text, y.Value?.Text, StringComparison.CurrentCulture)); - } + _blocks.Clear(); + _blocks.AddRange(await Mediator.Send(new GetAllBlocks(), _cts.Token)); _blockName = null; await InvokeAsync(StateHasChanged); @@ -171,47 +208,70 @@ } } - private async Task DeleteItem(BlockTreeItemViewModel treeItem) + private void OnSearch(string query) { - foreach (int blockGroupId in Optional(treeItem.BlockGroupId)) - { - var parameters = new DialogParameters { { "EntityType", "block group" }, { "EntityName", treeItem.Text } }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + _searchString = query; + } - IDialogReference dialog = await Dialog.ShowAsync("Delete Block Group", parameters, options); - DialogResult result = await dialog.Result; - if (result is not null && !result.Canceled) - { - await Mediator.Send(new DeleteBlockGroup(blockGroupId), _cts.Token); - _treeItems.RemoveAll(i => i.Value?.BlockGroupId == blockGroupId); - if (_selectedBlockGroup?.Id == blockGroupId) - { - _selectedBlockGroup = null; - } + private bool FilterBlocks(BlockViewModel block) => FilterBlocks(block, _searchString); - _blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token); - await InvokeAsync(StateHasChanged); - } + private bool FilterBlocks(BlockViewModel block, string searchString) + { + if (string.IsNullOrWhiteSpace(searchString)) + { + return true; } - foreach (int blockId in Optional(treeItem.BlockId)) + if (block.Name.Contains(searchString, StringComparison.OrdinalIgnoreCase)) { - var parameters = new DialogParameters { { "EntityType", "block" }, { "EntityName", treeItem.Text } }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + return true; + } - IDialogReference dialog = await Dialog.ShowAsync("Delete Block", parameters, options); - DialogResult result = await dialog.Result; - if (result is not null && !result.Canceled) - { - await Mediator.Send(new DeleteBlock(blockId), _cts.Token); - foreach (BlockTreeItemViewModel parent in _treeItems.Map(i => i.Value)) - { - parent.TreeItems.RemoveAll(i => i.Value == treeItem); - } + return false; + } - await InvokeAsync(StateHasChanged); + private async Task DeleteBlockGroup(BlockViewModel block) + { + if (block is null) + { + return; + } + + var parameters = new DialogParameters { { "EntityType", "block group" }, { "EntityName", block.GroupName } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = await Dialog.ShowAsync("Delete Block Group", parameters, options); + DialogResult result = await dialog.Result; + if (result is not null && !result.Canceled) + { + await Mediator.Send(new DeleteBlockGroup(block.GroupId), _cts.Token); + if (_selectedBlockGroup?.Id == block.GroupId) + { + _selectedBlockGroup = null; } + + _blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token); + _blocks.Clear(); + _blocks.AddRange(await Mediator.Send(new GetAllBlocks(), _cts.Token)); + + await InvokeAsync(StateHasChanged); } } + private async Task DeleteBlock(BlockViewModel block) + { + var parameters = new DialogParameters { { "EntityType", "block" }, { "EntityName", block.Name } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = await Dialog.ShowAsync("Delete Block", parameters, options); + DialogResult result = await dialog.Result; + if (result is not null && !result.Canceled) + { + await Mediator.Send(new DeleteBlock(block.Id), _cts.Token); + _blocks.Clear(); + _blocks.AddRange(await Mediator.Send(new GetAllBlocks(), _cts.Token)); + + await InvokeAsync(StateHasChanged); + } + } }