Skip to content

Commit 5a44284

Browse files
committed
feat: add light/dark theme switching
Extract the Catppuccin Mocha palette out of Styles.xaml into Theme.Dark.xaml/Theme.Light.xaml behind DynamicResource-bound keys, and add ThemeService to swap the active palette at runtime (mirroring LocalizationService). SalarySettings gains a Theme field ("auto"/ "light"/"dark"), with a dropdown in the settings Display tab that applies and persists immediately.
1 parent 14e47a5 commit 5a44284

18 files changed

Lines changed: 523 additions & 313 deletions

src/PayBeat.App/App.xaml.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
using Microsoft.Win32;
12
using PayBeat.App.Helpers;
23
using PayBeat.App.Models;
34
using PayBeat.App.Services;
45
using PayBeat.App.ViewModels;
56
using PayBeat.App.Views;
6-
using Microsoft.Win32;
77
using System.Windows.Interop;
88

99
namespace PayBeat.App;
@@ -62,6 +62,7 @@ protected override void OnStartup(StartupEventArgs e)
6262
var settings = _settingsService.Load();
6363
_startupSettings = settings;
6464
LocalizationService.Apply(settings.Language);
65+
ThemeService.Apply(settings.Theme);
6566

6667
_singleInstanceMutex = new Mutex(initiallyOwned: true, "PayBeat_SingleInstance", out var createdNew);
6768
if (!createdNew)

src/PayBeat.App/GlobalUsings.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
global using System.Windows;
2-
global using System.Windows.Controls;
32
global using System.Windows.Input;
43
global using System.Windows.Threading;
54
global using System.IO;

