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 @@ + + + L + 3 + M + 2 + 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 @@ - + - + - + @@ -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 @@ 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 +