509 lines
23 KiB
Plaintext
509 lines
23 KiB
Plaintext
@page "/blocks/{Id:int}"
|
|
@using ErsatzTV.Application.MediaCollections
|
|
@using ErsatzTV.Application.MediaItems
|
|
@using ErsatzTV.Application.Scheduling
|
|
@using ErsatzTV.Application.Search
|
|
@using ErsatzTV.Core.Domain.Scheduling
|
|
@implements IDisposable
|
|
@inject NavigationManager NavigationManager
|
|
@inject ILogger<BlockEditor> Logger
|
|
@inject ISnackbar Snackbar
|
|
@inject IMediator Mediator
|
|
|
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
|
<MudText Typo="Typo.h4" Class="mb-4">Edit Block</MudText>
|
|
<div style="max-width: 400px">
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudTextField Label="Name" @bind-Value="_block.Name" For="@(() => _block.Name)"/>
|
|
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
|
|
<MudItem xs="6">
|
|
<MudTextField T="int"
|
|
Label="Duration"
|
|
@bind-Value="_durationHours"
|
|
Adornment="Adornment.End"
|
|
AdornmentText="hours"/>
|
|
</MudItem>
|
|
<MudItem xs="6">
|
|
<MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes">
|
|
<MudSelectItem Value="0"/>
|
|
<MudSelectItem Value="5"/>
|
|
<MudSelectItem Value="10"/>
|
|
<MudSelectItem Value="15"/>
|
|
<MudSelectItem Value="20"/>
|
|
<MudSelectItem Value="25"/>
|
|
<MudSelectItem Value="30"/>
|
|
<MudSelectItem Value="35"/>
|
|
<MudSelectItem Value="40"/>
|
|
<MudSelectItem Value="45"/>
|
|
<MudSelectItem Value="50"/>
|
|
<MudSelectItem Value="55"/>
|
|
</MudSelect>
|
|
</MudItem>
|
|
</MudGrid>
|
|
<MudSelect T="BlockStopScheduling"
|
|
Class="mt-3"
|
|
@bind-Value="_block.StopScheduling"
|
|
For="@(() => _block.StopScheduling)"
|
|
Label="Stop scheduling block items">
|
|
<MudSelectItem Value="BlockStopScheduling.BeforeDurationEnd">Before Duration End</MudSelectItem>
|
|
<MudSelectItem Value="BlockStopScheduling.AfterDurationEnd">After Duration End</MudSelectItem>
|
|
</MudSelect>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</div>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddBlockItem())" Class="mt-4">
|
|
Add Block Item
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
|
|
Save Changes
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(_ => PreviewPlayout())" Class="mt-4 ml-4">
|
|
Preview Block Playout
|
|
</MudButton>
|
|
<MudGrid>
|
|
<MudItem xs="8">
|
|
<MudTable Class="mt-6" Hover="true" Items="_block.Items.OrderBy(i => i.Index)" Dense="true" @bind-SelectedItem="_selectedItem">
|
|
<ColGroup>
|
|
<col/>
|
|
<col/>
|
|
<col/>
|
|
<col style="width: 60px;"/>
|
|
<col style="width: 60px;"/>
|
|
<col style="width: 60px;"/>
|
|
<col style="width: 60px;"/>
|
|
</ColGroup>
|
|
<HeaderContent>
|
|
<MudTh>Collection</MudTh>
|
|
<MudTh>Playback Order</MudTh>
|
|
<MudTh>Show In EPG</MudTh>
|
|
<MudTh/>
|
|
<MudTh/>
|
|
<MudTh/>
|
|
<MudTh/>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Collection">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@context.CollectionName
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd DataLabel="Playback Order">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@context.PlaybackOrder
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudCheckBox T="bool" Value="@context.IncludeInProgramGuide" ValueChanged="@(e => UpdateEPG(context, e))"/>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
|
|
OnClick="@(_ => CopyItem(context))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
|
|
OnClick="@(_ => MoveItemUp(context))"
|
|
Disabled="@(_block.Items.All(x => x.Index >= context.Index))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
|
|
OnClick="@(_ => MoveItemDown(context))"
|
|
Disabled="@(_block.Items.All(x => x.Index <= context.Index))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
OnClick="@(_ => RemoveBlockItem(context))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudItem>
|
|
</MudGrid>
|
|
<div class="mt-4">
|
|
@if (_selectedItem is not null)
|
|
{
|
|
<EditForm Model="_selectedItem">
|
|
<FluentValidationValidator/>
|
|
<div style="max-width: 400px;" class="mr-6">
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
|
|
<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 (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection)
|
|
{
|
|
<MudAutocomplete Class="mt-3" T="MediaCollectionViewModel" Label="Collection"
|
|
@bind-Value="_selectedItem.Collection" SearchFunc="@SearchCollections"
|
|
ToStringFunc="@(c => c?.Name)" Placeholder="Type to search...">
|
|
<MoreItemsTemplate>
|
|
<MudText Align="Align.Center" Class="px-4 py-1">
|
|
Only the first 10 items are shown
|
|
</MudText>
|
|
</MoreItemsTemplate>
|
|
</MudAutocomplete>
|
|
}
|
|
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
|
|
{
|
|
<MudAutocomplete Class="mt-3" T="MultiCollectionViewModel" Label="Multi Collection"
|
|
@bind-Value="_selectedItem.MultiCollection" SearchFunc="@SearchMultiCollections"
|
|
ToStringFunc="@(c => c?.Name)" Placeholder="Type to search...">
|
|
<MoreItemsTemplate>
|
|
<MudText Align="Align.Center" Class="px-4 py-1">
|
|
Only the first 10 items are shown
|
|
</MudText>
|
|
</MoreItemsTemplate>
|
|
</MudAutocomplete>
|
|
}
|
|
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
|
|
{
|
|
<MudAutocomplete Class="mt-3" T="SmartCollectionViewModel" Label="Smart Collection"
|
|
@bind-Value="_selectedItem.SmartCollection" SearchFunc="@SearchSmartCollections"
|
|
ToStringFunc="@(c => c?.Name)" Placeholder="Type to search...">
|
|
<MoreItemsTemplate>
|
|
<MudText Align="Align.Center" Class="px-4 py-1">
|
|
Only the first 10 items are shown
|
|
</MudText>
|
|
</MoreItemsTemplate>
|
|
</MudAutocomplete>
|
|
}
|
|
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
|
|
{
|
|
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Television Show"
|
|
@bind-Value="_selectedItem.MediaItem" SearchFunc="@SearchTelevisionShows"
|
|
ToStringFunc="@(c => c?.Name)" Placeholder="Type to search...">
|
|
<MoreItemsTemplate>
|
|
<MudText Align="Align.Center" Class="px-4 py-1">
|
|
Only the first 10 items are shown
|
|
</MudText>
|
|
</MoreItemsTemplate>
|
|
</MudAutocomplete>
|
|
}
|
|
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
|
|
{
|
|
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Television Season"
|
|
@bind-Value="_selectedItem.MediaItem" SearchFunc="@SearchTelevisionSeasons"
|
|
ToStringFunc="@(c => c?.Name)" Placeholder="Type to search..."
|
|
MaxItems="20">
|
|
<MoreItemsTemplate>
|
|
<MudText Align="Align.Center" Class="px-4 py-1">
|
|
Only the first 20 items are shown
|
|
</MudText>
|
|
</MoreItemsTemplate>
|
|
</MudAutocomplete>
|
|
}
|
|
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Artist)
|
|
{
|
|
<MudAutocomplete Class="mt-3" T="NamedMediaItemViewModel" Label="Artist"
|
|
@bind-Value="_selectedItem.MediaItem" SearchFunc="@SearchArtists"
|
|
ToStringFunc="@(c => c?.Name)" Placeholder="Type to search..."
|
|
MaxItems="10">
|
|
<MoreItemsTemplate>
|
|
<MudText Align="Align.Center" Class="px-4 py-1">
|
|
Only the first 10 items are shown
|
|
</MudText>
|
|
</MoreItemsTemplate>
|
|
</MudAutocomplete>
|
|
}
|
|
|
|
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
|
|
@switch (_selectedItem.CollectionType)
|
|
{
|
|
case ProgramScheduleItemCollectionType.MultiCollection:
|
|
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
|
@* <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> *@
|
|
break;
|
|
case ProgramScheduleItemCollectionType.Collection:
|
|
case ProgramScheduleItemCollectionType.SmartCollection:
|
|
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.RandomRotation">Random Rotation</MudSelectItem>
|
|
@* <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> *@
|
|
break;
|
|
case ProgramScheduleItemCollectionType.TelevisionShow:
|
|
<MudSelectItem Value="PlaybackOrder.SeasonEpisode">Season, Episode</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
|
@* <MudSelectItem Value="PlaybackOrder.MultiEpisodeShuffle">Multi-Episode Shuffle</MudSelectItem> *@
|
|
break;
|
|
case ProgramScheduleItemCollectionType.TelevisionSeason:
|
|
case ProgramScheduleItemCollectionType.Artist:
|
|
case ProgramScheduleItemCollectionType.FakeCollection:
|
|
default:
|
|
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
|
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
|
break;
|
|
}
|
|
</MudSelect>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</div>
|
|
</EditForm>
|
|
}
|
|
</div>
|
|
@if (_previewItems != null)
|
|
{
|
|
<MudTable Class="mt-8"
|
|
Hover="true"
|
|
Dense="true"
|
|
Items="_previewItems">
|
|
<ToolBarContent>
|
|
<MudText Typo="Typo.h6">Block Playout Preview</MudText>
|
|
</ToolBarContent>
|
|
<HeaderContent>
|
|
<MudTh>Start</MudTh>
|
|
<MudTh>Finish</MudTh>
|
|
<MudTh>Media Item</MudTh>
|
|
<MudTh>Duration</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Start">@context.Start.ToString(@"hh\:mm\:ss")</MudTd>
|
|
<MudTd DataLabel="Finish">@context.Finish.ToString(@"hh\:mm\:ss")</MudTd>
|
|
<MudTd DataLabel="Media Item">@context.Title</MudTd>
|
|
<MudTd DataLabel="Duration">@context.Duration</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
[Parameter]
|
|
public int Id { get; set; }
|
|
|
|
private BlockItemsEditViewModel _block = new() { Items = [] };
|
|
private BlockItemEditViewModel _selectedItem;
|
|
private List<PlayoutItemPreviewViewModel> _previewItems;
|
|
private int _durationHours;
|
|
private int _durationMinutes = 15;
|
|
|
|
public void Dispose()
|
|
{
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
}
|
|
|
|
protected override async Task OnParametersSetAsync() => await LoadBlockItems();
|
|
|
|
private async Task LoadBlockItems()
|
|
{
|
|
Option<BlockViewModel> maybeBlock = await Mediator.Send(new GetBlockById(Id), _cts.Token);
|
|
if (maybeBlock.IsNone)
|
|
{
|
|
NavigationManager.NavigateTo("blocks");
|
|
return;
|
|
}
|
|
|
|
foreach (BlockViewModel block in maybeBlock)
|
|
{
|
|
_block = new BlockItemsEditViewModel
|
|
{
|
|
Name = block.Name,
|
|
Minutes = block.Minutes,
|
|
StopScheduling = block.StopScheduling,
|
|
Items = []
|
|
};
|
|
|
|
_durationHours = _block.Minutes / 60;
|
|
_durationMinutes = _block.Minutes % 60;
|
|
}
|
|
|
|
Option<IEnumerable<BlockItemViewModel>> maybeResults = await Mediator.Send(new GetBlockItems(Id), _cts.Token);
|
|
foreach (IEnumerable<BlockItemViewModel> items in maybeResults)
|
|
{
|
|
_block.Items.AddRange(items.Map(ProjectToEditViewModel));
|
|
if (_block.Items.Count == 1)
|
|
{
|
|
_selectedItem = _block.Items.Head();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<IEnumerable<MediaCollectionViewModel>> SearchCollections(string value, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return new List<MediaCollectionViewModel>();
|
|
}
|
|
|
|
return await Mediator.Send(new SearchCollections(value), _cts.Token);
|
|
}
|
|
|
|
private async Task<IEnumerable<MultiCollectionViewModel>> SearchMultiCollections(string value, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return new List<MultiCollectionViewModel>();
|
|
}
|
|
|
|
return await Mediator.Send(new SearchMultiCollections(value), _cts.Token);
|
|
}
|
|
|
|
private async Task<IEnumerable<SmartCollectionViewModel>> SearchSmartCollections(string value, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return new List<SmartCollectionViewModel>();
|
|
}
|
|
|
|
return await Mediator.Send(new SearchSmartCollections(value), _cts.Token);
|
|
}
|
|
|
|
private async Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionShows(string value, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return new List<NamedMediaItemViewModel>();
|
|
}
|
|
|
|
return await Mediator.Send(new SearchTelevisionShows(value), _cts.Token);
|
|
}
|
|
|
|
private async Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionSeasons(string value, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return new List<NamedMediaItemViewModel>();
|
|
}
|
|
|
|
return await Mediator.Send(new SearchTelevisionSeasons(value), _cts.Token);
|
|
}
|
|
|
|
private async Task<IEnumerable<NamedMediaItemViewModel>> SearchArtists(string value, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return new List<NamedMediaItemViewModel>();
|
|
}
|
|
|
|
return await Mediator.Send(new SearchArtists(value), _cts.Token);
|
|
}
|
|
|
|
private static BlockItemEditViewModel ProjectToEditViewModel(BlockItemViewModel item) =>
|
|
new()
|
|
{
|
|
Id = item.Id,
|
|
Index = item.Index,
|
|
CollectionType = item.CollectionType,
|
|
Collection = item.Collection,
|
|
MultiCollection = item.MultiCollection,
|
|
SmartCollection = item.SmartCollection,
|
|
MediaItem = item.MediaItem,
|
|
PlaybackOrder = item.PlaybackOrder,
|
|
IncludeInProgramGuide = item.IncludeInProgramGuide
|
|
};
|
|
|
|
private void AddBlockItem()
|
|
{
|
|
var item = new BlockItemEditViewModel
|
|
{
|
|
Index = _block.Items.Map(i => i.Index).DefaultIfEmpty().Max() + 1,
|
|
PlaybackOrder = PlaybackOrder.Chronological,
|
|
CollectionType = ProgramScheduleItemCollectionType.Collection
|
|
};
|
|
|
|
_block.Items.Add(item);
|
|
_selectedItem = item;
|
|
}
|
|
|
|
private void CopyItem(BlockItemEditViewModel item)
|
|
{
|
|
var newItem = new BlockItemEditViewModel
|
|
{
|
|
Index = item.Index + 1,
|
|
PlaybackOrder = item.PlaybackOrder,
|
|
CollectionType = item.CollectionType,
|
|
Collection = item.Collection,
|
|
MultiCollection = item.MultiCollection,
|
|
SmartCollection = item.SmartCollection,
|
|
MediaItem = item.MediaItem,
|
|
IncludeInProgramGuide = item.IncludeInProgramGuide
|
|
};
|
|
|
|
foreach (BlockItemEditViewModel i in _block.Items.Filter(bi => bi.Index >= newItem.Index))
|
|
{
|
|
i.Index += 1;
|
|
}
|
|
|
|
_block.Items.Add(newItem);
|
|
_selectedItem = newItem;
|
|
}
|
|
|
|
private void RemoveBlockItem(BlockItemEditViewModel item)
|
|
{
|
|
_selectedItem = null;
|
|
_block.Items.Remove(item);
|
|
}
|
|
|
|
private void MoveItemUp(BlockItemEditViewModel item)
|
|
{
|
|
// swap with lower index
|
|
BlockItemEditViewModel toSwap = _block.Items.OrderByDescending(x => x.Index).First(x => x.Index < item.Index);
|
|
(toSwap.Index, item.Index) = (item.Index, toSwap.Index);
|
|
}
|
|
|
|
private void MoveItemDown(BlockItemEditViewModel item)
|
|
{
|
|
// swap with higher index
|
|
BlockItemEditViewModel toSwap = _block.Items.OrderBy(x => x.Index).First(x => x.Index > item.Index);
|
|
(toSwap.Index, item.Index) = (item.Index, toSwap.Index);
|
|
}
|
|
|
|
private async Task SaveChanges()
|
|
{
|
|
Seq<BaseError> errorMessages = await Mediator
|
|
.Send(GenerateReplaceRequest(), _cts.Token)
|
|
.Map(e => e.LeftToSeq());
|
|
|
|
errorMessages.HeadOrNone().Match(
|
|
error =>
|
|
{
|
|
Snackbar.Add($"Unexpected error saving block: {error.Value}", Severity.Error);
|
|
Logger.LogError("Unexpected error saving block: {Error}", error.Value);
|
|
},
|
|
() => NavigationManager.NavigateTo("blocks"));
|
|
}
|
|
|
|
private ReplaceBlockItems GenerateReplaceRequest()
|
|
{
|
|
var items = _block.Items.Map(
|
|
item => new ReplaceBlockItem(
|
|
item.Index,
|
|
item.CollectionType,
|
|
item.Collection?.Id,
|
|
item.MultiCollection?.Id,
|
|
item.SmartCollection?.Id,
|
|
item.MediaItem?.MediaItemId,
|
|
item.PlaybackOrder,
|
|
item.IncludeInProgramGuide)).ToList();
|
|
|
|
_block.Minutes = _durationHours * 60 + _durationMinutes;
|
|
|
|
return new ReplaceBlockItems(Id, _block.Name, _block.Minutes, _block.StopScheduling, items);
|
|
}
|
|
|
|
private async Task PreviewPlayout()
|
|
{
|
|
_selectedItem = null;
|
|
_previewItems = await Mediator.Send(new PreviewBlockPlayout(GenerateReplaceRequest()), _cts.Token);
|
|
}
|
|
|
|
private static void UpdateEPG(BlockItemEditViewModel context, bool includeInProgramGuide) => context.IncludeInProgramGuide = includeInProgramGuide;
|
|
|
|
} |