Skip to content

Commit b34bc0e

Browse files
CopilotKeboo
andcommitted
Add light/dark theme toggle feature
Co-authored-by: Keboo <[email protected]>
1 parent 3ff3234 commit b34bc0e

File tree

4 files changed

+157
-4
lines changed

4 files changed

+157
-4
lines changed

PointerStar/Client/Cookies/WellKnownCookieValues.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public static class WellKnownCookieValues
55
private const string NameKey = "Name";
66
private const string RoleKey = "RoleId";
77
private const string RoomKey = "RoomId";
8+
private const string ThemePreferenceKey = "ThemePreference";
89

910
public static ValueTask<string> GetNameAsync(this ICookie cookie)
1011
=> cookie.GetValueAsync(NameKey);
@@ -26,5 +27,10 @@ public static ValueTask SetRoleAsync(this ICookie cookie, Guid? value)
2627
=> cookie.SetValueAsync(RoleKey, value?.ToString("D") ?? "");
2728
public static ValueTask SetRoomAsync(this ICookie cookie, string value)
2829
=> cookie.SetValueAsync(RoomKey, value);
30+
31+
public static ValueTask<string> GetThemePreferenceAsync(this ICookie cookie)
32+
=> cookie.GetValueAsync(ThemePreferenceKey);
33+
public static ValueTask SetThemePreferenceAsync(this ICookie cookie, string value)
34+
=> cookie.SetValueAsync(ThemePreferenceKey, value);
2935
}
3036

PointerStar/Client/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using MudBlazor.Services;
66
using PointerStar.Client;
77
using PointerStar.Client.Cookies;
8+
using PointerStar.Client.Services;
89
using PointerStar.Client.ViewModels;
910
using PointerStar.Shared;
1011
using Toolbelt.Blazor.Extensions.DependencyInjection;
@@ -34,5 +35,6 @@
3435
builder.Services.AddScoped<UserDialogViewModel>();
3536
builder.Services.AddScoped<ICookie, Cookie>();
3637
builder.Services.AddScoped<IClipboardService, ClipboardService>();
38+
builder.Services.AddScoped<IThemeService, ThemeService>();
3739

3840
await builder.Build().RunAsync();
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
namespace PointerStar.Client.Services;
2+
3+
using PointerStar.Client.Cookies;
4+
5+
public enum ThemePreference
6+
{
7+
System,
8+
Light,
9+
Dark
10+
}
11+
12+
public interface IThemeService
13+
{
14+
ThemePreference CurrentPreference { get; }
15+
bool IsDarkMode { get; }
16+
event EventHandler? ThemeChanged;
17+
Task InitializeAsync(Func<Task<bool>> getSystemPreference);
18+
Task SetPreferenceAsync(ThemePreference preference);
19+
Task CycleThemeAsync();
20+
}
21+
22+
public class ThemeService : IThemeService
23+
{
24+
private readonly ICookie _cookie;
25+
private Func<Task<bool>>? _getSystemPreference;
26+
private bool _systemPreference;
27+
28+
public ThemePreference CurrentPreference { get; private set; }
29+
30+
public bool IsDarkMode => CurrentPreference switch
31+
{
32+
ThemePreference.Dark => true,
33+
ThemePreference.Light => false,
34+
ThemePreference.System => _systemPreference,
35+
_ => _systemPreference
36+
};
37+
38+
public event EventHandler? ThemeChanged;
39+
40+
public ThemeService(ICookie cookie)
41+
{
42+
_cookie = cookie ?? throw new ArgumentNullException(nameof(cookie));
43+
}
44+
45+
public async Task InitializeAsync(Func<Task<bool>> getSystemPreference)
46+
{
47+
_getSystemPreference = getSystemPreference ?? throw new ArgumentNullException(nameof(getSystemPreference));
48+
49+
// Get system preference
50+
_systemPreference = await _getSystemPreference();
51+
52+
// Load saved preference from cookie
53+
string preferenceValue = await _cookie.GetThemePreferenceAsync();
54+
if (!string.IsNullOrEmpty(preferenceValue) && Enum.TryParse<ThemePreference>(preferenceValue, out var preference))
55+
{
56+
CurrentPreference = preference;
57+
}
58+
else
59+
{
60+
CurrentPreference = ThemePreference.System;
61+
}
62+
}
63+
64+
public async Task SetPreferenceAsync(ThemePreference preference)
65+
{
66+
if (CurrentPreference != preference)
67+
{
68+
CurrentPreference = preference;
69+
await _cookie.SetThemePreferenceAsync(preference.ToString());
70+
ThemeChanged?.Invoke(this, EventArgs.Empty);
71+
}
72+
}
73+
74+
public async Task CycleThemeAsync()
75+
{
76+
var nextPreference = CurrentPreference switch
77+
{
78+
ThemePreference.System => ThemePreference.Light,
79+
ThemePreference.Light => ThemePreference.Dark,
80+
ThemePreference.Dark => ThemePreference.System,
81+
_ => ThemePreference.System
82+
};
83+
await SetPreferenceAsync(nextPreference);
84+
}
85+
86+
public async Task UpdateSystemPreferenceAsync(bool isDark)
87+
{
88+
if (_systemPreference != isDark)
89+
{
90+
_systemPreference = isDark;
91+
if (CurrentPreference == ThemePreference.System)
92+
{
93+
ThemeChanged?.Invoke(this, EventArgs.Empty);
94+
}
95+
}
96+
await Task.CompletedTask;
97+
}
98+
}

