diff --git a/App.axaml.cs b/App.axaml.cs index df82ee2..a436ff3 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -17,20 +17,16 @@ public override void Initialize() public static void SetThemeStyle(string styleName) { - if (Current == null) return; - try + if (styleName.Equals("semi", StringComparison.OrdinalIgnoreCase)) { - Current.Styles.Clear(); - if (styleName.Equals("semi", StringComparison.OrdinalIgnoreCase)) - { - Current.Styles.Add(new Semi.Avalonia.SemiTheme()); - } - else - { - Current.Styles.Add(new Avalonia.Themes.Fluent.FluentTheme()); - } + LocalLLMServerManager.Shared.Services.ThemeService.Instance.SetTheme( + LocalLLMServerManager.Shared.Services.AppTheme.MatteCarbon); + } + else if (styleName.Equals("fluent", StringComparison.OrdinalIgnoreCase)) + { + LocalLLMServerManager.Shared.Services.ThemeService.Instance.SetTheme( + LocalLLMServerManager.Shared.Services.AppTheme.OledBlack); } - catch { } } public override void OnFrameworkInitializationCompleted() diff --git a/LocalLLMServerManager.Shared/Services/ThemeService.cs b/LocalLLMServerManager.Shared/Services/ThemeService.cs index 73a970c..378710b 100644 --- a/LocalLLMServerManager.Shared/Services/ThemeService.cs +++ b/LocalLLMServerManager.Shared/Services/ThemeService.cs @@ -16,6 +16,11 @@ public class ThemeService : IThemeService public AppTheme CurrentTheme { get; private set; } = AppTheme.MatteCarbon; public event EventHandler? ThemeChanged; + public event Action? ThemeStyleChanging; + public event Action? ThemeStyleChanged; + + public void NotifyThemeStyleChanging() => ThemeStyleChanging?.Invoke(); + public void NotifyThemeStyleChanged() => ThemeStyleChanged?.Invoke(); public ThemeService(IResourceDictionary? resourceDictionary = null) { @@ -24,6 +29,12 @@ public ThemeService(IResourceDictionary? resourceDictionary = null) public void SetTheme(AppTheme theme) { + if (Application.Current != null && !Avalonia.Threading.Dispatcher.UIThread.CheckAccess()) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => SetTheme(theme)); + return; + } + CurrentTheme = theme; var resources = _resourceDictionary ?? Application.Current?.Resources; diff --git a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs index 542c31f..eaf9d67 100644 --- a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs @@ -439,14 +439,16 @@ public void SwitchThemeStyle(string style) { if (string.IsNullOrWhiteSpace(style)) return; SelectedThemeStyle = style; - try - { - var appType = Type.GetType("LocalLLMServerManager.App, LocalLLMServerManager"); - var method = appType?.GetMethod("SetThemeStyle", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - method?.Invoke(null, new object[] { style }); - ToastService.Instance.Show($"Switched theme to '{style.ToUpperInvariant()}' style.", ToastType.Info); - } - catch { } + ThemeService.Instance.NotifyThemeStyleChanging(); + + var appType = Type.GetType("LocalLLMServerManager.App, LocalLLMServerManager") + ?? Type.GetType("LocalLLMServerManager.App, LocalLLMServerManager.Web") + ?? Avalonia.Application.Current?.GetType(); + var method = appType?.GetMethod("SetThemeStyle", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + method?.Invoke(null, new object[] { style }); + + ThemeService.Instance.NotifyThemeStyleChanged(); + ToastService.Instance.Show($"Switched theme to '{style.ToUpperInvariant()}' style.", ToastType.Info); } [RelayCommand] diff --git a/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs b/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs index 885cc60..7d12435 100644 --- a/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs +++ b/LocalLLMServerManager.Tests/AvaloniaHeadlessInteractionTests.cs @@ -370,5 +370,315 @@ public void OllamaModelsTabControl_DeleteButton_OpensStyledConfirmationModal() window.Close(); } + + [AvaloniaFact] + public void SettingsTabControl_ThemeSwitching_DoesNotCrashUI() + { + var vm = new MainViewModel(); + var view = new MainView { DataContext = vm }; + var window = new Window { Content = view, Width = 1024, Height = 768 }; + window.Show(); + + // Switch to Settings Tab + var tabControl = view.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(tabControl); + tabControl.SelectedIndex = 5; + Avalonia.Threading.Dispatcher.UIThread.RunJobs(); + + var settingsControl = view.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(settingsControl); + + // 1. Test Theme Palette ComboBox UI element + var comboBoxes = settingsControl.GetVisualDescendants().OfType().ToList(); + var themeComboBox = comboBoxes.FirstOrDefault(c => c.ItemsSource == vm.Settings.AvailableThemes); + Assert.NotNull(themeComboBox); + + themeComboBox.SelectedItem = "Clean Light"; + Avalonia.Threading.Dispatcher.UIThread.RunJobs(); + Assert.Equal(AppTheme.Light, ThemeService.Instance.CurrentTheme); + + themeComboBox.SelectedItem = "OLED Pure Black"; + Avalonia.Threading.Dispatcher.UIThread.RunJobs(); + Assert.Equal(AppTheme.OledBlack, ThemeService.Instance.CurrentTheme); + + themeComboBox.SelectedItem = "Matte Carbon (Default)"; + Avalonia.Threading.Dispatcher.UIThread.RunJobs(); + Assert.Equal(AppTheme.MatteCarbon, ThemeService.Instance.CurrentTheme); + + // 2. Test Framework Theme Style switching via UI buttons + var buttons = settingsControl.GetVisualDescendants().OfType