src/PayBeat.App/Models/SalarySettings.cs

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,44 @@ public record SalarySettings
88
{
99
public const decimal MaxDailySalary = 99_999_999m;
1010

11-
/// <summary>Daily gross salary used to compute the per-second earnings rate.</summary>
11+
/// <summary>
12+
/// Daily gross salary used to compute the per-second earnings rate.
13+
/// </summary>
1214
public decimal DailySalary { get; init; } = 500m;
1315

14-
/// <summary>Work day start time; earnings accrue from this moment.</summary>
16+
/// <summary>
17+
/// Work day start time; earnings accrue from this moment.
18+
/// </summary>
1519
public TimeOnly WorkStart { get; init; } = new(9, 0);
1620

17-
/// <summary>Work day end time; earnings are capped at <see cref="DailySalary"/> after this.</summary>
21+
/// <summary>
22+
/// Work day end time; earnings are capped at <see cref="DailySalary"/> after this.
23+
/// </summary>
1824
public TimeOnly WorkEnd { get; init; } = new(18, 0);
1925

20-
/// <summary>Currency symbol prepended to the formatted earnings string.</summary>
26+
/// <summary>
27+
/// Currency symbol prepended to the formatted earnings string.
28+
/// </summary>
2129
public string Currency { get; init; } = "¥";
2230

23-
/// <summary>Active widget display mode, determining which view template is shown.</summary>
31+
/// <summary>
32+
/// Active widget display mode, determining which view template is shown.
33+
/// </summary>
2434
public DisplayMode DisplayMode { get; init; } = DisplayMode.Normal;
2535

26-
/// <summary>Whether the widget window is pinned above all other windows.</summary>
36+
/// <summary>
37+
/// Whether the widget window is pinned above all other windows.
38+
/// </summary>
2739
public bool AlwaysOnTop { get; init; } = true;
2840

29-
/// <summary>Timer tick interval in seconds for refreshing the earnings display.</summary>
41+
/// <summary>
42+
/// Timer tick interval in seconds for refreshing the earnings display.
43+
/// </summary>
3044
public int RefreshInterval { get; init; } = 1;
3145

32-
/// <summary>Window opacity when the mouse is not hovering (0.1–1.0).</summary>
46+
/// <summary>
47+
/// Window opacity when the mouse is not hovering (0.1–1.0).
48+
/// </summary>
3349
public double Opacity { get; init; } = 1.0;
3450

3551
/// <summary>
@@ -44,48 +60,78 @@ public record SalarySettings
4460
/// </summary>
4561
public int HotkeyModifiers { get; init; } = 0x0003;
4662

47-
/// <summary>Virtual-key code for the global show/hide hotkey. Default <c>0x58</c> = X.</summary>
63+
/// <summary>
64+
/// Virtual-key code for the global show/hide hotkey. Default <c>0x58</c> = X.
65+
/// </summary>
4866
public int HotkeyVirtualKey { get; init; } = 0x58;
4967

50-
/// <summary>Last saved position for <see cref="DisplayMode.Normal"/> mode.</summary>
68+
/// <summary>
69+
/// Last saved position for <see cref="DisplayMode.Normal"/> mode.
70+
/// </summary>
5171
public WindowPosition? NormalPosition
5272
{
5373
get; init;
5474
}
5575

56-
/// <summary>Last saved position for <see cref="DisplayMode.Mini"/> mode.</summary>
76+
/// <summary>
77+
/// Last saved position for <see cref="DisplayMode.Mini"/> mode.
78+
/// </summary>
5779
public WindowPosition? MiniPosition
5880
{
5981
get; init;
6082
}
6183

62-
/// <summary>Last saved monitor for <see cref="DisplayMode.Flex"/> mode. Only <see cref="WindowPosition.ScreenDeviceName"/> is used.</summary>
84+
/// <summary>
85+
/// Last saved monitor for <see cref="DisplayMode.Flex"/> mode. Only <see cref="WindowPosition.ScreenDeviceName"/> is used.
86+
/// </summary>
6387
public WindowPosition? FlexPosition
6488
{
6589
get; init;
6690
}
6791

68-
/// <summary>Whether earnings should be deducted for a daily lunch break.</summary>
92+
/// <summary>
93+
/// Whether earnings should be deducted for a daily lunch break.
94+
/// </summary>
6995
public bool LunchBreakEnabled { get; init; } = false;
7096

71-
/// <summary>Lunch break start time; only used when <see cref="LunchBreakEnabled"/> is true.</summary>
97+
/// <summary>
98+
/// Lunch break start time; only used when <see cref="LunchBreakEnabled"/> is true.
99+
/// </summary>
72100
public TimeOnly LunchBreakStart { get; init; } = new(12, 0);
73101

74-
/// <summary>Lunch break end time; only used when <see cref="LunchBreakEnabled"/> is true.</summary>
102+
/// <summary>
103+
/// Lunch break end time; only used when <see cref="LunchBreakEnabled"/> is true.
104+
/// </summary>
75105
public TimeOnly LunchBreakEnd { get; init; } = new(13, 0);
76106

77-
/// <summary>Whether earnings accrue on Saturdays and Sundays. When false, weekends earn nothing.</summary>
107+
/// <summary>
108+
/// Whether earnings accrue on Saturdays and Sundays. When false, weekends earn nothing.
109+
/// </summary>
78110
public bool WorkOnWeekends { get; init; } = false;
79111

80-
/// <summary>Whether to show a tray balloon notification shortly before work ends.</summary>
112+
/// <summary>
113+
/// Whether to show a tray balloon notification shortly before work ends.
114+
/// </summary>
81115
public bool EnableEndOfDayReminder { get; init; } = false;
82116

83-
/// <summary>How many minutes before <see cref="WorkEnd"/> the end-of-day reminder fires.</summary>
117+
/// <summary>
118+
/// How many minutes before <see cref="WorkEnd"/> the end-of-day reminder fires.
119+
/// </summary>
84120
public int EndOfDayReminderMinutes { get; init; } = 5;
85121

86-
/// <summary>Whether to show a tray balloon notification each time earnings cross a <see cref="MilestoneAmount"/> increment.</summary>
122+
/// <summary>
123+
/// Whether to show a tray balloon notification each time earnings cross a <see cref="MilestoneAmount"/> increment.
124+
/// </summary>
87125
public bool EnableMilestoneNotifications { get; init; } = false;
88126

89-
/// <summary>Earnings increment that triggers a milestone notification (e.g. every ¥100).</summary>
127+
/// <summary>
128+
/// Earnings increment that triggers a milestone notification (e.g. every ¥100).
129+
/// </summary>
90130
public decimal MilestoneAmount { get; init; } = 100m;
131+
132+
/// <summary>
133+
/// UI color theme. <c>"auto"</c> resolves from the Windows apps theme setting;
134+
/// supported explicit values are <c>"light"</c> and <c>"dark"</c>.
135+
/// </summary>
136+
public string Theme { get; init; } = "auto";
91137
}

