diff --git a/App.axaml b/App.axaml
index 8ccae54..5f2dfa2 100644
--- a/App.axaml
+++ b/App.axaml
@@ -12,7 +12,7 @@
-
+
diff --git a/Assets/app-icon.ico b/Assets/app-icon.ico
index 0fcc77b..f7f840c 100644
Binary files a/Assets/app-icon.ico and b/Assets/app-icon.ico differ
diff --git a/Assets/app-icon.png b/Assets/app-icon.png
new file mode 100644
index 0000000..b39c515
Binary files /dev/null and b/Assets/app-icon.png differ
diff --git a/Assets/app_tray_icon.jpg b/Assets/app_tray_icon.jpg
index 3b99368..4de9c5e 100644
Binary files a/Assets/app_tray_icon.jpg and b/Assets/app_tray_icon.jpg differ
diff --git a/Assets/logo.svg b/Assets/logo.svg
new file mode 100644
index 0000000..740f397
--- /dev/null
+++ b/Assets/logo.svg
@@ -0,0 +1,7 @@
+
diff --git a/LocalLLMServerManager.Shared/Services/IThemeService.cs b/LocalLLMServerManager.Shared/Services/IThemeService.cs
new file mode 100644
index 0000000..c5906bd
--- /dev/null
+++ b/LocalLLMServerManager.Shared/Services/IThemeService.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace LocalLLMServerManager.Shared.Services;
+
+public enum AppTheme
+{
+ MatteCarbon,
+ OledBlack,
+ Light
+}
+
+public interface IThemeService
+{
+ AppTheme CurrentTheme { get; }
+ void SetTheme(AppTheme theme);
+ event EventHandler? ThemeChanged;
+}
diff --git a/LocalLLMServerManager.Shared/Services/ThemeService.cs b/LocalLLMServerManager.Shared/Services/ThemeService.cs
new file mode 100644
index 0000000..73a970c
--- /dev/null
+++ b/LocalLLMServerManager.Shared/Services/ThemeService.cs
@@ -0,0 +1,131 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Styling;
+
+namespace LocalLLMServerManager.Shared.Services;
+
+public class ThemeService : IThemeService
+{
+ private static ThemeService? _instance;
+ public static ThemeService Instance => _instance ??= new ThemeService();
+
+ private readonly IResourceDictionary? _resourceDictionary;
+
+ public AppTheme CurrentTheme { get; private set; } = AppTheme.MatteCarbon;
+
+ public event EventHandler? ThemeChanged;
+
+ public ThemeService(IResourceDictionary? resourceDictionary = null)
+ {
+ _resourceDictionary = resourceDictionary;
+ }
+
+ public void SetTheme(AppTheme theme)
+ {
+ CurrentTheme = theme;
+
+ var resources = _resourceDictionary ?? Application.Current?.Resources;
+ if (resources != null)
+ {
+ ApplyThemeResources(resources, theme);
+ }
+
+ if (Application.Current != null)
+ {
+ Application.Current.RequestedThemeVariant = theme == AppTheme.Light ? ThemeVariant.Light : ThemeVariant.Dark;
+ }
+
+ ThemeChanged?.Invoke(this, theme);
+ }
+
+ public static void ApplyThemeResources(IResourceDictionary resources, AppTheme theme)
+ {
+ Color bgDark, bgSurface, bgCard, border, textMain, textMuted, primary, secondary, accent;
+
+ switch (theme)
+ {
+ case AppTheme.OledBlack:
+ bgDark = Color.Parse("#000000");
+ bgSurface = Color.Parse("#121212");
+ bgCard = Color.Parse("#18181b");
+ border = Color.Parse("#27272a");
+ textMain = Color.Parse("#f4f4f5");
+ textMuted = Color.Parse("#71717a");
+ primary = Color.Parse("#3b82f6");
+ secondary = Color.Parse("#60a5fa");
+ accent = Color.Parse("#60a5fa");
+ break;
+
+ case AppTheme.Light:
+ bgDark = Color.Parse("#ffffff");
+ bgSurface = Color.Parse("#f6f8fa");
+ bgCard = Color.Parse("#ffffff");
+ border = Color.Parse("#d0d7de");
+ textMain = Color.Parse("#1f2328");
+ textMuted = Color.Parse("#656d76");
+ primary = Color.Parse("#0969da");
+ secondary = Color.Parse("#218bff");
+ accent = Color.Parse("#218bff");
+ break;
+
+ case AppTheme.MatteCarbon:
+ default:
+ bgDark = Color.Parse("#0d1117");
+ bgSurface = Color.Parse("#161b22");
+ bgCard = Color.Parse("#1c2128");
+ border = Color.Parse("#30363d");
+ textMain = Color.Parse("#f0f6fc");
+ textMuted = Color.Parse("#8b949e");
+ primary = Color.Parse("#388bfd");
+ secondary = Color.Parse("#58a6ff");
+ accent = Color.Parse("#79c0ff");
+ break;
+ }
+
+ resources["BgDarkColor"] = bgDark;
+ resources["BgSurfaceColor"] = bgSurface;
+ resources["BgCardColor"] = bgCard;
+ resources["BorderColor"] = border;
+ resources["TextMainColor"] = textMain;
+ resources["TextMutedColor"] = textMuted;
+ resources["PrimaryColor"] = primary;
+ resources["SecondaryColor"] = secondary;
+ resources["AccentColor"] = accent;
+
+ SetOrUpdateBrush(resources, "BgDarkBrush", bgDark);
+ SetOrUpdateBrush(resources, "BgSurfaceBrush", bgSurface);
+ SetOrUpdateBrush(resources, "BgCardBrush", bgCard);
+ SetOrUpdateBrush(resources, "GlassBorderBrush", border);
+ SetOrUpdateBrush(resources, "BorderColorBrush", border);
+ SetOrUpdateBrush(resources, "BorderGlowBrush", primary, 0.4);
+ SetOrUpdateBrush(resources, "TextMainBrush", textMain);
+ SetOrUpdateBrush(resources, "TextMutedBrush", textMuted);
+ SetOrUpdateBrush(resources, "PrimaryBrush", primary);
+ SetOrUpdateBrush(resources, "SecondaryBrush", secondary);
+ SetOrUpdateBrush(resources, "AccentBrush", accent);
+ SetOrUpdateBrush(resources, "GlassBackgroundBrush", bgDark);
+ SetOrUpdateBrush(resources, "PrimaryGradientBrush", primary);
+ }
+
+ private static void SetOrUpdateBrush(IResourceDictionary resources, string key, Color color, double opacity = 1.0)
+ {
+ if (resources.TryGetResource(key, null, out var existing) && existing is SolidColorBrush existingBrush)
+ {
+ existingBrush.Color = color;
+ existingBrush.Opacity = opacity;
+ resources[key] = existingBrush;
+ }
+ else if (resources.TryGetValue(key, out var dictVal) && dictVal is SolidColorBrush dictBrush)
+ {
+ dictBrush.Color = color;
+ dictBrush.Opacity = opacity;
+ resources[key] = dictBrush;
+ }
+ else
+ {
+ resources[key] = new SolidColorBrush(color) { Opacity = opacity };
+ }
+ }
+}
diff --git a/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml b/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml
index a4637fd..62616d0 100644
--- a/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml
+++ b/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml
@@ -1,35 +1,36 @@
- #0b0e14
- #161b26
- #1e2536
- #8b5cf6
- #06b6d4
- #c084fc
+ #0d1117
+ #161b22
+ #1c2128
+ #30363d
+ #388bfd
+ #58a6ff
+ #79c0ff
+ #f0f6fc
+ #8b949e
+ #238636
+ #da3633
+ #d29922
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/LocalLLMServerManager.Shared/Styles/GlassmorphicTheme.axaml b/LocalLLMServerManager.Shared/Styles/GlassmorphicTheme.axaml
index 0ee9c9f..03ea244 100644
--- a/LocalLLMServerManager.Shared/Styles/GlassmorphicTheme.axaml
+++ b/LocalLLMServerManager.Shared/Styles/GlassmorphicTheme.axaml
@@ -1,139 +1,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/LocalLLMServerManager.Shared/Styles/MatteTheme.axaml b/LocalLLMServerManager.Shared/Styles/MatteTheme.axaml
new file mode 100644
index 0000000..d79eff7
--- /dev/null
+++ b/LocalLLMServerManager.Shared/Styles/MatteTheme.axaml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs
index 17f5b27..df1125a 100644
--- a/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs
+++ b/LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs
@@ -174,6 +174,8 @@ private static string GetDefaultApiBase()
public string ComfyUiExecutablePath { get => Settings.ComfyUiExecutablePath; set => Settings.ComfyUiExecutablePath = value; }
public string ForgeExecutablePath { get => Settings.ForgeExecutablePath; set => Settings.ForgeExecutablePath = value; }
public string LanAccessUrl { get => Settings.LanAccessUrl; set => Settings.LanAccessUrl = value; }
+ public string SelectedTheme { get => Settings.SelectedTheme; set => Settings.SelectedTheme = value; }
+ public System.Collections.Generic.IReadOnlyList AvailableThemes => Settings.AvailableThemes;
private async Task StartBackgroundPollingAsync()
{
diff --git a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs
index 124be37..48c1274 100644
--- a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs
+++ b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs
@@ -41,8 +41,25 @@ public partial class SettingsViewModel : ObservableObject
public string OllamaExecutableStatus => OllamaStatus;
- public SettingsViewModel()
+ private readonly IThemeService _themeService;
+
+ public IReadOnlyList AvailableThemes { get; } = new[]
+ {
+ "Matte Carbon (Default)",
+ "OLED Pure Black",
+ "Clean Light"
+ };
+
+ [ObservableProperty] private string _selectedTheme = "Matte Carbon (Default)";
+
+ public SettingsViewModel() : this(ThemeService.Instance)
+ {
+ }
+
+ public SettingsViewModel(IThemeService themeService)
{
+ _themeService = themeService ?? ThemeService.Instance;
+ _selectedTheme = MapThemeToString(_themeService.CurrentTheme);
RefreshAllStatuses();
}
@@ -58,6 +75,26 @@ partial void OnOllamaExecutablePathChanged(string value)
OnPropertyChanged(nameof(OllamaExecutableStatus));
}
+ partial void OnSelectedThemeChanged(string value)
+ {
+ var theme = MapStringToTheme(value);
+ _themeService?.SetTheme(theme);
+ }
+
+ public static string MapThemeToString(AppTheme theme) => theme switch
+ {
+ AppTheme.OledBlack => "OLED Pure Black",
+ AppTheme.Light => "Clean Light",
+ _ => "Matte Carbon (Default)"
+ };
+
+ public static AppTheme MapStringToTheme(string? themeName) => themeName switch
+ {
+ "OLED Pure Black" => AppTheme.OledBlack,
+ "Clean Light" => AppTheme.Light,
+ _ => AppTheme.MatteCarbon
+ };
+
public void RefreshAllStatuses()
{
ComfyUiExecutableStatus = EvaluateExecutableStatus(ComfyUiExecutablePath);
diff --git a/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml
index c0d9246..6b58aaf 100644
--- a/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml
+++ b/LocalLLMServerManager.Shared/Views/Controls/CivitaiTabControl.axaml
@@ -6,10 +6,10 @@
-
+
@@ -18,15 +18,15 @@
-
+
-
+
-
+
diff --git a/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml
index 2ef09ea..04b0135 100644
--- a/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml
+++ b/LocalLLMServerManager.Shared/Views/Controls/EngineStudioTabControl.axaml
@@ -12,46 +12,46 @@
-
+
-
+
-
+
+ Classes="matte-primary" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,4,0,0"/>
-
+
-
+
-
+
+ Classes="matte-primary" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,4,0,0"/>
diff --git a/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml
index 8f827fe..7a2cf8b 100644
--- a/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml
+++ b/LocalLLMServerManager.Shared/Views/Controls/HuggingFaceTabControl.axaml
@@ -6,10 +6,10 @@
-
+
@@ -18,18 +18,18 @@
-
+
-
-
+
+
-
-
+
+
diff --git a/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml
index 695a8b5..bd408ba 100644
--- a/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml
+++ b/LocalLLMServerManager.Shared/Views/Controls/OllamaModelsTabControl.axaml
@@ -11,7 +11,7 @@
@@ -20,23 +20,23 @@
-
+
-
+
-
+
-
+
-
+
@@ -46,15 +46,19 @@
-
-
+
+
-
+
+
+
-
-
+
+
+
+
diff --git a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml
index d225b58..eb8b3bc 100644
--- a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml
+++ b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml
@@ -5,9 +5,9 @@
x:DataType="vm:SettingsViewModel">
-
+
-
+
@@ -16,39 +16,48 @@
-
+
-
+
+
+
+
+
+
+
+ Classes="matte-primary"/>
+ Classes="matte-secondary"/>
-
+
-
-
+
+
-
+
@@ -56,13 +65,13 @@
-
+
-
-
+
+
@@ -70,13 +79,13 @@
-
+
-
-
+
+
@@ -84,13 +93,13 @@
-
+
-
-
+
+
@@ -98,13 +107,13 @@
-
+
-
-
+
+
@@ -112,13 +121,13 @@
-
+
-
-
+
+
@@ -126,13 +135,13 @@
-
+
-
-
+
+
@@ -140,30 +149,30 @@
-
+
-
-
+
+
-
+
-
+
diff --git a/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml
index f985f0d..af9dede 100644
--- a/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml
+++ b/LocalLLMServerManager.Shared/Views/Controls/TelemetryHeaderControl.axaml
@@ -1,62 +1,84 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vm="using:LocalLLMServerManager.Shared.ViewModels"
+ x:Class="LocalLLMServerManager.Shared.Views.Controls.TelemetryHeaderControl"
+ x:DataType="vm:TelemetryViewModel">
-
+
-
+
-
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
-
diff --git a/LocalLLMServerManager.Shared/Views/MainView.axaml b/LocalLLMServerManager.Shared/Views/MainView.axaml
index bc8f87e..5c446ab 100644
--- a/LocalLLMServerManager.Shared/Views/MainView.axaml
+++ b/LocalLLMServerManager.Shared/Views/MainView.axaml
@@ -21,7 +21,7 @@
-
+
@@ -57,18 +57,18 @@
-
-
+
+
-
+
-
-
+
+
diff --git a/LocalLLMServerManager.Tests/SettingsViewModelTests.cs b/LocalLLMServerManager.Tests/SettingsViewModelTests.cs
index b2cd030..84b5411 100644
--- a/LocalLLMServerManager.Tests/SettingsViewModelTests.cs
+++ b/LocalLLMServerManager.Tests/SettingsViewModelTests.cs
@@ -37,6 +37,8 @@ public void DefaultState_HasExpectedPropertiesAndMissingStatus()
Assert.Equal("http://127.0.0.1:8188", vm.ComfyUiUrl);
Assert.Equal("semi", vm.SelectedThemeStyle);
Assert.Equal("http://127.0.0.1:5246", vm.LanAccessUrl);
+ Assert.Equal("Matte Carbon (Default)", vm.SelectedTheme);
+ Assert.Equal(3, vm.AvailableThemes.Count);
Assert.Contains("Missing", vm.ForgeModelsStatus);
Assert.Contains("Missing", vm.ThreeDModelsStatus);
diff --git a/LocalLLMServerManager.Tests/ThemeServiceTests.cs b/LocalLLMServerManager.Tests/ThemeServiceTests.cs
new file mode 100644
index 0000000..bf38b54
--- /dev/null
+++ b/LocalLLMServerManager.Tests/ThemeServiceTests.cs
@@ -0,0 +1,192 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using Avalonia.Media;
+using Avalonia.Styling;
+using LocalLLMServerManager.Shared.Services;
+using LocalLLMServerManager.Shared.ViewModels;
+using Moq;
+using Xunit;
+
+namespace LocalLLMServerManager.Tests;
+
+public class ThemeServiceTests
+{
+ [Fact]
+ public void DefaultTheme_IsMatteCarbon()
+ {
+ var dict = new ResourceDictionary();
+ var service = new ThemeService(dict);
+
+ Assert.Equal(AppTheme.MatteCarbon, service.CurrentTheme);
+ }
+
+ [Fact]
+ public void SetTheme_OledBlack_UpdatesCurrentThemeAndFiresThemeChangedAndMutatesResources()
+ {
+ var dict = new ResourceDictionary();
+ var service = new ThemeService(dict);
+
+ AppTheme? changedTheme = null;
+ service.ThemeChanged += (_, theme) => changedTheme = theme;
+
+ service.SetTheme(AppTheme.OledBlack);
+
+ Assert.Equal(AppTheme.OledBlack, service.CurrentTheme);
+ Assert.Equal(AppTheme.OledBlack, changedTheme);
+
+ // Assert brushes
+ Assert.Equal(Color.Parse("#000000"), ((SolidColorBrush)dict["BgDarkBrush"]!).Color);
+ Assert.Equal(Color.Parse("#121212"), ((SolidColorBrush)dict["BgSurfaceBrush"]!).Color);
+ Assert.Equal(Color.Parse("#18181b"), ((SolidColorBrush)dict["BgCardBrush"]!).Color);
+ Assert.Equal(Color.Parse("#27272a"), ((SolidColorBrush)dict["GlassBorderBrush"]!).Color);
+ Assert.Equal(Color.Parse("#27272a"), ((SolidColorBrush)dict["BorderColorBrush"]!).Color);
+ Assert.Equal(Color.Parse("#f4f4f5"), ((SolidColorBrush)dict["TextMainBrush"]!).Color);
+ Assert.Equal(Color.Parse("#71717a"), ((SolidColorBrush)dict["TextMutedBrush"]!).Color);
+ Assert.Equal(Color.Parse("#3b82f6"), ((SolidColorBrush)dict["PrimaryBrush"]!).Color);
+ Assert.Equal(Color.Parse("#60a5fa"), ((SolidColorBrush)dict["SecondaryBrush"]!).Color);
+ Assert.Equal(Color.Parse("#60a5fa"), ((SolidColorBrush)dict["AccentBrush"]!).Color);
+ Assert.Equal(Color.Parse("#000000"), ((SolidColorBrush)dict["GlassBackgroundBrush"]!).Color);
+ Assert.Equal(Color.Parse("#3b82f6"), ((SolidColorBrush)dict["PrimaryGradientBrush"]!).Color);
+
+ var glow = (SolidColorBrush)dict["BorderGlowBrush"]!;
+ Assert.Equal(Color.Parse("#3b82f6"), glow.Color);
+ Assert.Equal(0.4, glow.Opacity, precision: 2);
+
+ // Assert colors
+ Assert.Equal(Color.Parse("#000000"), (Color)dict["BgDarkColor"]!);
+ Assert.Equal(Color.Parse("#121212"), (Color)dict["BgSurfaceColor"]!);
+ Assert.Equal(Color.Parse("#18181b"), (Color)dict["BgCardColor"]!);
+ Assert.Equal(Color.Parse("#27272a"), (Color)dict["BorderColor"]!);
+ Assert.Equal(Color.Parse("#f4f4f5"), (Color)dict["TextMainColor"]!);
+ Assert.Equal(Color.Parse("#71717a"), (Color)dict["TextMutedColor"]!);
+ Assert.Equal(Color.Parse("#3b82f6"), (Color)dict["PrimaryColor"]!);
+ Assert.Equal(Color.Parse("#60a5fa"), (Color)dict["SecondaryColor"]!);
+ Assert.Equal(Color.Parse("#60a5fa"), (Color)dict["AccentColor"]!);
+ }
+
+ [Fact]
+ public void SetTheme_Light_UpdatesCurrentThemeAndFiresThemeChangedAndMutatesResources()
+ {
+ var dict = new ResourceDictionary();
+ var service = new ThemeService(dict);
+
+ AppTheme? changedTheme = null;
+ service.ThemeChanged += (_, theme) => changedTheme = theme;
+
+ service.SetTheme(AppTheme.Light);
+
+ Assert.Equal(AppTheme.Light, service.CurrentTheme);
+ Assert.Equal(AppTheme.Light, changedTheme);
+
+ // Assert brushes
+ Assert.Equal(Color.Parse("#ffffff"), ((SolidColorBrush)dict["BgDarkBrush"]!).Color);
+ Assert.Equal(Color.Parse("#f6f8fa"), ((SolidColorBrush)dict["BgSurfaceBrush"]!).Color);
+ Assert.Equal(Color.Parse("#ffffff"), ((SolidColorBrush)dict["BgCardBrush"]!).Color);
+ Assert.Equal(Color.Parse("#d0d7de"), ((SolidColorBrush)dict["GlassBorderBrush"]!).Color);
+ Assert.Equal(Color.Parse("#d0d7de"), ((SolidColorBrush)dict["BorderColorBrush"]!).Color);
+ Assert.Equal(Color.Parse("#1f2328"), ((SolidColorBrush)dict["TextMainBrush"]!).Color);
+ Assert.Equal(Color.Parse("#656d76"), ((SolidColorBrush)dict["TextMutedBrush"]!).Color);
+ Assert.Equal(Color.Parse("#0969da"), ((SolidColorBrush)dict["PrimaryBrush"]!).Color);
+ Assert.Equal(Color.Parse("#218bff"), ((SolidColorBrush)dict["SecondaryBrush"]!).Color);
+ Assert.Equal(Color.Parse("#218bff"), ((SolidColorBrush)dict["AccentBrush"]!).Color);
+ Assert.Equal(Color.Parse("#ffffff"), ((SolidColorBrush)dict["GlassBackgroundBrush"]!).Color);
+ Assert.Equal(Color.Parse("#0969da"), ((SolidColorBrush)dict["PrimaryGradientBrush"]!).Color);
+
+ var glow = (SolidColorBrush)dict["BorderGlowBrush"]!;
+ Assert.Equal(Color.Parse("#0969da"), glow.Color);
+ Assert.Equal(0.4, glow.Opacity, precision: 2);
+ }
+
+ [Fact]
+ public void SetTheme_MatteCarbon_UpdatesCurrentThemeAndFiresThemeChangedAndMutatesResources()
+ {
+ var dict = new ResourceDictionary();
+ var service = new ThemeService(dict);
+
+ // Switch to Light first, then back to MatteCarbon
+ service.SetTheme(AppTheme.Light);
+
+ AppTheme? changedTheme = null;
+ service.ThemeChanged += (_, theme) => changedTheme = theme;
+
+ service.SetTheme(AppTheme.MatteCarbon);
+
+ Assert.Equal(AppTheme.MatteCarbon, service.CurrentTheme);
+ Assert.Equal(AppTheme.MatteCarbon, changedTheme);
+
+ Assert.Equal(Color.Parse("#0d1117"), ((SolidColorBrush)dict["BgDarkBrush"]!).Color);
+ Assert.Equal(Color.Parse("#161b22"), ((SolidColorBrush)dict["BgSurfaceBrush"]!).Color);
+ Assert.Equal(Color.Parse("#1c2128"), ((SolidColorBrush)dict["BgCardBrush"]!).Color);
+ Assert.Equal(Color.Parse("#30363d"), ((SolidColorBrush)dict["GlassBorderBrush"]!).Color);
+ Assert.Equal(Color.Parse("#f0f6fc"), ((SolidColorBrush)dict["TextMainBrush"]!).Color);
+ Assert.Equal(Color.Parse("#8b949e"), ((SolidColorBrush)dict["TextMutedBrush"]!).Color);
+ Assert.Equal(Color.Parse("#388bfd"), ((SolidColorBrush)dict["PrimaryBrush"]!).Color);
+ Assert.Equal(Color.Parse("#58a6ff"), ((SolidColorBrush)dict["SecondaryBrush"]!).Color);
+ Assert.Equal(Color.Parse("#79c0ff"), ((SolidColorBrush)dict["AccentBrush"]!).Color);
+ }
+
+ [Fact]
+ public void SetTheme_WithExistingBrushInstances_MutatesColorInPlace()
+ {
+ var dict = new ResourceDictionary();
+ var existingBrush = new SolidColorBrush(Color.Parse("#111111"));
+ dict["BgDarkBrush"] = existingBrush;
+
+ var service = new ThemeService(dict);
+ service.SetTheme(AppTheme.Light);
+
+ // Ensure the exact instance in memory was updated
+ Assert.Same(existingBrush, dict["BgDarkBrush"]);
+ Assert.Equal(Color.Parse("#ffffff"), existingBrush.Color);
+ }
+
+ [AvaloniaFact]
+ public void SetTheme_WithHeadlessApp_MutatesApplicationRequestedThemeVariant()
+ {
+ var service = new ThemeService();
+ service.SetTheme(AppTheme.Light);
+ Assert.Equal(ThemeVariant.Light, Avalonia.Application.Current?.RequestedThemeVariant);
+
+ service.SetTheme(AppTheme.OledBlack);
+ Assert.Equal(ThemeVariant.Dark, Avalonia.Application.Current?.RequestedThemeVariant);
+
+ service.SetTheme(AppTheme.MatteCarbon);
+ Assert.Equal(ThemeVariant.Dark, Avalonia.Application.Current?.RequestedThemeVariant);
+ }
+
+ [Fact]
+ public void SettingsViewModel_ThemeIntegration_ChangesThemeOnPropertySet()
+ {
+ var mockThemeService = new Mock();
+ mockThemeService.SetupGet(t => t.CurrentTheme).Returns(AppTheme.MatteCarbon);
+
+ var vm = new SettingsViewModel(mockThemeService.Object);
+
+ Assert.Equal("Matte Carbon (Default)", vm.SelectedTheme);
+ Assert.Equal(3, vm.AvailableThemes.Count);
+
+ vm.SelectedTheme = "OLED Pure Black";
+ mockThemeService.Verify(t => t.SetTheme(AppTheme.OledBlack), Times.Once);
+
+ vm.SelectedTheme = "Clean Light";
+ mockThemeService.Verify(t => t.SetTheme(AppTheme.Light), Times.Once);
+
+ vm.SelectedTheme = "Matte Carbon (Default)";
+ mockThemeService.Verify(t => t.SetTheme(AppTheme.MatteCarbon), Times.Once);
+ }
+
+ [Fact]
+ public void SettingsViewModel_MappingHelpers_CoverAllThemeVariants()
+ {
+ Assert.Equal("Matte Carbon (Default)", SettingsViewModel.MapThemeToString(AppTheme.MatteCarbon));
+ Assert.Equal("OLED Pure Black", SettingsViewModel.MapThemeToString(AppTheme.OledBlack));
+ Assert.Equal("Clean Light", SettingsViewModel.MapThemeToString(AppTheme.Light));
+
+ Assert.Equal(AppTheme.MatteCarbon, SettingsViewModel.MapStringToTheme("Matte Carbon (Default)"));
+ Assert.Equal(AppTheme.OledBlack, SettingsViewModel.MapStringToTheme("OLED Pure Black"));
+ Assert.Equal(AppTheme.Light, SettingsViewModel.MapStringToTheme("Clean Light"));
+ Assert.Equal(AppTheme.MatteCarbon, SettingsViewModel.MapStringToTheme("unknown"));
+ Assert.Equal(AppTheme.MatteCarbon, SettingsViewModel.MapStringToTheme(null));
+ }
+}
diff --git a/LocalLLMServerManager.Web/App.axaml b/LocalLLMServerManager.Web/App.axaml
index a094a28..99c14c2 100644
--- a/LocalLLMServerManager.Web/App.axaml
+++ b/LocalLLMServerManager.Web/App.axaml
@@ -12,6 +12,6 @@
-
+
diff --git a/LocalLLMServerManager.Web/wwwroot/favicon.ico b/LocalLLMServerManager.Web/wwwroot/favicon.ico
new file mode 100644
index 0000000..f7f840c
Binary files /dev/null and b/LocalLLMServerManager.Web/wwwroot/favicon.ico differ
diff --git a/LocalLLMServerManager.Web/wwwroot/index.html b/LocalLLMServerManager.Web/wwwroot/index.html
index 1a61903..95d9fbb 100644
--- a/LocalLLMServerManager.Web/wwwroot/index.html
+++ b/LocalLLMServerManager.Web/wwwroot/index.html
@@ -3,7 +3,8 @@
- Local LLM Server Manager
+ LยณMยฒ โ Local LLM Server Manager
+