Browse Source

update layout for group editors (#2140)

* update block group editor

* update playlist group editor

* update template group editor

* update deco group editor

* update deco template group editor

* update deco editor

* update logs layout

* update changelog
pull/2142/head
Jason Dove 6 months ago committed by GitHub
parent
commit
2a9f23cce6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/MediaCollections/Mapper.cs
  3. 5
      ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTree.cs
  4. 24
      ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTreeHandler.cs
  5. 2
      ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs
  6. 3
      ErsatzTV.Application/Scheduling/BlockTreeBlockGroupViewModel.cs
  7. 3
      ErsatzTV.Application/Scheduling/BlockTreeBlockViewModel.cs
  8. 3
      ErsatzTV.Application/Scheduling/BlockTreeViewModel.cs
  9. 33
      ErsatzTV.Application/Scheduling/Mapper.cs
  10. 1
      ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs
  11. 3
      ErsatzTV.Application/Scheduling/Queries/GetBlockTree.cs
  12. 21
      ErsatzTV.Application/Scheduling/Queries/GetBlockTreeHandler.cs
  13. 5
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTree.cs
  14. 25
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs
  15. 5
      ErsatzTV.Application/Scheduling/Queries/GetDecoTree.cs
  16. 25
      ErsatzTV.Application/Scheduling/Queries/GetDecoTreeHandler.cs
  17. 5
      ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs
  18. 25
      ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs
  19. 3
      ErsatzTV.Application/Tree/TemplateTreeViewModel.cs
  20. 3
      ErsatzTV.Application/Tree/TreeGroupViewModel.cs
  21. 3
      ErsatzTV.Application/Tree/TreeItemViewModel.cs
  22. 137
      ErsatzTV/Pages/Blocks.razor
  23. 4
      ErsatzTV/Pages/Collections.razor
  24. 533
      ErsatzTV/Pages/DecoEditor.razor
  25. 152
      ErsatzTV/Pages/DecoTemplates.razor
  26. 146
      ErsatzTV/Pages/Decos.razor
  27. 4
      ErsatzTV/Pages/FFmpegEditor.razor
  28. 94
      ErsatzTV/Pages/Logs.razor
  29. 138
      ErsatzTV/Pages/Playlists.razor
  30. 2
      ErsatzTV/Pages/Playouts.razor
  31. 140
      ErsatzTV/Pages/Templates.razor
  32. 2
      ErsatzTV/Pages/YamlPlayoutEditor.razor
  33. 2
      ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor
  34. 33
      ErsatzTV/ViewModels/BlockTreeItemViewModel.cs
  35. 19
      ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs
  36. 19
      ErsatzTV/ViewModels/DecoTreeItemViewModel.cs
  37. 20
      ErsatzTV/ViewModels/PlaylistTreeItemViewModel.cs
  38. 18
      ErsatzTV/ViewModels/TemplateTreeItemViewModel.cs
  39. 7
      ErsatzTV/wwwroot/css/site.css

8
CHANGELOG.md

@ -83,13 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

10
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -1,4 +1,5 @@ @@ -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 @@ -46,6 +47,13 @@ internal static class Mapper
multiCollectionSmartItem.ScheduleAsGroup,
multiCollectionSmartItem.PlaybackOrder);
internal static TreeViewModel ProjectToViewModel(List<PlaylistGroup> 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);

5
ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTree.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Application.Tree;
namespace ErsatzTV.Application.MediaCollections;
public record GetPlaylistTree : IRequest<TreeViewModel>;

24
ErsatzTV.Application/MediaCollections/Queries/GetPlaylistTreeHandler.cs

@ -0,0 +1,24 @@ @@ -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<TvContext> dbContextFactory)
: IRequestHandler<GetPlaylistTree, TreeViewModel>
{
public async Task<TreeViewModel> Handle(
GetPlaylistTree request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<PlaylistGroup> playlistGroups = await dbContext.PlaylistGroups
.AsNoTracking()
.Include(g => g.Playlists)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(playlistGroups);
}
}

2
ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record BlockGroupViewModel(int Id, string Name, int BlockCount);
public record BlockGroupViewModel(int Id, string Name);

3
ErsatzTV.Application/Scheduling/BlockTreeBlockGroupViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record BlockTreeBlockGroupViewModel(int Id, string Name, List<BlockTreeBlockViewModel> Blocks);

3
ErsatzTV.Application/Scheduling/BlockTreeBlockViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record BlockTreeBlockViewModel(int Id, string Name, int Minutes);

3
ErsatzTV.Application/Scheduling/BlockTreeViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record BlockTreeViewModel(List<BlockTreeBlockGroupViewModel> Groups);

33
ErsatzTV.Application/Scheduling/Mapper.cs

@ -1,12 +1,41 @@ @@ -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<DecoTemplateGroup> 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<DecoGroup> 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<TemplateGroup> 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<BlockGroup> 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);

1
ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs

