* add letter bar with no links * use lucene for search, add paged search results * add search index version * index library_name; rebuild index when folder is missing * maintain index as local movies change * fix tests * maintain index as local shows change * maintain index as plex movies change * maintain index as plex shows change * code cleanup * add duplicate filter to search * add links to letter bar * code cleanup
302 lines
15 KiB
Plaintext
302 lines
15 KiB
Plaintext
@page "/schedules/{Id:int}/items"
|
|
@using ErsatzTV.Application.MediaCollections
|
|
@using ErsatzTV.Application.MediaCollections.Queries
|
|
@using ErsatzTV.Application.MediaItems
|
|
@using ErsatzTV.Application.ProgramSchedules
|
|
@using ErsatzTV.Application.ProgramSchedules.Commands
|
|
@using ErsatzTV.Application.ProgramSchedules.Queries
|
|
@using ErsatzTV.Application.Television.Queries
|
|
@inject NavigationManager NavigationManager
|
|
@inject ILogger<ScheduleItemsEditor> Logger
|
|
@inject ISnackbar Snackbar
|
|
@inject IMediator Mediator
|
|
|
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
|
<MudTable Hover="true" Items="_schedule.Items.OrderBy(i => i.Index)" Dense="true" @bind-SelectedItem="_selectedItem">
|
|
<ToolBarContent>
|
|
<MudText Typo="Typo.h6">@_schedule.Name Items</MudText>
|
|
</ToolBarContent>
|
|
<ColGroup>
|
|
<col/>
|
|
<col/>
|
|
<col/>
|
|
<col style="width: 60px;"/>
|
|
<col style="width: 60px;"/>
|
|
<col style="width: 60px;"/>
|
|
</ColGroup>
|
|
<HeaderContent>
|
|
<MudTh>Start Time</MudTh>
|
|
<MudTh>Collection</MudTh>
|
|
<MudTh>Playout Mode</MudTh>
|
|
<MudTh/>
|
|
<MudTh/>
|
|
<MudTh/>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Start Time">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd DataLabel="Collection">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@context.CollectionName
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd DataLabel="Playout Mode">
|
|
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
|
@context.PlayoutMode
|
|
@if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue)
|
|
{
|
|
@($" ({context.MultipleCount})")
|
|
}
|
|
</MudText>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
|
|
OnClick="@(_ => MoveItemUp(context))"
|
|
Disabled="@(_schedule.Items.All(x => x.Index >= context.Index))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
|
|
OnClick="@(_ => MoveItemDown(context))"
|
|
Disabled="@(_schedule.Items.All(x => x.Index <= context.Index))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
OnClick="@(_ => RemoveScheduleItem(context))">
|
|
</MudIconButton>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<PagerContent>
|
|
<MudTablePager/>
|
|
</PagerContent>
|
|
</MudTable>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddScheduleItem())" Class="mt-4">
|
|
Add Schedule Item
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
|
|
Save Changes
|
|
</MudButton>
|
|
|
|
@if (_selectedItem is not null)
|
|
{
|
|
<div style="display: flex; flex-direction: row;" class="mt-6">
|
|
<div style="flex-grow: 1; max-width: 400px;">
|
|
<EditForm Model="_selectedItem">
|
|
<FluentValidator/>
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudSelect Label="Start Type" @bind-Value="_selectedItem.StartType" For="@(() => _selectedItem.StartType)">
|
|
@foreach (StartType startType in Enum.GetValues<StartType>())
|
|
{
|
|
<MudSelectItem Value="@startType">@startType</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
|
|
<MudSelect Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
|
|
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
|
|
{
|
|
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="MediaCollectionViewModel"
|
|
Label="Collection"
|
|
@bind-value="_selectedItem.Collection"
|
|
SearchFunc="@SearchMediaCollections"
|
|
ToStringFunc="@(c => c?.Name)"/>
|
|
}
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="NamedMediaItemViewModel"
|
|
Label="Television Show"
|
|
@bind-value="_selectedItem.MediaItem"
|
|
SearchFunc="@SearchTelevisionShows"
|
|
ToStringFunc="@(s => s?.Name)"/>
|
|
}
|
|
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
|
|
{
|
|
<MudAutocomplete Class="mt-3"
|
|
T="NamedMediaItemViewModel"
|
|
Label="Television Season"
|
|
@bind-value="_selectedItem.MediaItem"
|
|
SearchFunc="@SearchTelevisionSeasons"
|
|
ToStringFunc="@(s => s?.Name)"/>
|
|
}
|
|
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
|
|
@foreach (PlayoutMode playoutMode in Enum.GetValues<PlayoutMode>())
|
|
{
|
|
<MudSelectItem Value="@playoutMode">@playoutMode</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
<MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/>
|
|
<MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
|
<MudElement HtmlTag="div" Class="mt-3">
|
|
<MudSwitch Label="Offline Tail" @bind-Checked="@_selectedItem.OfflineTail" For="@(() => _selectedItem.OfflineTail)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
|
</MudElement>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</EditForm>
|
|
</div>
|
|
<div style="max-width: 600px;" class="ml-6">
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudText Class="mt-3" Typo="Typo.h6">
|
|
Start Type
|
|
</MudText>
|
|
<MudText Class="mt-3">
|
|
A <b>fixed</b> start type requires a <b>start time</b>, while a <b>dynamic</b> start type means the schedule item will start immediately after the preceding schedule item.
|
|
</MudText>
|
|
<MudText Class="mt-3 mb-2" Typo="Typo.h6">Playout Mode</MudText>
|
|
<ul class="mud-typography-body1">
|
|
<li><b>One</b> - to play one media item from the collection before advancing to the next schedule item.</li>
|
|
<li><b>Multiple</b> - to play a specified <b>count</b> of media items from the collection before advancing to the next schedule item.</li>
|
|
<li><b>Duration</b> - to play the maximum number of complete media items that will fit in the specified <b>playout duration</b>, before either going offline for the remainder of the <b>playout duration</b> (an <b>offline tail</b>), or immediately advancing to the next schedule item.</li>
|
|
<li><b>Flood</b> - to play media items from the collection forever, or until the next schedule item's <b>start time</b> if one exists.</li>
|
|
</ul>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</div>
|
|
</div>
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
|
|
[Parameter]
|
|
public int Id { get; set; }
|
|
|
|
private ProgramScheduleItemsEditViewModel _schedule;
|
|
private List<MediaCollectionViewModel> _mediaCollections;
|
|
private List<NamedMediaItemViewModel> _televisionShows;
|
|
private List<NamedMediaItemViewModel> _televisionSeasons;
|
|
|
|
private ProgramScheduleItemEditViewModel _selectedItem;
|
|
|
|
protected override Task OnParametersSetAsync() => LoadScheduleItems();
|
|
|
|
private async Task LoadScheduleItems()
|
|
{
|
|
_mediaCollections = await Mediator.Send(new GetAllCollections());
|
|
_televisionShows = await Mediator.Send(new GetAllTelevisionShows());
|
|
_televisionSeasons = await Mediator.Send(new GetAllTelevisionSeasons());
|
|
|
|
string name = string.Empty;
|
|
Option<ProgramScheduleViewModel> maybeSchedule = await Mediator.Send(new GetProgramScheduleById(Id));
|
|
maybeSchedule.IfSome(vm => name = vm.Name);
|
|
|
|
Option<IEnumerable<ProgramScheduleItemViewModel>> maybeResults = await Mediator.Send(new GetProgramScheduleItems(Id));
|
|
maybeResults.IfSome(items => _schedule = new ProgramScheduleItemsEditViewModel
|
|
{
|
|
Name = name,
|
|
Items = items.Map(ProjectToEditViewModel).ToList()
|
|
});
|
|
}
|
|
|
|
private ProgramScheduleItemEditViewModel ProjectToEditViewModel(ProgramScheduleItemViewModel item)
|
|
{
|
|
var result = new ProgramScheduleItemEditViewModel
|
|
{
|
|
Id = item.Id,
|
|
Index = item.Index,
|
|
StartType = item.StartType,
|
|
StartTime = item.StartTime,
|
|
PlayoutMode = item.PlayoutMode,
|
|
CollectionType = item.CollectionType,
|
|
Collection = item.Collection,
|
|
MediaItem = item.MediaItem
|
|
};
|
|
|
|
switch (item)
|
|
{
|
|
case ProgramScheduleItemMultipleViewModel multiple:
|
|
result.MultipleCount = multiple.Count;
|
|
break;
|
|
case ProgramScheduleItemDurationViewModel duration:
|
|
result.PlayoutDuration = duration.PlayoutDuration;
|
|
result.OfflineTail = duration.OfflineTail;
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void AddScheduleItem()
|
|
{
|
|
var item = new ProgramScheduleItemEditViewModel
|
|
{
|
|
Index = _schedule.Items.Map(i => i.Index).DefaultIfEmpty().Max() + 1,
|
|
StartType = StartType.Dynamic,
|
|
PlayoutMode = PlayoutMode.One,
|
|
CollectionType = ProgramScheduleItemCollectionType.Collection
|
|
};
|
|
|
|
_schedule.Items.Add(item);
|
|
_selectedItem = item;
|
|
}
|
|
|
|
private void RemoveScheduleItem(ProgramScheduleItemEditViewModel item)
|
|
{
|
|
_selectedItem = null;
|
|
_schedule.Items.Remove(item);
|
|
}
|
|
|
|
private void MoveItemUp(ProgramScheduleItemEditViewModel item)
|
|
{
|
|
// swap with lower index
|
|
ProgramScheduleItemEditViewModel toSwap = _schedule.Items.OrderByDescending(x => x.Index).First(x => x.Index < item.Index);
|
|
int temp = toSwap.Index;
|
|
toSwap.Index = item.Index;
|
|
item.Index = temp;
|
|
}
|
|
|
|
private void MoveItemDown(ProgramScheduleItemEditViewModel item)
|
|
{
|
|
// swap with higher index
|
|
ProgramScheduleItemEditViewModel toSwap = _schedule.Items.OrderBy(x => x.Index).First(x => x.Index > item.Index);
|
|
int temp = toSwap.Index;
|
|
toSwap.Index = item.Index;
|
|
item.Index = temp;
|
|
}
|
|
|
|
private Task<IEnumerable<MediaCollectionViewModel>> SearchMediaCollections(string value) =>
|
|
_mediaCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionShows(string value) =>
|
|
_televisionShows.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private Task<IEnumerable<NamedMediaItemViewModel>> SearchTelevisionSeasons(string value) =>
|
|
_televisionSeasons.Filter(s => s.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
|
|
|
|
private async Task SaveChanges()
|
|
{
|
|
var items = _schedule.Items.Map(item => new ReplaceProgramScheduleItem(
|
|
item.Index,
|
|
item.StartType,
|
|
item.StartTime,
|
|
item.PlayoutMode,
|
|
item.CollectionType,
|
|
item.Collection?.Id,
|
|
item.MediaItem?.MediaItemId,
|
|
item.MultipleCount,
|
|
item.PlayoutDuration,
|
|
item.PlayoutMode == PlayoutMode.Duration ? item.OfflineTail.IfNone(false) : null)).ToList();
|
|
|
|
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceProgramScheduleItems(Id, items)).Map(e => e.LeftToSeq());
|
|
|
|
errorMessages.HeadOrNone().Match(
|
|
error =>
|
|
{
|
|
Snackbar.Add($"Unexpected error saving schedule: {error.Value}", Severity.Error);
|
|
Logger.LogError("Unexpected error saving schedule: {Error}", error.Value);
|
|
},
|
|
() => NavigationManager.NavigateTo("/schedules"));
|
|
}
|
|
|
|
} |