From 1eab8568eb2760d86d1bbf92d11ad39d6a1b4cfc Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:12:13 -0600 Subject: [PATCH] move dark/light mode toggle to ui settings page --- CHANGELOG.md | 3 + .../Commands/UpdateUiSettings.cs | 5 ++ .../Commands/UpdateUiSettingsHandler.cs | 26 +++++++ .../Configuration/Queries/GetUiSettings.cs | 3 + .../Queries/GetUiSettingsHandler.cs | 20 +++++ .../Configuration/UiSettingsViewModel.cs | 6 ++ .../Notifications/ThemeUpdatedNotification.cs | 5 ++ ErsatzTV/Pages/Settings/UISettings.razor | 75 +++++++++++++++++++ ErsatzTV/Shared/MainLayout.razor | 33 ++++---- 9 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 ErsatzTV.Application/Configuration/Commands/UpdateUiSettings.cs create mode 100644 ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs create mode 100644 ErsatzTV.Application/Configuration/Queries/GetUiSettings.cs create mode 100644 ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs create mode 100644 ErsatzTV.Application/Configuration/UiSettingsViewModel.cs create mode 100644 ErsatzTV.Core/Notifications/ThemeUpdatedNotification.cs create mode 100644 ErsatzTV/Pages/Settings/UISettings.razor diff --git a/CHANGELOG.md b/CHANGELOG.md index 516238176..03e34bb57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add log warnings when actual transcoding speed is potentially insufficient to support smooth playback - Log messages will include media item id, channel number and transcoding speed +### Changed +- Move dark/light mode toggle to **Settings** > **UI** + ## [26.2.0] - 2026-02-02 ### Added - Channel stream selector: add zero-based culture-specific `day_of_week` to `content_condition`, for example: diff --git a/ErsatzTV.Application/Configuration/Commands/UpdateUiSettings.cs b/ErsatzTV.Application/Configuration/Commands/UpdateUiSettings.cs new file mode 100644 index 000000000..919016bd9 --- /dev/null +++ b/ErsatzTV.Application/Configuration/Commands/UpdateUiSettings.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Configuration; + +public record UpdateUiSettings(UiSettingsViewModel UiSettings) : IRequest>; diff --git a/ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs b/ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs new file mode 100644 index 000000000..6eabeeef0 --- /dev/null +++ b/ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs @@ -0,0 +1,26 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; + +namespace ErsatzTV.Application.Configuration; + +public class UpdateUiSettingsHandler(IConfigElementRepository configElementRepository) + : IRequestHandler> +{ + public async Task> Handle( + UpdateUiSettings request, + CancellationToken cancellationToken) + { + return await ApplyUpdate(request.UiSettings, cancellationToken); + } + + private async Task ApplyUpdate(UiSettingsViewModel uiSettings, CancellationToken cancellationToken) + { + await configElementRepository.Upsert( + ConfigElementKey.PagesIsDarkMode, + uiSettings.PagesIsDarkMode, + cancellationToken); + + return Unit.Default; + } +} diff --git a/ErsatzTV.Application/Configuration/Queries/GetUiSettings.cs b/ErsatzTV.Application/Configuration/Queries/GetUiSettings.cs new file mode 100644 index 000000000..608c06fa1 --- /dev/null +++ b/ErsatzTV.Application/Configuration/Queries/GetUiSettings.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Configuration; + +public record GetUiSettings : IRequest; diff --git a/ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs b/ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs new file mode 100644 index 000000000..e074d71ae --- /dev/null +++ b/ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs @@ -0,0 +1,20 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; + +namespace ErsatzTV.Application.Configuration; + +public class GetUiSettingsHandler(IConfigElementRepository configElementRepository) + : IRequestHandler +{ + public async Task Handle(GetUiSettings request, CancellationToken cancellationToken) + { + Option pagesIsDarkMode = await configElementRepository.GetValue( + ConfigElementKey.PagesIsDarkMode, + cancellationToken); + + return new UiSettingsViewModel + { + PagesIsDarkMode = await pagesIsDarkMode.IfNoneAsync(true) + }; + } +} diff --git a/ErsatzTV.Application/Configuration/UiSettingsViewModel.cs b/ErsatzTV.Application/Configuration/UiSettingsViewModel.cs new file mode 100644 index 000000000..101fc74c9 --- /dev/null +++ b/ErsatzTV.Application/Configuration/UiSettingsViewModel.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Application.Configuration; + +public class UiSettingsViewModel +{ + public bool PagesIsDarkMode { get; set; } +} diff --git a/ErsatzTV.Core/Notifications/ThemeUpdatedNotification.cs b/ErsatzTV.Core/Notifications/ThemeUpdatedNotification.cs new file mode 100644 index 000000000..24506f2de --- /dev/null +++ b/ErsatzTV.Core/Notifications/ThemeUpdatedNotification.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace ErsatzTV.Core.Notifications; + +public record ThemeUpdatedNotification : INotification; diff --git a/ErsatzTV/Pages/Settings/UISettings.razor b/ErsatzTV/Pages/Settings/UISettings.razor new file mode 100644 index 000000000..4cd908eae --- /dev/null +++ b/ErsatzTV/Pages/Settings/UISettings.razor @@ -0,0 +1,75 @@ +@page "/settings/ui" +@using ErsatzTV.Application.Configuration +@using ErsatzTV.Core.Notifications +@implements IDisposable +@inject IMediator Mediator +@inject ISnackbar Snackbar +@inject ILogger Logger + + + + Save Settings + +
+ + UI + + +
+ Default Theme +
+ + Dark + Light + +
+
+
+
+ +@code { + private CancellationTokenSource _cts; + + private UiSettingsViewModel _uiSettings = new(); + + public void Dispose() + { + _cts?.Cancel(); + _cts?.Dispose(); + } + + protected override async Task OnParametersSetAsync() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + try + { + _uiSettings = await Mediator.Send(new GetUiSettings(), token); + } + catch (OperationCanceledException) + { + // do nothing + } + } + + private async Task SaveUiSettings() + { + Either result = await Mediator.Send(new UpdateUiSettings(_uiSettings), _cts.Token); + + foreach (var error in result.LeftToSeq()) + { + Snackbar.Add(error.Value, Severity.Error); + Logger.LogError("Unexpected error saving ui settings: {Error}", error.Value); + } + + if (result.IsRight) + { + Snackbar.Add("Successfully saved UI settings", Severity.Success); + await Mediator.Publish(new ThemeUpdatedNotification(), _cts.Token); + } + } + +} diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index 708a7add3..03e45128b 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -103,7 +103,6 @@ -
@@ -189,6 +188,7 @@ HDHomeRun Scanner Playout + UI XMLTV @@ -286,6 +286,7 @@ Courier.Subscribe(HandleHealthCheckSummary); Courier.Subscribe(HandlePlayoutUpdated); + Courier.Subscribe(HandleThemeUpdated); } protected override async Task OnInitializedAsync() @@ -294,18 +295,6 @@ 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; @@ -314,6 +303,8 @@ SearchTargets.OnSearchTargetsChanged -= OnSearchTargetsChanged; Courier.UnSubscribe(HandleHealthCheckSummary); + Courier.UnSubscribe(HandlePlayoutUpdated); + Courier.UnSubscribe(HandleThemeUpdated); _cts?.Cancel(); _cts?.Dispose(); @@ -529,4 +520,20 @@ } } + private async Task HandleThemeUpdated(ThemeUpdatedNotification _, CancellationToken cancellationToken) + { + try + { + _isDarkMode = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PagesIsDarkMode), cancellationToken) + .MapT(result => !bool.TryParse(result.Value, out bool value) || value) + .IfNoneAsync(true); + + await InvokeAsync(StateHasChanged); + } + catch (Exception) + { + // do nothing + } + } + }