@ -13,7 +13,6 @@ public class GetAllBlockGroupsHandler(IDbContextFactory<TvContext> dbContextFact @@ -13,7 +13,6 @@ public class GetAllBlockGroupsHandler(IDbContextFactory<TvContext> dbContextFact
List<BlockGroup> blockGroups = await dbContext.BlockGroups
.AsNoTracking()
.Include(g => g.Blocks)
.ToListAsync(cancellationToken);
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();

3
ErsatzTV.Application/Scheduling/Queries/GetBlockTree.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetBlockTree : IRequest<BlockTreeViewModel>;

21
ErsatzTV.Application/Scheduling/Queries/GetBlockTreeHandler.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetBlockTreeHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetBlockTree, BlockTreeViewModel>
{
public async Task<BlockTreeViewModel> Handle(GetBlockTree request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<BlockGroup> blockGroups = await dbContext.BlockGroups
.AsNoTracking()
.Include(g => g.Blocks)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(blockGroups);
}
}

5
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTree.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Application.Tree;
namespace ErsatzTV.Application.Scheduling;
public record GetDecoTemplateTree : IRequest<TreeViewModel>;

25
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs

@ -0,0 +1,25 @@ @@ -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<TvContext> dbContextFactory)
: IRequestHandler<GetDecoTemplateTree, TreeViewModel>
{
public async Task<TreeViewModel> Handle(
GetDecoTemplateTree request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<DecoTemplateGroup> templateGroups = await dbContext.DecoTemplateGroups
.AsNoTracking()
.Include(g => g.DecoTemplates)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(templateGroups);
}
}

5
ErsatzTV.Application/Scheduling/Queries/GetDecoTree.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Application.Tree;
namespace ErsatzTV.Application.Scheduling;
public record GetDecoTree : IRequest<TreeViewModel>;

25
ErsatzTV.Application/Scheduling/Queries/GetDecoTreeHandler.cs

@ -0,0 +1,25 @@ @@ -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<TvContext> dbContextFactory)
: IRequestHandler<GetDecoTree, TreeViewModel>
{
public async Task<TreeViewModel> Handle(
GetDecoTree request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<DecoGroup> templateGroups = await dbContext.DecoGroups
.AsNoTracking()
.Include(g => g.Decos)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(templateGroups);
}
}

5
ErsatzTV.Application/Scheduling/Queries/GetTemplateTree.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Application.Tree;
namespace ErsatzTV.Application.Scheduling;
public record GetTemplateTree : IRequest<TreeViewModel>;

25
ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs

@ -0,0 +1,25 @@ @@ -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<TvContext> dbContextFactory)
: IRequestHandler<GetTemplateTree, TreeViewModel>
{
public async Task<TreeViewModel> Handle(
GetTemplateTree request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<TemplateGroup> templateGroups = await dbContext.TemplateGroups
.AsNoTracking()
.Include(g => g.Templates)
.ToListAsync(cancellationToken);
return Mapper.ProjectToViewModel(templateGroups);
}
}

3
ErsatzTV.Application/Tree/TemplateTreeViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Tree;
public record TreeViewModel(List<TreeGroupViewModel> Groups);

3
ErsatzTV.Application/Tree/TreeGroupViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Tree;
public record TreeGroupViewModel(int Id, string Name, List<TreeItemViewModel> Children);

3
ErsatzTV.Application/Tree/TreeItemViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Tree;
public record TreeItemViewModel(int Id, string Name);

137
ErsatzTV/Pages/Blocks.razor

@ -6,50 +6,53 @@ @@ -6,50 +6,53 @@
@inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Blocks</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3 mx-3" Label="Block Group Name" @bind-Value="_blockGroupName" For="@(() => _blockGroupName)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlockGroup())" Class="ml-4 mb-4">
Add Block Group
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardContent>
<div class="mx-4">
<MudSelect Label="Block Group" @bind-Value="_selectedBlockGroup" Class="mt-3">
@foreach (BlockGroupViewModel blockGroup in _blockGroups)
{
<MudSelectItem Value="@blockGroup">@blockGroup.Name</MudSelectItem>
}
</MudSelect>
<MudTextField Class="mt-3" Label="Block Name" @bind-Value="_blockName" For="@(() => _blockName)"/>
</div>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlock())" Class="ml-4 mb-4">
Add Block
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCard>
<MudTreeView T="BlockTreeItemViewModel" ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Block Groups</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Block Group Name</MudText>
</div>
<MudTextField @bind-Value="_blockGroupName" For="@(() => _blockGroupName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlockGroup())" StartIcon="@Icons.Material.Filled.Add">
Add Block Group
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Blocks</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Block Group</MudText>
</div>
<MudSelect @bind-Value="_selectedBlockGroup">
@foreach (BlockGroupViewModel blockGroup in _blockGroups)
{
<MudSelectItem Value="@blockGroup">@blockGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Block Name</MudText>
</div>
<MudTextField @bind-Value="_blockName" For="@(() => _blockName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlock())" StartIcon="@Icons.Material.Filled.Add">
Add Block
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudTreeView T="BlockTreeItemViewModel" Items="@_treeItems" Hover="true" Style="width: 100%">
<ItemTemplate Context="item">
<MudTreeViewItem T="BlockTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" CanExpand="@item.Value.CanExpand" Value="@item.Value">
<MudTreeViewItem T="BlockTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" Value="@item.Value">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
@ -75,14 +78,14 @@ @@ -75,14 +78,14 @@
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private List<TreeItemData<BlockTreeItemViewModel>> TreeItems { get; set; } = [];
private readonly List<TreeItemData<BlockTreeItemViewModel>> _treeItems = [];
private List<BlockGroupViewModel> _blockGroups = [];
private BlockGroupViewModel _selectedBlockGroup;
private string _blockGroupName;
@ -96,14 +99,20 @@ @@ -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<BlockTreeItemViewModel> { 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<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(group) });
}
}
private async Task AddBlockGroup()
@ -120,7 +129,7 @@ @@ -120,7 +129,7 @@
foreach (BlockGroupViewModel blockGroup in result.RightToSeq())
{
TreeItems.Add(new TreeItemData<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(blockGroup) });
_treeItems.Add(new TreeItemData<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(blockGroup) });
_blockGroupName = null;
_blockGroups = await Mediator.Send(new GetAllBlockGroups(), _cts.Token);
@ -143,7 +152,7 @@ @@ -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<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(block) });
}
@ -154,20 +163,6 @@ @@ -154,20 +163,6 @@
}
}
private async Task<IReadOnlyCollection<TreeItemData<BlockTreeItemViewModel>>> LoadServerData(BlockTreeItemViewModel parentNode)
{
foreach (int blockGroupId in Optional(parentNode.BlockGroupId))
{
List<BlockViewModel> result = await Mediator.Send(new GetBlocksByBlockGroupId(blockGroupId), _cts.Token);
foreach (BlockViewModel block in result)
{
parentNode.TreeItems.Add(new TreeItemData<BlockTreeItemViewModel> { Value = new BlockTreeItemViewModel(block) });
}
}
return parentNode.TreeItems;
}
private async Task DeleteItem(BlockTreeItemViewModel treeItem)
{
foreach (int blockGroupId in Optional(treeItem.BlockGroupId))
@ -180,7 +175,11 @@ @@ -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 @@ @@ -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);
}

