diff --git a/CHANGELOG.md b/CHANGELOG.md index cd765b8c3..723ab106f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,13 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Remove some limits on multithreading that are no longer needed with latest ffmpeg - Mixed transcoding (software decode, hardware filters/encode) can now use multiple decode threads - Split main `Settings` page into multiple pages -- Update form layout to be less cramped and to work better on mobile - - All new (split) settings pages - - Channel editor - - FFmpeg Profile editor - - Schedule editor - - Watermark editor - - Local library editor +- Update UI layout on most pages to be less cramped and to work better on mobile ### Fixed - Fix QSV acceleration in docker with older Intel devices diff --git a/ErsatzTV.Application/MediaCollections/Mapper.cs b/ErsatzTV.Application/MediaCollections/Mapper.cs index 3c68c2827..cc7c88088 100644 --- a/ErsatzTV.Application/MediaCollections/Mapper.cs +++ b/ErsatzTV.Application/MediaCollections/Mapper.cs @@ -1,4 +1,5 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Application.Tree; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaCollections; @@ -46,6 +47,13 @@ internal static class Mapper multiCollectionSmartItem.ScheduleAsGroup, multiCollectionSmartItem.PlaybackOrder); + internal static TreeViewModel ProjectToViewModel(List playlistGroups) => + new( + playlistGroups.Map(bg => new TreeGroupViewModel( + bg.Id, + bg.Name, + bg.Playlists.Map(b => new TreeItemViewModel(b.Id, b.Name)).ToList())).ToList()); + internal static PlaylistGroupViewModel ProjectToViewModel(PlaylistGroup playlistGroup) => new(playlistGroup.Id, playlistGroup.Name, playlistGroup.Playlists.Count); diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTree.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTree.cs new file mode 100644 index 000000000..2284b87ee --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTree.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Application.Tree; + +namespace ErsatzTV.Application.MediaCollections; + +public record GetPlaylistTree : IRequest; diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTreeHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTreeHandler.cs new file mode 100644 index 000000000..63639a6df --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTreeHandler.cs @@ -0,0 +1,24 @@ +using ErsatzTV.Application.Tree; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.MediaCollections; + +public class GetPlaylistTreeHandler(IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle( + GetPlaylistTree request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + List playlistGroups = await dbContext.PlaylistGroups + .AsNoTracking() + .Include(g => g.Playlists) + .ToListAsync(cancellationToken); + + return Mapper.ProjectToViewModel(playlistGroups); + } +} diff --git a/ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs b/ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs index 89d8e3a57..55ffb26c1 100644 --- a/ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs +++ b/ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs @@ -1,3 +1,3 @@ namespace ErsatzTV.Application.Scheduling; -public record BlockGroupViewModel(int Id, string Name, int BlockCount); +public record BlockGroupViewModel(int Id, string Name); diff --git a/ErsatzTV.Application/Scheduling/BlockTreeBlockGroupViewModel.cs b/ErsatzTV.Application/Scheduling/BlockTreeBlockGroupViewModel.cs new file mode 100644 index 000000000..796ebcd3c --- /dev/null +++ b/ErsatzTV.Application/Scheduling/BlockTreeBlockGroupViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record BlockTreeBlockGroupViewModel(int Id, string Name, List Blocks); diff --git a/ErsatzTV.Application/Scheduling/BlockTreeBlockViewModel.cs b/ErsatzTV.Application/Scheduling/BlockTreeBlockViewModel.cs new file mode 100644 index 000000000..7f4db1a99 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/BlockTreeBlockViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record BlockTreeBlockViewModel(int Id, string Name, int Minutes); diff --git a/ErsatzTV.Application/Scheduling/BlockTreeViewModel.cs b/ErsatzTV.Application/Scheduling/BlockTreeViewModel.cs new file mode 100644 index 000000000..6d6e7552e --- /dev/null +++ b/ErsatzTV.Application/Scheduling/BlockTreeViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record BlockTreeViewModel(List Groups); diff --git a/ErsatzTV.Application/Scheduling/Mapper.cs b/ErsatzTV.Application/Scheduling/Mapper.cs index b550fd20f..a49c29aaf 100644 --- a/ErsatzTV.Application/Scheduling/Mapper.cs +++ b/ErsatzTV.Application/Scheduling/Mapper.cs @@ -1,12 +1,41 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Application.Tree; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; namespace ErsatzTV.Application.Scheduling; internal static class Mapper { + internal static TreeViewModel ProjectToViewModel(List decoTemplateGroups) => + new( + decoTemplateGroups.Map(dtg => new TreeGroupViewModel( + dtg.Id, + dtg.Name, + dtg.DecoTemplates.Map(dt => new TreeItemViewModel(dt.Id, dt.Name)).ToList())).ToList()); + + internal static TreeViewModel ProjectToViewModel(List decoGroups) => + new( + decoGroups.Map(dg => new TreeGroupViewModel( + dg.Id, + dg.Name, + dg.Decos.Map(d => new TreeItemViewModel(d.Id, d.Name)).ToList())).ToList()); + + internal static TreeViewModel ProjectToViewModel(List templateGroups) => + new( + templateGroups.Map(tg => new TreeGroupViewModel( + tg.Id, + tg.Name, + tg.Templates.Map(t => new TreeItemViewModel(t.Id, t.Name)).ToList())).ToList()); + + internal static BlockTreeViewModel ProjectToViewModel(List blockGroups) => + new( + blockGroups.Map(bg => new BlockTreeBlockGroupViewModel( + bg.Id, + bg.Name, + bg.Blocks.Map(b => new BlockTreeBlockViewModel(b.Id, b.Name, b.Minutes)).ToList())).ToList()); + internal static BlockGroupViewModel ProjectToViewModel(BlockGroup blockGroup) => - new(blockGroup.Id, blockGroup.Name, blockGroup.Blocks.Count); + new(blockGroup.Id, blockGroup.Name); internal static BlockViewModel ProjectToViewModel(Block block) => new(block.Id, block.Name, block.Minutes, block.StopScheduling); diff --git a/ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs b/ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs index 7f5c111bb..2be66d5d0 100644 --- a/ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs +++ b/ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs @@ -13,7 +13,6 @@ public class GetAllBlockGroupsHandler(IDbContextFactory dbContextFact List blockGroups = await dbContext.BlockGroups .AsNoTracking() - .Include(g => g.Blocks) .ToListAsync(cancellationToken); return blockGroups.Map(Mapper.ProjectToViewModel).ToList(); diff --git a/ErsatzTV.Application/Scheduling/Queries/GetBlockTree.cs b/ErsatzTV.Application/Scheduling/Queries/GetBlockTree.cs new file mode 100644 index 000000000..8e0093886 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetBlockTree.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record GetBlockTree : IRequest; diff --git a/ErsatzTV.Application/Scheduling/Queries/GetBlockTreeHandler.cs b/ErsatzTV.Application/Scheduling/Queries/GetBlockTreeHandler.cs new file mode 100644 index 000000000..95523a00a --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetBlockTreeHandler.cs @@ -0,0 +1,21 @@ +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class GetBlockTreeHandler(IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle(GetBlockTree request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + List blockGroups = await dbContext.BlockGroups + .AsNoTracking() + .Include(g => g.Blocks) + .ToListAsync(cancellationToken); + + return Mapper.ProjectToViewModel(blockGroups); + } +} diff --git a/ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTree.cs b/ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTree.cs new file mode 100644 index 000000000..e38b1508a --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTree.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Application.Tree; + +namespace ErsatzTV.Application.Scheduling; + +public record GetDecoTemplateTree : IRequest; diff --git a/ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs b/ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs new file mode 100644 index 000000000..5c5183b28 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs @@ -0,0 +1,25 @@ +using ErsatzTV.Application.Tree; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class GetDecoTemplateTreeHandler(IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle( + GetDecoTemplateTree request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + List templateGroups = await dbContext.DecoTemplateGroups + .AsNoTracking() + .Include(g => g.DecoTemplates) + .ToListAsync(cancellationToken); + + return Mapper.ProjectToViewModel(templateGroups); + } +} diff --git a/ErsatzTV.Application/Scheduling/Queries/GetDecoTree.cs b/ErsatzTV.Application/Scheduling/Queries/GetDecoTree.cs new file mode 100644 index 000000000..e1c61908d --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetDecoTree.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Application.Tree; + +namespace ErsatzTV.Application.Scheduling; + +public record GetDecoTree : IRequest; diff --git a/ErsatzTV.Application/Scheduling/Queries/GetDecoTreeHandler.cs b/ErsatzTV.Application/Scheduling/Queries/GetDecoTreeHandler.cs new file mode 100644 index 000000000..31db7484f --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetDecoTreeHandler.cs @@ -0,0 +1,25 @@ +using ErsatzTV.Application.Tree; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class GetDecoTreeHandler(IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle( + GetDecoTree request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + List templateGroups = await dbContext.DecoGroups + .AsNoTracking() + .Include(g => g.Decos) + .ToListAsync(cancellationToken); + + return Mapper.ProjectToViewModel(templateGroups); + } +} diff --git a/ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs b/ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs new file mode 100644 index 000000000..f1e751e9b --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Application.Tree; + +namespace ErsatzTV.Application.Scheduling; + +public record GetTemplateTree : IRequest; diff --git a/ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs b/ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs new file mode 100644 index 000000000..fd0dd253d --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs @@ -0,0 +1,25 @@ +using ErsatzTV.Application.Tree; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class GetTemplateTreeHandler(IDbContextFactory dbContextFactory) + : IRequestHandler +{ + public async Task Handle( + GetTemplateTree request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + List templateGroups = await dbContext.TemplateGroups + .AsNoTracking() + .Include(g => g.Templates) + .ToListAsync(cancellationToken); + + return Mapper.ProjectToViewModel(templateGroups); + } +} diff --git a/ErsatzTV.Application/Tree/TemplateTreeViewModel.cs b/ErsatzTV.Application/Tree/TemplateTreeViewModel.cs new file mode 100644 index 000000000..422b2e077 --- /dev/null +++ b/ErsatzTV.Application/Tree/TemplateTreeViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Tree; + +public record TreeViewModel(List Groups); diff --git a/ErsatzTV.Application/Tree/TreeGroupViewModel.cs b/ErsatzTV.Application/Tree/TreeGroupViewModel.cs new file mode 100644 index 000000000..ab48236e3 --- /dev/null +++ b/ErsatzTV.Application/Tree/TreeGroupViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Tree; + +public record TreeGroupViewModel(int Id, string Name, List Children); diff --git a/ErsatzTV.Application/Tree/TreeItemViewModel.cs b/ErsatzTV.Application/Tree/TreeItemViewModel.cs new file mode 100644 index 000000000..6a77f3deb --- /dev/null +++ b/ErsatzTV.Application/Tree/TreeItemViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Tree; + +public record TreeItemViewModel(int Id, string Name); diff --git a/ErsatzTV/Pages/Blocks.razor b/ErsatzTV/Pages/Blocks.razor index 80be65b6c..e52d96f1f 100644 --- a/ErsatzTV/Pages/Blocks.razor +++ b/ErsatzTV/Pages/Blocks.razor @@ -6,50 +6,53 @@ @inject IMediator Mediator @inject IDialogService Dialog - - Blocks - - -
- - - - - - - Add Block Group - - - -
-
- -
- - -
- - @foreach (BlockGroupViewModel blockGroup in _blockGroups) - { - @blockGroup.Name - } - - -
-
- - - Add Block - - -
-
-
- - - + +
+ + Block Groups + + +
+ Block Group Name +
+ +
+ +
+ + Add Block Group + +
+ Blocks + + +
+ Block Group +
+ + @foreach (BlockGroupViewModel blockGroup in _blockGroups) + { + @blockGroup.Name + } + +
+ +
+ Block Name +
+ +
+ +
+ + Add Block + +
+ +
+ - +
@@ -75,14 +78,14 @@ - - - - + + +
+ @code { private readonly CancellationTokenSource _cts = new(); - private List> TreeItems { get; set; } = []; + private readonly List> _treeItems = []; private List _blockGroups = []; private BlockGroupViewModel _selectedBlockGroup; private string _blockGroupName; @@ -96,14 +99,20 @@ protected override async Task OnParametersSetAsync() { - await ReloadBlockGroups(); + await ReloadBlockTree(); await InvokeAsync(StateHasChanged); } - private async Task ReloadBlockGroups() + private async Task ReloadBlockTree() { _blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token); - TreeItems = _blockGroups.Map(g => new TreeItemData { Value = new BlockTreeItemViewModel(g) }).ToList(); + + _treeItems.Clear(); + BlockTreeViewModel tree = await Mediator.Send(new GetBlockTree(), _cts.Token); + foreach (BlockTreeBlockGroupViewModel group in tree.Groups) + { + _treeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(group) }); + } } private async Task AddBlockGroup() @@ -120,7 +129,7 @@ foreach (BlockGroupViewModel blockGroup in result.RightToSeq()) { - TreeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(blockGroup) }); + _treeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(blockGroup) }); _blockGroupName = null; _blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token); @@ -143,7 +152,7 @@ foreach (BlockViewModel block in result.RightToSeq()) { - foreach (BlockTreeItemViewModel item in TreeItems.Map(i => i.Value).Where(item => item.BlockGroupId == _selectedBlockGroup.Id)) + foreach (BlockTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.BlockGroupId == _selectedBlockGroup.Id)) { item.TreeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(block) }); } @@ -154,20 +163,6 @@ } } - private async Task>> LoadServerData(BlockTreeItemViewModel parentNode) - { - foreach (int blockGroupId in Optional(parentNode.BlockGroupId)) - { - List result = await Mediator.Send(new GetBlocksByBlockGroupId(blockGroupId), _cts.Token); - foreach (BlockViewModel block in result) - { - parentNode.TreeItems.Add(new TreeItemData { Value = new BlockTreeItemViewModel(block) }); - } - } - - return parentNode.TreeItems; - } - private async Task DeleteItem(BlockTreeItemViewModel treeItem) { foreach (int blockGroupId in Optional(treeItem.BlockGroupId)) @@ -180,7 +175,11 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteBlockGroup(blockGroupId), _cts.Token); - TreeItems.RemoveAll(i => i.Value?.BlockGroupId == blockGroupId); + _treeItems.RemoveAll(i => i.Value?.BlockGroupId == blockGroupId); + if (_selectedBlockGroup?.Id == blockGroupId) + { + _selectedBlockGroup = null; + } _blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token); await InvokeAsync(StateHasChanged); @@ -197,7 +196,7 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteBlock(blockId), _cts.Token); - foreach (BlockTreeItemViewModel parent in TreeItems.Map(i => i.Value)) + foreach (BlockTreeItemViewModel parent in _treeItems.Map(i => i.Value)) { parent.TreeItems.RemoveAll(i => i.Value == treeItem); } diff --git a/ErsatzTV/Pages/Collections.razor b/ErsatzTV/Pages/Collections.razor index 4c3eb6c72..8f1f61cc2 100644 --- a/ErsatzTV/Pages/Collections.razor +++ b/ErsatzTV/Pages/Collections.razor @@ -66,7 +66,7 @@ - Multi Collections + Multi Collections - Smart Collections + Smart Collections - - Edit Deco - - - - - - - Save Changes - - - - - - - Watermark - - - - - Inherit - Disable - Override - - - (none) - @foreach (WatermarkViewModel watermark in _watermarks) - { - @watermark.Name - } - - - - - - - - - - - Default Filler -   - - - - - After all blocks have been scheduled, a second pass will be made to fill unscheduled time using random items from this collection. - - - - - - - Inherit - Disable - Override - - - Collection - Television Show - Television Season - Artist - Multi Collection - Smart Collection - - @if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Collection) - { - - @foreach (MediaCollectionViewModel collection in _mediaCollections) + + + + Save Changes + + +
+ + Deco + + +
+ Name +
+ +
+ Watermark + + +
+ Watermark Mode +
+ + Inherit + Disable + Override + +
+ +
+ Watermark Override +
+ + (none) + @foreach (WatermarkViewModel watermark in _watermarks) { - @collection.Name + @watermark.Name } +
+ +
+ Use Watermark During Filler +
+ +
+ Default Filler + + After all blocks have been scheduled, a second pass will be made to fill unscheduled time using random items from this collection. + +
+ Default Filler Mode +
+ + Inherit + Disable + Override + +
+ +
+ Default Filler Collection Type +
+ + Collection + Television Show + Television Season + Artist + Multi Collection + Smart Collection + +
+ @if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Collection) + { + +
+ Collection +
+ + @foreach (MediaCollectionViewModel collection in _mediaCollections) + { + @collection.Name + } + +
} @if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.MultiCollection) { - - @foreach (MultiCollectionViewModel collection in _multiCollections) - { - @collection.Name - } - + +
+ Multi Collection +
+ + @foreach (MultiCollectionViewModel collection in _multiCollections) + { + @collection.Name + } + +
} @if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.SmartCollection) { - - @foreach (SmartCollectionViewModel collection in _smartCollections) - { - @collection.Name - } - + +
+ Smart Collection +
+ + @foreach (SmartCollectionViewModel collection in _smartCollections) + { + @collection.Name + } + +
} @if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.TelevisionShow) { - - @foreach (NamedMediaItemViewModel show in _televisionShows) - { - @show.Name - } - + +
+ Television Show +
+ + @foreach (NamedMediaItemViewModel show in _televisionShows) + { + @show.Name + } + +
} @if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason) { - - @foreach (NamedMediaItemViewModel season in _televisionSeasons) - { - @season.Name - } - + +
+ Television Season +
+ + @foreach (NamedMediaItemViewModel season in _televisionSeasons) + { + @season.Name + } + +
} @if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Artist) { - - @foreach (NamedMediaItemViewModel artist in _artists) - { - @artist.Name - } - + +
+ Artist +
+ + @foreach (NamedMediaItemViewModel artist in _artists) + { + @artist.Name + } + +
} - - - - - - - - - - Dead Air Fallback -   - - - - - When no playout item is found for the current time, *one* item will be randomly selected from this collection and looped and trimmed to exactly fit until the start of the next playout item. - This replaces the "Channel is Offline" image that would otherwise display. - - - - - - - Inherit - Disable - Override - - - Collection - Television Show - Television Season - Artist - Multi Collection - Smart Collection - + +
+ Trim To Fit +
+ +
+ Dead Air Fallback + + When no playout item is found for the current time, *one* item will be randomly selected from this collection and looped and trimmed to exactly fit until the start of the next playout item. + This replaces the "Channel is Offline" image that would otherwise display. + +
+ Dead Air Fallback Mode +
+ + Inherit + Disable + Override + +
+ +
+ Dead Air Fallback Collection Type +
+ + Collection + Television Show + Television Season + Artist + Multi Collection + Smart Collection + +
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Collection) { - - @foreach (MediaCollectionViewModel collection in _mediaCollections) - { - @collection.Name - } - + +
+ Collection +
+ + @foreach (MediaCollectionViewModel collection in _mediaCollections) + { + @collection.Name + } + +
} @if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.MultiCollection) { - - @foreach (MultiCollectionViewModel collection in _multiCollections) - { - @collection.Name - } - + +
+ Multi Collection +
+ + @foreach (MultiCollectionViewModel collection in _multiCollections) + { + @collection.Name + } + +
} @if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.SmartCollection) { - - @foreach (SmartCollectionViewModel collection in _smartCollections) - { - @collection.Name - } - + +
+ Smart Collection +
+ + @foreach (SmartCollectionViewModel collection in _smartCollections) + { + @collection.Name + } + +
} @if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionShow) { - - @foreach (NamedMediaItemViewModel show in _televisionShows) - { - @show.Name - } - + +
+ Television Show +
+ + @foreach (NamedMediaItemViewModel show in _televisionShows) + { + @show.Name + } + +
} @if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason) { - - @foreach (NamedMediaItemViewModel season in _televisionSeasons) - { - @season.Name - } - + +
+ Television Season +
+ + @foreach (NamedMediaItemViewModel season in _televisionSeasons) + { + @season.Name + } + +
} @if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Artist) { - - @foreach (NamedMediaItemViewModel artist in _artists) - { - @artist.Name - } - + +
+ Artist +
+ + @foreach (NamedMediaItemViewModel artist in _artists) + { + @artist.Name + } + +
} -
-
- -
+ +
+
@code { private readonly CancellationTokenSource _cts = new(); @@ -360,34 +395,34 @@ DefaultFillerMode = deco.DefaultFillerMode, DefaultFillerCollectionType = deco.DefaultFillerCollectionType, DefaultFillerCollection = deco.DefaultFillerCollectionId.HasValue - ? _mediaCollections.Find(c => c.Id == deco.DefaultFillerCollectionId.Value) + ? _mediaCollections.Find(c => c.Id == deco.DefaultFillerCollectionId!.Value) : null, DefaultFillerMediaItem = deco.DefaultFillerMediaItemId.HasValue ? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList() - .Find(vm => vm.MediaItemId == deco.DefaultFillerMediaItemId.Value) + .Find(vm => vm.MediaItemId == deco.DefaultFillerMediaItemId!.Value) : null, DefaultFillerMultiCollection = deco.DefaultFillerMultiCollectionId.HasValue - ? _multiCollections.Find(c => c.Id == deco.DefaultFillerMultiCollectionId.Value) + ? _multiCollections.Find(c => c.Id == deco.DefaultFillerMultiCollectionId!.Value) : null, DefaultFillerSmartCollection = deco.DefaultFillerSmartCollectionId.HasValue - ? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId.Value) + ? _smartCollections.Find(c => c.Id == deco.DefaultFillerSmartCollectionId!.Value) : null, DefaultFillerTrimToFit = deco.DefaultFillerTrimToFit, DeadAirFallbackMode = deco.DeadAirFallbackMode, DeadAirFallbackCollectionType = deco.DeadAirFallbackCollectionType, DeadAirFallbackCollection = deco.DeadAirFallbackCollectionId.HasValue - ? _mediaCollections.Find(c => c.Id == deco.DeadAirFallbackCollectionId.Value) + ? _mediaCollections.Find(c => c.Id == deco.DeadAirFallbackCollectionId!.Value) : null, DeadAirFallbackMediaItem = deco.DeadAirFallbackMediaItemId.HasValue ? _televisionShows.Append(_televisionSeasons).Append(_artists).ToList() - .Find(vm => vm.MediaItemId == deco.DeadAirFallbackMediaItemId.Value) + .Find(vm => vm.MediaItemId == deco.DeadAirFallbackMediaItemId!.Value) : null, DeadAirFallbackMultiCollection = deco.DeadAirFallbackMultiCollectionId.HasValue - ? _multiCollections.Find(c => c.Id == deco.DeadAirFallbackMultiCollectionId.Value) + ? _multiCollections.Find(c => c.Id == deco.DeadAirFallbackMultiCollectionId!.Value) : null, DeadAirFallbackSmartCollection = deco.DeadAirFallbackSmartCollectionId.HasValue - ? _smartCollections.Find(c => c.Id == deco.DeadAirFallbackSmartCollectionId.Value) + ? _smartCollections.Find(c => c.Id == deco.DeadAirFallbackSmartCollectionId!.Value) : null }; } diff --git a/ErsatzTV/Pages/DecoTemplates.razor b/ErsatzTV/Pages/DecoTemplates.razor index 36ef01acb..a187f12c8 100644 --- a/ErsatzTV/Pages/DecoTemplates.razor +++ b/ErsatzTV/Pages/DecoTemplates.razor @@ -1,59 +1,63 @@ @page "/deco-templates" @using ErsatzTV.Application.Scheduling +@using ErsatzTV.Application.Tree @implements IDisposable @inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator @inject IDialogService Dialog - - Deco Templates - - -
- - - - - - - Add Deco Template Group - - - -
-
- -
- - -
- - @foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups) - { - @decoTemplateGroup.Name - } - - -
-
- - - Add Deco Template - - -
-
-
- - - + +
+ + Deco Template Groups + + +
+ Deco Template Group Name +
+ +
+ +
+ + Add Deco Template Group + +
+ Deco Templates + + +
+ Deco Template Group +
+ + @foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups) + { + @decoTemplateGroup.Name + } + +
+ +
+ Deco Template Name +
+ +
+ +
+ + Add Deco Template + +
+ +
+ - +
- + @item.Value.Text @@ -69,14 +73,14 @@ - - - - + + +
+ @code { private readonly CancellationTokenSource _cts = new(); - private List> TreeItems { get; set; } = []; + private readonly List> _treeItems = []; private List _decoTemplateGroups = []; private DecoTemplateGroupViewModel _selectedDecoTemplateGroup; private string _decoTemplateGroupName; @@ -90,14 +94,20 @@ protected override async Task OnParametersSetAsync() { - await ReloadDecoTemplateGroups(); + await ReloadDecoTemplateTree(); await InvokeAsync(StateHasChanged); } - private async Task ReloadDecoTemplateGroups() + private async Task ReloadDecoTemplateTree() { _decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token); - TreeItems = _decoTemplateGroups.Map(g => new TreeItemData { Value = new DecoTemplateTreeItemViewModel(g) }).ToList(); + + _treeItems.Clear(); + TreeViewModel tree = await Mediator.Send(new GetDecoTemplateTree(), _cts.Token); + foreach (TreeGroupViewModel group in tree.Groups) + { + _treeItems.Add(new TreeItemData { Value = new DecoTemplateTreeItemViewModel(group) }); + } } private async Task AddDecoTemplateGroup() @@ -109,12 +119,12 @@ foreach (BaseError error in result.LeftToSeq()) { Snackbar.Add(error.Value, Severity.Error); - Logger.LogError("Unexpected error adding deco template group: {Error}", error.Value); + Logger.LogError("Unexpected error adding decoTemplate group: {Error}", error.Value); } foreach (DecoTemplateGroupViewModel decoTemplateGroup in result.RightToSeq()) { - TreeItems.Add(new TreeItemData { Value = new DecoTemplateTreeItemViewModel(decoTemplateGroup) }); + _treeItems.Add(new TreeItemData { Value = new DecoTemplateTreeItemViewModel(decoTemplateGroup) }); _decoTemplateGroupName = null; _decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token); @@ -132,12 +142,12 @@ foreach (BaseError error in result.LeftToSeq()) { Snackbar.Add(error.Value, Severity.Error); - Logger.LogError("Unexpected error adding deco template: {Error}", error.Value); + Logger.LogError("Unexpected error adding decoTemplate: {Error}", error.Value); } foreach (DecoTemplateViewModel decoTemplate in result.RightToSeq()) { - foreach (DecoTemplateTreeItemViewModel item in TreeItems.Map(i => i.Value).Where(item => item.DecoTemplateGroupId == _selectedDecoTemplateGroup.Id)) + foreach (DecoTemplateTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.DecoTemplateGroupId == _selectedDecoTemplateGroup.Id)) { item.TreeItems.Add(new TreeItemData { Value = new DecoTemplateTreeItemViewModel(decoTemplate) }); } @@ -148,33 +158,23 @@ } } - private async Task>> LoadServerData(DecoTemplateTreeItemViewModel parentNode) - { - foreach (int decoTemplateGroupId in Optional(parentNode.DecoTemplateGroupId)) - { - List result = await Mediator.Send(new GetDecoTemplatesByDecoTemplateGroupId(decoTemplateGroupId), _cts.Token); - foreach (DecoTemplateViewModel decoTemplate in result) - { - parentNode.TreeItems.Add(new TreeItemData { Value = new DecoTemplateTreeItemViewModel(decoTemplate) }); - } - } - - return parentNode.TreeItems; - } - private async Task DeleteItem(DecoTemplateTreeItemViewModel treeItem) { foreach (int decoTemplateGroupId in Optional(treeItem.DecoTemplateGroupId)) { - var parameters = new DialogParameters { { "EntityType", "Deco Template group" }, { "EntityName", treeItem.Text } }; + var parameters = new DialogParameters { { "EntityType", "decoTemplate group" }, { "EntityName", treeItem.Text } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; - IDialogReference dialog = await Dialog.ShowAsync("Delete Deco Template Group", parameters, options); + IDialogReference dialog = await Dialog.ShowAsync("Delete DecoTemplate Group", parameters, options); DialogResult result = await dialog.Result; if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteDecoTemplateGroup(decoTemplateGroupId), _cts.Token); - TreeItems.RemoveAll(i => i.Value?.DecoTemplateGroupId == decoTemplateGroupId); + _treeItems.RemoveAll(i => i.Value?.DecoTemplateGroupId == decoTemplateGroupId); + if (_selectedDecoTemplateGroup?.Id == decoTemplateGroupId) + { + _selectedDecoTemplateGroup = null; + } _decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token); await InvokeAsync(StateHasChanged); @@ -183,15 +183,15 @@ foreach (int decoTemplateId in Optional(treeItem.DecoTemplateId)) { - var parameters = new DialogParameters { { "EntityType", "Deco Template" }, { "EntityName", treeItem.Text } }; + var parameters = new DialogParameters { { "EntityType", "decoTemplate" }, { "EntityName", treeItem.Text } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; - IDialogReference dialog = await Dialog.ShowAsync("Delete Deco Template", parameters, options); + IDialogReference dialog = await Dialog.ShowAsync("Delete DecoTemplate", parameters, options); DialogResult result = await dialog.Result; if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteDecoTemplate(decoTemplateId), _cts.Token); - foreach (DecoTemplateTreeItemViewModel parent in TreeItems.Map(i => i.Value)) + foreach (DecoTemplateTreeItemViewModel parent in _treeItems.Map(i => i.Value)) { parent.TreeItems.RemoveAll(i => i.Value == treeItem); } diff --git a/ErsatzTV/Pages/Decos.razor b/ErsatzTV/Pages/Decos.razor index 2cd3d06b7..14c47cd51 100644 --- a/ErsatzTV/Pages/Decos.razor +++ b/ErsatzTV/Pages/Decos.razor @@ -1,67 +1,65 @@ @page "/decos" @using ErsatzTV.Application.Scheduling +@using ErsatzTV.Application.Tree @implements IDisposable @inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator @inject IDialogService Dialog - - Decos - - -
- - - - - - - Add Deco Group - - - -
-
- -
- - -
- - @foreach (DecoGroupViewModel decoGroup in _decoGroups) - { - @decoGroup.Name - } - - -
-
- - - Add Deco - - -
-
-
- - - + +
+ + Deco Groups + + +
+ Deco Group Name +
+ +
+ +
+ + Add Deco Group + +
+ Decos + + +
+ Deco Group +
+ + @foreach (DecoGroupViewModel decoGroup in _decoGroups) + { + @decoGroup.Name + } + +
+ +
+ Deco Name +
+ +
+ +
+ + Add Deco + +
+ +
+ - +
@item.Value.Text - @if (!string.IsNullOrWhiteSpace(item.Value.EndText)) - { - - @item.Value.EndText - - }
@foreach (int decoId in Optional(item.Value.DecoId)) @@ -75,14 +73,14 @@ - - - - + + +
+ @code { private readonly CancellationTokenSource _cts = new(); - private List> TreeItems { get; set; } = []; + private readonly List> _treeItems = []; private List _decoGroups = []; private DecoGroupViewModel _selectedDecoGroup; private string _decoGroupName; @@ -96,14 +94,20 @@ protected override async Task OnParametersSetAsync() { - await ReloadDecoGroups(); + await ReloadDecoTree(); await InvokeAsync(StateHasChanged); } - private async Task ReloadDecoGroups() + private async Task ReloadDecoTree() { _decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token); - TreeItems = _decoGroups.Map(g => new TreeItemData { Value = new DecoTreeItemViewModel(g) }).ToList(); + + _treeItems.Clear(); + TreeViewModel tree = await Mediator.Send(new GetDecoTree(), _cts.Token); + foreach (TreeGroupViewModel group in tree.Groups) + { + _treeItems.Add(new TreeItemData { Value = new DecoTreeItemViewModel(group) }); + } } private async Task AddDecoGroup() @@ -120,7 +124,7 @@ foreach (DecoGroupViewModel decoGroup in result.RightToSeq()) { - TreeItems.Add(new TreeItemData { Value = new DecoTreeItemViewModel(decoGroup) }); + _treeItems.Add(new TreeItemData { Value = new DecoTreeItemViewModel(decoGroup) }); _decoGroupName = null; _decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token); @@ -143,7 +147,7 @@ foreach (DecoViewModel deco in result.RightToSeq()) { - foreach (DecoTreeItemViewModel item in TreeItems.Where(item => item.Value!.DecoGroupId == _selectedDecoGroup.Id).Map(i => i.Value)) + foreach (DecoTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.DecoGroupId == _selectedDecoGroup.Id)) { item.TreeItems.Add(new TreeItemData { Value = new DecoTreeItemViewModel(deco) }); } @@ -154,20 +158,6 @@ } } - private async Task>> LoadServerData(DecoTreeItemViewModel parentNode) - { - foreach (int decoGroupId in Optional(parentNode.DecoGroupId)) - { - List result = await Mediator.Send(new GetDecosByDecoGroupId(decoGroupId), _cts.Token); - foreach (DecoViewModel deco in result) - { - parentNode.TreeItems.Add(new TreeItemData { Value = new DecoTreeItemViewModel(deco) }); - } - } - - return parentNode.TreeItems; - } - private async Task DeleteItem(DecoTreeItemViewModel treeItem) { foreach (int decoGroupId in Optional(treeItem.DecoGroupId)) @@ -180,7 +170,11 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteDecoGroup(decoGroupId), _cts.Token); - TreeItems.RemoveAll(i => i.Value?.DecoGroupId == decoGroupId); + _treeItems.RemoveAll(i => i.Value?.DecoGroupId == decoGroupId); + if (_selectedDecoGroup?.Id == decoGroupId) + { + _selectedDecoGroup = null; + } _decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token); await InvokeAsync(StateHasChanged); @@ -197,9 +191,9 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteDeco(decoId), _cts.Token); - foreach (DecoTreeItemViewModel parent in TreeItems.Map(i => i.Value)) + foreach (DecoTreeItemViewModel parent in _treeItems.Map(i => i.Value)) { - parent.TreeItems.RemoveAll(ti => ti.Value == treeItem); + parent.TreeItems.RemoveAll(i => i.Value == treeItem); } await InvokeAsync(StateHasChanged); diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index 30e09ca92..de4601321 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -59,7 +59,7 @@ Crop - Video + Video
@@ -220,7 +220,7 @@
- Audio + Audio
diff --git a/ErsatzTV/Pages/Logs.razor b/ErsatzTV/Pages/Logs.razor index 3e04a394d..45a882afb 100644 --- a/ErsatzTV/Pages/Logs.razor +++ b/ErsatzTV/Pages/Logs.razor @@ -1,55 +1,65 @@ @page "/system/logs" +@using System.Globalization @using ErsatzTV.Application.Configuration @using ErsatzTV.Application.Logs @implements IDisposable @inject IMediator Mediator - - - - Logs - - - - - - - - Timestamp - - - - - Level - - - Message - - - @context.Timestamp - @context.Level - @context.Message - - - No matching records found - - - Loading... - - - - - - + +
+ + Logs + + +
+ Search +
+ +
+ + + + + Timestamp + + + + + Level + + + Message + + + @context.Timestamp.ToString("G", _dtf) + @context.Level + @context.Message + + + No matching records found + + + Loading... + + + + + +
+
+
@code { private readonly CancellationTokenSource _cts = new(); + private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat; + private MudTable _table; private int _rowsPerPage = 10; private string _searchString; diff --git a/ErsatzTV/Pages/Playlists.razor b/ErsatzTV/Pages/Playlists.razor index 85c65ec16..3cedd0345 100644 --- a/ErsatzTV/Pages/Playlists.razor +++ b/ErsatzTV/Pages/Playlists.razor @@ -1,55 +1,59 @@ @page "/media/playlists" @using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.Tree @implements IDisposable @inject ILogger Logger @inject ISnackbar Snackbar @inject IMediator Mediator @inject IDialogService Dialog - - Playlists - - -
- - - - - - - Add Playlist Group - - - -
-
- -
- - -
- - @foreach (PlaylistGroupViewModel playlistGroup in _playlistGroups) - { - @playlistGroup.Name - } - - -
-
- - - Add Playlist - - -
-
-
- - - + +
+ + Playlist Groups + + +
+ Playlist Group Name +
+ +
+ +
+ + Add Playlist Group + +
+ Playlists + + +
+ Playlist Group +
+ + @foreach (PlaylistGroupViewModel playlistGroup in _playlistGroups) + { + @playlistGroup.Name + } + +
+ +
+ Playlist Name +
+ +
+ +
+ + Add Playlist + +
+ +
+ - +
@@ -75,14 +79,14 @@ - - - - + + +
+ @code { private readonly CancellationTokenSource _cts = new(); - private List> TreeItems { get; set; } = []; + private readonly List> _treeItems = []; private List _playlistGroups = []; private PlaylistGroupViewModel _selectedPlaylistGroup; private string _playlistGroupName; @@ -96,14 +100,20 @@ protected override async Task OnParametersSetAsync() { - await ReloadPlaylistGroups(); + await ReloadPlaylistTree(); await InvokeAsync(StateHasChanged); } - private async Task ReloadPlaylistGroups() + private async Task ReloadPlaylistTree() { _playlistGroups = await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token); - TreeItems = _playlistGroups.Map(g => new TreeItemData { Value = new PlaylistTreeItemViewModel(g) }).ToList(); + + _treeItems.Clear(); + TreeViewModel tree = await Mediator.Send(new GetPlaylistTree(), _cts.Token); + foreach (TreeGroupViewModel group in tree.Groups) + { + _treeItems.Add(new TreeItemData { Value = new PlaylistTreeItemViewModel(group) }); + } } private async Task AddPlaylistGroup() @@ -120,7 +130,7 @@ foreach (PlaylistGroupViewModel playlistGroup in result.RightToSeq()) { - TreeItems.Add(new TreeItemData { Value = new PlaylistTreeItemViewModel(playlistGroup) }); + _treeItems.Add(new TreeItemData { Value = new PlaylistTreeItemViewModel(playlistGroup) }); _playlistGroupName = null; _playlistGroups = await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token); @@ -143,7 +153,7 @@ foreach (PlaylistViewModel playlist in result.RightToSeq()) { - foreach (PlaylistTreeItemViewModel item in TreeItems.Map(i => i.Value).Where(item => item.PlaylistGroupId == _selectedPlaylistGroup.Id)) + foreach (PlaylistTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.PlaylistGroupId == _selectedPlaylistGroup.Id)) { item.TreeItems.Add(new TreeItemData { Value = new PlaylistTreeItemViewModel(playlist) }); } @@ -154,20 +164,6 @@ } } - private async Task>> LoadServerData(PlaylistTreeItemViewModel parentNode) - { - foreach (int playlistGroupId in Optional(parentNode.PlaylistGroupId)) - { - List result = await Mediator.Send(new GetPlaylistsByPlaylistGroupId(playlistGroupId), _cts.Token); - foreach (PlaylistViewModel playlist in result) - { - parentNode.TreeItems.Add(new TreeItemData { Value = new PlaylistTreeItemViewModel(playlist) }); - } - } - - return parentNode.TreeItems; - } - private async Task DeleteItem(PlaylistTreeItemViewModel treeItem) { foreach (int playlistGroupId in Optional(treeItem.PlaylistGroupId)) @@ -180,7 +176,11 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeletePlaylistGroup(playlistGroupId), _cts.Token); - TreeItems.RemoveAll(i => i.Value?.PlaylistGroupId == playlistGroupId); + _treeItems.RemoveAll(i => i.Value?.PlaylistGroupId == playlistGroupId); + if (_selectedPlaylistGroup?.Id == playlistGroupId) + { + _selectedPlaylistGroup = null; + } _playlistGroups = await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token); await InvokeAsync(StateHasChanged); @@ -197,7 +197,7 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeletePlaylist(playlistId), _cts.Token); - foreach (PlaylistTreeItemViewModel parent in TreeItems.Map(i => i.Value)) + foreach (PlaylistTreeItemViewModel parent in _treeItems.Map(i => i.Value)) { parent.TreeItems.RemoveAll(i => i.Value == treeItem); } diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 958d77413..3060f0ee9 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -202,7 +202,7 @@ @if (_selectedPlayoutId != null) { - Playout Detail + Playout Detail Logger @inject ISnackbar Snackbar @inject IMediator Mediator @inject IDialogService Dialog - - Templates - - -
- - - - - - - Add Template Group - - - -
-
- -
- - -
- - @foreach (TemplateGroupViewModel templateGroup in _templateGroups) - { - @templateGroup.Name - } - - -
-
- - - Add Template - - -
-
-
- - - + +
+ + Template Groups + + +
+ Template Group Name +
+ +
+ +
+ + Add Template Group + +
+ Templates + + +
+ Template Group +
+ + @foreach (TemplateGroupViewModel templateGroup in _templateGroups) + { + @templateGroup.Name + } + +
+ +
+ Template Name +
+ +
+ +
+ + Add Template + +
+ +
+ - +
- + @item.Value.Text @@ -69,14 +73,14 @@ - - - - + + +
+ @code { private readonly CancellationTokenSource _cts = new(); - private List> TreeItems { get; set; } = []; + private readonly List> _treeItems = []; private List _templateGroups = []; private TemplateGroupViewModel _selectedTemplateGroup; private string _templateGroupName; @@ -90,14 +94,20 @@ protected override async Task OnParametersSetAsync() { - await ReloadTemplateGroups(); + await ReloadTemplateTree(); await InvokeAsync(StateHasChanged); } - private async Task ReloadTemplateGroups() + private async Task ReloadTemplateTree() { _templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token); - TreeItems = _templateGroups.Map(g => new TreeItemData { Value = new TemplateTreeItemViewModel(g) }).ToList(); + + _treeItems.Clear(); + TreeViewModel tree = await Mediator.Send(new GetTemplateTree(), _cts.Token); + foreach (TreeGroupViewModel group in tree.Groups) + { + _treeItems.Add(new TreeItemData { Value = new TemplateTreeItemViewModel(group) }); + } } private async Task AddTemplateGroup() @@ -114,7 +124,7 @@ foreach (TemplateGroupViewModel templateGroup in result.RightToSeq()) { - TreeItems.Add(new TreeItemData { Value = new TemplateTreeItemViewModel(templateGroup) }); + _treeItems.Add(new TreeItemData { Value = new TemplateTreeItemViewModel(templateGroup) }); _templateGroupName = null; _templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token); @@ -137,7 +147,7 @@ foreach (TemplateViewModel template in result.RightToSeq()) { - foreach (TemplateTreeItemViewModel item in TreeItems.Map(i => i.Value).Where(item => item.TemplateGroupId == _selectedTemplateGroup.Id)) + foreach (TemplateTreeItemViewModel item in _treeItems.Map(i => i.Value).Where(item => item.TemplateGroupId == _selectedTemplateGroup.Id)) { item.TreeItems.Add(new TreeItemData { Value = new TemplateTreeItemViewModel(template) }); } @@ -148,20 +158,6 @@ } } - private async Task>> LoadServerData(TemplateTreeItemViewModel parentNode) - { - foreach (int templateGroupId in Optional(parentNode.TemplateGroupId)) - { - List result = await Mediator.Send(new GetTemplatesByTemplateGroupId(templateGroupId), _cts.Token); - foreach (TemplateViewModel template in result) - { - parentNode.TreeItems.Add(new TreeItemData { Value = new TemplateTreeItemViewModel(template) }); - } - } - - return parentNode.TreeItems; - } - private async Task DeleteItem(TemplateTreeItemViewModel treeItem) { foreach (int templateGroupId in Optional(treeItem.TemplateGroupId)) @@ -174,7 +170,11 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteTemplateGroup(templateGroupId), _cts.Token); - TreeItems.RemoveAll(i => i.Value?.TemplateGroupId == templateGroupId); + _treeItems.RemoveAll(i => i.Value?.TemplateGroupId == templateGroupId); + if (_selectedTemplateGroup?.Id == templateGroupId) + { + _selectedTemplateGroup = null; + } _templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token); await InvokeAsync(StateHasChanged); @@ -191,7 +191,7 @@ if (result is not null && !result.Canceled) { await Mediator.Send(new DeleteTemplate(templateId), _cts.Token); - foreach (TemplateTreeItemViewModel parent in TreeItems.Map(i => i.Value)) + foreach (TemplateTreeItemViewModel parent in _treeItems.Map(i => i.Value)) { parent.TreeItems.RemoveAll(i => i.Value == treeItem); } diff --git a/ErsatzTV/Pages/YamlPlayoutEditor.razor b/ErsatzTV/Pages/YamlPlayoutEditor.razor index 8aede4e6a..4b2e05baa 100644 --- a/ErsatzTV/Pages/YamlPlayoutEditor.razor +++ b/ErsatzTV/Pages/YamlPlayoutEditor.razor @@ -20,7 +20,7 @@ Edit YAML File - Playout Items and History + Playout Items and History Erase Items and History diff --git a/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor b/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor index 73faf632f..be88eba8a 100644 --- a/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor +++ b/ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor @@ -51,7 +51,7 @@ Add Path Replacement - Path Replacement + Path Replacement @if (_selectedItem is not null) { diff --git a/ErsatzTV/ViewModels/BlockTreeItemViewModel.cs b/ErsatzTV/ViewModels/BlockTreeItemViewModel.cs index 1ad5f9adf..b32b48fb9 100644 --- a/ErsatzTV/ViewModels/BlockTreeItemViewModel.cs +++ b/ErsatzTV/ViewModels/BlockTreeItemViewModel.cs @@ -10,7 +10,16 @@ public class BlockTreeItemViewModel Text = blockGroup.Name; EndText = string.Empty; TreeItems = []; - CanExpand = blockGroup.BlockCount > 0; + BlockGroupId = blockGroup.Id; + Icon = Icons.Material.Filled.Folder; + } + + public BlockTreeItemViewModel(BlockTreeBlockGroupViewModel blockGroup) + { + Text = blockGroup.Name; + EndText = string.Empty; + TreeItems = blockGroup.Blocks.Map(b => new TreeItemData + { Value = new BlockTreeItemViewModel(b) }).ToList(); BlockGroupId = blockGroup.Id; Icon = Icons.Material.Filled.Folder; } @@ -37,6 +46,28 @@ public class BlockTreeItemViewModel BlockId = block.Id; } + public BlockTreeItemViewModel(BlockTreeBlockViewModel block) + { + Text = block.Name; + if (block.Minutes / 60 >= 1) + { + string plural = block.Minutes / 60 >= 2 ? "s" : string.Empty; + EndText = $"{block.Minutes / 60} hour{plural}"; + if (block.Minutes % 60 != 0) + { + EndText += $", {block.Minutes % 60} minutes"; + } + } + else + { + EndText = $"{block.Minutes} minutes"; + } + + TreeItems = []; + CanExpand = false; + BlockId = block.Id; + } + public string Text { get; } public string EndText { get; } diff --git a/ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs b/ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs index 57f08abba..862da8a89 100644 --- a/ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs +++ b/ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs @@ -1,4 +1,5 @@ using ErsatzTV.Application.Scheduling; +using ErsatzTV.Application.Tree; using MudBlazor; namespace ErsatzTV.ViewModels; @@ -9,7 +10,15 @@ public class DecoTemplateTreeItemViewModel { Text = decoTemplateGroup.Name; TreeItems = []; - CanExpand = decoTemplateGroup.DecoTemplateCount > 0; + DecoTemplateGroupId = decoTemplateGroup.Id; + Icon = Icons.Material.Filled.Folder; + } + + public DecoTemplateTreeItemViewModel(TreeGroupViewModel decoTemplateGroup) + { + Text = decoTemplateGroup.Name; + TreeItems = decoTemplateGroup.Children.Map(d => new TreeItemData + { Value = new DecoTemplateTreeItemViewModel(d) }).ToList(); DecoTemplateGroupId = decoTemplateGroup.Id; Icon = Icons.Material.Filled.Folder; } @@ -22,6 +31,14 @@ public class DecoTemplateTreeItemViewModel DecoTemplateId = decoTemplate.Id; } + public DecoTemplateTreeItemViewModel(TreeItemViewModel decoTemplate) + { + Text = decoTemplate.Name; + TreeItems = []; + CanExpand = false; + DecoTemplateId = decoTemplate.Id; + } + public string Text { get; } public string Icon { get; } diff --git a/ErsatzTV/ViewModels/DecoTreeItemViewModel.cs b/ErsatzTV/ViewModels/DecoTreeItemViewModel.cs index 4f351f8e7..e7c53e122 100644 --- a/ErsatzTV/ViewModels/DecoTreeItemViewModel.cs +++ b/ErsatzTV/ViewModels/DecoTreeItemViewModel.cs @@ -1,4 +1,5 @@ using ErsatzTV.Application.Scheduling; +using ErsatzTV.Application.Tree; using MudBlazor; namespace ErsatzTV.ViewModels; @@ -10,7 +11,15 @@ public class DecoTreeItemViewModel Text = decoGroup.Name; EndText = string.Empty; TreeItems = []; - CanExpand = decoGroup.DecoCount > 0; + DecoGroupId = decoGroup.Id; + Icon = Icons.Material.Filled.Folder; + } + + public DecoTreeItemViewModel(TreeGroupViewModel decoGroup) + { + Text = decoGroup.Name; + TreeItems = decoGroup.Children.Map(d => new TreeItemData + { Value = new DecoTreeItemViewModel(d) }).ToList(); DecoGroupId = decoGroup.Id; Icon = Icons.Material.Filled.Folder; } @@ -23,6 +32,14 @@ public class DecoTreeItemViewModel DecoId = deco.Id; } + public DecoTreeItemViewModel(TreeItemViewModel deco) + { + Text = deco.Name; + TreeItems = []; + CanExpand = false; + DecoId = deco.Id; + } + public string Text { get; } public string EndText { get; } diff --git a/ErsatzTV/ViewModels/PlaylistTreeItemViewModel.cs b/ErsatzTV/ViewModels/PlaylistTreeItemViewModel.cs index e134e2281..4dcd5a443 100644 --- a/ErsatzTV/ViewModels/PlaylistTreeItemViewModel.cs +++ b/ErsatzTV/ViewModels/PlaylistTreeItemViewModel.cs @@ -1,4 +1,5 @@ using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Application.Tree; using MudBlazor; namespace ErsatzTV.ViewModels; @@ -10,7 +11,16 @@ public class PlaylistTreeItemViewModel Text = playlistGroup.Name; EndText = string.Empty; TreeItems = []; - CanExpand = playlistGroup.PlaylistCount > 0; + PlaylistGroupId = playlistGroup.Id; + Icon = Icons.Material.Filled.Folder; + } + + public PlaylistTreeItemViewModel(TreeGroupViewModel playlistGroup) + { + Text = playlistGroup.Name; + EndText = string.Empty; + TreeItems = playlistGroup.Children.Map(p => new TreeItemData + { Value = new PlaylistTreeItemViewModel(p) }).ToList(); PlaylistGroupId = playlistGroup.Id; Icon = Icons.Material.Filled.Folder; } @@ -23,6 +33,14 @@ public class PlaylistTreeItemViewModel PlaylistId = playlist.Id; } + public PlaylistTreeItemViewModel(TreeItemViewModel playlist) + { + Text = playlist.Name; + TreeItems = []; + CanExpand = false; + PlaylistId = playlist.Id; + } + public string Text { get; } public string EndText { get; } diff --git a/ErsatzTV/ViewModels/TemplateTreeItemViewModel.cs b/ErsatzTV/ViewModels/TemplateTreeItemViewModel.cs index 2c0b4faec..817776784 100644 --- a/ErsatzTV/ViewModels/TemplateTreeItemViewModel.cs +++ b/ErsatzTV/ViewModels/TemplateTreeItemViewModel.cs @@ -1,4 +1,5 @@ using ErsatzTV.Application.Scheduling; +using ErsatzTV.Application.Tree; using MudBlazor; namespace ErsatzTV.ViewModels; @@ -14,6 +15,15 @@ public class TemplateTreeItemViewModel Icon = Icons.Material.Filled.Folder; } + public TemplateTreeItemViewModel(TreeGroupViewModel templateGroup) + { + Text = templateGroup.Name; + TreeItems = templateGroup.Children.Map(t => new TreeItemData + { Value = new TemplateTreeItemViewModel(t) }).ToList(); + TemplateGroupId = templateGroup.Id; + Icon = Icons.Material.Filled.Folder; + } + public TemplateTreeItemViewModel(TemplateViewModel template) { Text = template.Name; @@ -22,6 +32,14 @@ public class TemplateTreeItemViewModel TemplateId = template.Id; } + public TemplateTreeItemViewModel(TreeItemViewModel template) + { + Text = template.Name; + TreeItems = []; + CanExpand = false; + TemplateId = template.Id; + } + public string Text { get; } public string Icon { get; } diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index a50ae5214..d018ef1b0 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -145,6 +145,10 @@ font-weight: bold; } +.form-field-stack ul.mud-treeview .mud-typography { + font-weight: unset; +} + .form-field-stack .mud-typography, .mud-form .mud-typography-h5 { color: rgba(255, 255, 255, 0.90); @@ -155,7 +159,8 @@ justify-content: flex-start; } -.form-field-stack div.mud-input-control { +.form-field-stack div.mud-input-control, +.form-field-stack ul.mud-treeview { max-width: 500px; }