Browse Source

more layout updates for mobile (#2137)

* update trakt, filler, filler editor ui

* update schedules and playouts

* update playout editor

* update dependencies

* update yaml playout editor

* update path replacement editor
pull/2138/head
Jason Dove 1 month ago committed by GitHub
parent
commit
e0df454ac6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      ErsatzTV.Application/ErsatzTV.Application.csproj
  2. 10
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  3. 10
      ErsatzTV.Core/ErsatzTV.Core.csproj
  4. 2
      ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
  5. 4
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  6. 2
      ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj
  7. 4
      ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj
  8. 6
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  9. 6
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj
  10. 10
      ErsatzTV/ErsatzTV.csproj
  11. 408
      ErsatzTV/Pages/ChannelEditor.razor
  12. 298
      ErsatzTV/Pages/FillerPresetEditor.razor
  13. 120
      ErsatzTV/Pages/FillerPresets.razor
  14. 153
      ErsatzTV/Pages/PlayoutEditor.razor
  15. 412
      ErsatzTV/Pages/Playouts.razor
  16. 183
      ErsatzTV/Pages/Schedules.razor
  17. 120
      ErsatzTV/Pages/TraktLists.razor
  18. 2
      ErsatzTV/Pages/Trash.razor
  19. 66
      ErsatzTV/Pages/YamlPlayoutEditor.razor
  20. 134
      ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor
  21. 15
      ErsatzTV/Validators/FillerPresetEditViewModelValidator.cs
  22. 15
      ErsatzTV/Validators/PlayoutEditViewModelValidator.cs

4
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -13,8 +13,8 @@ @@ -13,8 +13,8 @@
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

10
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -10,11 +10,11 @@ @@ -10,11 +10,11 @@
<PackageReference Include="Bugsnag" Version="4.0.0" />
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>

10
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -15,16 +15,16 @@ @@ -15,16 +15,16 @@
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="LanguageExt.Transformers" Version="4.4.8" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NCalcSync" Version="5.4.2" />
<PackageReference Include="NCalcSync" Version="5.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />

2
ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.3.2" />

4
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -11,8 +11,8 @@ @@ -11,8 +11,8 @@
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
</ItemGroup>

2
ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
</ItemGroup>

4
ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj

@ -13,8 +13,8 @@ @@ -13,8 +13,8 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
</ItemGroup>

6
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -18,12 +18,12 @@ @@ -18,12 +18,12 @@
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00017" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

6
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -24,9 +24,9 @@ @@ -24,9 +24,9 @@
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />

10
ErsatzTV/ErsatzTV.csproj

@ -25,11 +25,11 @@ @@ -25,11 +25,11 @@
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Markdig" Version="0.41.3" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

408
ErsatzTV/Pages/ChannelEditor.razor

@ -19,241 +19,241 @@ @@ -19,241 +19,241 @@
@inject IMediator Mediator
<MudForm Model="@_model" @ref="@_form" Validation="@(_validator.ValidateValue)" ValidationDelay="0" Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="HandleSubmitAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Channel" : "Add Channel")</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">Channel</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>Number</MudText>
</div>
<MudTextField @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
</div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Group</MudText>
</div>
<MudTextField @bind-Value="_model.Group" For="@(() => _model.Group)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Categories</MudText>
</div>
<MudTextField @bind-Value="_model.Categories" For="@(() => _model.Categories)" HelperText="Comma-separated list of categories"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Active Mode</MudText>
</div>
<MudSelect @bind-Value="_model.ActiveMode" For="@(() => _model.ActiveMode)">
<MudSelectItem Value="@(ChannelActiveMode.Active)">Active</MudSelectItem>
<MudSelectItem Value="@(ChannelActiveMode.Hidden)">Hidden</MudSelectItem>
<MudSelectItem Value="@(ChannelActiveMode.Inactive)">Inactive</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Progress Mode</MudText>
</div>
<MudSelect @bind-Value="_model.ProgressMode" For="@(() => _model.ProgressMode)">
<MudSelectItem Value="@(ChannelProgressMode.Always)">Always</MudSelectItem>
<MudSelectItem Value="@(ChannelProgressMode.OnDemand)">On Demand</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Streaming Mode</MudText>
</div>
<MudSelect @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
<MudSelectItem Value="@(StreamingMode.TransportStreamHybrid)">MPEG-TS</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingDirect)">HLS Direct</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenter)">HLS Segmenter</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenterV2)">HLS Segmenter V2</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>FFmpeg Profile</MudText>
</div>
<MudSelect @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)">
@foreach (FFmpegProfileViewModel profile in _ffmpegProfiles)
{
<MudSelectItem Value="@profile.Id">@profile.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Stream Selector Mode</MudText>
</div>
<MudSelect @bind-Value="_model.StreamSelectorMode" For="@(() => _model.StreamSelectorMode)">
<MudSelectItem Value="@(ChannelStreamSelectorMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelStreamSelectorMode.Custom)">Custom</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.StreamSelectorMode is ChannelStreamSelectorMode.Default)
{
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="HandleSubmitAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Channel" : "Add Channel")</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">Channel</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>Preferred Audio Language</MudText>
<MudText>Number</MudText>
</div>
<MudSelect @bind-Value="_model.PreferredAudioLanguageCode"
For="@(() => _model.PreferredAudioLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (LanguageCodeViewModel culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
<MudTextField @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
</div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Group</MudText>
</div>
<MudTextField @bind-Value="_model.Group" For="@(() => _model.Group)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Categories</MudText>
</div>
<MudTextField @bind-Value="_model.Categories" For="@(() => _model.Categories)" HelperText="Comma-separated list of categories"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Active Mode</MudText>
</div>
<MudSelect @bind-Value="_model.ActiveMode" For="@(() => _model.ActiveMode)">
<MudSelectItem Value="@(ChannelActiveMode.Active)">Active</MudSelectItem>
<MudSelectItem Value="@(ChannelActiveMode.Hidden)">Hidden</MudSelectItem>
<MudSelectItem Value="@(ChannelActiveMode.Inactive)">Inactive</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preferred Audio Title</MudText>
<MudText>Progress Mode</MudText>
</div>
<MudTextField @bind-Value="_model.PreferredAudioTitle" For="@(() => _model.PreferredAudioTitle)"/>
<MudSelect @bind-Value="_model.ProgressMode" For="@(() => _model.ProgressMode)">
<MudSelectItem Value="@(ChannelProgressMode.Always)">Always</MudSelectItem>
<MudSelectItem Value="@(ChannelProgressMode.OnDemand)">On Demand</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preferred Subtitle Language</MudText>
<MudText>Streaming Mode</MudText>
</div>
<MudSelect @bind-Value="_model.PreferredSubtitleLanguageCode"
For="@(() => _model.PreferredSubtitleLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (LanguageCodeViewModel culture in _availableCultures)
<MudSelect @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
<MudSelectItem Value="@(StreamingMode.TransportStreamHybrid)">MPEG-TS</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingDirect)">HLS Direct</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenter)">HLS Segmenter</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenterV2)">HLS Segmenter V2</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>FFmpeg Profile</MudText>
</div>
<MudSelect @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)">
@foreach (FFmpegProfileViewModel profile in _ffmpegProfiles)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
<MudSelectItem Value="@profile.Id">@profile.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Subtitle Mode</MudText>
<MudText>Stream Selector Mode</MudText>
</div>
<MudSelect @bind-Value="_model.SubtitleMode" For="@(() => _model.SubtitleMode)">
<MudSelectItem Value="@(ChannelSubtitleMode.None)">None</MudSelectItem>
<MudSelectItem Value="@(ChannelSubtitleMode.Forced)">Forced</MudSelectItem>
<MudSelectItem Value="@(ChannelSubtitleMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelSubtitleMode.Any)">Any</MudSelectItem>
<MudSelect @bind-Value="_model.StreamSelectorMode" For="@(() => _model.StreamSelectorMode)">
<MudSelectItem Value="@(ChannelStreamSelectorMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelStreamSelectorMode.Custom)">Custom</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.StreamSelectorMode is ChannelStreamSelectorMode.Default)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preferred Audio Language</MudText>
</div>
<MudSelect @bind-Value="_model.PreferredAudioLanguageCode"
For="@(() => _model.PreferredAudioLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (LanguageCodeViewModel culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preferred Audio Title</MudText>
</div>
<MudTextField @bind-Value="_model.PreferredAudioTitle" For="@(() => _model.PreferredAudioTitle)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preferred Subtitle Language</MudText>
</div>
<MudSelect @bind-Value="_model.PreferredSubtitleLanguageCode"
For="@(() => _model.PreferredSubtitleLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (LanguageCodeViewModel culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Subtitle Mode</MudText>
</div>
<MudSelect @bind-Value="_model.SubtitleMode" For="@(() => _model.SubtitleMode)">
<MudSelectItem Value="@(ChannelSubtitleMode.None)">None</MudSelectItem>
<MudSelectItem Value="@(ChannelSubtitleMode.Forced)">Forced</MudSelectItem>
<MudSelectItem Value="@(ChannelSubtitleMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelSubtitleMode.Any)">Any</MudSelectItem>
</MudSelect>
</MudStack>
}
else
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Stream Selector</MudText>
</div>
<MudSelect @bind-Value="_model.StreamSelector"
For="@(() => _model.StreamSelector)">
<MudSelectItem T="string" Value="@((string)null)">(none)</MudSelectItem>
@foreach (string selector in _streamSelectors)
{
<MudSelectItem T="string" Value="@selector">@selector</MudSelectItem>
}
</MudSelect>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Music Video Credits Mode</MudText>
</div>
<MudSelect @bind-Value="_model.MusicVideoCreditsMode" For="@(() => _model.MusicVideoCreditsMode)">
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.None)">None</MudSelectItem>
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.GenerateSubtitles)">Generate Subtitles</MudSelectItem>
</MudSelect>
</MudStack>
}
else
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Stream Selector</MudText>
<MudText>Music Video Credits Template</MudText>
</div>
<MudSelect @bind-Value="_model.StreamSelector"
For="@(() => _model.StreamSelector)">
<MudSelect @bind-Value="_model.MusicVideoCreditsTemplate"
For="@(() => _model.MusicVideoCreditsTemplate)"
Disabled="@(_model.MusicVideoCreditsMode != ChannelMusicVideoCreditsMode.GenerateSubtitles)">
<MudSelectItem T="string" Value="@((string)null)">(none)</MudSelectItem>
@foreach (string selector in _streamSelectors)
@foreach (string template in _musicVideoCreditsTemplates)
{
<MudSelectItem T="string" Value="@selector">@selector</MudSelectItem>
<MudSelectItem T="string" Value="@template">@template</MudSelectItem>
}
</MudSelect>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Music Video Credits Mode</MudText>
</div>
<MudSelect @bind-Value="_model.MusicVideoCreditsMode" For="@(() => _model.MusicVideoCreditsMode)">
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.None)">None</MudSelectItem>
<MudSelectItem Value="@(ChannelMusicVideoCreditsMode.GenerateSubtitles)">Generate Subtitles</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Music Video Credits Template</MudText>
</div>
<MudSelect @bind-Value="_model.MusicVideoCreditsTemplate"
For="@(() => _model.MusicVideoCreditsTemplate)"
Disabled="@(_model.MusicVideoCreditsMode != ChannelMusicVideoCreditsMode.GenerateSubtitles)">
<MudSelectItem T="string" Value="@((string)null)">(none)</MudSelectItem>
@foreach (string template in _musicVideoCreditsTemplates)
{
<MudSelectItem T="string" Value="@template">@template</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Song Video Mode</MudText>
</div>
<MudSelect @bind-Value="_model.SongVideoMode" For="@(() => _model.SongVideoMode)">
<MudSelectItem Value="@(ChannelSongVideoMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelSongVideoMode.WithProgress)">With Progress</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Logo</MudText>
</div>
<InputFile id="fileInput" OnChange="UploadLogo" style="display: none;"/>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="fileInput">
Upload Logo
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>External Logo URL</MudText>
</div>
<MudTextField @bind-Value="_model.ExternalLogoUrl" For="@(() => _model.ExternalLogoUrl)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Logo Preview</MudText>
</div>
@if (!string.IsNullOrWhiteSpace(_model.Logo?.Path) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl))
{
<MudElement HtmlTag="img" src="@(string.IsNullOrWhiteSpace(_model.ExternalLogoUrl) ? _model.Logo.UrlWithContentType : _model.ExternalLogoUrl)" Style="max-height: 50px"/>
}
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Watermark</MudText>
</div>
<MudSelect @bind-Value="_model.WatermarkId" For="@(() => _model.WatermarkId)"
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Fallback Filler</MudText>
</div>
<MudSelect @bind-Value="_model.FallbackFillerId" For="@(() => _model.FallbackFillerId)" Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (FillerPresetViewModel fillerPreset in _fillerPresets)
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Song Video Mode</MudText>
</div>
<MudSelect @bind-Value="_model.SongVideoMode" For="@(() => _model.SongVideoMode)">
<MudSelectItem Value="@(ChannelSongVideoMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelSongVideoMode.WithProgress)">With Progress</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Logo</MudText>
</div>
<InputFile id="fileInput" OnChange="UploadLogo" style="display: none;"/>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="fileInput">
Upload Logo
</MudButton>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>External Logo URL</MudText>
</div>
<MudTextField @bind-Value="_model.ExternalLogoUrl" For="@(() => _model.ExternalLogoUrl)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Logo Preview</MudText>
</div>
@if (!string.IsNullOrWhiteSpace(_model.Logo?.Path) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl))
{
<MudSelectItem T="int?" Value="@fillerPreset.Id">@fillerPreset.Name</MudSelectItem>
<MudElement HtmlTag="img" src="@(string.IsNullOrWhiteSpace(_model.ExternalLogoUrl) ? _model.Logo.UrlWithContentType : _model.ExternalLogoUrl)" Style="max-height: 50px"/>
}
</MudSelect>
</MudStack>
</MudContainer>
</div>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Watermark</MudText>
</div>
<MudSelect @bind-Value="_model.WatermarkId" For="@(() => _model.WatermarkId)"
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Fallback Filler</MudText>
</div>
<MudSelect @bind-Value="_model.FallbackFillerId" For="@(() => _model.FallbackFillerId)" Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (FillerPresetViewModel fillerPreset in _fillerPresets)
{
<MudSelectItem T="int?" Value="@fillerPreset.Id">@fillerPreset.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {

298
ErsatzTV/Pages/FillerPresetEditor.razor

@ -6,137 +6,181 @@ @@ -6,137 +6,181 @@
@using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.Television
@using ErsatzTV.Core.Domain.Filler
@using ErsatzTV.Validators
@using FluentValidation.Results
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<FillerPresetEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div style="max-width: 400px;">
<MudText Typo="Typo.h4" Class="mb-4">@(IsEdit ? "Edit Filler Preset" : "Add Filler Preset")</MudText>
@if (_editContext is not null)
{
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidationValidator/>
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudSelect Class="mt-3" Label="Filler Kind" @bind-Value="_model.FillerKind" For="@(() => _model.FillerKind)" Disabled="@IsEdit">
<MudSelectItem Value="@(FillerKind.PreRoll)">Pre-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.MidRoll)">Mid-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.PostRoll)">Post-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.Tail)">Tail</MudSelectItem>
<MudSelectItem Value="@(FillerKind.Fallback)">Fallback</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="Filler Mode" @bind-Value="_model.FillerMode" For="@(() => _model.FillerMode)"
Disabled="@(_model.FillerKind is FillerKind.Fallback or FillerKind.Tail)">
<MudSelectItem Value="@(FillerMode.Duration)">Duration</MudSelectItem>
<MudSelectItem Value="@(FillerMode.Count)">Count</MudSelectItem>
<MudSelectItem Value="@(FillerMode.Pad)">Pad</MudSelectItem>
<MudSelectItem Value="@(FillerMode.RandomCount)">Random Count</MudSelectItem>
</MudSelect>
<MudTimePicker Class="mt-3" Label="Filler Duration" @bind-Time="@_model.Duration" For="@(() => _model.Duration)" Disabled="@(_model.FillerMode != FillerMode.Duration)"/>
<MudTextField Class="mt-3" Label="Filler Count" @bind-Value="@_model.Count" For="@(() => _model.Count)" Disabled="@(_model.FillerMode != FillerMode.Count && _model.FillerMode != FillerMode.RandomCount)"/>
<MudSelect Class="mt-3" Label="Filler Pad To Nearest Minute" @bind-Value="_model.PadToNearestMinute" For="@(() => _model.PadToNearestMinute)" Disabled="@(_model.FillerMode != FillerMode.Pad)">
<MudSelectItem T="int?" Value="5">5 (:00, :05, :10, :15, :20, etc)</MudSelectItem>
<MudSelectItem T="int?" Value="10">10 (:00, :10, :20, :30, :40, :50)</MudSelectItem>
<MudSelectItem T="int?" Value="15">15 (:00, :15, :30, :45)</MudSelectItem>
<MudSelectItem T="int?" Value="30">30 (:00, :30)</MudSelectItem>
</MudSelect>
<MudCheckBox Class="mt-3" Label="Allow Watermarks" @bind-Value="@_model.AllowWatermarks" For="@(() => _model.AllowWatermarks)"/>
<MudSelect Class="mt-3" Label="Filler Collection Type" @bind-Value="_model.CollectionType" For="@(() => _model.CollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
@if (_model.CollectionType == ProgramScheduleItemCollectionType.Collection)
<MudForm Model="@_model" @ref="@_form" Validation="@(_validator.ValidateValue)" ValidationDelay="0" Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="HandleSubmitAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Filler Preset" : "Add Filler Preset")</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">Filler Preset</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Name</MudText>
</div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Kind</MudText>
</div>
<MudSelect @bind-Value="_model.FillerKind" For="@(() => _model.FillerKind)" Disabled="@IsEdit">
<MudSelectItem Value="@(FillerKind.PreRoll)">Pre-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.MidRoll)">Mid-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.PostRoll)">Post-Roll</MudSelectItem>
<MudSelectItem Value="@(FillerKind.Tail)">Tail</MudSelectItem>
<MudSelectItem Value="@(FillerKind.Fallback)">Fallback</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Mode</MudText>
</div>
<MudSelect @bind-Value="_model.FillerMode" For="@(() => _model.FillerMode)"
Disabled="@(_model.FillerKind is FillerKind.Fallback or FillerKind.Tail)">
<MudSelectItem Value="@(FillerMode.Duration)">Duration</MudSelectItem>
<MudSelectItem Value="@(FillerMode.Count)">Count</MudSelectItem>
<MudSelectItem Value="@(FillerMode.Pad)">Pad</MudSelectItem>
<MudSelectItem Value="@(FillerMode.RandomCount)">Random Count</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Duration</MudText>
</div>
<MudTimePicker @bind-Time="@_model.Duration" For="@(() => _model.Duration)" Disabled="@(_model.FillerMode != FillerMode.Duration)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Count</MudText>
</div>
<MudTextField @bind-Value="@_model.Count" For="@(() => _model.Count)" Disabled="@(_model.FillerMode != FillerMode.Count && _model.FillerMode != FillerMode.RandomCount)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Pad To Nearest Minute</MudText>
</div>
<MudSelect @bind-Value="_model.PadToNearestMinute" For="@(() => _model.PadToNearestMinute)" Disabled="@(_model.FillerMode != FillerMode.Pad)">
<MudSelectItem T="int?" Value="5">5 (:00, :05, :10, :15, :20, etc)</MudSelectItem>
<MudSelectItem T="int?" Value="10">10 (:00, :10, :20, :30, :40, :50)</MudSelectItem>
<MudSelectItem T="int?" Value="15">15 (:00, :15, :30, :45)</MudSelectItem>
<MudSelectItem T="int?" Value="30">30 (:00, :30)</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Allow Watermarks</MudText>
</div>
<MudCheckBox @bind-Value="@_model.AllowWatermarks" For="@(() => _model.AllowWatermarks)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Collection Type</MudText>
</div>
<MudSelect @bind-Value="_model.CollectionType" For="@(() => _model.CollectionType)">
<MudSelectItem Value="ProgramScheduleItemCollectionType.Collection">Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionShow">Television Show</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.TelevisionSeason">Television Season</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.Artist">Artist</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.MultiCollection">Multi Collection</MudSelectItem>
<MudSelectItem Value="ProgramScheduleItemCollectionType.SmartCollection">Smart Collection</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.CollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Collection</MudText>
</div>
<MudSelect T="MediaCollectionViewModel" @bind-value="_model.Collection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelect Class="mt-3"
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_model.Collection">
@foreach (MediaCollectionViewModel collection in _mediaCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
</MudSelect>
</MudStack>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Multi Collection</MudText>
</div>
<MudSelect T="MultiCollectionViewModel" @bind-value="_model.MultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelect Class="mt-3"
T="MultiCollectionViewModel"
Label="Multi Collection"
@bind-value="_model.MultiCollection">
@foreach (MultiCollectionViewModel collection in _multiCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
</MudSelect>
</MudStack>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Smart Collection</MudText>
</div>
<MudSelect T="SmartCollectionViewModel" @bind-value="_model.SmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelect Class="mt-3"
T="SmartCollectionViewModel"
Label="Smart Collection"
@bind-value="_model.SmartCollection">
@foreach (SmartCollectionViewModel collection in _smartCollections)
{
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
</MudSelect>
<MudSelectItem Value="@collection">@collection.Name</MudSelectItem>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
</MudSelect>
</MudStack>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Television Show</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel" @bind-value="_model.MediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_model.MediaItem">
@foreach (NamedMediaItemViewModel show in _televisionShows)
{
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
</MudSelect>
<MudSelectItem Value="@show">@show.Name</MudSelectItem>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
</MudSelect>
</MudStack>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Television Season</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel" @bind-value="_model.MediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_model.MediaItem">
@foreach (NamedMediaItemViewModel season in _televisionSeasons)
{
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
</MudSelect>
<MudSelectItem Value="@season">@season.Name</MudSelectItem>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.Artist)
</MudSelect>
</MudStack>
}
@if (_model.CollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Artist</MudText>
</div>
<MudSelect T="NamedMediaItemViewModel" @bind-value="_model.MediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelect Class="mt-3"
T="NamedMediaItemViewModel"
Label="Artist"
@bind-value="_model.MediaItem">
@foreach (NamedMediaItemViewModel artist in _artists)
{
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudSelect>
<MudSelectItem Value="@artist">@artist.Name</MudSelectItem>
}
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">
@(IsEdit ? "Save Changes" : "Add Filler Preset")
</MudButton>
</MudCardActions>
</MudCard>
</EditForm>
}
</MudSelect>
</MudStack>
}
</MudContainer>
</div>
</MudContainer>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
@ -145,15 +189,15 @@ @@ -145,15 +189,15 @@
public int Id { get; set; }
private readonly FillerPresetEditViewModel _model = new();
private EditContext _editContext;
private ValidationMessageStore _messageStore;
private readonly FillerPresetEditViewModelValidator _validator = new();
private MudForm _form;
private List<MediaCollectionViewModel> _mediaCollections = new();
private List<MultiCollectionViewModel> _multiCollections = new();
private List<SmartCollectionViewModel> _smartCollections = new();
private List<NamedMediaItemViewModel> _televisionShows = new();
private List<NamedMediaItemViewModel> _televisionSeasons = new();
private List<NamedMediaItemViewModel> _artists = new();
private List<MediaCollectionViewModel> _mediaCollections = [];
private List<MultiCollectionViewModel> _multiCollections = [];
private List<SmartCollectionViewModel> _smartCollections = [];
private List<NamedMediaItemViewModel> _televisionShows = [];
private List<NamedMediaItemViewModel> _televisionSeasons = [];
private List<NamedMediaItemViewModel> _artists = [];
public void Dispose()
{
@ -212,23 +256,17 @@ @@ -212,23 +256,17 @@
}
}
protected override void OnInitialized()
{
_editContext = new EditContext(_model);
_messageStore = new ValidationMessageStore(_editContext);
}
private bool IsEdit => Id != 0;
private async Task HandleSubmitAsync()
{
_messageStore.Clear();
if (_editContext.Validate())
await _form.Validate();
ValidationResult result = await _validator.ValidateAsync(_model, _cts.Token);
if (result.IsValid)
{
IRequest<Either<BaseError, Unit>> request = IsEdit ? _model.ToEdit() : _model.ToUpdate();
Seq<BaseError> errorMessage = await Mediator.Send(request, _cts.Token)
.Map(result => result.LeftToSeq());
Seq<BaseError> errorMessage = (await Mediator.Send(request, _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match(
error =>

120
ErsatzTV/Pages/FillerPresets.razor

@ -6,66 +6,70 @@ @@ -6,66 +6,70 @@
@inject IDialogService Dialog
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="media/filler/presets/add">
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" StartIcon="@Icons.Material.Filled.Add" Href="media/filler/presets/add">
Add Filler Preset
</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">Filler Presets</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
@bind-RowsPerPage="@_fillerPresetsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<FillerPresetViewModel>>>(ServerReloadFillerPresets))"
Dense="true"
@ref="_fillerPresetsTable">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col/>
<col style="width: 120px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Filler Kind</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Filler Kind">
@(
context.FillerKind switch
{
FillerKind.PreRoll => "Pre-Roll",
FillerKind.MidRoll => "Mid-Roll",
FillerKind.PostRoll => "Post-Roll",
FillerKind.Fallback => "Fallback",
FillerKind.Tail => "Tail",
_ => "None"
}
)
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Filler Preset">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"media/filler/presets/{context.Id}/edit")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Filler Preset">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteFillerPreset(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
</div>
<MudTable Class="mt-4"
Hover="true"
@bind-RowsPerPage="@_fillerPresetsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<FillerPresetViewModel>>>(ServerReloadFillerPresets))"
Dense="true"
@ref="_fillerPresetsTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Filler Presets</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
<col style="width: 120px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Filler Kind</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Filler Kind">
@(
context.FillerKind switch
{
FillerKind.PreRoll => "Pre-Roll",
FillerKind.MidRoll => "Mid-Roll",
FillerKind.PostRoll => "Post-Roll",
FillerKind.Fallback => "Fallback",
FillerKind.Tail => "Tail",
_ => "None"
}
)
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Filler Preset">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"media/filler/presets/{context.Id}/edit")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Filler Preset">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteFillerPreset(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
@ -90,7 +94,7 @@ @@ -90,7 +94,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Filler Preset", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
await Mediator.Send(new DeleteFillerPreset(fillerPreset.Id), _cts.Token);
if (_fillerPresetsTable != null)

153
ErsatzTV/Pages/PlayoutEditor.razor

@ -3,89 +3,99 @@ @@ -3,89 +3,99 @@
@using System.Globalization
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Validators
@using FluentValidation.Results
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<PlayoutEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">
@switch (Kind)
{
case PlayoutKind.ExternalJson:
<span>Add External Json Playout</span>
break;
case PlayoutKind.Yaml:
<span>Add YAML Playout</span>
break;
case PlayoutKind.Block:
<span>Add Block Playout</span>
break;
default:
<span>Add Playout</span>
break;
}
</MudText>
<div style="max-width: 400px;">
<EditForm EditContext="_editContext" OnSubmit="@HandleSubmitAsync">
<FluentValidationValidator/>
<MudCard>
<MudCardContent>
<MudSelect T="ChannelViewModel"
Label="Channel"
@bind-value="_model.Channel"
HelperText="Disabled channels already have a playout">
@foreach (ChannelViewModel channel in _channels)
{
<MudSelectItem Disabled="@(channel.PlayoutCount > 0)" Value="@channel">
@($"{channel.Number} - {channel.Name}")
</MudSelectItem>
}
</MudSelect>
@switch (Kind)
<MudForm @ref="_form" Validation="@(_validator.ValidateValue)" ValidationDelay="0" Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Class="ml-8" OnClick="HandleSubmitAsync" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add">
@switch (Kind)
{
case PlayoutKind.ExternalJson:
@:Add External Json Playout
break;
case PlayoutKind.Yaml:
@:Add YAML Playout
break;
case PlayoutKind.Block:
@:Add Block Playout
break;
default:
@:Add Playout
break;
}
</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">Playout</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>Channel</MudText>
</div>
<MudSelect T="ChannelViewModel" @bind-value="_model.Channel" HelperText="Disabled channels already have a playout">
@foreach (ChannelViewModel channel in _channels)
{
case PlayoutKind.ExternalJson:
<MudTextField Label="External Json File" @bind-Value="_model.ExternalJsonFile" For="@(() => _model.ExternalJsonFile)"/>
break;
case PlayoutKind.Yaml:
<MudTextField Label="YAML File" @bind-Value="_model.YamlFile" For="@(() => _model.YamlFile)"/>
break;
case PlayoutKind.Block:
break;
default:
<MudSelect Class="mt-3"
T="ProgramScheduleViewModel"
Label="Schedule"
@bind-value="_model.ProgramSchedule">
@foreach (ProgramScheduleViewModel schedule in _programSchedules)
{
<MudSelectItem Value="@schedule">@schedule.Name</MudSelectItem>
}
</MudSelect>
break;
<MudSelectItem Disabled="@(channel.PlayoutCount > 0)" Value="@channel">
@($"{channel.Number} - {channel.Name}")
</MudSelectItem>
}
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
Add Playout
</MudButton>
</MudCardActions>
</MudCard>
</EditForm>
</MudSelect>
</MudStack>
@switch (Kind)
{
case PlayoutKind.ExternalJson:
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>External Json File</MudText>
</div>
<MudTextField @bind-Value="_model.ExternalJsonFile" For="@(() => _model.ExternalJsonFile)"/>
</MudStack>
break;
case PlayoutKind.Yaml:
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>YAML File</MudText>
</div>
<MudTextField @bind-Value="_model.YamlFile" For="@(() => _model.YamlFile)"/>
</MudStack>
break;
case PlayoutKind.Block:
break;
default:
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Schedule</MudText>
</div>
<MudSelect T="ProgramScheduleViewModel" @bind-value="_model.ProgramSchedule">
@foreach (ProgramScheduleViewModel schedule in _programSchedules)
{
<MudSelectItem Value="@schedule">@schedule.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
break;
}
</MudContainer>
</div>
</MudContainer>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
private readonly PlayoutEditViewModel _model = new();
private readonly PlayoutEditViewModelValidator _validator = new();
private MudForm _form;
private List<ChannelViewModel> _channels = [];
private List<ProgramScheduleViewModel> _programSchedules = [];
private EditContext _editContext;
private ValidationMessageStore _messageStore;
[Parameter]
public string Kind { get; set; }
@ -109,16 +119,11 @@ @@ -109,16 +119,11 @@
}
}
protected override void OnInitialized()
{
_editContext = new EditContext(_model);
_messageStore = new ValidationMessageStore(_editContext);
}
private async Task HandleSubmitAsync()
{
_messageStore.Clear();
if (_editContext.Validate())
await _form.Validate();
ValidationResult result = await _validator.ValidateAsync(_model, _cts.Token);
if (result.IsValid)
{
Seq<BaseError> errorMessage = (await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();

412
ErsatzTV/Pages/Playouts.razor

@ -12,208 +12,226 @@ @@ -12,208 +12,226 @@
@inject IEntityLocker EntityLocker;
@inject ICourier Courier;
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="playouts/add">
Add Playout
</MudButton>
<MudTooltip Text="This feature is experimental">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Block}")">
Add Block Playout
</MudButton>
</MudTooltip>
<MudTooltip Text="This feature is experimental">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Yaml}")">
Add YAML Playout
</MudButton>
</MudTooltip>
<MudTooltip Text="This feature is experimental">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")">
Add External Json Playout
</MudButton>
</MudTooltip>
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@ResetAllPlayouts">
Reset All Playouts
</MudButton>
</div>
<MudTable Hover="true"
Dense="true"
Class="mt-4"
SelectedItemChanged="@(async (PlayoutNameViewModel x) => await PlayoutSelected(x))"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutNameViewModel>>>(ServerReload))"
@ref="_table">
<ToolBarContent>
<MudText Typo="Typo.h6">Playouts</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
<col/>
<col style="width: 225px;"/>
</ColGroup>
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<PlayoutViewModel, object>(x => decimal.Parse(x.Channel.Number, CultureInfo.InvariantCulture))">
Channel
</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<PlayoutViewModel, object>(x => x.ProgramSchedule.Name)">
Default Schedule
</MudTableSortLabel>
</MudTh>
<MudTh>Playout Type</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Channel">@context.ChannelNumber - @context.ChannelName</MudTd>
<MudTd DataLabel="Schedule">@context.ScheduleName</MudTd>
<MudTd DataLabel="Playout Type">
@switch (context.PlayoutType)
{
case ProgramSchedulePlayoutType.Block:
<span>Block</span>
break;
case ProgramSchedulePlayoutType.Yaml:
<span>YAML</span>
break;
case ProgramSchedulePlayoutType.ExternalJson:
<span>External Json</span>
break;
default:
<span></span>
break;
}
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
@if (EntityLocker.IsPlayoutLocked(context.PlayoutId))
{
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
}
</div>
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood)
{
if (context.ProgressMode is ChannelProgressMode.OnDemand)
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6">
<div class="flex-grow-1"></div>
<div style="margin-left: auto" class="d-none d-md-flex">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Href="playouts/add">
Add Playout
</MudButton>
<MudTooltip Text="This feature is experimental">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Block}")">
Add Block Playout
</MudButton>
</MudTooltip>
<MudTooltip Text="This feature is experimental">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Yaml}")">
Add YAML Playout
</MudButton>
</MudTooltip>
<MudTooltip Text="This feature is experimental">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")">
Add External Json Playout
</MudButton>
</MudTooltip>
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@ResetAllPlayouts">
Reset All Playouts
</MudButton>
</div>
<div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none">
<div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Playout" Href="playouts/add"/>
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add Block Playout" Href="@($"playouts/add/{PlayoutKind.Block}")"/>
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add YAML Playout" Href="@($"playouts/add/{PlayoutKind.Yaml}")"/>
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add External Json Playout" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")"/>
<MudMenuItem Icon="@Icons.Material.Filled.Refresh" Label="Reset All Playouts" OnClick="ResetAllPlayouts"/>
</MudMenu>
</div>
</div>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Playouts</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
Dense="true"
SelectedItemChanged="@(async (PlayoutNameViewModel x) => await PlayoutSelected(x))"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutNameViewModel>>>(ServerReload))"
@ref="_table">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col/>
<col/>
<col style="width: 225px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<PlayoutViewModel, object>(x => decimal.Parse(x.Channel.Number, CultureInfo.InvariantCulture))">
Channel
</MudTableSortLabel>
</MudTh>
<MudTh Class="d-none d-md-table-cell">
<MudTableSortLabel SortBy="new Func<PlayoutViewModel, object>(x => x.ProgramSchedule.Name)">
Default Schedule
</MudTableSortLabel>
</MudTh>
<MudTh Class="d-none d-md-table-cell">Playout Type</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd>@context.ChannelNumber - @context.ChannelName</MudTd>
<MudTd Class="d-none d-md-table-cell">@context.ScheduleName</MudTd>
<MudTd Class="d-none d-md-table-cell">
@switch (context.PlayoutType)
{
<MudTooltip Text="Alternate Schedules are not supported with On Demand progress">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="true">
</MudIconButton>
</MudTooltip>
case ProgramSchedulePlayoutType.Block:
<span>Block</span>
break;
case ProgramSchedulePlayoutType.Yaml:
<span>YAML</span>
break;
case ProgramSchedulePlayoutType.ExternalJson:
<span>External Json</span>
break;
default:
<span></span>
break;
}
else
{
<MudTooltip Text="Edit Alternate Schedules">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
@if (EntityLocker.IsPlayoutLocked(context.PlayoutId))
{
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
}
</div>
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood)
{
if (context.ProgressMode is ChannelProgressMode.OnDemand)
{
<MudTooltip Text="Alternate Schedules are not supported with On Demand progress">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="true">
</MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="Edit Alternate Schedules">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/{context.PlayoutId}/alternate-schedules")">
</MudIconButton>
</MudTooltip>
}
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Schedule Reset">
<MudIconButton Icon="@Icons.Material.Filled.Update"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ScheduleReset(context))">
</MudIconButton>
</MudTooltip>
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.ExternalJson)
{
<MudTooltip Text="Edit External Json File">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => EditExternalJsonFile(context))">
</MudIconButton>
</MudTooltip>
<div style="width: 48px"></div>
<div style="width: 48px"></div>
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.Yaml)
{
<MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/yaml/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<div style="width: 48px"></div>
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.Block)
{
<MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/block/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<div style="width: 48px"></div>
}
<MudTooltip Text="Delete Playout">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/{context.PlayoutId}/alternate-schedules")">
OnClick="@(_ => DeletePlayout(context))">
</MudIconButton>
</MudTooltip>
}
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Schedule Reset">
<MudIconButton Icon="@Icons.Material.Filled.Update"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ScheduleReset(context))">
</MudIconButton>
</MudTooltip>
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.ExternalJson)
{
<MudTooltip Text="Edit External Json File">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => EditExternalJsonFile(context))">
</MudIconButton>
</MudTooltip>
<div style="width: 48px"></div>
<div style="width: 48px"></div>
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.Yaml)
{
<MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/yaml/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<div style="width: 48px"></div>
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.Block)
{
<MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Href="@($"playouts/block/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => ResetPlayout(context))">
</MudIconButton>
</MudTooltip>
<div style="width: 48px"></div>
}
<MudTooltip Text="Delete Playout">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => DeletePlayout(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
@if (_selectedPlayoutId != null)
{
<MudTable Class="mt-8"
Hover="true"
Dense="true"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutItemViewModel>>>(DetailServerReload))"
@ref="_detailTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Playout Detail</MudText>
<MudSwitch T="bool" Class="ml-6" @bind-Value="@ShowFiller" Color="Color.Secondary" Label="Show Filler"/>
</ToolBarContent>
<HeaderContent>
<MudTh>Start</MudTh>
<MudTh>Finish</MudTh>
<MudTh>Media Item</MudTh>
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start">@context.Start.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Finish">@context.Finish.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Media Item">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
}
</MudContainer>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
@if (_selectedPlayoutId != null)
{
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Playout Detail</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
Dense="true"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<PlayoutItemViewModel>>>(DetailServerReload))"
@ref="_detailTable">
<ToolBarContent>
<MudSwitch T="bool" Class="ml-6" @bind-Value="@ShowFiller" Color="Color.Secondary" Label="Show Filler"/>
</ToolBarContent>
<HeaderContent>
<MudTh>Start</MudTh>
<MudTh>Finish</MudTh>
<MudTh>Media Item</MudTh>
<MudTh>Duration</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start">@context.Start.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Finish">@context.Finish.ToString("G", _dtf)</MudTd>
<MudTd DataLabel="Media Item">@context.Title</MudTd>
<MudTd DataLabel="Duration">@context.Duration</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
}
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();

