Browse Source

use new form layout for watermark editor (#2127)

* use new form layout for watermark editor

* cleanup
pull/2128/head
Jason Dove 1 month ago committed by GitHub
parent
commit
4a66f0ae43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 46
      ErsatzTV/Pages/ChannelEditor.razor
  3. 50
      ErsatzTV/Pages/FFmpegEditor.razor
  4. 12
      ErsatzTV/Pages/ScheduleEditor.razor
  5. 26
      ErsatzTV/Pages/Settings/FFmpegSettings.razor
  6. 4
      ErsatzTV/Pages/Settings/HDHRSettings.razor
  7. 10
      ErsatzTV/Pages/Settings/LoggingSettings.razor
  8. 4
      ErsatzTV/Pages/Settings/PlayoutSettings.razor
  9. 2
      ErsatzTV/Pages/Settings/ScannerSettings.razor
  10. 4
      ErsatzTV/Pages/Settings/XMLTVSettings.razor
  11. 189
      ErsatzTV/Pages/WatermarkEditor.razor
  12. 15
      ErsatzTV/Validators/WatermarkEditViewModelValidator.cs
  13. 1
      ErsatzTV/wwwroot/css/site.css

1
CHANGELOG.md

@ -77,6 +77,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Channel editor - Channel editor
- FFmpeg Profile editor - FFmpeg Profile editor
- Schedule editor - Schedule editor
- Watermark editor
### Fixed ### Fixed
- Fix QSV acceleration in docker with older Intel devices - Fix QSV acceleration in docker with older Intel devices

46
ErsatzTV/Pages/ChannelEditor.razor

@ -27,31 +27,31 @@
<MudText Typo="Typo.h5" Class="mb-2">@(IsEdit ? "Edit Channel" : "Add Channel")</MudText> <MudText Typo="Typo.h5" Class="mb-2">@(IsEdit ? "Edit Channel" : "Add Channel")</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Number</MudText> <MudText>Number</MudText>
</div> </div>
<MudTextField @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/> <MudTextField @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Name</MudText> <MudText>Name</MudText>
</div> </div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Group</MudText> <MudText>Group</MudText>
</div> </div>
<MudTextField @bind-Value="_model.Group" For="@(() => _model.Group)"/> <MudTextField @bind-Value="_model.Group" For="@(() => _model.Group)"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Categories</MudText> <MudText>Categories</MudText>
</div> </div>
<MudTextField @bind-Value="_model.Categories" For="@(() => _model.Categories)" HelperText="Comma-separated list of categories"/> <MudTextField @bind-Value="_model.Categories" For="@(() => _model.Categories)" HelperText="Comma-separated list of categories"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Active Mode</MudText> <MudText>Active Mode</MudText>
</div> </div>
<MudSelect @bind-Value="_model.ActiveMode" For="@(() => _model.ActiveMode)"> <MudSelect @bind-Value="_model.ActiveMode" For="@(() => _model.ActiveMode)">
@ -61,7 +61,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Progress Mode</MudText> <MudText>Progress Mode</MudText>
</div> </div>
<MudSelect @bind-Value="_model.ProgressMode" For="@(() => _model.ProgressMode)"> <MudSelect @bind-Value="_model.ProgressMode" For="@(() => _model.ProgressMode)">
@ -70,7 +70,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Streaming Mode</MudText> <MudText>Streaming Mode</MudText>
</div> </div>
<MudSelect @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)"> <MudSelect @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
@ -82,7 +82,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>FFmpeg Profile</MudText> <MudText>FFmpeg Profile</MudText>
</div> </div>
<MudSelect @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)" <MudSelect @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
@ -94,7 +94,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Stream Selector Mode</MudText> <MudText>Stream Selector Mode</MudText>
</div> </div>
<MudSelect @bind-Value="_model.StreamSelectorMode" For="@(() => _model.StreamSelectorMode)"> <MudSelect @bind-Value="_model.StreamSelectorMode" For="@(() => _model.StreamSelectorMode)">
@ -105,7 +105,7 @@
@if (_model.StreamSelectorMode is ChannelStreamSelectorMode.Default) @if (_model.StreamSelectorMode is ChannelStreamSelectorMode.Default)
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Preferred Audio Language</MudText> <MudText>Preferred Audio Language</MudText>
</div> </div>
<MudSelect @bind-Value="_model.PreferredAudioLanguageCode" <MudSelect @bind-Value="_model.PreferredAudioLanguageCode"
@ -119,13 +119,13 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Preferred Audio Title</MudText> <MudText>Preferred Audio Title</MudText>
</div> </div>
<MudTextField @bind-Value="_model.PreferredAudioTitle" For="@(() => _model.PreferredAudioTitle)"/> <MudTextField @bind-Value="_model.PreferredAudioTitle" For="@(() => _model.PreferredAudioTitle)"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Preferred Subtitle Language</MudText> <MudText>Preferred Subtitle Language</MudText>
</div> </div>
<MudSelect @bind-Value="_model.PreferredSubtitleLanguageCode" <MudSelect @bind-Value="_model.PreferredSubtitleLanguageCode"
@ -139,7 +139,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Subtitle Mode</MudText> <MudText>Subtitle Mode</MudText>
</div> </div>
<MudSelect @bind-Value="_model.SubtitleMode" For="@(() => _model.SubtitleMode)"> <MudSelect @bind-Value="_model.SubtitleMode" For="@(() => _model.SubtitleMode)">
@ -153,7 +153,7 @@
else else
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Stream Selector</MudText> <MudText>Stream Selector</MudText>
</div> </div>
<MudSelect @bind-Value="_model.StreamSelector" <MudSelect @bind-Value="_model.StreamSelector"
@ -167,7 +167,7 @@
</MudStack> </MudStack>
} }
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Music Video Credits Mode</MudText> <MudText>Music Video Credits Mode</MudText>
</div> </div>
<MudSelect @bind-Value="_model.MusicVideoCreditsMode" For="@(() => _model.MusicVideoCreditsMode)"> <MudSelect @bind-Value="_model.MusicVideoCreditsMode" For="@(() => _model.MusicVideoCreditsMode)">
@ -176,7 +176,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Music Video Credits Template</MudText> <MudText>Music Video Credits Template</MudText>
</div> </div>
<MudSelect @bind-Value="_model.MusicVideoCreditsTemplate" <MudSelect @bind-Value="_model.MusicVideoCreditsTemplate"
@ -190,7 +190,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Song Video Mode</MudText> <MudText>Song Video Mode</MudText>
</div> </div>
<MudSelect @bind-Value="_model.SongVideoMode" For="@(() => _model.SongVideoMode)"> <MudSelect @bind-Value="_model.SongVideoMode" For="@(() => _model.SongVideoMode)">
@ -199,10 +199,10 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Logo</MudText> <MudText>Logo</MudText>
</div> </div>
<InputFile id="fileInput" OnChange="UploadLogo" hidden/> <InputFile id="fileInput" OnChange="UploadLogo" style="display: none;"/>
<MudButton HtmlTag="label" <MudButton HtmlTag="label"
Variant="Variant.Filled" Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
@ -212,13 +212,13 @@
</MudButton> </MudButton>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>External Logo URL</MudText> <MudText>External Logo URL</MudText>
</div> </div>
<MudTextField @bind-Value="_model.ExternalLogoUrl" For="@(() => _model.ExternalLogoUrl)"/> <MudTextField @bind-Value="_model.ExternalLogoUrl" For="@(() => _model.ExternalLogoUrl)"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Logo Preview</MudText> <MudText>Logo Preview</MudText>
</div> </div>
@if (!string.IsNullOrWhiteSpace(_model.Logo?.Path) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl)) @if (!string.IsNullOrWhiteSpace(_model.Logo?.Path) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl))
@ -227,7 +227,7 @@
} }
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Watermark</MudText> <MudText>Watermark</MudText>
</div> </div>
<MudSelect @bind-Value="_model.WatermarkId" For="@(() => _model.WatermarkId)" <MudSelect @bind-Value="_model.WatermarkId" For="@(() => _model.WatermarkId)"
@ -241,7 +241,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Fallback Filler</MudText> <MudText>Fallback Filler</MudText>
</div> </div>
<MudSelect @bind-Value="_model.FallbackFillerId" For="@(() => _model.FallbackFillerId)" Clearable="true"> <MudSelect @bind-Value="_model.FallbackFillerId" For="@(() => _model.FallbackFillerId)" Clearable="true">

