* template editor improvements * more keyboard navigation * replace template tree view with template table
327 lines
13 KiB
Plaintext
327 lines
13 KiB
Plaintext
@page "/templates/{Id:int}"
|
|
@using System.Globalization
|
|
@using ErsatzTV.Application.Scheduling
|
|
@implements IDisposable
|
|
@inject NavigationManager NavigationManager
|
|
@inject ILogger<TemplateEditor> 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-6" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save">
|
|
Save 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>Template Group Name</MudText>
|
|
</div>
|
|
<MudTextField @bind-Value="_template.GroupName" Disabled="true"/>
|
|
</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="_template.Name" For="@(() => _template.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>Block Group</MudText>
|
|
</div>
|
|
<MudSelect T="string" ValueChanged="@(name => UpdateBlockGroupItems(name))">
|
|
@foreach (BlockGroupViewModel blockGroup in _blockGroups)
|
|
{
|
|
<MudSelectItem Value="@blockGroup.Name">@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</MudText>
|
|
</div>
|
|
<MudSelect T="string" ValueChanged="@(name => UpdateSelectedBlock(name))">
|
|
@foreach (BlockViewModel block in _blocks)
|
|
{
|
|
<MudSelectItem Value="@block.Name">@block.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="_selectedBlockStart">
|
|
@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"></div>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlockToTemplate())" Disabled="@(_selectedBlock is null)" StartIcon="@Icons.Material.Filled.Add">
|
|
Add Block 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>Block To Remove</MudText>
|
|
</div>
|
|
<MudSelect T="TemplateItemEditViewModel" @bind-Value="_blockToRemove">
|
|
<MudSelectItem Value="@((TemplateItemEditViewModel)null)">(none)</MudSelectItem>
|
|
@foreach (TemplateItemEditViewModel item in _template.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="@(_ => RemoveBlockFromTemplate())" Disabled="@(_blockToRemove is null)" StartIcon="@Icons.Material.Filled.Remove">
|
|
Remove Block From Template
|
|
</MudButton>
|
|
</MudStack>
|
|
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Content</MudText>
|
|
<MudDivider Class="mb-6"/>
|
|
<MudCalendar @ref="_calendar"
|
|
T="CalendarItem"
|
|
Class="mb-6"
|
|
Items="@_template.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 CancellationTokenSource _cts;
|
|
private readonly List<BlockGroupViewModel> _blockGroups = [];
|
|
private readonly List<BlockViewModel> _blocks = [];
|
|
private readonly List<DateTime> _startTimes = [];
|
|
|
|
[Parameter]
|
|
public int Id { get; set; }
|
|
|
|
private TemplateItemsEditViewModel _template = new();
|
|
private TemplateItemEditViewModel _blockToRemove;
|
|
private BlockGroupViewModel _selectedBlockGroup;
|
|
private BlockViewModel _selectedBlock;
|
|
private DateTime _selectedBlockStart;
|
|
private MudCalendar<CalendarItem> _calendar;
|
|
|
|
public void Dispose()
|
|
{
|
|
_cts?.Cancel();
|
|
_cts?.Dispose();
|
|
}
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
_cts?.Cancel();
|
|
_cts?.Dispose();
|
|
_cts = new CancellationTokenSource();
|
|
var token = _cts.Token;
|
|
|
|
try
|
|
{
|
|
await LoadTemplateItems(token);
|
|
|
|
DateTime start = DateTime.Today;
|
|
_selectedBlockStart = start;
|
|
while (start.Date == DateTime.Today.Date)
|
|
{
|
|
_startTimes.Add(start);
|
|
start = start.AddMinutes(5);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
private async Task LoadTemplateItems(CancellationToken cancellationToken)
|
|
{
|
|
Option<TemplateViewModel> maybeTemplate = await Mediator.Send(new GetTemplateById(Id), cancellationToken);
|
|
if (maybeTemplate.IsNone)
|
|
{
|
|
NavigationManager.NavigateTo("templates");
|
|
return;
|
|
}
|
|
|
|
foreach (TemplateViewModel template in maybeTemplate)
|
|
{
|
|
_template = new TemplateItemsEditViewModel
|
|
{
|
|
GroupId = template.TemplateGroupId,
|
|
GroupName = template.GroupName,
|
|
Name = template.Name
|
|
};
|
|
}
|
|
|
|
Option<IEnumerable<TemplateItemViewModel>> maybeResults = await Mediator.Send(new GetTemplateItems(Id), cancellationToken);
|
|
foreach (IEnumerable<TemplateItemViewModel> items in maybeResults)
|
|
{
|
|
_template.Items.AddRange(items.Map(ProjectToEditViewModel));
|
|
}
|
|
|
|
_blockGroups.AddRange(await Mediator.Send(new GetAllBlockGroups(), cancellationToken));
|
|
}
|
|
|
|
private static TemplateItemEditViewModel ProjectToEditViewModel(TemplateItemViewModel item) =>
|
|
new()
|
|
{
|
|
BlockId = item.BlockId,
|
|
BlockName = item.BlockName,
|
|
Start = item.StartTime,
|
|
End = item.EndTime
|
|
};
|
|
|
|
private async Task UpdateBlockGroupItems(string blockGroupName)
|
|
{
|
|
Option<BlockGroupViewModel> maybeBlockGroup = Optional(_blockGroups.Find(bg => bg.Name == blockGroupName));
|
|
foreach (var blockGroup in maybeBlockGroup)
|
|
{
|
|
_selectedBlockGroup = blockGroup;
|
|
|
|
_blocks.Clear();
|
|
_blocks.AddRange(await Mediator.Send(new GetBlocksByBlockGroupId(_selectedBlockGroup.Id), _cts.Token));
|
|
}
|
|
}
|
|
|
|
private void UpdateSelectedBlock(string blockName)
|
|
{
|
|
_selectedBlock = _blocks.Find(bg => bg.Name == blockName);
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void AddBlockToTemplate()
|
|
{
|
|
// find first time where this block will fit
|
|
DateTime maybeStart = _selectedBlockStart;
|
|
while (maybeStart.Date == DateTime.Today)
|
|
{
|
|
DateTime maybeEnd = maybeStart.AddMinutes(_selectedBlock.Minutes);
|
|
if (!IntersectsOthers(null, maybeStart, maybeEnd))
|
|
{
|
|
var item = new TemplateItemEditViewModel
|
|
{
|
|
BlockId = _selectedBlock.Id,
|
|
BlockName = _selectedBlock.Name,
|
|
Start = maybeStart,
|
|
End = maybeEnd,
|
|
LastStart = maybeStart,
|
|
LastEnd = maybeEnd
|
|
};
|
|
|
|
_template.Items.Add(item);
|
|
|
|
InvokeAsync(() => { _calendar.ScrollToTime(TimeOnly.FromDateTime(item.Start)); });
|
|
|
|
break;
|
|
}
|
|
|
|
maybeStart = maybeStart.AddMinutes(5);
|
|
}
|
|
}
|
|
|
|
private async Task RemoveBlockFromTemplate()
|
|
{
|
|
if (_blockToRemove is not null)
|
|
{
|
|
_template.Items.Remove(_blockToRemove);
|
|
|
|
await InvokeAsync(() => { _calendar.ScrollToTime(TimeOnly.FromDateTime(_blockToRemove.Start)); });
|
|
|
|
_blockToRemove = null;
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
private void CalendarItemChanged(CalendarItem calendarItem)
|
|
{
|
|
// don't allow any overlap
|
|
if (calendarItem is TemplateItemEditViewModel 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(TemplateItemEditViewModel item, DateTime start, DateTime end)
|
|
{
|
|
var willFit = true;
|
|
|
|
foreach (TemplateItemEditViewModel existing in _template.Items)
|
|
{
|
|
if (existing == item)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (start < existing.End && existing.Start < end)
|
|
{
|
|
willFit = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return !willFit;
|
|
}
|
|
|
|
// private void RemoveTemplateItem(TemplateItemEditViewModel item)
|
|
// {
|
|
// _selectedItem = null;
|
|
// _template.Items.Remove(item);
|
|
// }
|
|
|
|
private async Task SaveChanges()
|
|
{
|
|
var items = _template.Items.Map(item => new ReplaceTemplateItem(item.BlockId, item.Start.TimeOfDay)).ToList();
|
|
|
|
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceTemplateItems(_template.GroupId, Id, _template.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("templates"));
|
|
}
|
|
|
|
}
|