@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; @inject IStringLocalizer StringLocalizer; @if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) { } @if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) {
@if (!string.IsNullOrWhiteSpace(_query) && _query.Length >= 3) { var matches = _searchTargets.Where(s => s.Name.Contains(_query, StringComparison.CurrentCultureIgnoreCase)).ToList(); if (matches.Any()) { @foreach (SearchTargetViewModel searchTarget in matches) { @searchTarget.Name @( searchTarget.Kind switch { SearchTargetKind.Channel => StringLocalizer["LabelSearchTargetChannel"], SearchTargetKind.FFmpegProfile => StringLocalizer["LabelSearchTargetFFmpegProfile"], SearchTargetKind.ChannelWatermark => StringLocalizer["LabelSearchTargetChannelWatermark"], SearchTargetKind.Collection => StringLocalizer["LabelSearchTargetCollection"], SearchTargetKind.MultiCollection => StringLocalizer["LabelSearchTargetMultiCollection"], SearchTargetKind.SmartCollection => StringLocalizer["LabelSearchTargetSmartCollection"], SearchTargetKind.Schedule => StringLocalizer["LabelSearchTargetSchedule"], SearchTargetKind.ScheduleItems => StringLocalizer["LabelSearchTargetScheduleItems"], _ => string.Empty }) } } }
}
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) { @if (BuildConfiguration != "release") { @System.Environment.MachineName } @StringLocalizer["ButtonM3U"] @StringLocalizer["ButtonXmltv"] @StringLocalizer["ButtonApi"] } @* API *@
@if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) { @StringLocalizer["ButtonChannels"]@ChicoryCue @StringLocalizer["ButtonFFmpegProfiles"] @StringLocalizer["ButtonWatermarks"] @StringLocalizer["ButtonMediaSourcesLocal"] @StringLocalizer["ButtonMediaSourcesEmby"] @StringLocalizer["ButtonMediaSourcesJellyfin"] @StringLocalizer["ButtonMediaSourcesPlex"] @StringLocalizer["ButtonMediaLibraries"]@ChicoryCue @StringLocalizer["ButtonMediaTrash"] @StringLocalizer["ButtonMediaTvShows"] @StringLocalizer["ButtonMediaMovies"] @StringLocalizer["ButtonMediaMusic"] @StringLocalizer["ButtonMediaOtherVideos"] @StringLocalizer["ButtonMediaSongs"] @StringLocalizer["ButtonMediaImages"] @StringLocalizer["ButtonMediaRemoteStreams"] @StringLocalizer["ButtonListsManualCollections"] @StringLocalizer["ButtonListsSmartCollections"] @StringLocalizer["ButtonListsMultiCollections"] @StringLocalizer["ButtonListsRerunCollections"] @StringLocalizer["ButtonListsPlaylists"] @StringLocalizer["ButtonListsTraktLists"] @StringLocalizer["ButtonListsFillerPresets"] @if (_playoutWarnings > 0) {
@StringLocalizer["ButtonScheduling"]
} else { @StringLocalizer["ButtonScheduling"] }
@StringLocalizer["ButtonSchedulingSchedules"]@ChicoryCue @StringLocalizer["ButtonSchedulingBlocks"] @StringLocalizer["ButtonSchedulingTemplates"] @StringLocalizer["ButtonSchedulingDecos"] @StringLocalizer["ButtonSchedulingDecoTemplates"] @if (_playoutWarnings > 0) { @StringLocalizer["ButtonSchedulingPlayouts"] } else { @StringLocalizer["ButtonSchedulingPlayouts"] } @ChicoryCue
@StringLocalizer["ButtonSettingsFFmpeg"]@ChicoryCue @StringLocalizer["ButtonSettingsLogging"]@ChicoryCue @StringLocalizer["ButtonSettingsHdHomeRun"]@ChicoryCue @StringLocalizer["ButtonSettingsScanner"]@ChicoryCue @StringLocalizer["ButtonSettingsPlayout"]@ChicoryCue @StringLocalizer["ButtonSettingsUi"]@ChicoryCue @StringLocalizer["ButtonSettingsXmltv"]@ChicoryCue @if (_errors > 0) {
@StringLocalizer["ButtonSupport"]
} else if (_warnings > 0) {
@StringLocalizer["ButtonSupport"]
} else { @StringLocalizer["ButtonSupport"] }
@if (_errors > 0) { @StringLocalizer["ButtonSupportHealthChecks"] } else if (_warnings > 0) { @StringLocalizer["ButtonSupportHealthChecks"] } else { @StringLocalizer["ButtonSupportHealthChecks"] } @StringLocalizer["ButtonSupportLogs"] @StringLocalizer["ButtonSupportTroubleshooting"]
@StringLocalizer["LabelErsatzTVVersion"] @InfoVersion @if (BuildConfiguration != "release") { @BuildConfiguration }
} @Body
@code { // Lightweight cue (ersatztv#147): marks nav entries that 302-redirect into the // ChicoryTV SPA (see LegacyUiRedirects.Map) so it's clear before clicking that the // entry leaves this Blazor UI. Non-redirecting entries in the same MudNavGroup are // left bare. private RenderFragment ChicoryCue => @ ; private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute()?.InformationalVersion ?? "unknown"; private static readonly string BuildConfiguration = Assembly.GetEntryAssembly().GetCustomAttribute()?.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 _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(HandleHealthCheckSummary); Courier.Subscribe(HandlePlayoutUpdated); } protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); await HandleHealthCheckSummary(HealthCheckService.GetHealthCheckSummary(), CancellationToken.None); } public void Dispose() { SystemStartup.OnDatabaseReady -= OnStartupProgress; SystemStartup.OnSearchIndexReady -= OnStartupProgress; SearchTargets.OnSearchTargetsChanged -= OnSearchTargetsChanged; Courier.UnSubscribe(HandleHealthCheckSummary); Courier.UnSubscribe(HandlePlayoutUpdated); _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 } } }