4
ErsatzTV/Pages/Collections.razor

@ -66,7 +66,7 @@ @@ -66,7 +66,7 @@
<MudTablePager/>
</PagerContent>
</MudTable>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Multi Collections</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Multi Collections</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
@bind-RowsPerPage="@_multiCollectionsRowsPerPage"
@ -104,7 +104,7 @@ @@ -104,7 +104,7 @@
<MudTablePager/>
</PagerContent>
</MudTable>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Smart Collections</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Smart Collections</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
@bind-RowsPerPage="@_smartCollectionsRowsPerPage"

533
ErsatzTV/Pages/DecoEditor.razor

@ -12,284 +12,319 @@ @@ -12,284 +12,319 @@
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="display: flex; flex-direction: row">
<MudStack Class="mr-6">
<MudText Typo="Typo.h4" Class="mb-4">Edit Deco</MudText>
<MudCard Class="mb-6" Style="width: 350px">
<MudCardContent>
<MudTextField Label="Name" @bind-Value="_deco.Name" For="@(() => _deco.Name)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save">
Save Changes
</MudButton>
</MudCardActions>
</MudCard>
<MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Watermark</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSelect Label="Watermark Mode" @bind-Value="_deco.WatermarkMode" For="@(() => _deco.WatermarkMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
<MudSelect Disabled="@(_deco.WatermarkMode != DecoMode.Override)" Label="Watermark Override" @bind-Value="_deco.WatermarkId" For="@(() => _deco.WatermarkId)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
<MudSwitch T="bool"
Class="mt-3"
Disabled="@(_deco.WatermarkMode != DecoMode.Override)"
@bind-Value="_deco.UseWatermarkDuringFiller"
Color="Color.Primary"
Label="Use Watermark During Filler"/>
</MudCardContent>
</MudCard>
<MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudTooltip Style="max-width: 350px">
<ChildContent>
<MudText Typo="Typo.h6" Class="d-flex align-center justify-center">
Default Filler
&nbsp;
<MudIcon Icon="@Icons.Material.Filled.Info"/>
</MudText>
</ChildContent>
<TooltipContent>
<MudText Typo="Typo.body2">After all blocks have been scheduled, a second pass will be made to fill unscheduled time using random items from this collection.</MudText>
</TooltipContent>
</MudTooltip>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSelect Label="Default Filler Mode" @bind-Value="_deco.DefaultFillerMode" For="@(() => _deco.DefaultFillerMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
<MudSelect Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Default Filler Collection Type"
@bind-Value="_deco.DefaultFillerCollectionType"
For="@(() => _deco.DefaultFillerCollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudSelect Class="mt-3"
T="MediaCollectionViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Collection"
@bind-value="_deco.DefaultFillerCollection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="ml-8" StartIcon="@Icons.Material.Filled.Save">
Save Changes
</MudButton>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Deco</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
</div>
<MudTextField @bind-Value="_deco.Name" For="@(() => _deco.Name)"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Watermark</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Watermark Mode</MudText>
</div>
<MudSelect @bind-Value="_deco.WatermarkMode" For="@(() => _deco.WatermarkMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Watermark Override</MudText>
</div>
<MudSelect Disabled="@(_deco.WatermarkMode != DecoMode.Override)" @bind-Value="_deco.WatermarkId" For="@(() => _deco.WatermarkId)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Use Watermark During Filler</MudText>
</div>
<MudCheckBox T="bool"
Disabled="@(_deco.WatermarkMode != DecoMode.Override)"
@bind-Value="_deco.UseWatermarkDuringFiller"
Color="Color.Primary"
Dense="true" />
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Default Filler</MudText>
<MudDivider Class="mb-6"/>
<MudText Typo="Typo.body2" Class="mb-6">After all blocks have been scheduled, a second pass will be made to fill unscheduled time using random items from this collection.</MudText>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Default Filler Mode</MudText>
</div>
<MudSelect @bind-Value="_deco.DefaultFillerMode" For="@(() => _deco.DefaultFillerMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Default Filler Collection Type</MudText>
</div>
<MudSelect Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-Value="_deco.DefaultFillerCollectionType"
For="@(() => _deco.DefaultFillerCollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
</MudStack>
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Collection</MudText>
</div>
<MudSelect T="MediaCollectionViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-value="_deco.DefaultFillerCollection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudSelect Class="mt-3"
T="MultiCollectionViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Multi Collection"
@bind-value="_deco.DefaultFillerMultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Multi Collection</MudText>
</div>
<MudSelect T="MultiCollectionViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-value="_deco.DefaultFillerMultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudSelect Class="mt-3"
T="SmartCollectionViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Smart Collection"
@bind-value="_deco.DefaultFillerSmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Smart Collection</MudText>
</div>
<MudSelect T="SmartCollectionViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-value="_deco.DefaultFillerSmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Television Show"
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Television Show</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Television Season"
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Television Season</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DefaultFillerCollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Artist"
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Artist</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-value="_deco.DefaultFillerMediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
<MudSwitch T="bool"
Class="mt-3"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
Label="Trim To Fit"
@bind-Value="_deco.DefaultFillerTrimToFit"
Color="Color.Primary"/>
</MudCardContent>
</MudCard>
<MudCard Class="mb-6" Style="width: 350px">
<MudCardHeader>
<CardHeaderContent>
<MudTooltip Style="max-width: 350px">
<ChildContent>
<MudText Typo="Typo.h6" Class="d-flex align-center justify-center">
Dead Air Fallback
&nbsp;
<MudIcon Icon="@Icons.Material.Filled.Info"/>
</MudText>
</ChildContent>
<TooltipContent>
<MudText Typo="Typo.body2">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.</MudText>
<MudText Typo="Typo.body2" Class="mt-3">This replaces the "Channel is Offline" image that would otherwise display.</MudText>
</TooltipContent>
</MudTooltip>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSelect Label="Dead Air Fallback Mode" @bind-Value="_deco.DeadAirFallbackMode" For="@(() => _deco.DeadAirFallbackMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
<MudSelect Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Dead Air Fallback Collection Type"
@bind-Value="_deco.DeadAirFallbackCollectionType"
For="@(() => _deco.DeadAirFallbackCollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Trim To Fit</MudText>
</div>
<MudCheckBox T="bool"
Disabled="@(_deco.DefaultFillerMode != DecoMode.Override)"
@bind-Value="_deco.DefaultFillerTrimToFit"
Color="Color.Primary"
Dense="true"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Dead Air Fallback</MudText>
<MudDivider Class="mb-6"/>
<MudText Typo="Typo.body2">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.</MudText>
<MudText Typo="Typo.body2" Class="mb-6 mt-3">This replaces the "Channel is Offline" image that would otherwise display.</MudText>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Dead Air Fallback Mode</MudText>
</div>
<MudSelect @bind-Value="_deco.DeadAirFallbackMode" For="@(() => _deco.DeadAirFallbackMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Override</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Dead Air Fallback Collection Type</MudText>
</div>
<MudSelect Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
@bind-Value="_deco.DeadAirFallbackCollectionType"
For="@(() => _deco.DeadAirFallbackCollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
</MudStack>
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudSelect Class="mt-3"
T="MediaCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Collection"
@bind-value="_deco.DeadAirFallbackCollection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Collection</MudText>
</div>
<MudSelect T="MediaCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
@bind-value="_deco.DeadAirFallbackCollection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudSelect Class="mt-3"
T="MultiCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Multi Collection"
@bind-value="_deco.DeadAirFallbackMultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Multi Collection</MudText>
</div>
<MudSelect T="MultiCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
@bind-value="_deco.DeadAirFallbackMultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudSelect Class="mt-3"
T="SmartCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Smart Collection"
@bind-value="_deco.DeadAirFallbackSmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Smart Collection</MudText>
</div>
<MudSelect T="SmartCollectionViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
@bind-value="_deco.DeadAirFallbackSmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Television Show"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Television Show</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Television Season"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Television Season</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
@if (_deco.DeadAirFallbackCollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
Label="Artist"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Artist</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel"
Disabled="@(_deco.DeadAirFallbackMode != DecoMode.Override)"
@bind-value="_deco.DeadAirFallbackMediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
</MudCardContent>
</MudCard>
</MudStack>
</MudContainer>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
@ -360,34 +395,34 @@ @@ -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
};
}

