Make every theme setting reachable and the theme controls effective

With the default settings (follow the system, light grey) the nav's hue slider
and dark toggle did nothing visible, and no UI could leave that mode. The slider
also always wrote the light hue, even in dark mode.

PageSettings gains SetDarkMode, SetGray and SetHue, and CascadingState gains
UpdatePageSettings. They keep the flags consistent:
- choosing light or dark leaves system theming;
- picking a hue leaves grey;
- grey follows the variant shown.
theme.js reports which variant is showing (AppStatusService.IsDarkTheme), so the
nav toggle and slider edit that one, including the OS's choice in system mode.

Settings > General, which was empty, now edits every theme setting:
- follow the system;
- dark mode;
- grey;
- the light and dark hues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-03 12:43:07 +02:00
1 parent a097338121
commit 81b92e012b
6 files changed
+94 -44

No files matched your search

+7
View File
@@ -58,6 +58,13 @@
} }
} }
/// <summary>Changes the page settings, then saves and applies them like UpdatePublicCache.</summary>
public ValueTask UpdatePageSettings(Action<PageSettings> change)
{
change(PublicCacheData.PageSettings);
return UpdatePublicCache(PublicCacheData);
}
[JSInvokable] [JSInvokable]
public async Task LogFromJs(string message, string where) public async Task LogFromJs(string message, string where)
{ {
+36 -1
View File
@@ -1,4 +1,4 @@
using PrivaPub.ClientModels.Resources; using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@@ -24,5 +24,40 @@ namespace decePubClient.Models
public bool ThemeIsDarkGray { get; set; } = false; public bool ThemeIsDarkGray { get; set; } = false;
public bool ShowDonatorBadge { get; set; } = false; public bool ShowDonatorBadge { get; set; } = false;
// How the theme flags combine is in wwwroot/js/theme.js: with PreferSystemTheming the OS picks light or dark;
// otherwise ThemeIsDarkGray, then ThemeIsLightGray, then ThemeIsDarkMode decide; either grey flag drops the
// colour. These keep the flags consistent when the UI changes one aspect.
public const short DefaultLightThemeIndexColour = 25;
public const short DefaultDarkThemeIndexColour = 215;
public bool IsGray() => ThemeIsLightGray || ThemeIsDarkGray;
/// <summary>Leaves system theming and shows the light or dark variant, grey or coloured as before.</summary>
public void SetDarkMode(bool isDark)
{
PreferSystemTheming = false;
ThemeIsDarkMode = isDark;
if (IsGray())
SetGray(true, isDark);
}
/// <summary>Switches grey on or off for the variant currently shown.</summary>
public void SetGray(bool isGray, bool isDark)
{
ThemeIsDarkGray = isGray && isDark;
ThemeIsLightGray = isGray && !isDark;
}
/// <summary>Sets the hue of the light or dark variant; picking a hue means colour, so grey goes off.</summary>
public void SetHue(bool isDark, short hue)
{
if (isDark)
DarkThemeIndexColour = hue;
else
LightThemeIndexColour = hue;
SetGray(false, isDark);
}
} }
} }
+27 -5
View File
@@ -6,17 +6,33 @@
<div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto"> <div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
<DCollapse Icon="DCollapseIcon.Arrow"> <DCollapse Icon="DCollapseIcon.Arrow" IsOpen="true">
<Header> <Header>
<p class="inline-flex items-center gap-2"> <p class="inline-flex items-center gap-2">
<i class="ion-md-cog text-xl"></i> <span>@Localizer["General"]</span> <i class="ion-md-cog text-xl"></i> <span>@Localizer["General"]</span>
</p> </p>
</Header> </Header>
<ChildContent> <ChildContent>
<div class="block w-full p-3 md:p-4"> @* Every theme setting; NavMenu's toggle and slider are shortcuts to the dark and hue ones. *@
<p class="w-full text-center text-lg"> <div class="flex flex-col gap-4 w-full p-3 md:p-4">
<i class="ion-ios-remove-circle-outline"></i> @Localizer["Empty"] <DToggle LabelText="@Localizer["Follow the system's light or dark setting"]" Value="Theme.PreferSystemTheming"
</p> ValueChanged="v => Update(s => s.PreferSystemTheming = v)" ValueExpression="() => Theme.PreferSystemTheming" />
<DToggle LabelText="@Localizer["Dark mode"]" Value="IsDark"
ValueChanged="v => Update(s => s.SetDarkMode(v))" ValueExpression="() => IsDark" />
<DToggle LabelText="@Localizer["Grey"]" Value="IsGray"
ValueChanged="v => Update(s => s.SetGray(v, IsDark))" ValueExpression="() => IsGray" />
<label class="flex flex-col gap-2 max-w-md">
<span class="text-sm">@Localizer["Light theme colour"]</span>
<DRange TValue="short" class="neo-hue-track w-full" Size="DSize.Xs" Min="0" Max="359" Step="1"
Value="Theme.LightThemeIndexColour" ValueChanged="v => Update(s => s.SetHue(false, v))"
ValueExpression="() => Theme.LightThemeIndexColour" />
</label>
<label class="flex flex-col gap-2 max-w-md">
<span class="text-sm">@Localizer["Dark theme colour"]</span>
<DRange TValue="short" class="neo-hue-track w-full" Size="DSize.Xs" Min="0" Max="359" Step="1"
Value="Theme.DarkThemeIndexColour" ValueChanged="v => Update(s => s.SetHue(true, v))"
ValueExpression="() => Theme.DarkThemeIndexColour" />
</label>
</div> </div>
</ChildContent> </ChildContent>
</DCollapse> </DCollapse>
@@ -72,4 +88,10 @@
@code { @code {
[CascadingParameter] CascadingState CascadingState { get; set; } [CascadingParameter] CascadingState CascadingState { get; set; }
PageSettings Theme => CascadingState.PublicCacheData?.PageSettings ?? new();
bool IsDark => CascadingState.Status.IsDarkTheme();
bool IsGray => Theme.IsGray();
Task Update(Action<PageSettings> change) => CascadingState.UpdatePageSettings(change).AsTask();
} }
+2
View File
@@ -49,6 +49,8 @@ public class AppStatusService
// wwwroot/js/theme.js // wwwroot/js/theme.js
public void ApplyTheme(PageSettings pageSettings) => JsSyncRuntime.InvokeVoid("neoTheme.apply", pageSettings); public void ApplyTheme(PageSettings pageSettings) => JsSyncRuntime.InvokeVoid("neoTheme.apply", pageSettings);
public bool IsDarkTheme() => JsSyncRuntime.Invoke<bool>("neoTheme.isDark");
public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia"); public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia");
public bool CanShare() => JsSyncRuntime.Invoke<bool>("canShareStuff"); public bool CanShare() => JsSyncRuntime.Invoke<bool>("canShareStuff");
+15 -36
View File
@@ -74,7 +74,7 @@
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<DToggle Value="ThemeIsDarkMode" ValueChanged="async v => await UpdateThemeDarkMode(v)" ValueExpression="() => ThemeIsDarkMode"> <DToggle Value="IsDark" ValueChanged="UpdateThemeDarkMode" ValueExpression="() => IsDark">
<UncheckedIcon> <UncheckedIcon>
<i class="ion-md-sunny"></i> <i class="ion-md-sunny"></i>
</UncheckedIcon> </UncheckedIcon>
@@ -104,21 +104,16 @@
@code { @code {
[CascadingParameter] CascadingState CascadingState { get; set; } [CascadingParameter] CascadingState CascadingState { get; set; }
string menuToggle = "hidden"; string menuToggle = "hidden";
bool IsThemeChanging { get; set; } = false;
bool ThemeIsDarkMode { get; set; } = false;
short ThemeIndexColour { get; set; } = 25;
protected override async Task OnInitializedAsync() PageSettings Settings => CascadingState.PublicCacheData?.PageSettings ?? new();
// The toggle and the slider work on the variant that is showing; with system theming that is the OS's choice.
bool IsDark => CascadingState.Status.IsDarkTheme();
short ThemeIndexColour => IsDark ? Settings.DarkThemeIndexColour : Settings.LightThemeIndexColour;
protected override void OnInitialized()
{ {
await Task.Run(() =>
{
});
if (!CascadingState.Status.IsMobileMedia()) if (!CascadingState.Status.IsMobileMedia())
menuToggle = default; menuToggle = default;
ThemeIsDarkMode = CascadingState.PublicCacheData?.PageSettings.ThemeIsDarkMode ?? false;
ThemeIndexColour = ThemeIsDarkMode ? CascadingState.PublicCacheData?.PageSettings.DarkThemeIndexColour ?? 215 :
CascadingState.PublicCacheData?.PageSettings.LightThemeIndexColour ?? 25;
} }
private void ToggleNavMenu() private void ToggleNavMenu()
@@ -128,55 +123,39 @@
protected async Task ResetToOriginalColour() protected async Task ResetToOriginalColour()
{ {
IsThemeChanging = true; var isDark = IsDark;
CascadingState.PublicCacheData.PageSettings.LightThemeIndexColour = await CascadingState.UpdatePageSettings(settings => settings.SetHue(isDark,
ThemeIndexColour = isDark ? PageSettings.DefaultDarkThemeIndexColour : PageSettings.DefaultLightThemeIndexColour));
25;
// if (AuthData?.User != null) // if (AuthData?.User != null)
// { // {
// AuthData.User.UserSettings.ThemeIndexColour = ThemeIndexColour; // AuthData.User.UserSettings.ThemeIndexColour = ThemeIndexColour;
// await Storage.SetItemAsync(nameof(AuthData), AuthData); // await Storage.SetItemAsync(nameof(AuthData), AuthData);
// } // }
await CascadingState.UpdatePublicCache(CascadingState.PublicCacheData);
IsThemeChanging = false;
} }
// Picking a hue also leaves the grey themes, which otherwise ignore it.
protected async Task UpdateThemeColour(short indexColour) protected async Task UpdateThemeColour(short indexColour)
{ {
IsThemeChanging = true; var isDark = IsDark;
CascadingState.PublicCacheData.PageSettings.LightThemeIndexColour = await CascadingState.UpdatePageSettings(settings => settings.SetHue(isDark, indexColour));
ThemeIndexColour =
indexColour;
// if (AuthData?.User != null) // if (AuthData?.User != null)
// { // {
// AuthData.User.UserSettings.ThemeIndexColour = ThemeIndexColour; // AuthData.User.UserSettings.ThemeIndexColour = ThemeIndexColour;
// await Storage.SetItemAsync(nameof(AuthData), AuthData); // await Storage.SetItemAsync(nameof(AuthData), AuthData);
// } // }
await CascadingState.UpdatePublicCache(CascadingState.PublicCacheData);
IsThemeChanging = false;
} }
// Choosing light or dark here leaves system theming; Settings can turn it back on.
protected async Task UpdateThemeDarkMode(bool isDarkMode) protected async Task UpdateThemeDarkMode(bool isDarkMode)
{ {
IsThemeChanging = true; await CascadingState.UpdatePageSettings(settings => settings.SetDarkMode(isDarkMode));
CascadingState.PublicCacheData.PageSettings.ThemeIsDarkMode =
ThemeIsDarkMode = isDarkMode;
Console.WriteLine("Dark updated {0}", ThemeIsDarkMode);
// if (AuthData?.User != null) // if (AuthData?.User != null)
// { // {
// AuthData.User.UserSettings.ThemeIsDarkMode = ThemeIsDarkMode; // AuthData.User.UserSettings.ThemeIsDarkMode = ThemeIsDarkMode;
// await Storage.SetItemAsync(nameof(AuthData), AuthData); // await Storage.SetItemAsync(nameof(AuthData), AuthData);
// } // }
await CascadingState.UpdatePublicCache(CascadingState.PublicCacheData);
IsThemeChanging = false;
} }
} }
+7 -2
View File
@@ -1,7 +1,8 @@
// Applies PageSettings to the page. It picks the daisyUI theme from Styles/theme.css and sets the hue and chroma // Applies PageSettings to the page. It picks the daisyUI theme from Styles/theme.css and sets the hue and chroma
// variables that theme derives every colour from. It loads in <head> right after css/app.css and applies the // variables that theme derives every colour from. It loads in <head> right after css/app.css and applies the
// settings saved in localStorage before the first paint. Once Blazor runs, CascadingState calls neoTheme.apply // settings saved in localStorage before the first paint. Once Blazor runs, CascadingState calls neoTheme.apply
// through AppStatusService.ApplyTheme whenever the settings change. // through AppStatusService.ApplyTheme whenever the settings change, and the theme controls ask neoTheme.isDark
// (AppStatusService.IsDarkTheme) which variant they are editing.
(() => { (() => {
const root = document.documentElement const root = document.documentElement
let current = null let current = null
@@ -48,5 +49,9 @@
// With system theming the OS can switch between light and dark at any time. // With system theming the OS can switch between light and dark at any time.
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => apply(current)) window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => apply(current))
window.neoTheme = { apply } // Whether the dark variant is showing, chosen or taken from the OS.
const isDark = () => root.getAttribute("data-theme") === "neo-dark" ||
(!root.hasAttribute("data-theme") && window.matchMedia("(prefers-color-scheme: dark)").matches)
window.neoTheme = { apply, isDark }
})() })()