PointerStar/Client/Shared/MainLayout.razor

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
@using System.Reflection;
2+
@using PointerStar.Client.Services
23
@inherits LayoutComponentBase
4+
@inject IThemeService ThemeService
5+
@implements IDisposable
36

47
<Toolbelt.Blazor.PWA.Updater.PWAUpdater />
58
<MudThemeProvider @ref="@_mudThemeProvider" @bind-IsDarkMode="@_isDarkMode" />
@@ -11,6 +14,9 @@
1114
<MudAppBar Elevation="1">
1215
<MudText Typo="Typo.h5" Class="ml-3">Pointer*</MudText>
1316
<MudSpacer />
17+
<MudTooltip Text="@GetThemeTooltip()">
18+
<MudIconButton Icon="@GetThemeIcon()" Color="Color.Inherit" OnClick="@CycleThemeAsync" />
19+
</MudTooltip>
1420
<MudText Typo="Typo.subtitle1" Align="Align.End">@Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)</MudText>
1521
</MudAppBar>
1622
<MudMainContent>
@@ -26,16 +32,57 @@
2632
{
2733
if (firstRender && _mudThemeProvider is { } themeProvider)
2834
{
29-
_isDarkMode = await themeProvider.GetSystemDarkModeAsync();
35+
await ThemeService.InitializeAsync(() => themeProvider.GetSystemDarkModeAsync());
36+
_isDarkMode = ThemeService.IsDarkMode;
3037
await themeProvider.WatchSystemDarkModeAsync(OnSystemPreferenceChanged);
38+
ThemeService.ThemeChanged += OnThemeChanged;
3139
StateHasChanged();
3240
}
3341
}
3442

35-
private Task OnSystemPreferenceChanged(bool newValue)
43+
private async Task OnSystemPreferenceChanged(bool newValue)
3644
{
37-
_isDarkMode = newValue;
45+
if (ThemeService is ThemeService themeService)
46+
{
47+
await themeService.UpdateSystemPreferenceAsync(newValue);
48+
}
49+
}
50+
51+
private void OnThemeChanged(object? sender, EventArgs e)
52+
{
53+
_isDarkMode = ThemeService.IsDarkMode;
3854
StateHasChanged();
39-
return Task.CompletedTask;
55+
}
56+
57+
private async Task CycleThemeAsync()
58+
{
59+
await ThemeService.CycleThemeAsync();
60+
}
61+
62+
private string GetThemeIcon()
63+
{
64+
return ThemeService.CurrentPreference switch
65+
{
66+
ThemePreference.System => Icons.Material.Filled.Brightness4,
67+
ThemePreference.Light => Icons.Material.Filled.LightMode,
68+
ThemePreference.Dark => Icons.Material.Filled.DarkMode,
69+
_ => Icons.Material.Filled.Brightness4
70+
};
71+
}
72+
73+
private string GetThemeTooltip()
74+
{
75+
return ThemeService.CurrentPreference switch
76+
{
77+
ThemePreference.System => "Theme: System (click to switch to Light)",
78+
ThemePreference.Light => "Theme: Light (click to switch to Dark)",
79+
ThemePreference.Dark => "Theme: Dark (click to switch to System)",
80+
_ => "Toggle theme"
81+
};
82+
}
83+
84+
public void Dispose()
85+
{
86+
ThemeService.ThemeChanged -= OnThemeChanged;
4087
}
4188
}

0 commit comments

Comments
 (0)