152
ErsatzTV/Pages/DecoTemplates.razor

@ -1,59 +1,63 @@ @@ -1,59 +1,63 @@
@page "/deco-templates"
@using ErsatzTV.Application.Scheduling
@using ErsatzTV.Application.Tree
@implements IDisposable
@inject ILogger<DecoTemplates> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Deco Templates</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3 mx-3" Label="Deco Template Group Name" @bind-Value="_decoTemplateGroupName" For="@(() => _decoTemplateGroupName)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplateGroup())" Class="ml-4 mb-4">
Add Deco Template Group
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardContent>
<div class="mx-4">
<MudSelect Label="Deco Template Group" @bind-Value="_selectedDecoTemplateGroup" Class="mt-3">
@foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups)
{
<MudSelectItem Value="@decoTemplateGroup">@decoTemplateGroup.Name</MudSelectItem>
}
</MudSelect>
<MudTextField Class="mt-3" Label="Deco Template Name" @bind-Value="_decoTemplateName" For="@(() => _decoTemplateName)"/>
</div>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplate())" Class="ml-4 mb-4">
Add Deco Template
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCard>
<MudTreeView T="DecoTemplateTreeItemViewModel" ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Deco Template Groups</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Template Group Name</MudText>
</div>
<MudTextField @bind-Value="_decoTemplateGroupName" For="@(() => _decoTemplateGroupName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplateGroup())" StartIcon="@Icons.Material.Filled.Add">
Add Deco Template Group
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Deco Templates</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Template Group</MudText>
</div>
<MudSelect @bind-Value="_selectedDecoTemplateGroup">
@foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups)
{
<MudSelectItem Value="@decoTemplateGroup">@decoTemplateGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Template Name</MudText>
</div>
<MudTextField @bind-Value="_decoTemplateName" For="@(() => _decoTemplateName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplate())" StartIcon="@Icons.Material.Filled.Add">
Add Deco Template
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudTreeView T="DecoTemplateTreeItemViewModel" Items="@_treeItems" Hover="true" Style="width: 100%">
<ItemTemplate Context="item">
<MudTreeViewItem T="DecoTemplateTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" CanExpand="@item.Value.CanExpand" Value="@item.Value">
<MudTreeViewItem T="DecoTemplateTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" Value="@item.Value">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
<MudItem xs="8">
<MudItem xs="5">
<MudText>@item.Value.Text</MudText>
</MudItem>
</MudGrid>
@ -69,14 +73,14 @@ @@ -69,14 +73,14 @@
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private List<TreeItemData<DecoTemplateTreeItemViewModel>> TreeItems { get; set; } = [];
private readonly List<TreeItemData<DecoTemplateTreeItemViewModel>> _treeItems = [];
private List<DecoTemplateGroupViewModel> _decoTemplateGroups = [];
private DecoTemplateGroupViewModel _selectedDecoTemplateGroup;
private string _decoTemplateGroupName;
@ -90,14 +94,20 @@ @@ -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<DecoTemplateTreeItemViewModel> { 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<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(group) });
}
}
private async Task AddDecoTemplateGroup()
@ -109,12 +119,12 @@ @@ -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<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(decoTemplateGroup) });
_treeItems.Add(new TreeItemData<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(decoTemplateGroup) });
_decoTemplateGroupName = null;
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
@ -132,12 +142,12 @@ @@ -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<DecoTemplateTreeItemViewModel> { Value = new DecoTemplateTreeItemViewModel(decoTemplate) });
}
@ -148,33 +158,23 @@ @@ -148,33 +158,23 @@
}
}
private async Task<IReadOnlyCollection<TreeItemData<DecoTemplateTreeItemViewModel>>> LoadServerData(DecoTemplateTreeItemViewModel parentNode)
{
foreach (int decoTemplateGroupId in Optional(parentNode.DecoTemplateGroupId))
{
List<DecoTemplateViewModel> result = await Mediator.Send(new GetDecoTemplatesByDecoTemplateGroupId(decoTemplateGroupId), _cts.Token);
foreach (DecoTemplateViewModel decoTemplate in result)
{
parentNode.TreeItems.Add(new TreeItemData<DecoTemplateTreeItemViewModel> { 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<DeleteDialog>("Delete Deco Template Group", parameters, options);
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("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 @@ @@ -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<DeleteDialog>("Delete Deco Template", parameters, options);
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("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);
}

146
ErsatzTV/Pages/Decos.razor

@ -1,67 +1,65 @@ @@ -1,67 +1,65 @@
@page "/decos"
@using ErsatzTV.Application.Scheduling
@using ErsatzTV.Application.Tree
@implements IDisposable
@inject ILogger<Decos> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Decos</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3 mx-3" Label="Deco Group Name" @bind-Value="_decoGroupName" For="@(() => _decoGroupName)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoGroup())" Class="ml-4 mb-4">
Add Deco Group
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardContent>
<div class="mx-4">
<MudSelect Label="Deco Group" @bind-Value="_selectedDecoGroup" Class="mt-3">
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
{
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
}
</MudSelect>
<MudTextField Class="mt-3" Label="Deco Name" @bind-Value="_decoName" For="@(() => _decoName)"/>
</div>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDeco())" Class="ml-4 mb-4">
Add Deco
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCard>
<MudTreeView T="DecoTreeItemViewModel" ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Deco Groups</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Group Name</MudText>
</div>
<MudTextField @bind-Value="_decoGroupName" For="@(() => _decoGroupName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoGroup())" StartIcon="@Icons.Material.Filled.Add">
Add Deco Group
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Decos</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Group</MudText>
</div>
<MudSelect @bind-Value="_selectedDecoGroup">
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
{
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Deco Name</MudText>
</div>
<MudTextField @bind-Value="_decoName" For="@(() => _decoName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDeco())" StartIcon="@Icons.Material.Filled.Add">
Add Deco
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudTreeView T="DecoTreeItemViewModel" Items="@_treeItems" Hover="true" Style="width: 100%">
<ItemTemplate Context="item">
<MudTreeViewItem T="DecoTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" CanExpand="@item.Value.CanExpand" Value="@item.Value">
<MudTreeViewItem T="DecoTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" Value="@item.Value">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
<MudItem xs="5">
<MudText>@item.Value.Text</MudText>
</MudItem>
@if (!string.IsNullOrWhiteSpace(item.Value.EndText))
{
<MudItem xs="6">
<MudText>@item.Value.EndText</MudText>
</MudItem>
}
</MudGrid>
<div style="justify-self: end;">
@foreach (int decoId in Optional(item.Value.DecoId))
@ -75,14 +73,14 @@ @@ -75,14 +73,14 @@
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private List<TreeItemData<DecoTreeItemViewModel>> TreeItems { get; set; } = [];
private readonly List<TreeItemData<DecoTreeItemViewModel>> _treeItems = [];
private List<DecoGroupViewModel> _decoGroups = [];
private DecoGroupViewModel _selectedDecoGroup;
private string _decoGroupName;
@ -96,14 +94,20 @@ @@ -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<DecoTreeItemViewModel> { 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<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(group) });
}
}
private async Task AddDecoGroup()
@ -120,7 +124,7 @@ @@ -120,7 +124,7 @@
foreach (DecoGroupViewModel decoGroup in result.RightToSeq())
{
TreeItems.Add(new TreeItemData<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(decoGroup) });
_treeItems.Add(new TreeItemData<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(decoGroup) });
_decoGroupName = null;
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
@ -143,7 +147,7 @@ @@ -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<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(deco) });
}
@ -154,20 +158,6 @@ @@ -154,20 +158,6 @@
}
}
private async Task<IReadOnlyCollection<TreeItemData<DecoTreeItemViewModel>>> LoadServerData(DecoTreeItemViewModel parentNode)
{
foreach (int decoGroupId in Optional(parentNode.DecoGroupId))
{
List<DecoViewModel> result = await Mediator.Send(new GetDecosByDecoGroupId(decoGroupId), _cts.Token);
foreach (DecoViewModel deco in result)
{
parentNode.TreeItems.Add(new TreeItemData<DecoTreeItemViewModel> { Value = new DecoTreeItemViewModel(deco) });
}
}
return parentNode.TreeItems;
}
private async Task DeleteItem(DecoTreeItemViewModel treeItem)
{
foreach (int decoGroupId in Optional(treeItem.DecoGroupId))
@ -180,7 +170,11 @@ @@ -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 @@ @@ -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);

