mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
532 lines
23 KiB
532 lines
23 KiB
@inherits LayoutComponentBase |
|
@using System.Reflection |
|
@using ErsatzTV.Application.Configuration |
|
@using ErsatzTV.Application.Playouts |
|
@using ErsatzTV.Application.Search |
|
@using ErsatzTV.Core.Health |
|
@using ErsatzTV.Core.Interfaces.Search |
|
@using ErsatzTV.Core.Notifications |
|
@using ErsatzTV.Extensions |
|
@using MediatR.Courier |
|
@implements IDisposable |
|
@inject NavigationManager NavigationManager |
|
@inject IMediator Mediator |
|
@inject SystemStartup SystemStartup |
|
@inject ISearchTargets SearchTargets; |
|
@inject ICourier Courier |
|
@inject IHealthCheckService HealthCheckService; |
|
|
|
<MudThemeProvider Theme="ErsatzTvTheme" IsDarkMode="_isDarkMode"/> |
|
<MudDialogProvider BackdropClick="false"/> |
|
<MudSnackbarProvider/> |
|
<MudPopoverProvider/> |
|
|
|
<MudLayout @onclick="@(() => _isOpen = false)" Class="@(_isDarkMode ? "d-flex d-flex-column ersatztv-dark" : "d-flex d-flex-column ersatztv-light")" Style="height: 100vh"> |
|
<MudAppBar Elevation="1" Class="app-bar"> |
|
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) |
|
{ |
|
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer"/> |
|
} |
|
<div style="min-width: 180px" class="ml-1 d-none d-md-flex align-center"> |
|
<a href="" class="d-flex"> |
|
<img src="images/ersatztv.png" alt="ErsatzTV"/> |
|
</a> |
|
</div> |
|
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) |
|
{ |
|
<div class="search-form"> |
|
<EditForm Model="@_dummyModel" OnSubmit="@(_ => PerformSearch())"> |
|
<MudTextField T="string" |
|
@bind-Value="@Query" |
|
AdornmentIcon="@Icons.Material.Filled.Search" |
|
Adornment="Adornment.Start" |
|
Variant="Variant.Outlined" |
|
Immediate="true" |
|
Class="search-bar" |
|
@onclick="@(() => _isOpen = true)" |
|
OnKeyUp="@OnKeyUp"> |
|
</MudTextField> |
|
<MudPopover Open="@_isOpen" MaxHeight="300" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter" RelativeWidth="DropdownWidth.Relative"> |
|
@if (!string.IsNullOrWhiteSpace(_query) && _query.Length >= 3) |
|
{ |
|
var matches = _searchTargets.Where(s => s.Name.Contains(_query, StringComparison.CurrentCultureIgnoreCase)).ToList(); |
|
if (matches.Any()) |
|
{ |
|
<MudList T="SearchTargetViewModel" ReadOnly="false" Dense="true"> |
|
@foreach (SearchTargetViewModel searchTarget in matches) |
|
{ |
|
<MudListItem @key="@searchTarget" OnClick="@(() => NavigateTo(searchTarget))"> |
|
<MudText Typo="Typo.body1">@searchTarget.Name</MudText> |
|
<MudText Typo="Typo.subtitle1" Class="mud-text-disabled"> |
|
@( |
|
searchTarget.Kind switch |
|
{ |
|
SearchTargetKind.Channel => "Channel", |
|
SearchTargetKind.FFmpegProfile => "FFmpeg Profile", |
|
SearchTargetKind.ChannelWatermark => "Channel Watermark", |
|
SearchTargetKind.Collection => "Collection", |
|
SearchTargetKind.MultiCollection => "Multi Collection", |
|
SearchTargetKind.SmartCollection => "Smart Collection", |
|
SearchTargetKind.Schedule => "Schedule", |
|
SearchTargetKind.ScheduleItems => "Schedule Items", |
|
_ => string.Empty |
|
}) |
|
</MudText> |
|
</MudListItem> |
|
} |
|
</MudList> |
|
} |
|
} |
|
</MudPopover> |
|
</EditForm> |
|
</div> |
|
} |
|
<div class="flex-grow-1 d-none d-md-flex"></div> |
|
<div style="align-items: center; display: flex;" class="d-none d-md-flex"> |
|
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) |
|
{ |
|
@if (BuildConfiguration != "release") |
|
{ |
|
<MudText Color="Color.Warning" Class="mx-4">@System.Environment.MachineName</MudText> |
|
} |
|
<MudLink Color="Color.Info" Href="iptv/channels.m3u" Target="_blank" Underline="Underline.None">M3U</MudLink> |
|
<MudLink Color="Color.Info" Href="iptv/xmltv.xml" Target="_blank" Class="mx-4" Underline="Underline.None">XMLTV</MudLink> |
|
<MudLink Color="Color.Primary" Href="docs" Target="_blank" Underline="Underline.None" Style="font-weight: bold">API</MudLink> |
|
} |
|
@* <MudLink Color="Color.Info" Href="/swagger" Target="_blank" Class="mr-4" Underline="Underline.None">API</MudLink> *@ |
|
<MudTooltip Text="Documentation"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Help" Color="Color.Primary" Href="https://ersatztv.org" Target="_blank"/> |
|
</MudTooltip> |
|
<MudTooltip Text="Discord"> |
|
<MudIconButton Icon="fab fa-discord" Color="Color.Primary" Href="https://discord.ersatztv.org" Target="_blank"/> |
|
</MudTooltip> |
|
<MudTooltip Text="GitHub"> |
|
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Primary" Href="https://github.com/ErsatzTV/ErsatzTV" Target="_blank"/> |
|
</MudTooltip> |
|
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Primary" OnClick="@DarkModeToggle"/> |
|
<AuthorizeView> |
|
<form action="/account/logout" method="post"> |
|
<MudTooltip Text="Logout"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Logout" Color="Color.Secondary" ButtonType="ButtonType.Submit"/> |
|
</MudTooltip> |
|
</form> |
|
</AuthorizeView> |
|
</div> |
|
</MudAppBar> |
|
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) |
|
{ |
|
<MudDrawer @bind-Open="@_drawerIsOpen" Elevation="2" ClipMode="DrawerClipMode.Always"> |
|
<MudNavMenu> |
|
<MudNavLink Href="channels">Channels</MudNavLink> |
|
<MudNavLink Href="ffmpeg">FFmpeg Profiles</MudNavLink> |
|
<MudNavLink Href="watermarks">Watermarks</MudNavLink> |
|
<MudNavGroup Title="Media Sources"> |
|
<MudNavLink Href="media/sources/local">Local</MudNavLink> |
|
<MudNavLink Href="media/sources/emby">Emby</MudNavLink> |
|
<MudNavLink Href="media/sources/jellyfin">Jellyfin</MudNavLink> |
|
<MudNavLink Href="media/sources/plex">Plex</MudNavLink> |
|
</MudNavGroup> |
|
<MudNavGroup Title="Media"> |
|
<MudNavLink Href="media/libraries">Libraries</MudNavLink> |
|
<MudNavLink Href="media/trash">Trash</MudNavLink> |
|
<MudNavLink Href="media/tv/shows">TV Shows</MudNavLink> |
|
<MudNavLink Href="media/movies">Movies</MudNavLink> |
|
<MudNavLink Href="media/music/artists">Music</MudNavLink> |
|
<MudNavLink Href="media/other/videos">Other Videos</MudNavLink> |
|
<MudNavLink Href="media/music/songs">Songs</MudNavLink> |
|
<MudNavLink Href="media/browser/images">Images</MudNavLink> |
|
<MudNavLink Href="media/remote/streams">Remote Streams</MudNavLink> |
|
</MudNavGroup> |
|
<MudNavGroup Title="Lists"> |
|
<MudNavLink Href="media/collections">Manual Collections</MudNavLink> |
|
<MudNavLink Href="media/smart-collections">Smart Collections</MudNavLink> |
|
<MudNavLink Href="media/multi-collections">Multi Collections</MudNavLink> |
|
<MudNavLink Href="media/rerun-collections">Rerun Collections</MudNavLink> |
|
<MudNavLink Href="media/playlists">Playlists</MudNavLink> |
|
<MudNavLink Href="media/trakt/lists">Trakt Lists</MudNavLink> |
|
<MudNavLink Href="media/filler/presets">Filler Presets</MudNavLink> |
|
</MudNavGroup> |
|
<MudNavGroup> |
|
<TitleContent> |
|
@if (_playoutWarnings > 0) |
|
{ |
|
<div style="align-items: center; display: flex"> |
|
Scheduling |
|
<MudIcon Color="@Color.Warning" Icon="@Icons.Material.Filled.Warning" Class="mx-2"/> |
|
</div> |
|
} |
|
else |
|
{ |
|
@:Scheduling |
|
} |
|
</TitleContent> |
|
<ChildContent> |
|
<MudNavLink Href="schedules">Schedules</MudNavLink> |
|
<MudNavLink Href="blocks">Blocks</MudNavLink> |
|
<MudNavLink Href="templates">Templates</MudNavLink> |
|
<MudNavLink Href="decos">Decos</MudNavLink> |
|
<MudNavLink Href="deco-templates">Deco Templates</MudNavLink> |
|
<MudNavLink Href="playouts"> |
|
@if (_playoutWarnings > 0) |
|
{ |
|
<MudBadge Content="_playoutWarnings" |
|
Color="Color.Warning" |
|
Origin="Origin.CenterRight" |
|
BadgeClass="mx-3"> |
|
Playouts |
|
</MudBadge> |
|
} |
|
else |
|
{ |
|
@:Playouts |
|
} |
|
</MudNavLink> |
|
</ChildContent> |
|
</MudNavGroup> |
|
<MudNavGroup Title="Settings"> |
|
<MudNavLink Href="settings/ffmpeg">FFmpeg</MudNavLink> |
|
<MudNavLink Href="settings/logging">Logging</MudNavLink> |
|
<MudNavLink Href="settings/hdhr">HDHomeRun</MudNavLink> |
|
<MudNavLink Href="settings/scanner">Scanner</MudNavLink> |
|
<MudNavLink Href="settings/playout">Playout</MudNavLink> |
|
<MudNavLink Href="settings/xmltv">XMLTV</MudNavLink> |
|
</MudNavGroup> |
|
<MudNavGroup Expanded="true"> |
|
<TitleContent> |
|
@if (_errors > 0) |
|
{ |
|
<div style="align-items: center; display: flex"> |
|
Support |
|
<MudIcon Color="@Color.Error" Icon="@Icons.Material.Filled.Error" Class="mx-2"/> |
|
</div> |
|
} |
|
else if (_warnings > 0) |
|
{ |
|
<div style="align-items: center; display: flex"> |
|
Support |
|
<MudIcon Color="@Color.Warning" Icon="@Icons.Material.Filled.Warning" Class="mx-2"/> |
|
</div> |
|
} |
|
else |
|
{ |
|
@:Support |
|
} |
|
</TitleContent> |
|
<ChildContent> |
|
<MudNavLink Href="system/health"> |
|
@if (_errors > 0) |
|
{ |
|
<MudBadge Content="_errors" |
|
Color="Color.Error" |
|
Origin="Origin.CenterRight" |
|
BadgeClass="mx-3"> |
|
Health Checks |
|
</MudBadge> |
|
} |
|
else if (_warnings > 0) |
|
{ |
|
<MudBadge Content="_warnings" |
|
Color="Color.Warning" |
|
Origin="Origin.CenterRight" |
|
BadgeClass="mx-3"> |
|
Health Checks |
|
</MudBadge> |
|
} |
|
else |
|
{ |
|
@:Health Checks |
|
} |
|
</MudNavLink> |
|
<MudNavLink Href="system/logs">Logs</MudNavLink> |
|
<MudNavLink Href="system/troubleshooting">Troubleshooting</MudNavLink> |
|
</ChildContent> |
|
</MudNavGroup> |
|
<MudDivider Class="my-6" DividerType="DividerType.Middle"/> |
|
<MudContainer Style="text-align: right" Class="mr-6"> |
|
<MudText Typo="Typo.body2">ErsatzTV Version</MudText> |
|
<MudText Typo="Typo.body2" Color="Color.Info">@InfoVersion</MudText> |
|
@if (BuildConfiguration != "release") |
|
{ |
|
<MudText Typo="Typo.body2" Color="Color.Warning">@BuildConfiguration</MudText> |
|
} |
|
</MudContainer> |
|
</MudNavMenu> |
|
</MudDrawer> |
|
} |
|
<MudMainContent> |
|
@Body |
|
</MudMainContent> |
|
</MudLayout> |
|
|
|
@code { |
|
private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown"; |
|
private static readonly string BuildConfiguration = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>()?.Configuration?.ToLower() ?? "unset"; |
|
|
|
private CancellationTokenSource _cts; |
|
|
|
private string _query; |
|
|
|
private record SearchModel; |
|
|
|
private readonly SearchModel _dummyModel = new(); |
|
private bool _drawerIsOpen = true; |
|
private bool _isOpen; |
|
private List<SearchTargetViewModel> _searchTargets; |
|
private int _playoutWarnings; |
|
private int _errors; |
|
private int _warnings; |
|
private bool _isDarkMode = true; |
|
|
|
protected override void OnInitialized() |
|
{ |
|
SystemStartup.OnDatabaseReady += OnStartupProgress; |
|
SystemStartup.OnSearchIndexReady += OnStartupProgress; |
|
|
|
SearchTargets.OnSearchTargetsChanged += OnSearchTargetsChanged; |
|
|
|
Courier.Subscribe<HealthCheckSummary>(HandleHealthCheckSummary); |
|
Courier.Subscribe<PlayoutUpdatedNotification>(HandlePlayoutUpdated); |
|
} |
|
|
|
protected override async Task OnInitializedAsync() |
|
{ |
|
await base.OnInitializedAsync(); |
|
await HandleHealthCheckSummary(HealthCheckService.GetHealthCheckSummary(), CancellationToken.None); |
|
} |
|
|
|
private async Task DarkModeToggle() |
|
{ |
|
_isDarkMode = !_isDarkMode; |
|
await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.PagesIsDarkMode, _isDarkMode.ToString())); |
|
} |
|
|
|
public string DarkLightModeButtonIcon => _isDarkMode switch |
|
{ |
|
true => Icons.Material.Rounded.DarkMode, |
|
false => Icons.Material.Outlined.LightMode |
|
}; |
|
|
|
public void Dispose() |
|
{ |
|
SystemStartup.OnDatabaseReady -= OnStartupProgress; |
|
SystemStartup.OnSearchIndexReady -= OnStartupProgress; |
|
|
|
SearchTargets.OnSearchTargetsChanged -= OnSearchTargetsChanged; |
|
|
|
Courier.UnSubscribe<HealthCheckSummary>(HandleHealthCheckSummary); |
|
|
|
_cts?.Cancel(); |
|
_cts?.Dispose(); |
|
} |
|
|
|
private static MudTheme ErsatzTvTheme => new() |
|
{ |
|
PaletteDark = new PaletteDark |
|
{ |
|
ActionDefault = "rgba(255,255,255, 0.80)", |
|
Primary = "#009000", |
|
Secondary = "#009090", |
|
Surface = "#1f1f1f", |
|
AppbarBackground = "#121212", |
|
AppbarText = "rgba(255,255,255, 0.80)", |
|
DrawerBackground = "#1f1f1f", |
|
DrawerText = "rgba(255,255,255, 0.80)", |
|
Divider = "rgba(255,255,255, 0.40)", |
|
Background = "#272727", |
|
BackgroundGray = "#272727", |
|
TextPrimary = "rgba(255,255,255, 0.90)", |
|
TextSecondary = "rgba(255,255,255, 0.80)", |
|
TextDisabled = "rgba(255,255,255, 0.40)", |
|
ActionDisabled = "rgba(255,255,255, 0.40)", |
|
TableHover = "rgba(255,255,255, 0.10)", |
|
TableLines = "rgba(255,255,255,0.11)", |
|
Info = "#00c0c0", |
|
Tertiary = "#00c000", |
|
White = Colors.Shades.White |
|
}, |
|
|
|
PaletteLight = new PaletteLight |
|
{ |
|
ActionDefault = "#546E7A", |
|
Primary = "#546E7A", |
|
Secondary = "#EC407A", |
|
AppbarBackground = "#ECEFF1", |
|
AppbarText = "#424242", |
|
DrawerBackground = "#FFFFFF", |
|
DrawerText = "#424242", |
|
Surface = "#FFFFFF", |
|
Background = "#F5F5F5", |
|
TextPrimary = "#212121", |
|
TextSecondary = "rgba(0,0,0, 0.7)", |
|
TextDisabled = "rgba(0,0,0, 0.5)", |
|
ActionDisabled = "rgba(0,0,0, 0.3)", |
|
Divider = "rgba(0,0,0, 0.12)", |
|
TableHover = "rgba(0,0,0, 0.02)", |
|
TableLines = "rgba(0,0,0,0.08)", |
|
Info = "#00c0c0", |
|
Tertiary = "#00c000" |
|
} |
|
}; |
|
|
|
private string Query |
|
{ |
|
get => _query; |
|
set |
|
{ |
|
if (_query == value) |
|
{ |
|
return; |
|
} |
|
|
|
_query = value; |
|
_isOpen = true; |
|
StateHasChanged(); |
|
} |
|
} |
|
|
|
private async void OnStartupProgress(object sender, EventArgs e) |
|
{ |
|
try |
|
{ |
|
await InvokeAsync(StateHasChanged); |
|
} |
|
catch |
|
{ |
|
// do nothing |
|
} |
|
} |
|
|
|
protected override async Task OnParametersSetAsync() |
|
{ |
|
if (!SystemStartup.IsDatabaseReady || !SystemStartup.IsSearchIndexReady) |
|
{ |
|
string currentUri = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); |
|
if (!string.IsNullOrEmpty(currentUri)) |
|
{ |
|
NavigationManager.NavigateTo(string.Empty); |
|
return; |
|
} |
|
} |
|
|
|
_cts?.Cancel(); |
|
_cts?.Dispose(); |
|
_cts = new CancellationTokenSource(); |
|
var token = _cts.Token; |
|
|
|
try |
|
{ |
|
await base.OnParametersSetAsync(); |
|
_query = NavigationManager.Uri.GetSearchQuery(); |
|
|
|
if (SystemStartup.IsDatabaseReady) |
|
{ |
|
_searchTargets ??= await Mediator.Send(new QuerySearchTargets(), token); |
|
|
|
_isDarkMode = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PagesIsDarkMode), token) |
|
.MapT(result => !bool.TryParse(result.Value, out bool value) || value) |
|
.IfNoneAsync(true); |
|
|
|
_playoutWarnings = await Mediator.Send(new GetPlayoutWarningsCount(), token); |
|
} |
|
} |
|
catch (OperationCanceledException) |
|
{ |
|
// do nothing |
|
} |
|
} |
|
|
|
protected async void OnSearchTargetsChanged(object sender, EventArgs e) |
|
{ |
|
try |
|
{ |
|
_searchTargets = await Mediator.Send(new QuerySearchTargets(), _cts.Token); |
|
} |
|
catch (Exception) |
|
{ |
|
// do nothing |
|
} |
|
} |
|
|
|
private void PerformSearch() |
|
{ |
|
NavigationManager.NavigateTo(_query.GetRelativeSearchQuery(), true); |
|
StateHasChanged(); |
|
} |
|
|
|
private void OnKeyUp(KeyboardEventArgs args) |
|
{ |
|
switch (args.Key) |
|
{ |
|
case "Enter": |
|
case "NumpadEnter": |
|
_isOpen = false; |
|
break; |
|
case "Escape": |
|
_isOpen = false; |
|
break; |
|
} |
|
} |
|
|
|
private void NavigateTo(SearchTargetViewModel searchTarget) => |
|
// need to force smart collections to navigate since the query string is all that differs |
|
NavigationManager.NavigateTo(UrlFor(searchTarget), searchTarget.Kind is SearchTargetKind.SmartCollection); |
|
|
|
private string UrlFor(SearchTargetViewModel searchTarget) => |
|
searchTarget.Kind switch |
|
{ |
|
SearchTargetKind.Channel => $"channels/{searchTarget.Id}", |
|
SearchTargetKind.FFmpegProfile => $"ffmpeg/{searchTarget.Id}", |
|
SearchTargetKind.ChannelWatermark => $"watermarks/{searchTarget.Id}", |
|
SearchTargetKind.Collection => $"media/collections/{searchTarget.Id}", |
|
SearchTargetKind.MultiCollection => $"media/multi-collections/{searchTarget.Id}/edit", |
|
SearchTargetKind.SmartCollection when searchTarget is SmartCollectionSearchTargetViewModel sc => |
|
sc.Query.GetRelativeSearchQuery(), |
|
SearchTargetKind.Schedule => $"schedules/{searchTarget.Id}", |
|
SearchTargetKind.ScheduleItems => $"schedules/{searchTarget.Id}/items", |
|
_ => null |
|
}; |
|
|
|
private void ToggleDrawer() => _drawerIsOpen = !_drawerIsOpen; |
|
|
|
private async Task HandleHealthCheckSummary(HealthCheckSummary healthCheckSummary, CancellationToken cancellationToken) |
|
{ |
|
try |
|
{ |
|
if (healthCheckSummary.Errors > 0) |
|
{ |
|
_errors = healthCheckSummary.Errors; |
|
_warnings = 0; |
|
} |
|
else if (healthCheckSummary.Warnings > 0) |
|
{ |
|
_warnings = healthCheckSummary.Warnings; |
|
_errors = 0; |
|
} |
|
else |
|
{ |
|
_warnings = 0; |
|
_errors = 0; |
|
} |
|
|
|
await InvokeAsync(StateHasChanged); |
|
} |
|
catch (Exception) |
|
{ |
|
// ignore |
|
} |
|
} |
|
|
|
private async Task HandlePlayoutUpdated(PlayoutUpdatedNotification _, CancellationToken cancellationToken) |
|
{ |
|
try |
|
{ |
|
_playoutWarnings = await Mediator.Send(new GetPlayoutWarningsCount(), cancellationToken); |
|
await InvokeAsync(StateHasChanged); |
|
} |
|
catch (Exception) |
|
{ |
|
// do nothing |
|
} |
|
} |
|
|
|
}
|
|
|