50
ErsatzTV/Pages/FFmpegEditor.razor

@ -27,19 +27,19 @@
<MudText Typo="Typo.h5" Class="mb-2">General</MudText> <MudText Typo="Typo.h5" Class="mb-2">General</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Name</MudText> <MudText>Name</MudText>
</div> </div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Immediate="true"/> <MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Immediate="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Thread Count</MudText> <MudText>Thread Count</MudText>
</div> </div>
<MudTextField @bind-Value="@_model.ThreadCount" For="@(() => _model.ThreadCount)"/> <MudTextField @bind-Value="@_model.ThreadCount" For="@(() => _model.ThreadCount)"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Preferred Resolution</MudText> <MudText>Preferred Resolution</MudText>
</div> </div>
<MudSelect @bind-Value="_model.Resolution" For="@(() => _model.Resolution)"> <MudSelect @bind-Value="_model.Resolution" For="@(() => _model.Resolution)">
@ -50,7 +50,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Scaling Behavior</MudText> <MudText>Scaling Behavior</MudText>
</div> </div>
<MudSelect @bind-Value="_model.ScalingBehavior" For="@(() => _model.ScalingBehavior)"> <MudSelect @bind-Value="_model.ScalingBehavior" For="@(() => _model.ScalingBehavior)">
@ -62,7 +62,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Video</MudText> <MudText Typo="Typo.h5" Class="mb-2">Video</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Format</MudText> <MudText>Format</MudText>
</div> </div>
<MudSelect @bind-Value="_model.VideoFormat" For="@(() => _model.VideoFormat)"> <MudSelect @bind-Value="_model.VideoFormat" For="@(() => _model.VideoFormat)">
@ -72,7 +72,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Profile</MudText> <MudText>Profile</MudText>
</div> </div>
<MudSelect @bind-Value="_model.VideoProfile" <MudSelect @bind-Value="_model.VideoProfile"
@ -84,7 +84,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Preset</MudText> <MudText>Preset</MudText>
</div> </div>
@{ @{
@ -104,13 +104,13 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Allow B-Frames</MudText> <MudText>Allow B-Frames</MudText>
</div> </div>
<MudCheckBox @bind-Value="@_model.AllowBFrames" For="@(() => _model.AllowBFrames)" Dense="true"/> <MudCheckBox @bind-Value="@_model.AllowBFrames" For="@(() => _model.AllowBFrames)" Dense="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Bit Depth</MudText> <MudText>Bit Depth</MudText>
</div> </div>
<MudSelect @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)"> <MudSelect @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)">
@ -119,19 +119,19 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Bitrate</MudText> <MudText>Bitrate</MudText>
</div> </div>
<MudTextField @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudTextField @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Buffer Size</MudText> <MudText>Buffer Size</MudText>
</div> </div>
<MudTextField @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/> <MudTextField @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Hardware Acceleration</MudText> <MudText>Hardware Acceleration</MudText>
</div> </div>
<MudSelect @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)"> <MudSelect @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
@ -146,7 +146,7 @@
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi) @if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi)
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>VAAPI Driver</MudText> <MudText>VAAPI Driver</MudText>
</div> </div>
<MudSelect @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)"> <MudSelect @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)">
@ -157,7 +157,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>VAAPI Display</MudText> <MudText>VAAPI Display</MudText>
</div> </div>
<MudSelect @bind-Value="_model.VaapiDisplay" For="@(() => _model.VaapiDisplay)"> <MudSelect @bind-Value="_model.VaapiDisplay" For="@(() => _model.VaapiDisplay)">
@ -172,7 +172,7 @@
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv) @if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>@(_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi ? "VAAPI Device" : "QSV Device")</MudText> <MudText>@(_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi ? "VAAPI Device" : "QSV Device")</MudText>
</div> </div>
<MudSelect @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)"> <MudSelect @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)">
@ -188,7 +188,7 @@
@if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv) @if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv)
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>QSV Extra Hardware Frames</MudText> <MudText>QSV Extra Hardware Frames</MudText>
</div> </div>
<MudTextField @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/> <MudTextField @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/>
@ -197,7 +197,7 @@
else else
{ {
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Tonemap Algorithm</MudText> <MudText>Tonemap Algorithm</MudText>
</div> </div>
<MudSelect @bind-Value="_model.TonemapAlgorithm" For="@(() => _model.TonemapAlgorithm)"> <MudSelect @bind-Value="_model.TonemapAlgorithm" For="@(() => _model.TonemapAlgorithm)">
@ -209,13 +209,13 @@
</MudStack> </MudStack>
} }
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Normalize Frame Rate</MudText> <MudText>Normalize Frame Rate</MudText>
</div> </div>
<MudCheckBox @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)" Dense="true"/> <MudCheckBox @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)" Dense="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Auto Deinterlace Video</MudText> <MudText>Auto Deinterlace Video</MudText>
</div> </div>
<MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/> <MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/>
@ -223,7 +223,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Audio</MudText> <MudText Typo="Typo.h5" Class="mb-2">Audio</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Format</MudText> <MudText>Format</MudText>
</div> </div>
<MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)"> <MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
@ -232,31 +232,31 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Bitrate</MudText> <MudText>Bitrate</MudText>
</div> </div>
<MudTextField @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudTextField @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Buffer Size</MudText> <MudText>Buffer Size</MudText>
</div> </div>
<MudTextField @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/> <MudTextField @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Channels</MudText> <MudText>Channels</MudText>
</div> </div>
<MudTextField @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/> <MudTextField @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Sample Rate</MudText> <MudText>Sample Rate</MudText>
</div> </div>
<MudTextField @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/> <MudTextField @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Normalize Loudness</MudText> <MudText>Normalize Loudness</MudText>
</div> </div>
<MudSelect @bind-Value="_model.NormalizeLoudnessMode" For="@(() => _model.NormalizeLoudnessMode)"> <MudSelect @bind-Value="_model.NormalizeLoudnessMode" For="@(() => _model.NormalizeLoudnessMode)">