4
ErsatzTV/Pages/FFmpegEditor.razor

@ -59,7 +59,7 @@ @@ -59,7 +59,7 @@
<MudSelectItem Value="@ScalingBehavior.Crop">Crop</MudSelectItem>
</MudSelect>
</MudStack>
<MudText Typo="Typo.h5" Class="mb-2">Video</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Video</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
@ -220,7 +220,7 @@ @@ -220,7 +220,7 @@
</div>
<MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mb-2">Audio</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Audio</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">

94
ErsatzTV/Pages/Logs.razor

@ -1,55 +1,65 @@ @@ -1,55 +1,65 @@
@page "/system/logs"
@using System.Globalization
@using ErsatzTV.Application.Configuration
@using ErsatzTV.Application.Logs
@implements IDisposable
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable FixedHeader="true"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<LogEntryViewModel>>>(ServerReload))"
Dense="true"
@ref="_table">
<ToolBarContent>
<MudText Typo="Typo.h6">Logs</MudText>
<MudSpacer/>
<MudTextField T="string" ValueChanged="@(s => OnSearch(s))" Placeholder="Search" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0">
</MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh>
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Timestamp">
Timestamp
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Level">
Level
</MudTableSortLabel>
</MudTh>
<MudTh>Message</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Timestamp">@context.Timestamp</MudTd>
<MudTd DataLabel="Level">@context.Level</MudTd>
<MudTd DataLabel="Message">@context.Message</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>No matching records found</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Logs</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Search</MudText>
</div>
<MudTextField T="string" ValueChanged="@(s => OnSearch(s))" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" />
</MudStack>
<MudTable FixedHeader="true"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<LogEntryViewModel>>>(ServerReload))"
Dense="true"
Class="mt-10"
@ref="_table">
<HeaderContent>
<MudTh>
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Timestamp">
Timestamp
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel T="LogEntryViewModel" SortLabel="Level">
Level
</MudTableSortLabel>
</MudTh>
<MudTh>Message</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Timestamp.ToString("G", _dtf)</MudTd>
<MudTd>@context.Level</MudTd>
<MudTd>@context.Message</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>No matching records found</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat;
private MudTable<LogEntryViewModel> _table;
private int _rowsPerPage = 10;
private string _searchString;

