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.
414 lines
21 KiB
414 lines
21 KiB
@page "/playouts" |
|
@using System.Globalization |
|
@using ErsatzTV.Application.Configuration |
|
@using ErsatzTV.Application.Playouts |
|
@using ErsatzTV.Core.Notifications |
|
@using ErsatzTV.Core.Scheduling |
|
@using MediatR.Courier |
|
@implements IDisposable |
|
@inject IDialogService Dialog |
|
@inject IMediator Mediator |
|
@inject ChannelWriter<IBackgroundServiceRequest> WorkerChannel |
|
@inject IEntityLocker EntityLocker; |
|
@inject ICourier Courier; |
|
|
|
<MudForm Style="max-height: 100%"> |
|
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center"> |
|
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6"> |
|
<div class="flex-grow-1"></div> |
|
<div style="margin-left: auto" class="d-none d-md-flex"> |
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Href="playouts/add"> |
|
Add Playout |
|
</MudButton> |
|
<MudTooltip Text="This feature is experimental"> |
|
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Block}")"> |
|
Add Block Playout |
|
</MudButton> |
|
</MudTooltip> |
|
<MudTooltip Text="This feature is experimental"> |
|
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Yaml}")"> |
|
Add YAML Playout |
|
</MudButton> |
|
</MudTooltip> |
|
<MudTooltip Text="This feature is experimental"> |
|
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")"> |
|
Add External Json Playout |
|
</MudButton> |
|
</MudTooltip> |
|
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@ResetAllPlayouts"> |
|
Reset All Playouts |
|
</MudButton> |
|
</div> |
|
<div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none"> |
|
<div class="flex-grow-1"></div> |
|
<MudMenu Icon="@Icons.Material.Filled.MoreVert"> |
|
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Playout" Href="playouts/add"/> |
|
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add Block Playout" Href="@($"playouts/add/{PlayoutKind.Block}")"/> |
|
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add YAML Playout" Href="@($"playouts/add/{PlayoutKind.Yaml}")"/> |
|
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add External Json Playout" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")"/> |
|
<MudMenuItem Icon="@Icons.Material.Filled.Refresh" Label="Reset All Playouts" OnClick="ResetAllPlayouts"/> |
|
</MudMenu> |
|
</div> |
|
</div> |
|
</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">Playouts</MudText> |
|
<MudDivider Class="mb-6"/> |
|
<MudTable Hover="true" |
|
Dense="true" |
|
SelectedItemChanged="@(async (PlayoutNameViewModel x) => await PlayoutSelected(x))" |
|
@bind-RowsPerPage="@_rowsPerPage" |
|
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutNameViewModel>>>(ServerReload))" |
|
@ref="_table"> |
|
<ColGroup> |
|
<MudHidden Breakpoint="Breakpoint.Xs"> |
|
<col/> |
|
<col/> |
|
<col/> |
|
<col style="width: 225px;"/> |
|
</MudHidden> |
|
</ColGroup> |
|
<HeaderContent> |
|
<MudTh> |
|
<MudTableSortLabel SortBy="new Func<PlayoutViewModel, object>(x => decimal.Parse(x.Channel.Number, CultureInfo.InvariantCulture))"> |
|
Channel |
|
</MudTableSortLabel> |
|
</MudTh> |
|
<MudTh Class="d-none d-md-table-cell"> |
|
<MudTableSortLabel SortBy="new Func<PlayoutViewModel, object>(x => x.ProgramSchedule.Name)"> |
|
Default Schedule |
|
</MudTableSortLabel> |
|
</MudTh> |
|
<MudTh Class="d-none d-md-table-cell">Playout Type</MudTh> |
|
<MudTh/> |
|
</HeaderContent> |
|
<RowTemplate> |
|
<MudTd>@context.ChannelNumber - @context.ChannelName</MudTd> |
|
<MudTd Class="d-none d-md-table-cell">@context.ScheduleName</MudTd> |
|
<MudTd Class="d-none d-md-table-cell"> |
|
@switch (context.PlayoutType) |
|
{ |
|
case ProgramSchedulePlayoutType.Block: |
|
<span>Block</span> |
|
break; |
|
case ProgramSchedulePlayoutType.Yaml: |
|
<span>YAML</span> |
|
break; |
|
case ProgramSchedulePlayoutType.ExternalJson: |
|
<span>External Json</span> |
|
break; |
|
default: |
|
<span></span> |
|
break; |
|
} |
|
</MudTd> |
|
<MudTd> |
|
<div style="align-items: center; display: flex;"> |
|
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;"> |
|
@if (EntityLocker.IsPlayoutLocked(context.PlayoutId)) |
|
{ |
|
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/> |
|
} |
|
</div> |
|
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood) |
|
{ |
|
if (context.ProgressMode is ChannelProgressMode.OnDemand) |
|
{ |
|
<MudTooltip Text="Alternate Schedules are not supported with On Demand progress"> |
|
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar" |
|
Disabled="true"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
} |
|
else |
|
{ |
|
<MudTooltip Text="Edit Alternate Schedules"> |
|
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
Href="@($"playouts/{context.PlayoutId}/alternate-schedules")"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
} |
|
|
|
<MudTooltip Text="Reset Playout"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
OnClick="@(_ => ResetPlayout(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
<MudTooltip Text="Schedule Reset"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Update" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
OnClick="@(_ => ScheduleReset(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
} |
|
else if (context.PlayoutType == ProgramSchedulePlayoutType.ExternalJson) |
|
{ |
|
<MudTooltip Text="Edit External Json File"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
OnClick="@(_ => EditExternalJsonFile(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
<div style="width: 48px"></div> |
|
<div style="width: 48px"></div> |
|
} |
|
else if (context.PlayoutType == ProgramSchedulePlayoutType.Yaml) |
|
{ |
|
<MudTooltip Text="Edit Playout"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
Href="@($"playouts/yaml/{context.PlayoutId}")"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
<MudTooltip Text="Reset Playout"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
OnClick="@(_ => ResetPlayout(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
<div style="width: 48px"></div> |
|
} |
|
else if (context.PlayoutType == ProgramSchedulePlayoutType.Block) |
|
{ |
|
<MudTooltip Text="Edit Playout"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
Href="@($"playouts/block/{context.PlayoutId}")"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
<MudTooltip Text="Reset Playout"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
OnClick="@(_ => ResetPlayout(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
<div style="width: 48px"></div> |
|
} |
|
<MudTooltip Text="Delete Playout"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" |
|
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)" |
|
OnClick="@(_ => DeletePlayout(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
</div> |
|
</MudTd> |
|
</RowTemplate> |
|
<PagerContent> |
|
<MudTablePager/> |
|
</PagerContent> |
|
</MudTable> |
|
@if (_selectedPlayoutId != null) |
|
{ |
|
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Playout Detail</MudText> |
|
<MudDivider Class="mb-6"/> |
|
<MudTable Hover="true" |
|
Dense="true" |
|
@bind-RowsPerPage="@_detailRowsPerPage" |
|
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutItemViewModel>>>(DetailServerReload))" |
|
@ref="_detailTable"> |
|
<ToolBarContent> |
|
<MudSwitch T="bool" Class="ml-6" @bind-Value="@ShowFiller" Color="Color.Secondary" Label="Show Filler"/> |
|
</ToolBarContent> |
|
<HeaderContent> |
|
<MudTh>Start</MudTh> |
|
<MudTh>Finish</MudTh> |
|
<MudTh>Media Item</MudTh> |
|
<MudTh>Duration</MudTh> |
|
</HeaderContent> |
|
<RowTemplate> |
|
<MudTd DataLabel="Start">@context.Start.ToString("G", _dtf)</MudTd> |
|
<MudTd DataLabel="Finish">@context.Finish.ToString("G", _dtf)</MudTd> |
|
<MudTd DataLabel="Media Item">@context.Title</MudTd> |
|
<MudTd DataLabel="Duration">@context.Duration</MudTd> |
|
</RowTemplate> |
|
<PagerContent> |
|
<MudTablePager/> |
|
</PagerContent> |
|
</MudTable> |
|
} |
|
</MudContainer> |
|
</div> |
|
</MudForm> |
|
|
|
@code { |
|
private readonly CancellationTokenSource _cts = new(); |
|
|
|
private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat; |
|
|
|
private MudTable<PlayoutNameViewModel> _table; |
|
private MudTable<PlayoutItemViewModel> _detailTable; |
|
private int _rowsPerPage = 10; |
|
private int _detailRowsPerPage = 10; |
|
private int? _selectedPlayoutId; |
|
private bool _showFiller; |
|
|
|
private bool ShowFiller |
|
{ |
|
get => _showFiller; |
|
set |
|
{ |
|
if (_showFiller != value) |
|
{ |
|
_showFiller = value; |
|
if (_detailTable != null && _selectedPlayoutId != null) |
|
{ |
|
_detailTable.ReloadServerData(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
protected override void OnInitialized() => Courier.Subscribe<PlayoutUpdatedNotification>(HandlePlayoutUpdated); |
|
|
|
public void Dispose() |
|
{ |
|
Courier.UnSubscribe<PlayoutUpdatedNotification>(HandlePlayoutUpdated); |
|
|
|
_cts.Cancel(); |
|
_cts.Dispose(); |
|
} |
|
|
|
public async Task HandlePlayoutUpdated(PlayoutUpdatedNotification notification, CancellationToken cancellationToken) |
|
{ |
|
// only refresh detail table on unlock operations (after playout has been modified) |
|
if (notification.IsLocked == false) |
|
{ |
|
if (notification.PlayoutId == _selectedPlayoutId && _detailTable is not null) |
|
{ |
|
await InvokeAsync(() => _detailTable.ReloadServerData()); |
|
} |
|
} |
|
|
|
await InvokeAsync(StateHasChanged); |
|
} |
|
|
|
protected override async Task OnParametersSetAsync() |
|
{ |
|
_rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsPageSize), _cts.Token) |
|
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); |
|
_detailRowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize), _cts.Token) |
|
.Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); |
|
_showFiller = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller), _cts.Token) |
|
.Map(maybeShow => maybeShow.Match(ce => bool.TryParse(ce.Value, out bool show) && show, () => false)); |
|
} |
|
|
|
private async Task PlayoutSelected(PlayoutNameViewModel playout) |
|
{ |
|
// only show details for flood, block and YAML playouts |
|
_selectedPlayoutId = playout.PlayoutType is ProgramSchedulePlayoutType.Flood or ProgramSchedulePlayoutType.Block or ProgramSchedulePlayoutType.Yaml |
|
? playout.PlayoutId |
|
: null; |
|
|
|
if (_detailTable != null) |
|
{ |
|
await _detailTable.ReloadServerData(); |
|
} |
|
} |
|
|
|
private async Task EditExternalJsonFile(PlayoutNameViewModel playout) |
|
{ |
|
var parameters = new DialogParameters { { "ExternalJsonFile", $"{playout.ExternalJsonFile}" } }; |
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge }; |
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<EditExternalJsonFileDialog>("Edit External Json File", parameters, options); |
|
DialogResult result = await dialog.Result; |
|
if (result is { Canceled: false }) |
|
{ |
|
await Mediator.Send(new UpdateExternalJsonPlayout(playout.PlayoutId, result.Data as string ?? playout.ExternalJsonFile), _cts.Token); |
|
if (_table != null) |
|
{ |
|
await _table.ReloadServerData(); |
|
} |
|
|
|
_selectedPlayoutId = null; |
|
} |
|
} |
|
|
|
private async Task DeletePlayout(PlayoutNameViewModel playout) |
|
{ |
|
var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } }; |
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Playout", parameters, options); |
|
DialogResult result = await dialog.Result; |
|
if (result is { Canceled: false }) |
|
{ |
|
await Mediator.Send(new DeletePlayout(playout.PlayoutId), _cts.Token); |
|
if (_table != null) |
|
{ |
|
await _table.ReloadServerData(); |
|
} |
|
|
|
if (_selectedPlayoutId == playout.PlayoutId) |
|
{ |
|
_selectedPlayoutId = null; |
|
} |
|
} |
|
} |
|
|
|
private async Task ResetAllPlayouts() |
|
{ |
|
_selectedPlayoutId = null; |
|
await Mediator.Send(new ResetAllPlayouts(), _cts.Token); |
|
} |
|
|
|
private async Task ResetPlayout(PlayoutNameViewModel playout) => await WorkerChannel.WriteAsync(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token); |
|
|
|
private async Task ScheduleReset(PlayoutNameViewModel playout) |
|
{ |
|
var parameters = new DialogParameters |
|
{ |
|
{ "PlayoutId", playout.PlayoutId }, |
|
{ "ChannelName", playout.ChannelName }, |
|
{ "ScheduleName", playout.ScheduleName }, |
|
{ "DailyResetTime", playout.DailyRebuildTime } |
|
}; |
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<SchedulePlayoutReset>("Schedule Playout Reset", parameters, options); |
|
await dialog.Result; |
|
|
|
if (_table != null) |
|
{ |
|
await _table.ReloadServerData(); |
|
} |
|
} |
|
|
|
private async Task<TableData<PlayoutNameViewModel>> ServerReload(TableState state, CancellationToken cancellationToken) |
|
{ |
|
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsPageSize, state.PageSize.ToString()), _cts.Token); |
|
|
|
List<PlayoutNameViewModel> playouts = await Mediator.Send(new GetAllPlayouts(), _cts.Token); |
|
IOrderedEnumerable<PlayoutNameViewModel> sorted = playouts.OrderBy(p => decimal.Parse(p.ChannelNumber, CultureInfo.InvariantCulture)); |
|
|
|
// TODO: properly page this data |
|
return new TableData<PlayoutNameViewModel> |
|
{ |
|
TotalItems = playouts.Count, |
|
Items = sorted.Skip(state.Page * state.PageSize).Take(state.PageSize) |
|
}; |
|
} |
|
|
|
private async Task<TableData<PlayoutItemViewModel>> DetailServerReload(TableState state, CancellationToken cancellationToken) |
|
{ |
|
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailPageSize, state.PageSize.ToString()), _cts.Token); |
|
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PlayoutsDetailShowFiller, _showFiller.ToString()), _cts.Token); |
|
|
|
if (_selectedPlayoutId.HasValue) |
|
{ |
|
PagedPlayoutItemsViewModel data = |
|
await Mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, _showFiller, state.Page, state.PageSize), _cts.Token); |
|
return new TableData<PlayoutItemViewModel> |
|
{ |
|
TotalItems = data.TotalCount, |
|
Items = data.Page |
|
}; |
|
} |
|
|
|
return new TableData<PlayoutItemViewModel> { TotalItems = 0 }; |
|
} |
|
|
|
} |