12
ErsatzTV/Pages/ScheduleEditor.razor

@ -17,13 +17,13 @@
<MudText Typo="Typo.h5" Class="mb-2">Schedule</MudText> <MudText Typo="Typo.h5" Class="mb-2">Schedule</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Name</MudText> <MudText>Name</MudText>
</div> </div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Required="true" RequiredError="Schedule name is required!"/> <MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Required="true" RequiredError="Schedule name is required!"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Keep Multi-Part Episodes Together</MudText> <MudText>Keep Multi-Part Episodes Together</MudText>
</div> </div>
<MudCheckBox @bind-Value="@_model.KeepMultiPartEpisodesTogether" For="@(() => _model.KeepMultiPartEpisodesTogether)" Dense="true"> <MudCheckBox @bind-Value="@_model.KeepMultiPartEpisodesTogether" For="@(() => _model.KeepMultiPartEpisodesTogether)" Dense="true">
@ -31,7 +31,7 @@
</MudCheckBox> </MudCheckBox>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Treat Collections As Shows</MudText> <MudText>Treat Collections As Shows</MudText>
</div> </div>
<MudCheckBox @bind-Value="@_model.TreatCollectionsAsShows" <MudCheckBox @bind-Value="@_model.TreatCollectionsAsShows"
@ -41,7 +41,7 @@
</MudCheckBox> </MudCheckBox>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Shuffle Schedule Items</MudText> <MudText>Shuffle Schedule Items</MudText>
</div> </div>
<MudCheckBox @bind-Value="@_model.ShuffleScheduleItems" For="@(() => _model.ShuffleScheduleItems)" Dense="true"> <MudCheckBox @bind-Value="@_model.ShuffleScheduleItems" For="@(() => _model.ShuffleScheduleItems)" Dense="true">
@ -49,13 +49,13 @@
</MudCheckBox> </MudCheckBox>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Random Start Point</MudText> <MudText>Random Start Point</MudText>
</div> </div>
<MudCheckBox @bind-Value="@_model.RandomStartPoint" For="@(() => _model.RandomStartPoint)" Dense="true"/> <MudCheckBox @bind-Value="@_model.RandomStartPoint" For="@(() => _model.RandomStartPoint)" Dense="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Fixed Start Time Behavior</MudText> <MudText>Fixed Start Time Behavior</MudText>
</div> </div>
<MudSelect @bind-Value="@_model.FixedStartTimeBehavior" For="@(() => _model.FixedStartTimeBehavior)"> <MudSelect @bind-Value="@_model.FixedStartTimeBehavior" For="@(() => _model.FixedStartTimeBehavior)">