src/PayBeat.App/Resources/Strings.en.xaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@
6464
<sys:String x:Key="Settings.RunAtStartup">Run at Startup</sys:String>
6565
<sys:String x:Key="Settings.Language">Language</sys:String>
6666
<sys:String x:Key="Settings.LanguageAuto">Auto</sys:String>
67+
<sys:String x:Key="Settings.Theme">Theme</sys:String>
68+
<sys:String x:Key="Settings.ThemeAuto">Auto</sys:String>
69+
<sys:String x:Key="Settings.ThemeLight">Light</sys:String>
70+
<sys:String x:Key="Settings.ThemeDark">Dark</sys:String>
6771
<sys:String x:Key="Settings.Hotkey">Toggle Visibility Hotkey</sys:String>
6872
<sys:String x:Key="Settings.HotkeyHint">Click, then press a key combination</sys:String>
6973
<sys:String x:Key="Settings.HotkeyCapturing">Press a key combination...</sys:String>

src/PayBeat.App/Resources/Strings.zh-CN.xaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@
6464
<sys:String x:Key="Settings.RunAtStartup">开机自动启动</sys:String>
6565
<sys:String x:Key="Settings.Language">语言</sys:String>
6666
<sys:String x:Key="Settings.LanguageAuto">自动</sys:String>
67+
<sys:String x:Key="Settings.Theme">主题</sys:String>
68+
<sys:String x:Key="Settings.ThemeAuto">自动</sys:String>
69+
<sys:String x:Key="Settings.ThemeLight">浅色</sys:String>
70+
<sys:String x:Key="Settings.ThemeDark">深色</sys:String>
6771
<sys:String x:Key="Settings.Hotkey">显示/隐藏快捷键</sys:String>
6872
<sys:String x:Key="Settings.HotkeyHint">点击后输入快捷键组合</sys:String>
6973
<sys:String x:Key="Settings.HotkeyCapturing">请输入快捷键组合...</sys:String>

src/PayBeat.App/Resources/Styles.xaml

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,6 @@
11
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
33

4-
<!-- Color palette: Catppuccin Mocha -->
5-
<Color x:Key="BaseColor">#1E1E2E</Color>
6-
<Color x:Key="SurfaceColor">#313244</Color>
7-
<Color x:Key="OverlayColor">#45475A</Color>
8-
<Color x:Key="TextColor">#CDD6F4</Color>
9-
<Color x:Key="SubtextColor">#9399B2</Color>
10-
<Color x:Key="MutedColor">#6C7086</Color>
11-
<Color x:Key="GreenColor">#A6E3A1</Color>
12-
<Color x:Key="BlueColor">#89B4FA</Color>
13-
<Color x:Key="RedColor">#F38BA8</Color>
14-
15-
<SolidColorBrush x:Key="BaseBrush" Color="{StaticResource BaseColor}"/>
16-
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
17-
<SolidColorBrush x:Key="OverlayBrush" Color="{StaticResource OverlayColor}"/>
18-
<SolidColorBrush x:Key="TextBrush" Color="{StaticResource TextColor}"/>
19-
<SolidColorBrush x:Key="SubtextBrush" Color="{StaticResource SubtextColor}"/>
20-
<SolidColorBrush x:Key="MutedBrush" Color="{StaticResource MutedColor}"/>
21-
<SolidColorBrush x:Key="GreenBrush" Color="{StaticResource GreenColor}"/>
22-
<SolidColorBrush x:Key="BlueBrush" Color="{StaticResource BlueColor}"/>
23-
<SolidColorBrush x:Key="RedBrush" Color="{StaticResource RedColor}"/>
24-
254
<!-- Flat Button -->
265
<Style TargetType="Button">
276
<Setter Property="Cursor" Value="Hand"/>
@@ -56,7 +35,7 @@
5635
<Style TargetType="TextBox">
5736
<Setter Property="FontFamily" Value="Segoe UI"/>
5837
<Setter Property="FontSize" Value="13"/>
59-
<Setter Property="CaretBrush" Value="{StaticResource BlueBrush}"/>
38+
<Setter Property="CaretBrush" Value="{DynamicResource BlueBrush}"/>
6039
<Setter Property="Template">
6140
<Setter.Value>
6241
<ControlTemplate TargetType="TextBox">
@@ -68,7 +47,7 @@
6847
</Border>
6948
<ControlTemplate.Triggers>
7049
<Trigger Property="IsFocused" Value="True">
71-
<Setter Property="BorderBrush" Value="{StaticResource BlueBrush}"/>
50+
<Setter Property="BorderBrush" Value="{DynamicResource BlueBrush}"/>
7251
</Trigger>
7352
</ControlTemplate.Triggers>
7453
</ControlTemplate>
@@ -78,9 +57,9 @@
7857

