Browse Source

more mobile layout updates (#2141)

* update trash layout

* cleanup block and yaml playout editors

* spacing cleanup

* rework multi-collection editor

* rework deco template editor

* rework template editor
pull/2142/head
Jason Dove 11 months ago committed by GitHub
parent
commit
174c743cb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
  2. 2
      ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
  3. 4
      ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDeco.cs
  4. 22
      ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDecoHandler.cs
  5. 154
      ErsatzTV/Pages/BlockPlayoutEditor.razor
  6. 18
      ErsatzTV/Pages/CollectionItems.razor
  7. 219
      ErsatzTV/Pages/DecoTemplateEditor.razor
  8. 2
      ErsatzTV/Pages/Libraries.razor
  9. 211
      ErsatzTV/Pages/MultiCollectionEditor.razor
  10. 18
      ErsatzTV/Pages/Search.razor
  11. 170
      ErsatzTV/Pages/TemplateEditor.razor
  12. 700
      ErsatzTV/Pages/Trash.razor
  13. 52
      ErsatzTV/Pages/YamlPlayoutEditor.razor
  14. 42
      ErsatzTV/Shared/EditYamlFileDialog.razor
  15. 9
      ErsatzTV/Validators/MultiCollectionEditViewModelValidator.cs

1
ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings

@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artworks_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean>

2
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs

@ -14,4 +14,6 @@ public record PlayoutNameViewModel(
TimeSpan? DbDailyRebuildTime) TimeSpan? DbDailyRebuildTime)
{ {
public Option<TimeSpan> DailyRebuildTime => Optional(DbDailyRebuildTime); public Option<TimeSpan> DailyRebuildTime => Optional(DbDailyRebuildTime);
public string TemplateFile { get; set; } = TemplateFile;
} }

4
ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDeco.cs

@ -1,3 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling; namespace ErsatzTV.Application.Scheduling;
public record UpdateDefaultDeco(int PlayoutId, int? DecoId) : IRequest; public record UpdateDefaultDeco(int PlayoutId, int? DecoId) : IRequest<Option<BaseError>>;

22
ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDecoHandler.cs

@ -1,17 +1,27 @@
using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling; namespace ErsatzTV.Application.Scheduling;
public class UpdateDefaultDecoHandler(IDbContextFactory<TvContext> dbContextFactory) public class UpdateDefaultDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<UpdateDefaultDeco> : IRequestHandler<UpdateDefaultDeco, Option<BaseError>>
{ {
public async Task Handle(UpdateDefaultDeco request, CancellationToken cancellationToken) public async Task<Option<BaseError>> Handle(UpdateDefaultDeco request, CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); try
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await dbContext.Playouts await dbContext.Playouts
.Where(p => p.Id == request.PlayoutId) .Where(p => p.Id == request.PlayoutId)
.ExecuteUpdateAsync(u => u.SetProperty(p => p.DecoId, p => request.DecoId), cancellationToken); .ExecuteUpdateAsync(u => u.SetProperty(p => p.DecoId, p => request.DecoId), cancellationToken);
return Option<BaseError>.None;
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
} }
} }

154
ErsatzTV/Pages/BlockPlayoutEditor.razor

@ -6,84 +6,80 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
@inject IEntityLocker EntityLocker; @inject IEntityLocker EntityLocker;
@inject ILogger<BlockPlayoutEditor> Logger
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Edit Block Playout - @_channelName</MudText> <MudForm Style="max-height: 100%">
<MudGrid Class="mt-4"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudCard Class="mr-6 mb-6" Style="width: 400px"> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="@(_ => SaveDefaultDeco())" StartIcon="@Icons.Material.Filled.Save">
<MudCardHeader> Save Default Deco
<CardHeaderContent> </MudButton>
<MudText Typo="Typo.h5">Playout Templates</MudText> </MudPaper>
</CardHeaderContent> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
</MudCardHeader> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCardContent> <MudText Typo="Typo.h5" Class="mb-2">@_channelName - Block Playout</MudText>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" Href="@($"playouts/{Id}/templates")" Class="mt-4"> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Playout Templates</MudText>
</div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" Href="@($"playouts/{Id}/templates")" StartIcon="@Icons.Material.Filled.Edit">
Edit Templates Edit Templates
</MudButton> </MudButton>
</MudCardContent> </MudStack>
</MudCard> <MudText Typo="Typo.h5" Class="mt-10 mb-2">Default Deco</MudText>
<MudCard Class="mr-6 mb-6" Style="width: 400px"> <MudDivider Class="mb-6"/>
<MudCardHeader> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<CardHeaderContent> <div class="d-flex">
<MudText Typo="Typo.h5">Playout Items and History</MudText> <MudText>Enable Default Deco</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Warning" OnClick="@(_ => EraseItems(eraseHistory: false))" Class="mt-4">
Erase Items
</MudButton>
</div> </div>
<div> <MudCheckBox @bind-Value="_enableDefaultDeco" Color="Color.Primary" Dense="true"/>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" Class="mt-4"> </MudStack>
Erase Items and History @if (_enableDefaultDeco)
</MudButton> {
<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"
Value="@_selectedDefaultDecoGroup"
ValueChanged="@(vm => UpdateDefaultDecoTemplateGroupItems(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 @bind-Value="_defaultDeco" For="@(() => _defaultDeco)">
@foreach (DecoViewModel deco in _decos)
{
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
}
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Maintenance</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>Playout Items and History</MudText>
</div> </div>
</MudCardContent> <MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Warning" OnClick="@(_ => EraseItems(eraseHistory: false))" StartIcon="@Icons.Material.Filled.Delete">
</MudCard> Erase Items
<MudCard Class="mr-6 mb-6" Style="width: 400px"> </MudButton>
<MudCardHeader> </MudStack>
<CardHeaderContent> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudText Typo="Typo.h5">Default Deco</MudText> <div class="d-flex"></div>
</CardHeaderContent> <MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" StartIcon="@Icons.Material.Filled.Delete">
</MudCardHeader> Erase Items and History
<MudCardContent>
<MudElement HtmlTag="div" Class="mt-3">
<MudSwitch T="bool" Label="Enable Default Deco" @bind-Value="_enableDefaultDeco" Color="Color.Primary"/>
</MudElement>
@if (_enableDefaultDeco)
{
<MudElement HtmlTag="div" Class="mt-2">
<MudSelect T="DecoGroupViewModel"
Label="Deco Group"
Value="@_selectedDefaultDecoGroup"
ValueChanged="@(vm => UpdateDefaultDecoTemplateGroupItems(vm))">
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
{
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
}
</MudSelect>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudSelect Label="Deco"
@bind-Value="_defaultDeco"
For="@(() => _defaultDeco)">
@foreach (DecoViewModel deco in _decos)
{
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
}
</MudSelect>
</MudElement>
}
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveDefaultDeco())">
Save Changes
</MudButton> </MudButton>
</MudCardActions> </MudStack>
</MudCard> </MudContainer>
</MudGrid> </div>
</MudContainer> </MudForm>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
@ -152,7 +148,17 @@
private async Task SaveDefaultDeco() private async Task SaveDefaultDeco()
{ {
int? decoId = _enableDefaultDeco ? _defaultDeco?.Id : null; int? decoId = _enableDefaultDeco ? _defaultDeco?.Id : null;
await Mediator.Send(new UpdateDefaultDeco(Id, decoId), _cts.Token); Option<BaseError> result = await Mediator.Send(new UpdateDefaultDeco(Id, decoId), _cts.Token);
result.Match(
error =>
{
Snackbar.Add($"Unexpected error saving default deco: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving default deco: {Error}", error.Value);
},
() =>
{
Snackbar.Add($"Saved default deco for playout {_channelName}", Severity.Success);
});
} }
} }