26
ErsatzTV/Pages/Settings/FFmpegSettings.razor

@ -21,19 +21,19 @@
<MudText Typo="Typo.h5" Class="mb-2">FFmpeg</MudText> <MudText Typo="Typo.h5" Class="mb-2">FFmpeg</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>FFmpeg Path</MudText> <MudText>FFmpeg Path</MudText>
</div> </div>
<MudTextField @bind-Value="_ffmpegSettings.FFmpegPath" HelperText="The full path to the ffmpeg executable file" Validation="@(new Func<string, string>(ValidatePathExists))" Required="true" RequiredError="FFmpeg path is required!"/> <MudTextField @bind-Value="_ffmpegSettings.FFmpegPath" HelperText="The full path to the ffmpeg executable file" Validation="@(new Func<string, string>(ValidatePathExists))" Required="true" RequiredError="FFmpeg path is required!"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>FFprobe Path</MudText> <MudText>FFprobe Path</MudText>
</div> </div>
<MudTextField @bind-Value="_ffmpegSettings.FFprobePath" HelperText="The full path to the ffprobe executable file" Validation="@(new Func<string, string>(ValidatePathExists))" Required="true" RequiredError="FFprobe path is required!"/> <MudTextField @bind-Value="_ffmpegSettings.FFprobePath" HelperText="The full path to the ffprobe executable file" Validation="@(new Func<string, string>(ValidatePathExists))" Required="true" RequiredError="FFprobe path is required!"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Default FFmpeg Profile</MudText> <MudText>Default FFmpeg Profile</MudText>
</div> </div>
<MudSelect @bind-Value="_ffmpegSettings.DefaultFFmpegProfileId" HelperText="The FFmpeg Profile to use when creating new channels"> <MudSelect @bind-Value="_ffmpegSettings.DefaultFFmpegProfileId" HelperText="The FFmpeg Profile to use when creating new channels">
@ -44,7 +44,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Preferred Audio Language</MudText> <MudText>Preferred Audio Language</MudText>
</div> </div>
<MudSelect @bind-Value="_ffmpegSettings.PreferredAudioLanguageCode" Required="true" RequiredError="Preferred Language Code is required!"> <MudSelect @bind-Value="_ffmpegSettings.PreferredAudioLanguageCode" Required="true" RequiredError="Preferred Language Code is required!">
@ -55,25 +55,25 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Use Embedded Subtitles</MudText> <MudText>Use Embedded Subtitles</MudText>
</div> </div>
<MudCheckBox @bind-Value="_ffmpegSettings.UseEmbeddedSubtitles" Dense="true"/> <MudCheckBox @bind-Value="_ffmpegSettings.UseEmbeddedSubtitles" Dense="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Extract Embedded (Text) Subtitles</MudText> <MudText>Extract Embedded (Text) Subtitles</MudText>
</div> </div>
<MudCheckBox @bind-Value="_ffmpegSettings.ExtractEmbeddedSubtitles" Disabled="@(_ffmpegSettings.UseEmbeddedSubtitles == false)" Dense="true"/> <MudCheckBox @bind-Value="_ffmpegSettings.ExtractEmbeddedSubtitles" Disabled="@(_ffmpegSettings.UseEmbeddedSubtitles == false)" Dense="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Save Troubleshooting Reports To Disk</MudText> <MudText>Save Troubleshooting Reports To Disk</MudText>
</div> </div>
<MudCheckBox @bind-Value="_ffmpegSettings.SaveReports" Dense="true"/> <MudCheckBox @bind-Value="_ffmpegSettings.SaveReports" Dense="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Global Watermark</MudText> <MudText>Global Watermark</MudText>
</div> </div>
<MudSelect @bind-Value="_ffmpegSettings.GlobalWatermarkId" Clearable="true"> <MudSelect @bind-Value="_ffmpegSettings.GlobalWatermarkId" Clearable="true">
@ -85,7 +85,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Global Fallback Filler</MudText> <MudText>Global Fallback Filler</MudText>
</div> </div>
<MudSelect @bind-Value="_ffmpegSettings.GlobalFallbackFillerId" Clearable="true"> <MudSelect @bind-Value="_ffmpegSettings.GlobalFallbackFillerId" Clearable="true">
@ -97,25 +97,25 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>HLS Segmenter Idle Timeout</MudText> <MudText>HLS Segmenter Idle Timeout</MudText>
</div> </div>
<MudTextField @bind-Value="_ffmpegSettings.HlsSegmenterIdleTimeout" Validation="@(new Func<int, string>(ValidateHlsSegmenterIdleTimeout))" Required="true" RequiredError="HLS Segmenter idle timeout is required!" Adornment="Adornment.End" AdornmentText="seconds" HelperText="The number of seconds to continue transcoding a channel while no requests have been received from any client"/> <MudTextField @bind-Value="_ffmpegSettings.HlsSegmenterIdleTimeout" Validation="@(new Func<int, string>(ValidateHlsSegmenterIdleTimeout))" Required="true" RequiredError="HLS Segmenter idle timeout is required!" Adornment="Adornment.End" AdornmentText="seconds" HelperText="The number of seconds to continue transcoding a channel while no requests have been received from any client"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Work-Ahead HLS Segmenter Limit</MudText> <MudText>Work-Ahead HLS Segmenter Limit</MudText>
</div> </div>
<MudTextField @bind-Value="_ffmpegSettings.WorkAheadSegmenterLimit" Validation="@(new Func<int, string>(ValidateWorkAheadSegmenterLimit))" Required="true" RequiredError="Work-ahead HLS Segmenter limit is required!" HelperText="The number of segmenters (channels) that will work-ahead (transcode at maximum speed) simultaneously, if multiple channels are being watched"/> <MudTextField @bind-Value="_ffmpegSettings.WorkAheadSegmenterLimit" Validation="@(new Func<int, string>(ValidateWorkAheadSegmenterLimit))" Required="true" RequiredError="Work-ahead HLS Segmenter limit is required!" HelperText="The number of segmenters (channels) that will work-ahead (transcode at maximum speed) simultaneously, if multiple channels are being watched"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>HLS Segmenter Initial Segment Count</MudText> <MudText>HLS Segmenter Initial Segment Count</MudText>
</div> </div>
<MudTextField @bind-Value="_ffmpegSettings.InitialSegmentCount" Validation="@(new Func<int, string>(ValidateInitialSegmentCount))" Required="true" RequiredError="HLS Segmenter initial segment count is required!" HelperText="Delays stream start until the specified number of (4-second) segments have been transcoded; a larger number will mean slower initial playback, but potentially less buffering"/> <MudTextField @bind-Value="_ffmpegSettings.InitialSegmentCount" Validation="@(new Func<int, string>(ValidateInitialSegmentCount))" Required="true" RequiredError="HLS Segmenter initial segment count is required!" HelperText="Delays stream start until the specified number of (4-second) segments have been transcoded; a larger number will mean slower initial playback, but potentially less buffering"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>HLS Direct Output Format</MudText> <MudText>HLS Direct Output Format</MudText>
</div> </div>
<MudSelect @bind-Value="_ffmpegSettings.HlsDirectOutputFormat" Clearable="true"> <MudSelect @bind-Value="_ffmpegSettings.HlsDirectOutputFormat" Clearable="true">

