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

46
ErsatzTV/Pages/ChannelEditor.razor

@ -27,31 +27,31 @@ @@ -27,31 +27,31 @@
<MudText Typo="Typo.h5" Class="mb-2">@(IsEdit ? "Edit Channel" : "Add 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 justify-md-end">
<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 justify-md-end">
<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 justify-md-end">
<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 justify-md-end">
<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 justify-md-end">
<div class="d-flex">
<MudText>Active Mode</MudText>
</div>
<MudSelect @bind-Value="_model.ActiveMode" For="@(() => _model.ActiveMode)">
@ -61,7 +61,7 @@ @@ -61,7 +61,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.ProgressMode" For="@(() => _model.ProgressMode)">
@ -70,7 +70,7 @@ @@ -70,7 +70,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
@ -82,7 +82,7 @@ @@ -82,7 +82,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
@ -94,7 +94,7 @@ @@ -94,7 +94,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.StreamSelectorMode" For="@(() => _model.StreamSelectorMode)">
@ -105,7 +105,7 @@ @@ -105,7 +105,7 @@
@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 justify-md-end">
<div class="d-flex">
<MudText>Preferred Audio Language</MudText>
</div>
<MudSelect @bind-Value="_model.PreferredAudioLanguageCode"
@ -119,13 +119,13 @@ @@ -119,13 +119,13 @@
</MudSelect>
</MudStack>
<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>
</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 justify-md-end">
<div class="d-flex">
<MudText>Preferred Subtitle Language</MudText>
</div>
<MudSelect @bind-Value="_model.PreferredSubtitleLanguageCode"
@ -139,7 +139,7 @@ @@ -139,7 +139,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.SubtitleMode" For="@(() => _model.SubtitleMode)">
@ -153,7 +153,7 @@ @@ -153,7 +153,7 @@
else
{
<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>
</div>
<MudSelect @bind-Value="_model.StreamSelector"
@ -167,7 +167,7 @@ @@ -167,7 +167,7 @@
</MudStack>
}
<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>
</div>
<MudSelect @bind-Value="_model.MusicVideoCreditsMode" For="@(() => _model.MusicVideoCreditsMode)">
@ -176,7 +176,7 @@ @@ -176,7 +176,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.MusicVideoCreditsTemplate"
@ -190,7 +190,7 @@ @@ -190,7 +190,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.SongVideoMode" For="@(() => _model.SongVideoMode)">
@ -199,10 +199,10 @@ @@ -199,10 +199,10 @@
</MudSelect>
</MudStack>
<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>
</div>
<InputFile id="fileInput" OnChange="UploadLogo" hidden/>
<InputFile id="fileInput" OnChange="UploadLogo" style="display: none;"/>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
@ -212,13 +212,13 @@ @@ -212,13 +212,13 @@
</MudButton>
</MudStack>
<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>
</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 justify-md-end">
<div class="d-flex">
<MudText>Logo Preview</MudText>
</div>
@if (!string.IsNullOrWhiteSpace(_model.Logo?.Path) || !string.IsNullOrWhiteSpace(_model.ExternalLogoUrl))
@ -227,7 +227,7 @@ @@ -227,7 +227,7 @@
}
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.WatermarkId" For="@(() => _model.WatermarkId)"
@ -241,7 +241,7 @@ @@ -241,7 +241,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.FallbackFillerId" For="@(() => _model.FallbackFillerId)" Clearable="true">

50
ErsatzTV/Pages/FFmpegEditor.razor