183
ErsatzTV/Pages/Schedules.razor

@ -7,91 +7,100 @@ @@ -7,91 +7,100 @@
@inject IMediator Mediator
@inject NavigationManager NavigationManager
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true"
Dense="true"
SelectedItemChanged="@(async (ProgramScheduleViewModel x) => await ScheduleSelected(x))"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<ProgramScheduleViewModel>>>(ServerReload))"
@ref="_table">
<ToolBarContent>
<MudText Typo="Typo.h6">Schedules</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col style="width: 240px;"/>
</ColGroup>
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<ProgramScheduleViewModel, object>(x => x.Name)">
Name
</MudTableSortLabel>
</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Properties">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"schedules/{context.Id}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Edit Schedule Items">
<MudIconButton Icon="@Icons.Material.Filled.FormatListNumbered"
Href="@($"schedules/{context.Id}/items")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Copy Schedule">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
OnClick="@(_ => CopySchedule(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Schedule">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteSchedule(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="schedules/add" Class="mt-4">
Add Schedule
</MudButton>
@if (_selectedSchedule != null)
{
<MudTable Hover="true"
Class="mt-8"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<ProgramScheduleItemViewModel>>>(DetailServerReload))"
@ref="_detailTable">
<ToolBarContent>
<MudText Typo="Typo.h6">@_selectedSchedule.Name Items</MudText>
</ToolBarContent>
<HeaderContent>
<MudTh>Start Time</MudTh>
<MudTh>Collection</MudTh>
<MudTh>Playout Mode</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start Time">
@(context.StartType == StartType.Fixed ? context.StartTime == null ? string.Empty : DateTime.Today.Add(context.StartTime.Value).ToShortTimeString() : "Dynamic")
</MudTd>
<MudTd DataLabel="Collection">@context.Name</MudTd>
<MudTd DataLabel="Playout Mode">@context.PlayoutMode</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
}
</MudContainer>
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" StartIcon="@Icons.Material.Filled.Add" Href="schedules/add">
Add Schedule
</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">Schedules</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
Dense="true"
Breakpoint="Breakpoint.None"
SelectedItemChanged="@(async (ProgramScheduleViewModel x) => await ScheduleSelected(x))"
@bind-RowsPerPage="@_rowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<ProgramScheduleViewModel>>>(ServerReload))"
@ref="_table">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col style="width: 240px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<ProgramScheduleViewModel, object>(x => x.Name)">
Name
</MudTableSortLabel>
</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Properties">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Href="@($"schedules/{context.Id}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Edit Schedule Items">
<MudIconButton Icon="@Icons.Material.Filled.FormatListNumbered"
Href="@($"schedules/{context.Id}/items")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Copy Schedule">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
OnClick="@(_ => CopySchedule(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Schedule">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteSchedule(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
@if (_selectedSchedule != null)
{
<MudHidden Breakpoint="Breakpoint.SmAndDown">
<MudTable Hover="true"
Class="mt-8"
@bind-RowsPerPage="@_detailRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<ProgramScheduleItemViewModel>>>(DetailServerReload))"
@ref="_detailTable">
<ToolBarContent>
<MudText Typo="Typo.h6">@_selectedSchedule.Name Items</MudText>
</ToolBarContent>
<HeaderContent>
<MudTh>Start Time</MudTh>
<MudTh>Collection</MudTh>
<MudTh>Playout Mode</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Start Time">
@(context.StartType == StartType.Fixed ? context.StartTime == null ? string.Empty : DateTime.Today.Add(context.StartTime.Value).ToShortTimeString() : "Dynamic")
</MudTd>
<MudTd DataLabel="Collection">@context.Name</MudTd>
<MudTd DataLabel="Playout Mode">@context.PlayoutMode</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudHidden>
}
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
@ -132,7 +141,7 @@ @@ -132,7 +141,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Schedule", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
await Mediator.Send(new DeleteProgramSchedule(programSchedule.Id), _cts.Token);
if (_table != null)
@ -154,7 +163,7 @@ @@ -154,7 +163,7 @@
IDialogReference dialog = await Dialog.ShowAsync<CopyScheduleDialog>("Copy Schedule", parameters, options);
DialogResult dialogResult = await dialog.Result;
if (!dialogResult.Canceled && dialogResult.Data is ProgramScheduleViewModel data)
if (dialogResult is { Canceled: false, Data: ProgramScheduleViewModel data })
{
NavigationManager.NavigateTo($"schedules/{data.Id}/items");
}

120
ErsatzTV/Pages/TraktLists.razor

@ -7,68 +7,74 @@ @@ -7,68 +7,74 @@
@inject IEntityLocker Locker
@inject ChannelWriter<IBackgroundServiceRequest> WorkerChannel
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div>
<MudButton Variant="Variant.Filled"
<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 Class="ml-8"
StartIcon="@Icons.Material.Filled.Add"
Variant="Variant.Filled"
Color="Color.Primary"
Disabled="@Locker.IsTraktLocked()"
OnClick="@(_ => AddTraktList())">
Add Trakt List
</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">Trakt Lists</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true"
@bind-RowsPerPage="@_traktListsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<TraktListViewModel>>>(ServerReloadTraktLists))"
Dense="true"
@ref="_traktListsTable">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col/>
<col/>
<col style="width: 180px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>Id</MudTh>
<MudTh>Name</MudTh>
<MudTh>Match Status</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Slug</MudTd>
<MudTd>@context.Name</MudTd>
<MudTd>@context.MatchCount of @context.ItemCount</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Search Trakt List">
<MudIconButton Icon="@Icons.Material.Filled.Search"
Disabled="@Locker.IsTraktLocked()"
Href="@($"search?query=trakt_list%3a{context.TraktId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Match Trakt List Items">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsTraktLocked()"
OnClick="@(_ => MatchListItems(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Trakt List">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Disabled="@Locker.IsTraktLocked()"
OnClick="@(_ => DeleteTraktList(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
</div>
<MudTable Class="mt-4"
Hover="true"
@bind-RowsPerPage="@_traktListsRowsPerPage"
ServerData="@(new Func<TableState, CancellationToken, Task<TableData<TraktListViewModel>>>(ServerReloadTraktLists))"
Dense="true"
@ref="_traktListsTable">
<ToolBarContent>
<MudText Typo="Typo.h6">Trakt Lists</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
<col/>
<col style="width: 180px;"/>
</ColGroup>
<HeaderContent>
<MudTh>Id</MudTh>
<MudTh>Name</MudTh>
<MudTh>Match Status</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Id">@context.Slug</MudTd>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Match Status">@context.MatchCount of @context.ItemCount</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Search Trakt List">
<MudIconButton Icon="@Icons.Material.Filled.Search"
Disabled="@Locker.IsTraktLocked()"
Href="@($"search?query=trakt_list%3a{context.TraktId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Match Trakt List Items">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsTraktLocked()"
OnClick="@(_ => MatchListItems(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete Trakt List">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Disabled="@Locker.IsTraktLocked()"
OnClick="@(_ => DeleteTraktList(context))">
</MudIconButton>
</MudTooltip>
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager/>
</PagerContent>
</MudTable>
</MudContainer>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
@ -115,7 +121,7 @@ @@ -115,7 +121,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Trakt List", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
await WorkerChannel.WriteAsync(new DeleteTraktList(traktList.Id), _cts.Token);
}

2
ErsatzTV/Pages/Trash.razor

@ -623,7 +623,7 @@ @@ -623,7 +623,7 @@
IDialogReference dialog = await Dialog.ShowAsync<DeleteFromDatabaseDialog>("Delete From Database", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
if (result is { Canceled: false })
{
var request = new DeleteItemsFromDatabase(
movieIds.Append(showIds)

66
ErsatzTV/Pages/YamlPlayoutEditor.razor

@ -9,53 +9,25 @@ @@ -9,53 +9,25 @@
@inject IMediator Mediator
@inject IEntityLocker EntityLocker;
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Edit YAML Playout - @_channelName</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">YAML File</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => EditYamlFile())" Class="mt-4">
Edit YAML File
</MudButton>
</MudCardContent>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Playout Items and History</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<!-- reset will erase all items -->
<!--
<div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Warning" OnClick="@(_ => EraseItems(eraseHistory: false))" Class="mt-4">
Erase Items
</MudButton>
</div>
-->
<div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" Class="mt-4">
Erase Items and History
</MudButton>
</div>
</MudCardContent>
</MudCard>
</div>
</MudItem>
</MudGrid>
</MudContainer>
<MudForm Style="max-height: 100%">
<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>
</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">YAML File</MudText>
<MudDivider Class="mb-6"/>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => EditYamlFile())">
Edit YAML File
</MudButton>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Playout Items and History</MudText>
<MudDivider Class="mb-6"/>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))">
Erase Items and History
</MudButton>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();

134
ErsatzTV/Shared/RemoteMediaSourcePathReplacementsEditor.razor

@ -5,68 +5,78 @@ @@ -5,68 +5,78 @@
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable Hover="true" Items="_pathReplacements.OrderBy(r => r.Id)" Dense="true" @bind-SelectedItem="_selectedItem">
<ToolBarContent>
<MudText Typo="Typo.h6"><b>@_source?.Name</b> Path Replacements</MudText>
</ToolBarContent>
<ColGroup>
<col/>
<col/>
<col style="width: 60px;"/>
</ColGroup>
<HeaderContent>
<MudTh>@Name Path</MudTh>
<MudTh>Local Path</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="@($"{Name} Path")">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@context.RemotePath
</MudText>
</MudTd>
<MudTd DataLabel="Local Path">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@context.LocalPath
</MudText>
</MudTd>
<MudTd>
<MudTooltip Text="Delete Path Replacement">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => RemovePathReplacement(context))">
</MudIconButton>
</MudTooltip>
</MudTd>
</RowTemplate>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddPathReplacement())" Class="mt-4">
Add Path Replacement
</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="max-width: 400px;">
<EditForm Model="_selectedItem">
<FluentValidationValidator/>
<MudCard Class="mt-6">
<MudCardContent>
<MudTextField Label="@($"{Name} Path")"
@bind-Value="@_selectedItem.RemotePath"
For="@(() => _selectedItem.RemotePath)"/>
<MudTextField Class="mt-3"
Label="Local Path"
@bind-Value="@_selectedItem.LocalPath"
For="@(() => _selectedItem.LocalPath)"/>
</MudCardContent>
</MudCard>
</EditForm>
</div>
}
</MudContainer>
<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" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save" Class="ml-8">
Save Path Replacements
</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">@_source?.Name</MudText>
<MudDivider Class="mb-6"/>
<MudTable Hover="true" Items="_pathReplacements.OrderBy(r => r.Id)" Dense="true" @bind-SelectedItem="_selectedItem">
<ColGroup>
<MudHidden Breakpoint="Breakpoint.Xs">
<col/>
<col/>
<col style="width: 60px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
<MudTh>@Name Path</MudTh>
<MudTh>Local Path</MudTh>
<MudTh/>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="@($"{Name} Path")">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@context.RemotePath
</MudText>
</MudTd>
<MudTd DataLabel="Local Path">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@context.LocalPath
</MudText>
</MudTd>
<MudTd>
<MudTooltip Text="Delete Path Replacement">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => RemovePathReplacement(context))">
</MudIconButton>
</MudTooltip>
</MudTd>
</RowTemplate>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddPathReplacement())" Class="mt-4">
Add Path Replacement
</MudButton>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Path Replacement</MudText>
<MudDivider Class="mb-6"/>
@if (_selectedItem is not null)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>@($"{Name} Path")</MudText>
</div>
<MudTextField @bind-Value="@_selectedItem.RemotePath"
For="@(() => _selectedItem.RemotePath)"
Required="true"
RequiredError="Remote path is required!"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Local Path</MudText>
</div>
<MudTextField @bind-Value="@_selectedItem.LocalPath"
For="@(() => _selectedItem.LocalPath)"
Required="true"
RequiredError="Local path is required!"/>
</MudStack>
}
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();