4
ErsatzTV/Pages/Settings/HDHRSettings.razor

@ -15,13 +15,13 @@
<MudText Typo="Typo.h5" Class="mb-2">HDHomeRun</MudText> <MudText Typo="Typo.h5" Class="mb-2">HDHomeRun</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>UUID</MudText> <MudText>UUID</MudText>
</div> </div>
<MudTextField @bind-Value="_uuid" Disabled="true"/> <MudTextField @bind-Value="_uuid" Disabled="true"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Tuner Count</MudText> <MudText>Tuner Count</MudText>
</div> </div>
<MudTextField @bind-Value="_tunerCount" Validation="@(new Func<int, string>(ValidateTunerCount))" Required="true" RequiredError="Tuner count is required!"/> <MudTextField @bind-Value="_tunerCount" Validation="@(new Func<int, string>(ValidateTunerCount))" Required="true" RequiredError="Tuner count is required!"/>

10
ErsatzTV/Pages/Settings/LoggingSettings.razor

@ -15,7 +15,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Logging</MudText> <MudText Typo="Typo.h5" Class="mb-2">Logging</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Default Minimum Log Level</MudText> <MudText>Default Minimum Log Level</MudText>
</div> </div>
<MudSelect @bind-Value="_loggingSettings.DefaultMinimumLogLevel"> <MudSelect @bind-Value="_loggingSettings.DefaultMinimumLogLevel">
@ -26,7 +26,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Scanning Minimum Log Level</MudText> <MudText>Scanning Minimum Log Level</MudText>
</div> </div>
<MudSelect @bind-Value="_loggingSettings.ScanningMinimumLogLevel"> <MudSelect @bind-Value="_loggingSettings.ScanningMinimumLogLevel">
@ -37,7 +37,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Scheduling Minimum Log Level</MudText> <MudText>Scheduling Minimum Log Level</MudText>
</div> </div>
<MudSelect @bind-Value="_loggingSettings.SchedulingMinimumLogLevel"> <MudSelect @bind-Value="_loggingSettings.SchedulingMinimumLogLevel">
@ -48,7 +48,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Streaming Minimum Log Level</MudText> <MudText>Streaming Minimum Log Level</MudText>
</div> </div>
<MudSelect @bind-Value="_loggingSettings.StreamingMinimumLogLevel"> <MudSelect @bind-Value="_loggingSettings.StreamingMinimumLogLevel">
@ -59,7 +59,7 @@
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Request Logging Minimum Log Level</MudText> <MudText>Request Logging Minimum Log Level</MudText>
</div> </div>
<MudSelect @bind-Value="_loggingSettings.HttpMinimumLogLevel"> <MudSelect @bind-Value="_loggingSettings.HttpMinimumLogLevel">

4
ErsatzTV/Pages/Settings/PlayoutSettings.razor