7958
<!-- ComboBox -->
8059
<Style TargetType="ComboBox">
81-
<Setter Property="Background" Value="#313244"/>
82-
<Setter Property="Foreground" Value="#CDD6F4"/>
83-
<Setter Property="BorderBrush" Value="#45475A"/>
60+
<Setter Property="Background" Value="{DynamicResource SurfaceBrush}"/>
61+
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
62+
<Setter Property="BorderBrush" Value="{DynamicResource OverlayBrush}"/>
8463
<Setter Property="BorderThickness" Value="1"/>
8564
<Setter Property="FontFamily" Value="Segoe UI"/>
8665
<Setter Property="FontSize" Value="13"/>
@@ -108,7 +87,7 @@
10887
IsHitTestVisible="False"/>
10988
<Path Grid.Column="1"
11089
Data="M 0 0 L 5 5 L 10 0"
111-
Stroke="#9399B2"
90+
Stroke="{DynamicResource SubtextBrush}"
11291
StrokeThickness="1.5"
11392
HorizontalAlignment="Center"
11493
VerticalAlignment="Center"
@@ -130,8 +109,8 @@
130109
Focusable="False"
131110
PopupAnimation="Fade"
132111
Placement="Bottom">
133-
<Border Background="#313244"
134-
BorderBrush="#45475A"
112+
<Border Background="{DynamicResource SurfaceBrush}"
113+
BorderBrush="{DynamicResource OverlayBrush}"
135114
BorderThickness="1"
136115
CornerRadius="6"
137116
MinWidth="{TemplateBinding ActualWidth}"
@@ -149,10 +128,10 @@
149128
</Grid>
150129
<ControlTemplate.Triggers>
151130
<Trigger Property="IsMouseOver" Value="True">
152-
<Setter TargetName="Border" Property="BorderBrush" Value="#89B4FA"/>
131+
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource BlueBrush}"/>
153132
</Trigger>
154133
<Trigger Property="IsDropDownOpen" Value="True">
155-
<Setter TargetName="Border" Property="BorderBrush" Value="#89B4FA"/>
134+
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource BlueBrush}"/>
156135
</Trigger>
157136
</ControlTemplate.Triggers>
158137
</ControlTemplate>
@@ -161,7 +140,7 @@
161140
<Setter Property="ItemContainerStyle">
162141
<Setter.Value>
163142
<Style TargetType="ComboBoxItem">
164-
<Setter Property="Foreground" Value="#CDD6F4"/>
143+
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
165144
<Setter Property="Background" Value="Transparent"/>
166145
<Setter Property="Padding" Value="10,7"/>
167146
<Setter Property="FontFamily" Value="Segoe UI"/>
@@ -177,11 +156,11 @@
177156
</Border>
178157
<ControlTemplate.Triggers>
179158
<Trigger Property="IsMouseOver" Value="True">
180-
<Setter TargetName="Bg" Property="Background" Value="#45475A"/>
159+
<Setter TargetName="Bg" Property="Background" Value="{DynamicResource OverlayBrush}"/>
181160
</Trigger>
182161
<Trigger Property="IsSelected" Value="True">
183-
<Setter TargetName="Bg" Property="Background" Value="#45475A"/>
184-
<Setter Property="Foreground" Value="#89B4FA"/>
162+
<Setter TargetName="Bg" Property="Background" Value="{DynamicResource OverlayBrush}"/>
163+
<Setter Property="Foreground" Value="{DynamicResource BlueBrush}"/>
185164
</Trigger>
186165
</ControlTemplate.Triggers>
187166
</ControlTemplate>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
3+
4+
<!-- Color palette: Catppuccin Mocha (dark) -->
5+
<Color x:Key="BaseColor">#1E1E2E</Color>
6+
<Color x:Key="SurfaceColor">#313244</Color>
7+
<Color x:Key="OverlayColor">#45475A</Color>
8+
<Color x:Key="TextColor">#CDD6F4</Color>
9+
<Color x:Key="SubtextColor">#9399B2</Color>
10+
<Color x:Key="MutedColor">#6C7086</Color>
11+
<Color x:Key="GreenColor">#A6E3A1</Color>
12+
<Color x:Key="BlueColor">#89B4FA</Color>
13+
<Color x:Key="RedColor">#F38BA8</Color>
14+
<Color x:Key="CaptureBackgroundColor">#1C3A5E</Color>
15+
<Color x:Key="FlexGradientMidColor">#181825</Color>
16+
17+
<SolidColorBrush x:Key="BaseBrush" Color="{StaticResource BaseColor}"/>
18+
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
19+
<SolidColorBrush x:Key="OverlayBrush" Color="{StaticResource OverlayColor}"/>
20+
<SolidColorBrush x:Key="TextBrush" Color="{StaticResource TextColor}"/>
21+
<SolidColorBrush x:Key="SubtextBrush" Color="{StaticResource SubtextColor}"/>
22+
<SolidColorBrush x:Key="MutedBrush" Color="{StaticResource MutedColor}"/>
23+
<SolidColorBrush x:Key="GreenBrush" Color="{StaticResource GreenColor}"/>
24+
<SolidColorBrush x:Key="BlueBrush" Color="{StaticResource BlueColor}"/>
25+
<SolidColorBrush x:Key="RedBrush" Color="{StaticResource RedColor}"/>
26+
<SolidColorBrush x:Key="CaptureBackgroundBrush" Color="{StaticResource CaptureBackgroundColor}"/>
27+
28+
</ResourceDictionary>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
3+
4+
<!-- Color palette: Catppuccin Latte (light) -->
5+
<Color x:Key="BaseColor">#EFF1F5</Color>
6+
<Color x:Key="SurfaceColor">#E6E9EF</Color>
7+
<Color x:Key="OverlayColor">#CCD0DA</Color>
8+
<Color x:Key="TextColor">#4C4F69</Color>
9+
<Color x:Key="SubtextColor">#6C6F85</Color>
10+
<Color x:Key="MutedColor">#8C8FA1</Color>
11+
<Color x:Key="GreenColor">#40A02B</Color>
12+
<Color x:Key="BlueColor">#1E66F5</Color>
13+
<Color x:Key="RedColor">#D20F39</Color>
14+
<Color x:Key="CaptureBackgroundColor">#DCE8FF</Color>
15+
<Color x:Key="FlexGradientMidColor">#DCE0E8</Color>
16+
17+
<SolidColorBrush x:Key="BaseBrush" Color="{StaticResource BaseColor}"/>
18+
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
19+
<SolidColorBrush x:Key="OverlayBrush" Color="{StaticResource OverlayColor}"/>
20+
<SolidColorBrush x:Key="TextBrush" Color="{StaticResource TextColor}"/>
21+
<SolidColorBrush x:Key="SubtextBrush" Color="{StaticResource SubtextColor}"/>
22+
<SolidColorBrush x:Key="MutedBrush" Color="{StaticResource MutedColor}"/>
23+
<SolidColorBrush x:Key="GreenBrush" Color="{StaticResource GreenColor}"/>
24+
<SolidColorBrush x:Key="BlueBrush" Color="{StaticResource BlueColor}"/>
25+
<SolidColorBrush x:Key="RedBrush" Color="{StaticResource RedColor}"/>
26+
<SolidColorBrush x:Key="CaptureBackgroundBrush" Color="{StaticResource CaptureBackgroundColor}"/>
27+
28+
</ResourceDictionary>

0 commit comments

Comments
 (0)