Stream custom live channels using your own media
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.

320 lines
13 KiB

@page "/deco-templates/{Id:int}"
@using System.Globalization
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<DecoTemplateEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<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" Class="ml-8" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save">
Save Deco Template
</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">General</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="_decoTemplate.Name" For="@(() => _decoTemplate.Name)"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Add Content</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 T="DecoGroupViewModel" ValueChanged="@(vm => UpdateDecoGroupItems(vm))">
@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</MudText>
</div>
<MudSelect T="DecoViewModel" @bind-value="_selectedDeco">
@foreach (DecoViewModel deco in _decos)
{
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Start Time On Or After</MudText>
</div>
<MudSelect T="DateTime" @bind-value="_selectedDecoStart">
@foreach (DateTime startTime in _startTimes)
{
<MudSelectItem Value="@startTime">
@startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern)
</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Duration</MudText>
</div>
<MudTextField T="int"
@bind-Value="_durationHours"
Adornment="Adornment.End"
AdornmentText="hours"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<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>
</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="@(_ => AddDecoToDecoTemplate())" Disabled="@(_selectedDeco is null)" StartIcon="@Icons.Material.Filled.Add">
Add Deco To Template
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Remove Content</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 To Remove</MudText>
</div>
<MudSelect T="DecoTemplateItemEditViewModel" @bind-Value="_decoToRemove">
<MudSelectItem Value="@((DecoTemplateItemEditViewModel)null)">(none)</MudSelectItem>
@foreach (DecoTemplateItemEditViewModel item in _decoTemplate.Items.OrderBy(i => i.Start))
{
<MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem>
}
</MudSelect>
</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="@(_ => RemoveDecoFromDecoTemplate())" Disabled="@(_decoToRemove is null)" StartIcon="@Icons.Material.Filled.Remove">
Remove Deco From Template
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Content</MudText>
<MudDivider Class="mb-6"/>
<MudCalendar T="CalendarItem"
Class="mb-6"
Items="@_decoTemplate.Items"
ShowMonth="false"
ShowWeek="false"
ShowPrevNextButtons="false"
ShowDatePicker="false"
ShowTodayButton="false"
DayTimeInterval="CalendarTimeInterval.Minutes10"
Use24HourClock="@(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern.Contains("H"))"
EnableDragItems="true"
EnableResizeItems="false"
ItemChanged="@(ci => CalendarItemChanged(ci))"/>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private readonly List<DecoGroupViewModel> _decoGroups = [];
private readonly List<DecoViewModel> _decos = [];
private readonly List<DateTime> _startTimes = [];
[Parameter]
public int Id { get; set; }
private DecoTemplateItemsEditViewModel _decoTemplate = new();
private DecoTemplateItemEditViewModel _decoToRemove;
private DecoGroupViewModel _selectedDecoGroup;
private DecoViewModel _selectedDeco;
private DateTime _selectedDecoStart;
private int _durationHours;
private int _durationMinutes = 15;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadDecoTemplateItems();
DateTime start = DateTime.Today;
_selectedDecoStart = start;
while (start.Date == DateTime.Today.Date)
{
_startTimes.Add(start);
start = start.AddMinutes(5);
}
}
private async Task LoadDecoTemplateItems()
{
Option<DecoTemplateViewModel> maybeDecoTemplate = await Mediator.Send(new GetDecoTemplateById(Id), _cts.Token);
if (maybeDecoTemplate.IsNone)
{
NavigationManager.NavigateTo("deco-templates");
return;
}
foreach (DecoTemplateViewModel template in maybeDecoTemplate)
{
_decoTemplate = new DecoTemplateItemsEditViewModel { Name = template.Name };
}
Option<IEnumerable<DecoTemplateItemViewModel>> maybeResults = await Mediator.Send(new GetDecoTemplateItems(Id), _cts.Token);
foreach (IEnumerable<DecoTemplateItemViewModel> items in maybeResults)
{
_decoTemplate.Items.AddRange(items.Map(ProjectToEditViewModel));
}
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token));
}
private static DecoTemplateItemEditViewModel ProjectToEditViewModel(DecoTemplateItemViewModel item) =>
new()
{
DecoId = item.DecoId,
DecoName = item.DecoName,
Start = item.StartTime,
End = item.EndTime
};
private async Task UpdateDecoGroupItems(DecoGroupViewModel decoGroup)
{
_selectedDecoGroup = decoGroup;
_decos.Clear();
_decos.AddRange(await Mediator.Send(new GetDecosByDecoGroupId(_selectedDecoGroup.Id), _cts.Token));
}
private void AddDecoToDecoTemplate()
{
// find first time where this deco will fit
DateTime maybeStart = _selectedDecoStart;
while (maybeStart.Date == DateTime.Today)
{
DateTime maybeEnd = maybeStart.AddHours(_durationHours).AddMinutes(_durationMinutes);
if (IntersectsOthers(null, maybeStart, maybeEnd) == false)
{
var item = new DecoTemplateItemEditViewModel
{
DecoId = _selectedDeco.Id,
DecoName = _selectedDeco.Name,
Start = maybeStart,
End = maybeEnd,
LastStart = maybeStart,
LastEnd = maybeEnd
};
_decoTemplate.Items.Add(item);
break;
}
maybeStart = maybeStart.AddMinutes(5);
}
}
private async Task RemoveDecoFromDecoTemplate()
{
if (_decoToRemove is not null)
{
_decoTemplate.Items.Remove(_decoToRemove);
_decoToRemove = null;
await InvokeAsync(StateHasChanged);
}
}
private void CalendarItemChanged(CalendarItem calendarItem)
{
// don't allow any overlap
if (calendarItem is DecoTemplateItemEditViewModel item)
{
bool intersects = item.End.HasValue && IntersectsOthers(item, item.Start, item.End.Value);
bool crossesMidnight = item.End.HasValue && item.End.Value.TimeOfDay > TimeSpan.Zero && item.End.Value.TimeOfDay < item.Start.TimeOfDay;
if (intersects || crossesMidnight)
{
// roll back
item.Start = item.LastStart;
item.End = item.LastEnd;
}
else
{
// commit
item.LastStart = item.Start;
item.LastEnd = item.End;
}
}
}
private bool IntersectsOthers(DecoTemplateItemEditViewModel item, DateTime start, DateTime end)
{
var willFit = true;
foreach (DecoTemplateItemEditViewModel existing in _decoTemplate.Items)
{
if (existing == item)
{
continue;
}
if (start < existing.End && existing.Start < end)
{
willFit = false;
break;
}
}
return willFit == false;
}
// private void RemoveDecoTemplateItem(DecoTemplateItemEditViewModel item)
// {
// _selectedItem = null;
// _decoTemplate.Items.Remove(item);
// }
private async Task SaveChanges()
{
if (_decoTemplate.Items.Any(i => i.End is null))
{
Snackbar.Add("Template item cannot end after midnight", Severity.Error);
return;
}
var items = _decoTemplate.Items.Map(item => new ReplaceDecoTemplateItem(item.DecoId, item.Start.TimeOfDay, item.End!.Value.TimeOfDay)).ToList();
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceDecoTemplateItems(Id, _decoTemplate.Name, items), _cts.Token)
.Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match(
error =>
{
Snackbar.Add($"Unexpected error saving template: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving template: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("deco-templates"));
}
}