@ -14,13 +14,13 @@
<MudText Typo="Typo.h5" Class="mb-2">Playout</MudText> <MudText Typo="Typo.h5" Class="mb-2">Playout</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Days To Build</MudText> <MudText>Days To Build</MudText>
</div> </div>
<MudTextField @bind-Value="_playoutSettings.DaysToBuild" Validation="@(new Func<int, string>(ValidatePlayoutDaysToBuild))" Required="true" RequiredError="Playout days to build is required!" Adornment="Adornment.End" AdornmentText="days"/> <MudTextField @bind-Value="_playoutSettings.DaysToBuild" Validation="@(new Func<int, string>(ValidatePlayoutDaysToBuild))" Required="true" RequiredError="Playout days to build is required!" Adornment="Adornment.End" AdornmentText="days"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Skip Missing Items</MudText> <MudText>Skip Missing Items</MudText>
</div> </div>
<MudCheckBox @bind-Value="_playoutSettings.SkipMissingItems" Dense="true"> <MudCheckBox @bind-Value="_playoutSettings.SkipMissingItems" Dense="true">

2
ErsatzTV/Pages/Settings/ScannerSettings.razor

@ -14,7 +14,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Scanner</MudText> <MudText Typo="Typo.h5" Class="mb-2">Scanner</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Library Refresh Interval</MudText> <MudText>Library Refresh Interval</MudText>
</div> </div>
<MudTextField @bind-Value="_libraryRefreshInterval" Validation="@(new Func<int, string>(ValidateLibraryRefreshInterval))" Required="true" RequiredError="Library refresh interval is required!" Adornment="Adornment.End" AdornmentText="hours"/> <MudTextField @bind-Value="_libraryRefreshInterval" Validation="@(new Func<int, string>(ValidateLibraryRefreshInterval))" Required="true" RequiredError="Library refresh interval is required!" Adornment="Adornment.End" AdornmentText="hours"/>

4
ErsatzTV/Pages/Settings/XMLTVSettings.razor

@ -14,13 +14,13 @@
<MudText Typo="Typo.h5" Class="mb-2">XMLTV</MudText> <MudText Typo="Typo.h5" Class="mb-2">XMLTV</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>Days To Build</MudText> <MudText>Days To Build</MudText>
</div> </div>
<MudTextField @bind-Value="_xmltvSettings.DaysToBuild" Validation="@(new Func<int, string>(ValidateXmltvDaysToBuild))" Required="true" RequiredError="XMLTV days to build is required!" Adornment="Adornment.End" AdornmentText="days"/> <MudTextField @bind-Value="_xmltvSettings.DaysToBuild" Validation="@(new Func<int, string>(ValidateXmltvDaysToBuild))" Required="true" RequiredError="XMLTV days to build is required!" Adornment="Adornment.End" AdornmentText="days"/>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end"> <div class="d-flex">
<MudText>XMLTV Time Zone</MudText> <MudText>XMLTV Time Zone</MudText>
</div> </div>
<MudSelect @bind-Value="_xmltvSettings.TimeZone"> <MudSelect @bind-Value="_xmltvSettings.TimeZone">

189
ErsatzTV/Pages/WatermarkEditor.razor