138
ErsatzTV/Pages/Playlists.razor

@ -1,55 +1,59 @@ @@ -1,55 +1,59 @@
@page "/media/playlists"
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.Tree
@implements IDisposable
@inject ILogger<Playlists> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Playlists</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3 mx-3" Label="Playlist Group Name" @bind-Value="_playlistGroupName" For="@(() => _playlistGroupName)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddPlaylistGroup())" Class="ml-4 mb-4">
Add Playlist Group
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardContent>
<div class="mx-4">
<MudSelect Label="Playlist Group" @bind-Value="_selectedPlaylistGroup" Class="mt-3">
@foreach (PlaylistGroupViewModel playlistGroup in _playlistGroups)
{
<MudSelectItem Value="@playlistGroup">@playlistGroup.Name</MudSelectItem>
}
</MudSelect>
<MudTextField Class="mt-3" Label="Playlist Name" @bind-Value="_playlistName" For="@(() => _playlistName)"/>
</div>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddPlaylist())" Class="ml-4 mb-4">
Add Playlist
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCard>
<MudTreeView T="PlaylistTreeItemViewModel" ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Playlist Groups</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playlist Group Name</MudText>
</div>
<MudTextField @bind-Value="_playlistGroupName" For="@(() => _playlistGroupName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddPlaylistGroup())" StartIcon="@Icons.Material.Filled.Add">
Add Playlist Group
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Playlists</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playlist Group</MudText>
</div>
<MudSelect @bind-Value="_selectedPlaylistGroup">
@foreach (PlaylistGroupViewModel playlistGroup in _playlistGroups)
{
<MudSelectItem Value="@playlistGroup">@playlistGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playlist Name</MudText>
</div>
<MudTextField @bind-Value="_playlistName" For="@(() => _playlistName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddPlaylist())" StartIcon="@Icons.Material.Filled.Add">
Add Playlist
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudTreeView T="PlaylistTreeItemViewModel" Items="@_treeItems" Hover="true" Style="width: 100%">
<ItemTemplate Context="item">
<MudTreeViewItem T="PlaylistTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" CanExpand="@item.Value.CanExpand" Value="@item.Value">
<MudTreeViewItem T="PlaylistTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" Value="@item.Value">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
@ -75,14 +79,14 @@ @@ -75,14 +79,14 @@
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private List<TreeItemData<PlaylistTreeItemViewModel>> TreeItems { get; set; } = [];
private readonly List<TreeItemData<PlaylistTreeItemViewModel>> _treeItems = [];
private List<PlaylistGroupViewModel> _playlistGroups = [];
private PlaylistGroupViewModel _selectedPlaylistGroup;
private string _playlistGroupName;
@ -96,14 +100,20 @@ @@ -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<PlaylistTreeItemViewModel> { 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<PlaylistTreeItemViewModel> { Value = new PlaylistTreeItemViewModel(group) });
}
}
private async Task AddPlaylistGroup()
@ -120,7 +130,7 @@ @@ -120,7 +130,7 @@
foreach (PlaylistGroupViewModel playlistGroup in result.RightToSeq())
{
TreeItems.Add(new TreeItemData<PlaylistTreeItemViewModel> { Value = new PlaylistTreeItemViewModel(playlistGroup) });
_treeItems.Add(new TreeItemData<PlaylistTreeItemViewModel> { Value = new PlaylistTreeItemViewModel(playlistGroup) });
_playlistGroupName = null;
_playlistGroups = await Mediator.Send(new GetAllPlaylistGroups(), _cts.Token);
@ -143,7 +153,7 @@ @@ -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<PlaylistTreeItemViewModel> { Value = new PlaylistTreeItemViewModel(playlist) });
}
@ -154,20 +164,6 @@ @@ -154,20 +164,6 @@
}
}
private async Task<IReadOnlyCollection<TreeItemData<PlaylistTreeItemViewModel>>> LoadServerData(PlaylistTreeItemViewModel parentNode)
{
foreach (int playlistGroupId in Optional(parentNode.PlaylistGroupId))
{
List<PlaylistViewModel> result = await Mediator.Send(new GetPlaylistsByPlaylistGroupId(playlistGroupId), _cts.Token);
foreach (PlaylistViewModel playlist in result)
{
parentNode.TreeItems.Add(new TreeItemData<PlaylistTreeItemViewModel> { Value = new PlaylistTreeItemViewModel(playlist) });
}
}
return parentNode.TreeItems;
}
private async Task DeleteItem(PlaylistTreeItemViewModel treeItem)
{
foreach (int playlistGroupId in Optional(treeItem.PlaylistGroupId))
@ -180,7 +176,11 @@ @@ -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 @@ @@ -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);
}

