mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
488 lines
23 KiB
488 lines
23 KiB
@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="15"/> |
|
<MudSelectItem Value="30"/> |
|
<MudSelectItem Value="45"/> |
|
</MudSelect> |
|
</MudItem> |
|
</MudGrid> |
|
<MudSelect T="BlockStopScheduling" |
|
@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 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/> |
|
<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> |
|
<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.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 = 0; |
|
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) |
|
{ |
|
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) |
|
{ |
|
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) |
|
{ |
|
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) |
|
{ |
|
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) |
|
{ |
|
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) |
|
{ |
|
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 |
|
}; |
|
|
|
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 |
|
}; |
|
|
|
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)).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); |
|
} |
|
}
|
|
|