@ -27,19 +27,19 @@ @@ -27,19 +27,19 @@
<MudText Typo="Typo.h5" Class="mb-2">General</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex justify-md-end">
<div class="d-flex">
<MudText>Name</MudText>
</div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Immediate="true"/>
</MudStack>
<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>
</div>
<MudTextField @bind-Value="@_model.ThreadCount" For="@(() => _model.ThreadCount)"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.Resolution" For="@(() => _model.Resolution)">
@ -50,7 +50,7 @@ @@ -50,7 +50,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.ScalingBehavior" For="@(() => _model.ScalingBehavior)">
@ -62,7 +62,7 @@ @@ -62,7 +62,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Video</MudText>
<MudDivider Class="mb-6"/>
<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>
</div>
<MudSelect @bind-Value="_model.VideoFormat" For="@(() => _model.VideoFormat)">
@ -72,7 +72,7 @@ @@ -72,7 +72,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.VideoProfile"
@ -84,7 +84,7 @@ @@ -84,7 +84,7 @@
</MudSelect>
</MudStack>
<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>
</div>
@{
@ -104,13 +104,13 @@ @@ -104,13 +104,13 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="@_model.AllowBFrames" For="@(() => _model.AllowBFrames)" Dense="true"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)">
@ -119,19 +119,19 @@ @@ -119,19 +119,19 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudTextField @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<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>
</div>
<MudTextField @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
@ -146,7 +146,7 @@ @@ -146,7 +146,7 @@
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi)
{
<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>
</div>
<MudSelect @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)">
@ -157,7 +157,7 @@ @@ -157,7 +157,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.VaapiDisplay" For="@(() => _model.VaapiDisplay)">
@ -172,7 +172,7 @@ @@ -172,7 +172,7 @@
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)
{
<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>
</div>
<MudSelect @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)">
@ -188,7 +188,7 @@ @@ -188,7 +188,7 @@
@if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv)
{
<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>
</div>
<MudTextField @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/>
@ -197,7 +197,7 @@ @@ -197,7 +197,7 @@
else
{
<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>
</div>
<MudSelect @bind-Value="_model.TonemapAlgorithm" For="@(() => _model.TonemapAlgorithm)">
@ -209,13 +209,13 @@ @@ -209,13 +209,13 @@
</MudStack>
}
<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>
</div>
<MudCheckBox @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)" Dense="true"/>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/>
@ -223,7 +223,7 @@ @@ -223,7 +223,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Audio</MudText>
<MudDivider Class="mb-6"/>
<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>
</div>
<MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
@ -232,31 +232,31 @@ @@ -232,31 +232,31 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudTextField @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<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>
</div>
<MudTextField @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<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>
</div>
<MudTextField @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
</MudStack>
<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>
</div>
<MudTextField @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_model.NormalizeLoudnessMode" For="@(() => _model.NormalizeLoudnessMode)">

12
ErsatzTV/Pages/ScheduleEditor.razor

@ -17,13 +17,13 @@ @@ -17,13 +17,13 @@
<MudText Typo="Typo.h5" Class="mb-2">Schedule</MudText>
<MudDivider Class="mb-6"/>
<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>
</div>
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Required="true" RequiredError="Schedule name is required!"/>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="@_model.KeepMultiPartEpisodesTogether" For="@(() => _model.KeepMultiPartEpisodesTogether)" Dense="true">
@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
</MudCheckBox>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="@_model.TreatCollectionsAsShows"
@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
</MudCheckBox>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="@_model.ShuffleScheduleItems" For="@(() => _model.ShuffleScheduleItems)" Dense="true">
@ -49,13 +49,13 @@ @@ -49,13 +49,13 @@
</MudCheckBox>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="@_model.RandomStartPoint" For="@(() => _model.RandomStartPoint)" Dense="true"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="@_model.FixedStartTimeBehavior" For="@(() => _model.FixedStartTimeBehavior)">

26
ErsatzTV/Pages/Settings/FFmpegSettings.razor