2
ErsatzTV/Pages/Playouts.razor

@ -202,7 +202,7 @@ @@ -202,7 +202,7 @@
</MudTable>
@if (_selectedPlayoutId != null)
{
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Playout Detail</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Playout Detail</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
Dense="true"

140
ErsatzTV/Pages/Templates.razor

@ -1,59 +1,63 @@ @@ -1,59 +1,63 @@
@page "/templates"
@using ErsatzTV.Application.Scheduling
@using ErsatzTV.Application.Tree
@implements IDisposable
@inject ILogger<Templates> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Templates</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3 mx-3" Label="Template Group Name" @bind-Value="_templateGroupName" For="@(() => _templateGroupName)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddTemplateGroup())" Class="ml-4 mb-4">
Add Template Group
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardContent>
<div class="mx-4">
<MudSelect Label="Template Group" @bind-Value="_selectedTemplateGroup" Class="mt-3">
@foreach (TemplateGroupViewModel templateGroup in _templateGroups)
{
<MudSelectItem Value="@templateGroup">@templateGroup.Name</MudSelectItem>
}
</MudSelect>
<MudTextField Class="mt-3" Label="Template Name" @bind-Value="_templateName" For="@(() => _templateName)"/>
</div>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddTemplate())" Class="ml-4 mb-4">
Add Template
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCard>
<MudTreeView T="TemplateTreeItemViewModel" ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
<MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Template Groups</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Template Group Name</MudText>
</div>
<MudTextField @bind-Value="_templateGroupName" For="@(() => _templateGroupName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddTemplateGroup())" StartIcon="@Icons.Material.Filled.Add">
Add Template Group
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Templates</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Template Group</MudText>
</div>
<MudSelect @bind-Value="_selectedTemplateGroup">
@foreach (TemplateGroupViewModel templateGroup in _templateGroups)
{
<MudSelectItem Value="@templateGroup">@templateGroup.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Template Name</MudText>
</div>
<MudTextField @bind-Value="_templateName" For="@(() => _templateName)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddTemplate())" StartIcon="@Icons.Material.Filled.Add">
Add Template
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudTreeView T="TemplateTreeItemViewModel" Items="@_treeItems" Hover="true" Style="width: 100%">
<ItemTemplate Context="item">
<MudTreeViewItem T="TemplateTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" CanExpand="@item.Value.CanExpand" Value="@item.Value">
<MudTreeViewItem T="TemplateTreeItemViewModel" Items="@item.Value!.TreeItems" Icon="@item.Value.Icon" Value="@item.Value">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
<MudItem xs="8">
<MudItem xs="5">
<MudText>@item.Value.Text</MudText>
</MudItem>
</MudGrid>
@ -69,14 +73,14 @@ @@ -69,14 +73,14 @@
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private List<TreeItemData<TemplateTreeItemViewModel>> TreeItems { get; set; } = [];
private readonly List<TreeItemData<TemplateTreeItemViewModel>> _treeItems = [];
private List<TemplateGroupViewModel> _templateGroups = [];
private TemplateGroupViewModel _selectedTemplateGroup;
private string _templateGroupName;
@ -90,14 +94,20 @@ @@ -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<TemplateTreeItemViewModel> { 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<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(group) });
}
}
private async Task AddTemplateGroup()
@ -114,7 +124,7 @@ @@ -114,7 +124,7 @@
foreach (TemplateGroupViewModel templateGroup in result.RightToSeq())
{
TreeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(templateGroup) });
_treeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(templateGroup) });
_templateGroupName = null;
_templateGroups = await Mediator.Send(new GetAllTemplateGroups(), _cts.Token);
@ -137,7 +147,7 @@ @@ -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<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(template) });
}
@ -148,20 +158,6 @@ @@ -148,20 +158,6 @@
}
}
private async Task<IReadOnlyCollection<TreeItemData<TemplateTreeItemViewModel>>> LoadServerData(TemplateTreeItemViewModel parentNode)
{
foreach (int templateGroupId in Optional(parentNode.TemplateGroupId))
{
List<TemplateViewModel> result = await Mediator.Send(new GetTemplatesByTemplateGroupId(templateGroupId), _cts.Token);
foreach (TemplateViewModel template in result)
{
parentNode.TreeItems.Add(new TreeItemData<TemplateTreeItemViewModel> { Value = new TemplateTreeItemViewModel(template) });
}
}
return parentNode.TreeItems;
}
private async Task DeleteItem(TemplateTreeItemViewModel treeItem)
{
foreach (int templateGroupId in Optional(treeItem.TemplateGroupId))
@ -174,7 +170,11 @@ @@ -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 @@ @@ -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);
}