18
ErsatzTV/Pages/CollectionItems.razor

@ -116,7 +116,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" UserAttributes="@(new Dictionary<string, object> { { "id", "sortable-collection" } })"> <MudStack Row="true" Wrap="Wrap.Wrap" UserAttributes="@(new Dictionary<string, object> { { "id", "sortable-collection" } })" Class="mb-10">
@foreach (MovieCardViewModel card in OrderMovies(_data.MovieCards)) @foreach (MovieCardViewModel card in OrderMovies(_data.MovieCards))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -140,7 +140,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle)) @foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -164,7 +164,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle)) @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -190,7 +190,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired)) @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -215,7 +215,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (ArtistCardViewModel card in _data.ArtistCards.OrderBy(e => e.SortTitle)) @foreach (ArtistCardViewModel card in _data.ArtistCards.OrderBy(e => e.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -240,7 +240,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (MusicVideoCardViewModel card in _data.MusicVideoCards.OrderBy(e => e.SortTitle)) @foreach (MusicVideoCardViewModel card in _data.MusicVideoCards.OrderBy(e => e.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -265,7 +265,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (OtherVideoCardViewModel card in _data.OtherVideoCards.OrderBy(e => e.SortTitle)) @foreach (OtherVideoCardViewModel card in _data.OtherVideoCards.OrderBy(e => e.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -290,7 +290,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (SongCardViewModel card in _data.SongCards.OrderBy(e => e.SortTitle)) @foreach (SongCardViewModel card in _data.SongCards.OrderBy(e => e.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -315,7 +315,7 @@
</MudText> </MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (ImageCardViewModel card in _data.ImageCards.OrderBy(e => e.SortTitle)) @foreach (ImageCardViewModel card in _data.ImageCards.OrderBy(e => e.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"

219
ErsatzTV/Pages/DecoTemplateEditor.razor

@ -7,112 +7,115 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudForm Style="max-height: 100%">
<MudText Typo="Typo.h4" Class="mb-4">Edit Deco Template</MudText> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudGrid> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save">
<MudItem xs="4"> Save Deco Template
<div style="max-width: 400px"> </MudButton>
<MudCard> </MudPaper>
<MudCardContent> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudTextField Label="Name" @bind-Value="_decoTemplate.Name" For="@(() => _decoTemplate.Name)"/> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
</MudCardContent> <MudText Typo="Typo.h5" Class="mb-2">General</MudText>
</MudCard> <MudDivider Class="mb-6"/>
</div> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4"> <div class="d-flex">
Save Changes <MudText>Name</MudText>
</MudButton> </div>
</MudItem> <MudTextField @bind-Value="_decoTemplate.Name" For="@(() => _decoTemplate.Name)"/>
<MudItem xs="4"> </MudStack>
<div style="max-width: 400px"> <MudText Typo="Typo.h5" Class="mt-10 mb-2">Add Content</MudText>
<MudCard> <MudDivider Class="mb-6"/>
<MudCardContent> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudSelect T="DecoGroupViewModel" <div class="d-flex">
Label="Deco Group" <MudText>Deco Group</MudText>
ValueChanged="@(vm => UpdateDecoGroupItems(vm))"> </div>
@foreach (DecoGroupViewModel decoGroup in _decoGroups) <MudSelect T="DecoGroupViewModel" ValueChanged="@(vm => UpdateDecoGroupItems(vm))">
{ @foreach (DecoGroupViewModel decoGroup in _decoGroups)
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem> {
} <MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
</MudSelect> }
<MudSelect Class="mt-3" </MudSelect>
T="DecoViewModel" </MudStack>
Label="Deco" <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
@bind-value="_selectedDeco"> <div class="d-flex">
@foreach (DecoViewModel deco in _decos) <MudText>Deco</MudText>
{ </div>
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem> <MudSelect T="DecoViewModel" @bind-value="_selectedDeco">
} @foreach (DecoViewModel deco in _decos)
</MudSelect> {
<MudSelect Class="mt-3" <MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
T="DateTime" }
Label="Start Time On Or After" </MudSelect>
@bind-value="_selectedDecoStart"> </MudStack>
@foreach (DateTime startTime in _startTimes) <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
{ <div class="d-flex">
<MudSelectItem Value="@startTime"> <MudText>Start Time On Or After</MudText>
@startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern) </div>
</MudSelectItem> <MudSelect T="DateTime" @bind-value="_selectedDecoStart">
} @foreach (DateTime startTime in _startTimes)
</MudSelect> {
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center"> <MudSelectItem Value="@startTime">
<MudItem xs="6"> @startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern)
<MudTextField T="int" </MudSelectItem>
Label="Duration" }
@bind-Value="_durationHours" </MudSelect>
Adornment="Adornment.End" </MudStack>
AdornmentText="hours"/> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
</MudItem> <div class="d-flex">
<MudItem xs="6"> <MudText>Duration</MudText>
<MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes"> </div>
<MudSelectItem Value="0"/> <MudTextField T="int"
<MudSelectItem Value="5"/> @bind-Value="_durationHours"
<MudSelectItem Value="10"/> Adornment="Adornment.End"
<MudSelectItem Value="15"/> AdornmentText="hours"/>
<MudSelectItem Value="20"/> </MudStack>
<MudSelectItem Value="25"/> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudSelectItem Value="30"/> <div class="d-flex"></div>
<MudSelectItem Value="35"/> <MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes">
<MudSelectItem Value="40"/> <MudSelectItem Value="0"/>
<MudSelectItem Value="45"/> <MudSelectItem Value="5"/>
<MudSelectItem Value="50"/> <MudSelectItem Value="10"/>
<MudSelectItem Value="55"/> <MudSelectItem Value="15"/>
</MudSelect> <MudSelectItem Value="20"/>
</MudItem> <MudSelectItem Value="25"/>
</MudGrid> <MudSelectItem Value="30"/>
</MudCardContent> <MudSelectItem Value="35"/>
<MudCardActions> <MudSelectItem Value="40"/>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoToDecoTemplate())" Disabled="@(_selectedDeco is null)"> <MudSelectItem Value="45"/>
Add Deco To Deco Template <MudSelectItem Value="50"/>
</MudButton> <MudSelectItem Value="55"/>
</MudCardActions> </MudSelect>
</MudCard> </MudStack>
</div> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
</MudItem> <div class="d-flex"></div>
<MudItem xs="4"> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoToDecoTemplate())" Disabled="@(_selectedDeco is null)" StartIcon="@Icons.Material.Filled.Add">
<div style="max-width: 400px"> Add Deco To Template
<MudCard> </MudButton>
<MudCardContent> </MudStack>
<MudSelect T="DecoTemplateItemEditViewModel" <MudText Typo="Typo.h5" Class="mt-10 mb-2">Remove Content</MudText>
Label="Deco To Remove" <MudDivider Class="mb-6"/>
@bind-Value="_decoToRemove"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudSelectItem Value="@((DecoTemplateItemEditViewModel)null)">(none)</MudSelectItem> <div class="d-flex">
@foreach (DecoTemplateItemEditViewModel item in _decoTemplate.Items.OrderBy(i => i.Start)) <MudText>Deco To Remove</MudText>
{ </div>
<MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem> <MudSelect T="DecoTemplateItemEditViewModel" @bind-Value="_decoToRemove">
} <MudSelectItem Value="@((DecoTemplateItemEditViewModel)null)">(none)</MudSelectItem>
</MudSelect> @foreach (DecoTemplateItemEditViewModel item in _decoTemplate.Items.OrderBy(i => i.Start))
</MudCardContent> {
<MudCardActions> <MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveDecoFromDecoTemplate())" Disabled="@(_decoToRemove is null)"> }
Remove Deco From Deco Template </MudSelect>
</MudButton> </MudStack>
</MudCardActions> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
</MudCard> <div class="d-flex"></div>
</div> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveDecoFromDecoTemplate())" Disabled="@(_decoToRemove is null)" StartIcon="@Icons.Material.Filled.Remove">
</MudItem> Remove Deco From Template
<MudItem xs="8"> </MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Content</MudText>
<MudDivider Class="mb-6"/>
<MudCalendar T="CalendarItem" <MudCalendar T="CalendarItem"
Class="mt-4" Class="mb-6"
Items="@_decoTemplate.Items" Items="@_decoTemplate.Items"
ShowMonth="false" ShowMonth="false"
ShowWeek="false" ShowWeek="false"
@ -124,9 +127,9 @@
EnableDragItems="true" EnableDragItems="true"
EnableResizeItems="false" EnableResizeItems="false"
ItemChanged="@(ci => CalendarItemChanged(ci))"/> ItemChanged="@(ci => CalendarItemChanged(ci))"/>
</MudItem> </MudContainer>
</MudGrid> </div>
</MudContainer> </MudForm>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();

2
ErsatzTV/Pages/Libraries.razor

@ -97,7 +97,7 @@
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>
<MudText Typo="Typo.h5" Class="mb-2">External Collections</MudText> <MudText Typo="Typo.h5" Class="mt-10 mb-2">External Collections</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
@if (_externalCollections.Any()) @if (_externalCollections.Any())
{ {

211
ErsatzTV/Pages/MultiCollectionEditor.razor

@ -7,103 +7,108 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject ILogger<MultiCollectionEditor> Logger @inject ILogger<MultiCollectionEditor> Logger
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudForm @ref="_form" @bind-IsValid="@_success" Style="max-height: 100%">
<div style="max-width: 400px;"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Multi Collection" : "Add Multi Collection")</MudText> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => HandleSubmitAsync())" Class="ml-8" StartIcon="@Icons.Material.Filled.Save">
@(IsEdit ? "Save Multi Collection" : "Add Multi Collection")
@if (_editContext is not null) </MudButton>
{ </MudPaper>
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync"> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<FluentValidationValidator/> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCard> <MudText Typo="Typo.h5" Class="mb-2">Multi Collection</MudText>
<MudCardContent> <MudDivider Class="mb-6"/>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudSelect @ref="_collectionSelect" <div class="d-flex">
Class="mt-4" <MudText>Name</MudText>
T="MediaCollectionViewModel" </div>
Label="Collection" <MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Required="true" RequiredError="Multi-collection name is required!"/>
@bind-value="_selectedCollection" </MudStack>
HelperText="Disabled collections are already present in this multi collection"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
@foreach (MediaCollectionViewModel collection in _collections) <div class="d-flex">
{ <MudText>Collections</MudText>
<MudSelectItem Disabled="@(_model.Items.Any(i => i.Collection.CollectionType == collection.CollectionType && i.Collection.Id == collection.Id))" </div>
Value="@collection"> <MudSelect @ref="_collectionSelect"
@collection.Name T="MediaCollectionViewModel"
</MudSelectItem> @bind-value="_selectedCollection"
} HelperText="Disabled collections are already present in this multi collection">
</MudSelect> @foreach (MediaCollectionViewModel collection in _collections)
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(_ => AddCollection())" Class="mt-4 mr-auto"> {
Add Collection <MudSelectItem Disabled="@(_model.Items.Any(i => i.Collection.CollectionType == collection.CollectionType && i.Collection.Id == collection.Id))"
</MudButton> Value="@collection">
<MudSelect @ref="_smartCollectionSelect" @collection.Name
Class="mt-4" </MudSelectItem>
T="SmartCollectionViewModel" }
Label="Smart Collection" </MudSelect>
@bind-value="_selectedSmartCollection" </MudStack>
HelperText="Disabled collections are already present in this multi collection"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
@foreach (SmartCollectionViewModel collection in _smartCollections) <div class="d-flex"></div>
{ <MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(_ => AddCollection())" StartIcon="@Icons.Material.Filled.Add">
<MudSelectItem Disabled="@(_model.Items.Any(i => i.Collection.CollectionType == ProgramScheduleItemCollectionType.SmartCollection && i.Collection.Id == collection.Id))" Add Collection
Value="@collection"> </MudButton>
@collection.Name </MudStack>
</MudSelectItem> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
} <div class="d-flex">
</MudSelect> <MudText>Smart Collections</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(_ => AddSmartCollection())" Class="mt-4 mr-auto"> </div>
Add Smart Collection <MudSelect @ref="_smartCollectionSelect"
</MudButton> T="SmartCollectionViewModel"
</MudCardContent> @bind-value="_selectedSmartCollection"
<MudCardActions> HelperText="Disabled collections are already present in this multi collection">
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="mr-2 ml-auto"> @foreach (SmartCollectionViewModel collection in _smartCollections)
@(IsEdit ? "Save Changes" : "Add Multi Collection") {
</MudButton> <MudSelectItem Disabled="@(_model.Items.Any(i => i.Collection.CollectionType == ProgramScheduleItemCollectionType.SmartCollection && i.Collection.Id == collection.Id))"
</MudCardActions> Value="@collection">
</MudCard> @collection.Name
</EditForm> </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.Secondary" OnClick="@(_ => AddSmartCollection())" StartIcon="@Icons.Material.Filled.Add">
Add Smart Collection
</MudButton>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">@_model.Name Items</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true" Items="_model.Items.OrderBy(i => i.Collection.Name, StringComparer.CurrentCultureIgnoreCase)" Dense="true">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col style="width: 20%"/>
<col style="width: 30%"/>
<col style="width: 60px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Collection</MudTh>
<MudTh>Schedule As Group</MudTh>
<MudTh>Playback Order</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd>
@context.Collection.Name
</MudTd>
<MudTd>
<MudCheckBox @bind-Value="@context.ScheduleAsGroup" For="@(() => context.ScheduleAsGroup)"/>
</MudTd>
<MudTd>
@if (context.ScheduleAsGroup)
{
@(context.Collection.UseCustomPlaybackOrder ? "Custom" : "Chronological")
}
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => RemoveCollection(context))">
</MudIconButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
</div> </div>
</MudForm>
<MudTable Hover="true" Items="_model.Items.OrderBy(i => i.Collection.Name, StringComparer.CurrentCultureIgnoreCase)" Dense="true" Class="mt-6">
<ToolBarContent>
<MudText Typo="Typo.h6">@_model.Name Items</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col style="width: 20%"/>
<col style="width: 30%"/>
<col style="width: 60px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Collection</MudTh>
<MudTh>Schedule As Group</MudTh>
<MudTh>Playback Order</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Collection">
<MudText Typo="Typo.body2">
@context.Collection.Name
</MudText>
</MudTd>
<MudTd DataLabel="Schedule As Group">
<MudCheckBox @bind-Value="@context.ScheduleAsGroup" For="@(() => context.ScheduleAsGroup)"/>
</MudTd>
<MudTd DataLabel="Playback Order">
@if (context.ScheduleAsGroup)
{
<MudText Typo="Typo.body2">
@(context.Collection.UseCustomPlaybackOrder ? "Custom" : "Chronological")
</MudText>
}
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => RemoveCollection(context))">
</MudIconButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
@ -114,8 +119,8 @@
private readonly MultiCollectionEditViewModel _model = private readonly MultiCollectionEditViewModel _model =
new() { Items = new List<MultiCollectionItemEditViewModel>() }; new() { Items = new List<MultiCollectionItemEditViewModel>() };
private EditContext _editContext; private MudForm _form;
private ValidationMessageStore _messageStore; private bool _success;
private List<MediaCollectionViewModel> _collections = new(); private List<MediaCollectionViewModel> _collections = new();
private List<SmartCollectionViewModel> _smartCollections = new(); private List<SmartCollectionViewModel> _smartCollections = new();
private MediaCollectionViewModel _selectedCollection; private MediaCollectionViewModel _selectedCollection;
@ -169,18 +174,12 @@
} }
} }
protected override void OnInitialized()
{
_editContext = new EditContext(_model);
_messageStore = new ValidationMessageStore(_editContext);
}
private bool IsEdit => Id != 0; private bool IsEdit => Id != 0;
private async Task HandleSubmitAsync() private async Task HandleSubmitAsync()
{ {
_messageStore.Clear(); await _form.Validate();
if (_editContext.Validate()) if (_success)
{ {
Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(new UpdateMultiCollection(Id, _model.Name, GetUpdateItems()), _cts.Token)).LeftToSeq() : (await Mediator.Send(new CreateMultiCollection(_model.Name, GetCreateItems()), _cts.Token)).LeftToSeq(); Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(new UpdateMultiCollection(Id, _model.Name, GetUpdateItems()), _cts.Token)).LeftToSeq() : (await Mediator.Send(new CreateMultiCollection(_model.Name, GetCreateItems()), _cts.Token)).LeftToSeq();

18
ErsatzTV/Pages/Search.razor

@ -150,7 +150,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle)) @foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -176,7 +176,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle)) @foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -202,7 +202,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle)) @foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -228,7 +228,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle)) @foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -255,7 +255,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle)) @foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -282,7 +282,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle)) @foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -309,7 +309,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle)) @foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -336,7 +336,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle)) @foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"
@ -363,7 +363,7 @@
</div> </div>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap"> <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (ImageCardViewModel card in _images.Cards.OrderBy(s => s.SortTitle)) @foreach (ImageCardViewModel card in _images.Cards.OrderBy(s => s.SortTitle))
{ {
<MediaCard Data="@card" <MediaCard Data="@card"

170
ErsatzTV/Pages/TemplateEditor.razor

@ -7,87 +7,89 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudForm Style="max-height: 100%">
<MudText Typo="Typo.h4" Class="mb-4">Edit Template</MudText> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudGrid> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save">
<MudItem xs="4"> Save Template
<div style="max-width: 400px"> </MudButton>
<MudCard> </MudPaper>
<MudCardContent> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudTextField Label="Name" @bind-Value="_template.Name" For="@(() => _template.Name)"/> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
</MudCardContent> <MudText Typo="Typo.h5" Class="mb-2">General</MudText>
</MudCard> <MudDivider Class="mb-6"/>
</div> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4"> <div class="d-flex">
Save Changes <MudText>Name</MudText>
</MudButton> </div>
</MudItem> <MudTextField @bind-Value="_template.Name" For="@(() => _template.Name)"/>
<MudItem xs="4"> </MudStack>
<div style="max-width: 400px"> <MudText Typo="Typo.h5" Class="mt-10 mb-2">Add Content</MudText>
<MudCard> <MudDivider Class="mb-6"/>
<MudCardContent> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudSelect T="BlockGroupViewModel" <div class="d-flex">
Label="Block Group" <MudText>Block Group</MudText>
ValueChanged="@(vm => UpdateBlockGroupItems(vm))"> </div>
@foreach (BlockGroupViewModel blockGroup in _blockGroups) <MudSelect T="BlockGroupViewModel" ValueChanged="@(vm => UpdateBlockGroupItems(vm))">
{ @foreach (BlockGroupViewModel blockGroup in _blockGroups)
<MudSelectItem Value="@blockGroup">@blockGroup.Name</MudSelectItem> {
} <MudSelectItem Value="@blockGroup">@blockGroup.Name</MudSelectItem>
</MudSelect> }
<MudSelect Class="mt-3" </MudSelect>
T="BlockViewModel" </MudStack>
Label="Block" <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
@bind-value="_selectedBlock"> <div class="d-flex">
@foreach (BlockViewModel block in _blocks) <MudText>Block</MudText>
{ </div>
<MudSelectItem Value="@block">@block.Name</MudSelectItem> <MudSelect T="BlockViewModel" @bind-value="_selectedBlock">
} @foreach (BlockViewModel block in _blocks)
</MudSelect> {
<MudSelect Class="mt-3" <MudSelectItem Value="@block">@block.Name</MudSelectItem>
T="DateTime" }
Label="Start Time On Or After" </MudSelect>
@bind-value="_selectedBlockStart"> </MudStack>
@foreach (DateTime startTime in _startTimes) <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
{ <div class="d-flex">
<MudSelectItem Value="@startTime"> <MudText>Start Time On Or After</MudText>
@startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern) </div>
</MudSelectItem> <MudSelect T="DateTime" @bind-value="_selectedBlockStart">
} @foreach (DateTime startTime in _startTimes)
</MudSelect> {
</MudCardContent> <MudSelectItem Value="@startTime">
<MudCardActions> @startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern)
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlockToTemplate())" Disabled="@(_selectedBlock is null)"> </MudSelectItem>
Add Block To Template }
</MudButton> </MudSelect>
</MudCardActions> </MudStack>
</MudCard> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
</div> <div class="d-flex"></div>
</MudItem> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddBlockToTemplate())" Disabled="@(_selectedBlock is null)" StartIcon="@Icons.Material.Filled.Add">
<MudItem xs="4"> Add Block To Template
<div style="max-width: 400px"> </MudButton>
<MudCard> </MudStack>
<MudCardContent> <MudText Typo="Typo.h5" Class="mt-10 mb-2">Remove Content</MudText>
<MudSelect T="TemplateItemEditViewModel" <MudDivider Class="mb-6"/>
Label="Block To Remove" <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
@bind-Value="_blockToRemove"> <div class="d-flex">
<MudSelectItem Value="@((TemplateItemEditViewModel)null)">(none)</MudSelectItem> <MudText>Block To Remove</MudText>
@foreach (TemplateItemEditViewModel item in _template.Items.OrderBy(i => i.Start)) </div>
{ <MudSelect T="TemplateItemEditViewModel" @bind-Value="_blockToRemove">
<MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem> <MudSelectItem Value="@((TemplateItemEditViewModel)null)">(none)</MudSelectItem>
} @foreach (TemplateItemEditViewModel item in _template.Items.OrderBy(i => i.Start))
</MudSelect> {
</MudCardContent> <MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem>
<MudCardActions> }
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveBlockFromTemplate())" Disabled="@(_blockToRemove is null)"> </MudSelect>
Remove Block From Template </MudStack>
</MudButton> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
</MudCardActions> <div class="d-flex"></div>
</MudCard> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveBlockFromTemplate())" Disabled="@(_blockToRemove is null)" StartIcon="@Icons.Material.Filled.Remove">
</div> Remove Block From Template
</MudItem> </MudButton>
<MudItem xs="8"> </MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Content</MudText>
<MudDivider Class="mb-6"/>
<MudCalendar T="CalendarItem" <MudCalendar T="CalendarItem"
Class="mt-4" Class="mb-6"
Items="@_template.Items" Items="@_template.Items"
ShowMonth="false" ShowMonth="false"
ShowWeek="false" ShowWeek="false"
@ -99,9 +101,9 @@
EnableDragItems="true" EnableDragItems="true"
EnableResizeItems="false" EnableResizeItems="false"
ItemChanged="@(ci => CalendarItemChanged(ci))"/> ItemChanged="@(ci => CalendarItemChanged(ci))"/>
</MudItem> </MudContainer>
</MudGrid> </div>
</MudContainer> </MudForm>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
@ -113,7 +115,7 @@
public int Id { get; set; } public int Id { get; set; }
private TemplateItemsEditViewModel _template = new(); private TemplateItemsEditViewModel _template = new();
private TemplateItemEditViewModel _blockToRemove = new(); private TemplateItemEditViewModel _blockToRemove;
private BlockGroupViewModel _selectedBlockGroup; private BlockGroupViewModel _selectedBlockGroup;
private BlockViewModel _selectedBlock; private BlockViewModel _selectedBlock;
private DateTime _selectedBlockStart; private DateTime _selectedBlockStart;