@ -21,19 +21,19 @@ @@ -21,19 +21,19 @@
<MudText Typo="Typo.h5" Class="mb-2">FFmpeg</MudText>
<MudDivider Class="mb-6"/>
<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>
</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!"/>
</MudStack>
<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>
</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!"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_ffmpegSettings.DefaultFFmpegProfileId" HelperText="The FFmpeg Profile to use when creating new channels">
@ -44,7 +44,7 @@ @@ -44,7 +44,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_ffmpegSettings.PreferredAudioLanguageCode" Required="true" RequiredError="Preferred Language Code is required!">
@ -55,25 +55,25 @@ @@ -55,25 +55,25 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="_ffmpegSettings.UseEmbeddedSubtitles" Dense="true"/>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="_ffmpegSettings.ExtractEmbeddedSubtitles" Disabled="@(_ffmpegSettings.UseEmbeddedSubtitles == false)" Dense="true"/>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="_ffmpegSettings.SaveReports" Dense="true"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_ffmpegSettings.GlobalWatermarkId" Clearable="true">
@ -85,7 +85,7 @@ @@ -85,7 +85,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_ffmpegSettings.GlobalFallbackFillerId" Clearable="true">
@ -97,25 +97,25 @@ @@ -97,25 +97,25 @@
</MudSelect>
</MudStack>
<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>
</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"/>
</MudStack>
<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>
</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"/>
</MudStack>
<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>
</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"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_ffmpegSettings.HlsDirectOutputFormat" Clearable="true">

4
ErsatzTV/Pages/Settings/HDHRSettings.razor

@ -15,13 +15,13 @@ @@ -15,13 +15,13 @@
<MudText Typo="Typo.h5" Class="mb-2">HDHomeRun</MudText>
<MudDivider Class="mb-6"/>
<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>
</div>
<MudTextField @bind-Value="_uuid" Disabled="true"/>
</MudStack>
<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>
</div>
<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 @@ @@ -15,7 +15,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Logging</MudText>
<MudDivider Class="mb-6"/>
<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>
</div>
<MudSelect @bind-Value="_loggingSettings.DefaultMinimumLogLevel">
@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_loggingSettings.ScanningMinimumLogLevel">
@ -37,7 +37,7 @@ @@ -37,7 +37,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_loggingSettings.SchedulingMinimumLogLevel">
@ -48,7 +48,7 @@ @@ -48,7 +48,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_loggingSettings.StreamingMinimumLogLevel">
@ -59,7 +59,7 @@ @@ -59,7 +59,7 @@
</MudSelect>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_loggingSettings.HttpMinimumLogLevel">

4
ErsatzTV/Pages/Settings/PlayoutSettings.razor

@ -14,13 +14,13 @@ @@ -14,13 +14,13 @@
<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 justify-md-end">
<div class="d-flex">
<MudText>Days To Build</MudText>
</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"/>
</MudStack>
<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>
</div>
<MudCheckBox @bind-Value="_playoutSettings.SkipMissingItems" Dense="true">

2
ErsatzTV/Pages/Settings/ScannerSettings.razor

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
<MudText Typo="Typo.h5" Class="mb-2">Scanner</MudText>
<MudDivider Class="mb-6"/>
<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>
</div>
<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 @@ @@ -14,13 +14,13 @@
<MudText Typo="Typo.h5" Class="mb-2">XMLTV</MudText>
<MudDivider Class="mb-6"/>
<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>
</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"/>
</MudStack>
<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>
</div>
<MudSelect @bind-Value="_xmltvSettings.TimeZone">

323
ErsatzTV/Pages/WatermarkEditor.razor

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

15
ErsatzTV/Validators/WatermarkEditViewModelValidator.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.FFmpeg.State;
using ErsatzTV.ViewModels;
using FluentValidation;
using FluentValidation.Results;
namespace ErsatzTV.Validators;
@ -42,4 +43,18 @@ public class WatermarkEditViewModelValidator : AbstractValidator<WatermarkEditVi @@ -42,4 +43,18 @@ public class WatermarkEditViewModelValidator : AbstractValidator<WatermarkEditVi
.LessThanOrEqualTo(100)
.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 @@ @@ -153,6 +153,7 @@
.form-field-stack > div.d-flex {
width: 300px;
justify-content: flex-start;
}
.form-field-stack div.mud-input-control {

Loading…
Cancel
Save