2
ErsatzTV/Pages/YamlPlayoutEditor.razor

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => EditYamlFile())">
Edit YAML File
</MudButton>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Playout Items and History</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Playout Items and History</MudText>
<MudDivider Class="mb-6"/>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))">
Erase Items and History

2
ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor

@ -51,7 +51,7 @@ @@ -51,7 +51,7 @@
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddPathReplacement())" Class="mt-4">
Add Path Replacement
</MudButton>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Path Replacement</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Path Replacement</MudText>
<MudDivider Class="mb-6"/>
@if (_selectedItem is not null)
{

33
ErsatzTV/ViewModels/BlockTreeItemViewModel.cs

@ -10,7 +10,16 @@ public class BlockTreeItemViewModel @@ -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<BlockTreeItemViewModel>
{ Value = new BlockTreeItemViewModel(b) }).ToList();
BlockGroupId = blockGroup.Id;
Icon = Icons.Material.Filled.Folder;
}
@ -37,6 +46,28 @@ public class BlockTreeItemViewModel @@ -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; }

19
ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Application.Tree;
using MudBlazor;
namespace ErsatzTV.ViewModels;
@ -9,7 +10,15 @@ public class DecoTemplateTreeItemViewModel @@ -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<DecoTemplateTreeItemViewModel>
{ Value = new DecoTemplateTreeItemViewModel(d) }).ToList();
DecoTemplateGroupId = decoTemplateGroup.Id;
Icon = Icons.Material.Filled.Folder;
}
@ -22,6 +31,14 @@ public class DecoTemplateTreeItemViewModel @@ -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; }

19
ErsatzTV/ViewModels/DecoTreeItemViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Application.Tree;
using MudBlazor;
namespace ErsatzTV.ViewModels;
@ -10,7 +11,15 @@ public class DecoTreeItemViewModel @@ -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<DecoTreeItemViewModel>
{ Value = new DecoTreeItemViewModel(d) }).ToList();
DecoGroupId = decoGroup.Id;
Icon = Icons.Material.Filled.Folder;
}
@ -23,6 +32,14 @@ public class DecoTreeItemViewModel @@ -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; }

20
ErsatzTV/ViewModels/PlaylistTreeItemViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Tree;
using MudBlazor;
namespace ErsatzTV.ViewModels;
@ -10,7 +11,16 @@ public class PlaylistTreeItemViewModel @@ -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<PlaylistTreeItemViewModel>
{ Value = new PlaylistTreeItemViewModel(p) }).ToList();
PlaylistGroupId = playlistGroup.Id;
Icon = Icons.Material.Filled.Folder;
}
@ -23,6 +33,14 @@ public class PlaylistTreeItemViewModel @@ -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; }

18
ErsatzTV/ViewModels/TemplateTreeItemViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Application.Tree;
using MudBlazor;
namespace ErsatzTV.ViewModels;
@ -14,6 +15,15 @@ public class TemplateTreeItemViewModel @@ -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<TemplateTreeItemViewModel>
{ 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 @@ -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; }

7
ErsatzTV/wwwroot/css/site.css

@ -145,6 +145,10 @@ @@ -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 @@ @@ -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;
}

Loading…
Cancel
Save