700
ErsatzTV/Pages/Trash.razor

@ -7,351 +7,365 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject PersistentComponentState ApplicationState @inject PersistentComponentState ApplicationState
<MudPaper Square="true" Style="display: flex; height: 64px; width: 100%; z-index: 100;"> <MudForm Style="max-height: 100%">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100;">
@if (IsSelectMode()) <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6">
{ @if (IsSelectMode())
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
<div style="margin-left: auto">
<MudButton Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteFromDatabase())">
Delete From Database
</MudButton>
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="@(_ => ClearSelection())">
Clear Selection
</MudButton>
</div>
}
else
{
if (_movies?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#movies")" Style="margin-bottom: auto; margin-top: auto">@_movies.Count Movies</MudLink>
}
if (_shows?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
}
if (_seasons?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#seasons")" Style="margin-bottom: auto; margin-top: auto">@_seasons.Count Seasons</MudLink>
}
if (_episodes?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")" Style="margin-bottom: auto; margin-top: auto">@_episodes.Count Episodes</MudLink>
}
if (_artists?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink>
}
if (_musicVideos?.Cards.Count > 0)
{ {
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")" Style="margin-bottom: auto; margin-top: auto">@_musicVideos.Count Music Videos</MudLink> <div class="flex-grow-1">
} <MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
</div>
if (_otherVideos?.Cards.Count > 0) <div style="margin-left: auto" class="d-none d-md-flex">
{ <MudButton Variant="Variant.Filled"
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink> Color="Color.Error"
} StartIcon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteFromDatabase())">
if (_songs?.Cards.Count > 0) Delete From Database
{ </MudButton>
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink> <MudButton Class="ml-3"
} Variant="Variant.Filled"
Color="Color.Secondary"
if (_images?.Cards.Count > 0) StartIcon="@Icons.Material.Filled.Check"
{ OnClick="@(_ => ClearSelection())">
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")" Style="margin-bottom: auto; margin-top: auto">@_images.Count Images</MudLink> Clear Selection
}
if (IsNotEmpty)
{
<div style="margin-left: auto">
<MudButton Variant="@Variant.Filled"
Color="@Color.Error"
StartIcon="@Icons.Material.Filled.DeleteForever"
OnClick="@(_ => EmptyTrash())">
Empty Trash
</MudButton> </MudButton>
</div> </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.Delete" Label="Delete From Database" OnClick="DeleteFromDatabase"/>
<MudMenuItem Icon="@Icons.Material.Filled.Check" Label="Clear Selection" OnClick="ClearSelection"/>
</MudMenu>
</div>
}
else if (IsNotEmpty)
{
<div style="align-items: center; display: flex; width: 100%" class="d-none d-md-flex">
@if (_movies?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#movies")" Style="margin-bottom: auto; margin-top: auto">@_movies.Count Movies</MudLink>
}
@if (_shows?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
}
@if (_seasons?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#seasons")" Style="margin-bottom: auto; margin-top: auto">@_seasons.Count Seasons</MudLink>
}
@if (_episodes?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")" Style="margin-bottom: auto; margin-top: auto">@_episodes.Count Episodes</MudLink>
}
@if (_artists?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink>
}
@if (_musicVideos?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")" Style="margin-bottom: auto; margin-top: auto">@_musicVideos.Count Music Videos</MudLink>
}
@if (_otherVideos?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink>
}
@if (_songs?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink>
}
@if (_images?.Cards.Count > 0)
{
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#images")" Style="margin-bottom: auto; margin-top: auto">@_images.Count Images</MudLink>
}
<div class="flex-grow-1 d-none d-md-flex"></div>
<div>
<MudButton Variant="@Variant.Filled"
Color="@Color.Error"
StartIcon="@Icons.Material.Filled.DeleteForever"
OnClick="@(_ => EmptyTrash())">
Empty Trash
</MudButton>
</div>
</div>
<div style="align-items: center; display: flex; width: 100%" class="d-md-none">
<div class="flex-grow-1"></div>
<div>
<MudButton Variant="@Variant.Filled"
Color="@Color.Error"
StartIcon="@Icons.Material.Filled.DeleteForever"
OnClick="@(_ => EmptyTrash())">
Empty Trash
</MudButton>
</div>
</div>
} }
else else
{ {
<MudText>Nothing to see here...</MudText> <MudText>Nothing to see here...</MudText>
} }
}
</div>
</MudPaper>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="margin-top: 96px">
@if (_movies?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
Movies
</MudText>
@if (_movies.Count > 50)
{
<MudLink Href="@GetMoviesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/movies/{card.MovieId}")"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_shows?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Shows
</MudText>
@if (_shows.Count > 50)
{
<MudLink Href="@GetShowsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/shows/{card.TelevisionShowId}")"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_seasons?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
Seasons
</MudText>
@if (_seasons.Count > 50)
{
<MudLink Href="@GetSeasonsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/seasons/{card.TelevisionSeasonId}")"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_episodes?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Episodes
</MudText>
@if (_episodes.Count > 50)
{
<MudLink Href="@GetEpisodesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
Href="@($"media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_artists?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists
</MudText>
@if (_artists.Count > 50)
{
<MudLink Href="@GetArtistsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/music/artists/{card.ArtistId}")"
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_musicVideos?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos
</MudText>
@if (_musicVideos.Count > 50)
{
<MudLink Href="@GetMusicVideosLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_otherVideos?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4"
Style="scroll-margin-top: 160px"
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos
</MudText>
@if (_otherVideos.Count > 50)
{
<MudLink Href="@GetOtherVideosLink()" Class="ml-4">See All >></MudLink>
}
</div> </div>
</MudPaper>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle)) <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
{ @if (_movies?.Cards.Count > 0)
<MediaCard Data="@card" {
Href="" <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
ArtworkKind="ArtworkKind.Thumbnail" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "movies" } })">
DeleteClicked="@DeleteItemFromDatabase" Movies
SelectColor="@Color.Error" </MudText>
SelectClicked="@(e => SelectClicked(card, e))" @if (_movies.Count > 50)
IsSelected="@IsSelected(card)" {
IsSelectMode="@IsSelectMode()"/> <MudLink Href="@GetMoviesLink()" Class="ml-4">See All >></MudLink>
} }
</MudContainer> </div>
} <MudDivider Class="mb-6"/>
@if (_songs?.Cards.Count > 0) <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
{ @foreach (MovieCardViewModel card in _movies.Cards.OrderBy(m => m.SortTitle))
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> {
<MudText Typo="Typo.h4" <MediaCard Data="@card"
Style="scroll-margin-top: 160px" Href="@($"media/movies/{card.MovieId}")"
UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })"> DeleteClicked="@DeleteItemFromDatabase"
Songs SelectColor="@Color.Error"
</MudText> SelectClicked="@(e => SelectClicked(card, e))"
@if (_songs.Count > 50) IsSelected="@IsSelected(card)"
{ IsSelectMode="@IsSelectMode()"/>
<MudLink Href="@GetSongsLink()" Class="ml-4">See All >></MudLink> }
} </MudStack>
</div> }
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> @if (_shows?.Cards.Count > 0)
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle)) {
{ <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MediaCard Data="@card" <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "shows" } })">
Href="" Shows
ArtworkKind="ArtworkKind.Thumbnail" </MudText>
DeleteClicked="@DeleteItemFromDatabase" @if (_shows.Count > 50)
SelectColor="@Color.Error" {
SelectClicked="@(e => SelectClicked(card, e))" <MudLink Href="@GetShowsLink()" Class="ml-4">See All >></MudLink>
IsSelected="@IsSelected(card)" }
IsSelectMode="@IsSelectMode()"/> </div>
} <MudDivider Class="mb-6"/>
</MudContainer>
} <MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionShowCardViewModel card in _shows.Cards.OrderBy(s => s.SortTitle))
@if (_images?.Cards.Count > 0) {
{ <MediaCard Data="@card"
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;"> Href="@($"media/tv/shows/{card.TelevisionShowId}")"
<MudText Typo="Typo.h4" DeleteClicked="@DeleteItemFromDatabase"
Style="scroll-margin-top: 160px" SelectColor="@Color.Error"
UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })"> SelectClicked="@(e => SelectClicked(card, e))"
Songs IsSelected="@IsSelected(card)"
</MudText> IsSelectMode="@IsSelectMode()"/>
@if (_images.Count > 50) }
{ </MudStack>
<MudLink Href="@GetImagesLink()" Class="ml-4">See All >></MudLink> }
}
</div> @if (_seasons?.Cards.Count > 0)
{
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> <div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
@foreach (ImageCardViewModel card in _images.Cards.OrderBy(s => s.SortTitle)) <MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "seasons" } })">
{ Seasons
<MediaCard Data="@card" </MudText>
Href="" @if (_seasons.Count > 50)
ArtworkKind="ArtworkKind.Thumbnail" {
DeleteClicked="@DeleteItemFromDatabase" <MudLink Href="@GetSeasonsLink()" Class="ml-4">See All >></MudLink>
SelectColor="@Color.Error" }
SelectClicked="@(e => SelectClicked(card, e))" </div>
IsSelected="@IsSelected(card)" <MudDivider Class="mb-6"/>
IsSelectMode="@IsSelectMode()"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/tv/seasons/{card.TelevisionSeasonId}")"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_episodes?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "episodes" } })">
Episodes
</MudText>
@if (_episodes.Count > 50)
{
<MudLink Href="@GetEpisodesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
Href="@($"media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_artists?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "artists" } })">
Artists
</MudText>
@if (_artists.Count > 50)
{
<MudLink Href="@GetArtistsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (ArtistCardViewModel card in _artists.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href="@($"media/music/artists/{card.ArtistId}")"
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_musicVideos?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
Music Videos
</MudText>
@if (_musicVideos.Count > 50)
{
<MudLink Href="@GetMusicVideosLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_otherVideos?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
Other Videos
</MudText>
@if (_otherVideos.Count > 50)
{
<MudLink Href="@GetOtherVideosLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_songs?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "songs" } })">
Songs
</MudText>
@if (_songs.Count > 50)
{
<MudLink Href="@GetSongsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (SongCardViewModel card in _songs.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
}
@if (_images?.Cards.Count > 0)
{
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
<MudText Typo="Typo.h4" UserAttributes="@(new Dictionary<string, object> { { "id", "images" } })">
Songs
</MudText>
@if (_images.Count > 50)
{
<MudLink Href="@GetImagesLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mb-10">
@foreach (ImageCardViewModel card in _images.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Href=""
ArtworkKind="ArtworkKind.Thumbnail"
DeleteClicked="@DeleteItemFromDatabase"
SelectColor="@Color.Error"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudStack>
} }
</MudContainer> </MudContainer>
} </div>
</MudContainer> </MudForm>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
@ -660,39 +674,39 @@
switch (vm) switch (vm)
{ {
case MovieCardViewModel movie: case MovieCardViewModel movie:
request = new DeleteItemsFromDatabase(new List<int> { movie.MovieId }); request = new DeleteItemsFromDatabase([movie.MovieId]);
await DeleteItemsWithConfirmation("movie", $"{movie.Title} ({movie.Subtitle})", request); await DeleteItemsWithConfirmation("movie", $"{movie.Title} ({movie.Subtitle})", request);
break; break;
case TelevisionShowCardViewModel show: case TelevisionShowCardViewModel show:
request = new DeleteItemsFromDatabase(new List<int> { show.TelevisionShowId }); request = new DeleteItemsFromDatabase([show.TelevisionShowId]);
await DeleteItemsWithConfirmation("show", $"{show.Title} ({show.Subtitle})", request); await DeleteItemsWithConfirmation("show", $"{show.Title} ({show.Subtitle})", request);
break; break;
case TelevisionSeasonCardViewModel season: case TelevisionSeasonCardViewModel season:
request = new DeleteItemsFromDatabase(new List<int> { season.TelevisionSeasonId }); request = new DeleteItemsFromDatabase([season.TelevisionSeasonId]);
await DeleteItemsWithConfirmation("season", $"{season.Title} ({season.Subtitle})", request); await DeleteItemsWithConfirmation("season", $"{season.Title} ({season.Subtitle})", request);
break; break;
case TelevisionEpisodeCardViewModel episode: case TelevisionEpisodeCardViewModel episode:
request = new DeleteItemsFromDatabase(new List<int> { episode.EpisodeId }); request = new DeleteItemsFromDatabase([episode.EpisodeId]);
await DeleteItemsWithConfirmation("episode", $"{episode.Title} ({episode.Subtitle})", request); await DeleteItemsWithConfirmation("episode", $"{episode.Title} ({episode.Subtitle})", request);
break; break;
case ArtistCardViewModel artist: case ArtistCardViewModel artist:
request = new DeleteItemsFromDatabase(new List<int> { artist.ArtistId }); request = new DeleteItemsFromDatabase([artist.ArtistId]);
await DeleteItemsWithConfirmation("artist", $"{artist.Title} ({artist.Subtitle})", request); await DeleteItemsWithConfirmation("artist", $"{artist.Title} ({artist.Subtitle})", request);
break; break;
case MusicVideoCardViewModel musicVideo: case MusicVideoCardViewModel musicVideo:
request = new DeleteItemsFromDatabase(new List<int> { musicVideo.MusicVideoId }); request = new DeleteItemsFromDatabase([musicVideo.MusicVideoId]);
await DeleteItemsWithConfirmation("music video", $"{musicVideo.Title} ({musicVideo.Subtitle})", request); await DeleteItemsWithConfirmation("music video", $"{musicVideo.Title} ({musicVideo.Subtitle})", request);
break; break;
case OtherVideoCardViewModel otherVideo: case OtherVideoCardViewModel otherVideo:
request = new DeleteItemsFromDatabase(new List<int> { otherVideo.OtherVideoId }); request = new DeleteItemsFromDatabase([otherVideo.OtherVideoId]);
await DeleteItemsWithConfirmation("other video", $"{otherVideo.Title} ({otherVideo.Subtitle})", request); await DeleteItemsWithConfirmation("other video", $"{otherVideo.Title} ({otherVideo.Subtitle})", request);
break; break;
case SongCardViewModel song: case SongCardViewModel song:
request = new DeleteItemsFromDatabase(new List<int> { song.SongId }); request = new DeleteItemsFromDatabase([song.SongId]);
await DeleteItemsWithConfirmation("song", $"{song.Title} ({song.Subtitle})", request); await DeleteItemsWithConfirmation("song", $"{song.Title} ({song.Subtitle})", request);
break; break;
case ImageCardViewModel image: case ImageCardViewModel image:
request = new DeleteItemsFromDatabase(new List<int> { image.ImageId }); request = new DeleteItemsFromDatabase([image.ImageId]);
await DeleteItemsWithConfirmation("image", $"{image.Title} ({image.Subtitle})", request); await DeleteItemsWithConfirmation("image", $"{image.Title} ({image.Subtitle})", request);
break; break;
} }

52
ErsatzTV/Pages/YamlPlayoutEditor.razor

@ -3,28 +3,38 @@
@using ErsatzTV.Application.Playouts @using ErsatzTV.Application.Playouts
@using ErsatzTV.Application.Scheduling @using ErsatzTV.Application.Scheduling
@implements IDisposable @implements IDisposable
@inject IDialogService Dialog
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
@inject IEntityLocker EntityLocker; @inject IEntityLocker EntityLocker;
@inject ILogger<YamlPlayoutEditor> Logger
<MudForm Style="max-height: 100%"> <MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudText Class="ml-8" >Edit YAML Playout - @_channelName</MudText> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save">
Save YAML File
</MudButton>
</MudPaper> </MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto"> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">YAML File</MudText> <MudText Typo="Typo.h5" Class="mb-2">@_channelName - YAML Playout</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => EditYamlFile())"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
Edit YAML File <div class="d-flex">
</MudButton> <MudText>YAML File</MudText>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Playout Items and History</MudText> </div>
<MudTextField @bind-Value="@_playout.TemplateFile" For="@(() => _playout.TemplateFile)"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Maintenance</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
Erase Items and History <div class="d-flex">
</MudButton> <MudText>Playout Items and History</MudText>
</div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" StartIcon="@Icons.Material.Filled.Delete">
Erase Items and History
</MudButton>
</MudStack>
</MudContainer> </MudContainer>
</div> </div>
</MudForm> </MudForm>
@ -74,22 +84,26 @@
Snackbar.Add(message, Severity.Info); Snackbar.Add(message, Severity.Info);
} }
private async Task EditYamlFile() private async Task SaveChanges()
{ {
if (_playout is null) if (_playout is null)
{ {
return; return;
} }
var parameters = new DialogParameters { { "YamlFile", $"{_playout.TemplateFile}" } }; Either<BaseError, PlayoutNameViewModel> result =
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge }; await Mediator.Send(new UpdateYamlPlayout(_playout.PlayoutId, _playout.TemplateFile), _cts.Token);
IDialogReference dialog = await Dialog.ShowAsync<EditYamlFileDialog>("Edit YAML File", parameters, options); result.Match(
DialogResult result = await dialog.Result; _ =>
if (result is not null && !result.Canceled) {
{ Snackbar.Add($"Saved YAML file for playout {_channelName}", Severity.Success);
await Mediator.Send(new UpdateYamlPlayout(_playout.PlayoutId, result.Data as string ?? _playout.TemplateFile), _cts.Token); },
} error =>
{
Snackbar.Add($"Unexpected error saving YAML file: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving YAML file: {Error}", error.Value);
});
} }
} }

42
ErsatzTV/Shared/EditYamlFileDialog.razor

@ -1,42 +0,0 @@
@implements IDisposable
<MudDialog>
<DialogContent>
<MudContainer Class="mb-6">
<MudText>
Edit the playout's YAML file
</MudText>
</MudContainer>
<MudTextField Label="YAML File" @bind-Value="_yamlFile"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(string.IsNullOrWhiteSpace(_yamlFile))" OnClick="Submit">
Save Changes
</MudButton>
</DialogActions>
</MudDialog>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public string YamlFile { get; set; }
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; }
private string _yamlFile;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override void OnParametersSet() => _yamlFile = YamlFile;
private void Submit() => MudDialog.Close(DialogResult.Ok(_yamlFile));
private void Cancel() => MudDialog.Cancel();
}

9
ErsatzTV/Validators/MultiCollectionEditViewModelValidator.cs

@ -1,9 +0,0 @@
using ErsatzTV.ViewModels;
using FluentValidation;
namespace ErsatzTV.Validators;
public class MultiCollectionEditViewModelValidator : AbstractValidator<MultiCollectionEditViewModel>
{
public MultiCollectionEditViewModelValidator() => RuleFor(c => c.Name).NotEmpty();
}
Loading…
Cancel
Save