@ -4,49 +4,55 @@
@using ErsatzTV.Application.Images @using ErsatzTV.Application.Images
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using ErsatzTV.FFmpeg.State @using ErsatzTV.FFmpeg.State
@using ErsatzTV.Validators
@using FluentValidation.Results
@implements IDisposable @implements IDisposable
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ILogger<WatermarkEditor> Logger @inject ILogger<WatermarkEditor> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudForm Model="@_model" @ref="@_form" Validation="@(_validator.ValidateValue)" ValidationDelay="0" 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">
@if (_editContext is not null) <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-8" OnClick="HandleSubmitAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Watermark" : "Add Watermark")</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">Watermark</MudText>
<MudCardHeader> <MudDivider Class="mb-6"/>
<CardHeaderContent> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudText Typo="Typo.h5">@(IsEdit ? "Edit Watermark" : "Add Watermark")</MudText> <div class="d-flex">
</CardHeaderContent> <MudText>Name</MudText>
</MudCardHeader> </div>
<MudCardContent> <MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudTextField Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> </MudStack>
<MudSelect Class="mt-3" Label="Mode" @bind-Value="_model.Mode" <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
For="@(() => _model.Mode)"> <div class="d-flex">
<MudText>Mode</MudText>
</div>
<MudSelect @bind-Value="_model.Mode" For="@(() => _model.Mode)">
<MudSelectItem Value="@(ChannelWatermarkMode.None)">None</MudSelectItem> <MudSelectItem Value="@(ChannelWatermarkMode.None)">None</MudSelectItem>
<MudSelectItem Value="@(ChannelWatermarkMode.Permanent)">Permanent</MudSelectItem> <MudSelectItem Value="@(ChannelWatermarkMode.Permanent)">Permanent</MudSelectItem>
<MudSelectItem Value="@(ChannelWatermarkMode.Intermittent)">Intermittent</MudSelectItem> <MudSelectItem Value="@(ChannelWatermarkMode.Intermittent)">Intermittent</MudSelectItem>
</MudSelect> </MudSelect>
<MudSelect Class="mt-3" Label="Image Source" @bind-Value="_model.ImageSource" </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Image Source</MudText>
</div>
<MudSelect @bind-Value="_model.ImageSource"
For="@(() => _model.ImageSource)" For="@(() => _model.ImageSource)"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)"> Disabled="@(_model.Mode == ChannelWatermarkMode.None)">
<MudSelectItem Value="@(ChannelWatermarkImageSource.Custom)">Custom</MudSelectItem> <MudSelectItem Value="@(ChannelWatermarkImageSource.Custom)">Custom</MudSelectItem>
<MudSelectItem Value="@(ChannelWatermarkImageSource.ChannelLogo)">Channel Logo</MudSelectItem> <MudSelectItem Value="@(ChannelWatermarkImageSource.ChannelLogo)">Channel Logo</MudSelectItem>
</MudSelect> </MudSelect>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center"> </MudStack>
<MudItem xs="6"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Image</MudText>
</div>
<InputFile id="watermarkFileInput" OnChange="UploadWatermark" style="display: none;"/> <InputFile id="watermarkFileInput" OnChange="UploadWatermark" style="display: none;"/>
@if (!string.IsNullOrWhiteSpace(_model.Image?.Path) && _model.ImageSource == ChannelWatermarkImageSource.Custom) <MudButton HtmlTag="label"
{
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image.UrlWithContentType}")" Style="max-height: 50px"/>
}
<ValidationMessage For="@(() => _model.Image)" style="color: #f44336 !important;"/>
</MudItem>
<MudItem xs="6">
<MudButton Class="ml-auto" HtmlTag="label"
Variant="Variant.Filled" Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload" StartIcon="@Icons.Material.Filled.CloudUpload"
@ -54,9 +60,21 @@
for="watermarkFileInput"> for="watermarkFileInput">
Upload Image Upload Image
</MudButton> </MudButton>
</MudItem> </MudStack>
</MudGrid> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudSelect Class="mt-3" Label="Location" @bind-Value="_model.Location" <div class="d-flex">
<MudText>Image Preview</MudText>
</div>
@if (!string.IsNullOrWhiteSpace(_model.Image?.Path) && _model.ImageSource == ChannelWatermarkImageSource.Custom)
{
<MudElement HtmlTag="img" src="@($"artwork/watermarks/{_model.Image.UrlWithContentType}")" 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>Location</MudText>
</div>
<MudSelect @bind-Value="_model.Location"
For="@(() => _model.Location)" For="@(() => _model.Location)"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)"> Disabled="@(_model.Mode == ChannelWatermarkMode.None)">
<MudSelectItem Value="@(WatermarkLocation.BottomRight)">Bottom Right</MudSelectItem> <MudSelectItem Value="@(WatermarkLocation.BottomRight)">Bottom Right</MudSelectItem>
@ -68,49 +86,64 @@
<MudSelectItem Value="@(WatermarkLocation.TopRight)">Top Right</MudSelectItem> <MudSelectItem Value="@(WatermarkLocation.TopRight)">Top Right</MudSelectItem>
<MudSelectItem Value="@(WatermarkLocation.RightMiddle)">Right Middle</MudSelectItem> <MudSelectItem Value="@(WatermarkLocation.RightMiddle)">Right Middle</MudSelectItem>
</MudSelect> </MudSelect>
<MudCheckBox Class="mt-3" Label="Place Within Source Content" </MudStack>
@bind-Value="_model.PlaceWithinSourceContent" <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Place Within Source Content</MudText>
</div>
<MudCheckBox @bind-Value="_model.PlaceWithinSourceContent"
For="@(() => _model.PlaceWithinSourceContent)" For="@(() => _model.PlaceWithinSourceContent)"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)"/> Disabled="@(_model.Mode == ChannelWatermarkMode.None)"
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> Dense="true"/>
<MudItem xs="6"> </MudStack>
<MudSelect Label="Size" @bind-Value="_model.Size" <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Size</MudText>
</div>
<MudSelect @bind-Value="_model.Size"
For="@(() => _model.Size)" For="@(() => _model.Size)"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)"> Disabled="@(_model.Mode == ChannelWatermarkMode.None)">
<MudSelectItem Value="@(WatermarkSize.Scaled)">Scaled</MudSelectItem> <MudSelectItem Value="@(WatermarkSize.Scaled)">Scaled</MudSelectItem>
<MudSelectItem Value="@(WatermarkSize.ActualSize)">Actual Size</MudSelectItem> <MudSelectItem Value="@(WatermarkSize.ActualSize)">Actual Size</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudStack>
<MudItem xs="6"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudTextField Label="Width" @bind-Value="_model.Width" <div class="d-flex">
For="@(() => _model.Width)" <MudText>Width</MudText>
</div>
<MudTextField For="@(() => _model.Width)"
Adornment="Adornment.End" Adornment="Adornment.End"
AdornmentText="%" AdornmentText="%"
Disabled="@(_model.Mode == ChannelWatermarkMode.None || _model.Size == WatermarkSize.ActualSize)" Disabled="@(_model.Mode == ChannelWatermarkMode.None || _model.Size == WatermarkSize.ActualSize)"
Immediate="true"/> Immediate="true"/>
</MudItem> </MudStack>
</MudGrid> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> <div class="d-flex">
<MudItem xs="6"> <MudText>Horizontal Margin</MudText>
<MudTextField Label="Horizontal Margin" @bind-Value="_model.HorizontalMargin" </div>
<MudTextField @bind-Value="_model.HorizontalMargin"
For="@(() => _model.HorizontalMargin)" For="@(() => _model.HorizontalMargin)"
Adornment="Adornment.End" Adornment="Adornment.End"
AdornmentText="%" AdornmentText="%"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)" Disabled="@(_model.Mode == ChannelWatermarkMode.None)"
Immediate="true"/> Immediate="true"/>
</MudItem> </MudStack>
<MudItem xs="6"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudTextField Label="Vertical Margin" @bind-Value="_model.VerticalMargin" <div class="d-flex">
<MudText>Vertical Margin</MudText>
</div>
<MudTextField @bind-Value="_model.VerticalMargin"
For="@(() => _model.VerticalMargin)" For="@(() => _model.VerticalMargin)"
Adornment="Adornment.End" Adornment="Adornment.End"
AdornmentText="%" AdornmentText="%"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)" Disabled="@(_model.Mode == ChannelWatermarkMode.None)"
Immediate="true"/> Immediate="true"/>
</MudItem> </MudStack>
</MudGrid> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> <div class="d-flex">
<MudItem xs="6"> <MudText>Frequency</MudText>
<MudSelect Label="Frequency" @bind-Value="_model.FrequencyMinutes" </div>
<MudSelect @bind-Value="_model.FrequencyMinutes"
For="@(() => _model.FrequencyMinutes)" For="@(() => _model.FrequencyMinutes)"
Disabled="@(_model.Mode != ChannelWatermarkMode.Intermittent)"> Disabled="@(_model.Mode != ChannelWatermarkMode.Intermittent)">
<MudSelectItem Value="5">5 minutes</MudSelectItem> <MudSelectItem Value="5">5 minutes</MudSelectItem>
@ -120,38 +153,32 @@
<MudSelectItem Value="30">30 minutes</MudSelectItem> <MudSelectItem Value="30">30 minutes</MudSelectItem>
<MudSelectItem Value="60">60 minutes</MudSelectItem> <MudSelectItem Value="60">60 minutes</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudStack>
<MudItem xs="6"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudTextField Label="Duration" @bind-Value="_model.DurationSeconds" <div class="d-flex">
<MudText>Duration</MudText>
</div>
<MudTextField @bind-Value="_model.DurationSeconds"
For="@(() => _model.DurationSeconds)" For="@(() => _model.DurationSeconds)"
Adornment="Adornment.End" Adornment="Adornment.End"
AdornmentText="seconds" AdornmentText="seconds"
Disabled="@(_model.Mode != ChannelWatermarkMode.Intermittent)" Disabled="@(_model.Mode != ChannelWatermarkMode.Intermittent)"
Immediate="true"/> Immediate="true"/>
</MudItem> </MudStack>
</MudGrid> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center"> <div class="d-flex">
<MudItem xs="6"> <MudText>Opacity</MudText>
<MudTextField Label="Opacity" @bind-Value="_model.Opacity" </div>
<MudTextField @bind-Value="_model.Opacity"
For="@(() => _model.Opacity)" For="@(() => _model.Opacity)"
Adornment="Adornment.End" Adornment="Adornment.End"
AdornmentText="%" AdornmentText="%"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)" Disabled="@(_model.Mode == ChannelWatermarkMode.None)"
Immediate="true"/> Immediate="true"/>
</MudItem> </MudStack>
<MudItem xs="6"/> </MudContainer>
</MudGrid>
</MudCardContent>
<MudCardActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
@(IsEdit ? "Save Changes" : "Add Watermark")
</MudButton>
</MudCardActions>
</MudCard>
</EditForm>
}
</div> </div>
</MudContainer> </MudForm>
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
@ -160,8 +187,8 @@
public int Id { get; set; } public int Id { get; set; }
private WatermarkEditViewModel _model = new(); private WatermarkEditViewModel _model = new();
private EditContext _editContext; private readonly WatermarkEditViewModelValidator _validator = new();
private ValidationMessageStore _messageStore; private MudForm _form;
public void Dispose() public void Dispose()
{ {
@ -196,17 +223,15 @@
PlaceWithinSourceContent = false PlaceWithinSourceContent = false
}; };
} }
_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()) ValidationResult result = await _validator.ValidateAsync(_model, _cts.Token);
if (result.IsValid)
{ {
Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() : (await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq(); Seq<BaseError> errorMessage = IsEdit ? (await Mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() : (await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
@ -231,8 +256,6 @@
relativeFileName => relativeFileName =>
{ {
_model.Image = new ArtworkContentTypeModel(relativeFileName, e.File.ContentType); _model.Image = new ArtworkContentTypeModel(relativeFileName, e.File.ContentType);
_messageStore.Clear();
_editContext.Validate();
StateHasChanged(); StateHasChanged();
}, },
error => error =>

15
ErsatzTV/Validators/WatermarkEditViewModelValidator.cs

@ -2,6 +2,7 @@
using ErsatzTV.FFmpeg.State; using ErsatzTV.FFmpeg.State;
using ErsatzTV.ViewModels; using ErsatzTV.ViewModels;
using FluentValidation; using FluentValidation;
using FluentValidation.Results;
namespace ErsatzTV.Validators; namespace ErsatzTV.Validators;
@ -42,4 +43,18 @@ public class WatermarkEditViewModelValidator : AbstractValidator<WatermarkEditVi
.LessThanOrEqualTo(100) .LessThanOrEqualTo(100)
.When(vm => vm.Mode != ChannelWatermarkMode.None); .When(vm => vm.Mode != ChannelWatermarkMode.None);
} }
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
ValidationResult result = await ValidateAsync(
ValidationContext<WatermarkEditViewModel>.CreateWithOptions(
(WatermarkEditViewModel)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
{
return [];
}
return result.Errors.Select(e => e.ErrorMessage);
};
} }

1
ErsatzTV/wwwroot/css/site.css

@ -153,6 +153,7 @@
.form-field-stack > div.d-flex { .form-field-stack > div.d-flex {
width: 300px; width: 300px;
justify-content: flex-start;
} }
.form-field-stack div.mud-input-control { .form-field-stack div.mud-input-control {

Loading…
Cancel
Save