15
ErsatzTV/Validators/FillerPresetEditViewModelValidator.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.ViewModels;
using FluentValidation;
using FluentValidation.Results;
namespace ErsatzTV.Validators;
@ -39,4 +40,18 @@ public class FillerPresetEditViewModelValidator : AbstractValidator<FillerPreset @@ -39,4 +40,18 @@ public class FillerPresetEditViewModelValidator : AbstractValidator<FillerPreset
.TelevisionShow or ProgramScheduleItemCollectionType.TelevisionSeason,
() => RuleFor(fp => fp.MediaItem).NotNull());
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
ValidationResult result = await ValidateAsync(
ValidationContext<FillerPresetEditViewModel>.CreateWithOptions(
(FillerPresetEditViewModel)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
{
return [];
}
return result.Errors.Select(e => e.ErrorMessage);
};
}

15
ErsatzTV/Validators/PlayoutEditViewModelValidator.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using ErsatzTV.ViewModels;
using FluentValidation;
using FluentValidation.Results;
namespace ErsatzTV.Validators;
@ -11,4 +12,18 @@ public class PlayoutEditViewModelValidator : AbstractValidator<PlayoutEditViewMo @@ -11,4 +12,18 @@ public class PlayoutEditViewModelValidator : AbstractValidator<PlayoutEditViewMo
RuleFor(p => p.ProgramSchedule).NotNull().When(p => string.IsNullOrWhiteSpace(p.Kind));
RuleFor(p => p.ExternalJsonFile).NotNull().When(p => p.Kind == PlayoutKind.ExternalJson);
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
ValidationResult result = await ValidateAsync(
ValidationContext<PlayoutEditViewModel>.CreateWithOptions(
(PlayoutEditViewModel)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
{
return [];
}
return result.Errors.Select(e => e.ErrorMessage);
};
}

Loading…
Cancel
Save