Persist frontend theme preference
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
<body>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["theme.js"]"></script>
|
||||
<script src="@Assets["tables.js"]"></script>
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject RolemasterDb.App.Frontend.AppState.ThemeState ThemeState
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
@@ -17,3 +19,25 @@
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ThemeState.Changed += HandleThemeChanged;
|
||||
}
|
||||
|
||||
protected override Task OnAfterRenderAsync(bool firstRender) =>
|
||||
firstRender
|
||||
? ThemeState.InitializeAsync()
|
||||
: Task.CompletedTask;
|
||||
|
||||
private void HandleThemeChanged()
|
||||
{
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ThemeState.Changed -= HandleThemeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace RolemasterDb.App.Frontend.AppState;
|
||||
|
||||
public static class BrowserStorageKeys
|
||||
{
|
||||
public const string ThemeMode = "rolemaster.theme.mode";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace RolemasterDb.App.Frontend.AppState;
|
||||
|
||||
public sealed class BrowserStorageService(IJSRuntime jsRuntime)
|
||||
{
|
||||
public ValueTask<string?> GetItemAsync(string key) =>
|
||||
jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
|
||||
|
||||
public ValueTask SetItemAsync(string key, string value) =>
|
||||
jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
|
||||
}
|
||||
@@ -1,19 +1,66 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace RolemasterDb.App.Frontend.AppState;
|
||||
|
||||
public sealed class ThemeState
|
||||
public sealed class ThemeState(BrowserStorageService browserStorage, IJSRuntime jsRuntime)
|
||||
{
|
||||
private bool isInitialized;
|
||||
|
||||
public ThemeMode CurrentMode { get; private set; } = ThemeMode.System;
|
||||
|
||||
public event Action? Changed;
|
||||
|
||||
public void SetMode(ThemeMode mode)
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (CurrentMode == mode)
|
||||
if (isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var storedMode = await browserStorage.GetItemAsync(BrowserStorageKeys.ThemeMode);
|
||||
CurrentMode = ParseMode(storedMode);
|
||||
await ApplyThemeAsync(CurrentMode);
|
||||
isInitialized = true;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// JS interop is unavailable during prerender. Retry on the next interactive render.
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetModeAsync(ThemeMode mode)
|
||||
{
|
||||
if (isInitialized && CurrentMode == mode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentMode = mode;
|
||||
await ApplyThemeAsync(mode);
|
||||
await browserStorage.SetItemAsync(BrowserStorageKeys.ThemeMode, ToStorageValue(mode));
|
||||
isInitialized = true;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private Task ApplyThemeAsync(ThemeMode mode) =>
|
||||
jsRuntime.InvokeVoidAsync("rolemasterTheme.apply", ToStorageValue(mode)).AsTask();
|
||||
|
||||
private static ThemeMode ParseMode(string? value) =>
|
||||
value?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"light" => ThemeMode.Light,
|
||||
"dark" => ThemeMode.Dark,
|
||||
_ => ThemeMode.System
|
||||
};
|
||||
|
||||
private static string ToStorageValue(ThemeMode mode) =>
|
||||
mode switch
|
||||
{
|
||||
ThemeMode.Light => "light",
|
||||
ThemeMode.Dark => "dark",
|
||||
_ => "system"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ builder.Services.AddRazorComponents()
|
||||
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
|
||||
builder.Services.AddSingleton<CriticalImportArtifactLocator>();
|
||||
builder.Services.AddScoped<LookupService>();
|
||||
builder.Services.AddScoped<BrowserStorageService>();
|
||||
builder.Services.AddScoped<ThemeState>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
5
src/RolemasterDb.App/wwwroot/theme.js
Normal file
5
src/RolemasterDb.App/wwwroot/theme.js
Normal file
@@ -0,0 +1,5 @@
|
||||
window.rolemasterTheme = {
|
||||
apply(mode) {
|
||||
document.documentElement.dataset.theme = mode || "system";
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user