diff --git a/Flow.Launcher.Avalonia/ViewModel/SettingPages/PluginsSettingsViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/SettingPages/PluginsSettingsViewModel.cs index 14a96cc83..1225dd0d4 100644 --- a/Flow.Launcher.Avalonia/ViewModel/SettingPages/PluginsSettingsViewModel.cs +++ b/Flow.Launcher.Avalonia/ViewModel/SettingPages/PluginsSettingsViewModel.cs @@ -1,20 +1,35 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Plugin; -using Flow.Launcher.Avalonia.Views.Controls; +using System; +using Avalonia; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using AvaloniaControl = Avalonia.Controls.Control; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Avalonia.Resource; +using Flow.Launcher.Avalonia.Views.Controls; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.Image; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using FluentAvalonia.UI.Controls; namespace Flow.Launcher.Avalonia.ViewModel.SettingPages; public partial class PluginsSettingsViewModel : ObservableObject { + private readonly Settings _settings; + private readonly Internationalization _i18n; + public PluginsSettingsViewModel() { + _settings = Ioc.Default.GetRequiredService(); + _i18n = Ioc.Default.GetRequiredService(); + + LoadDisplayModes(); LoadPlugins(); } @@ -27,33 +42,170 @@ public partial class PluginsSettingsViewModel : ObservableObject public IEnumerable FilteredPlugins => string.IsNullOrWhiteSpace(SearchText) ? Plugins - : Plugins.Where(p => p.Name.Contains(SearchText, System.StringComparison.OrdinalIgnoreCase)); + : Plugins.Where(p => + p.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase) || + p.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase) || + p.ActionKeywordsText.Contains(SearchText, StringComparison.OrdinalIgnoreCase) + ); partial void OnSearchTextChanged(string value) => OnPropertyChanged(nameof(FilteredPlugins)); private void LoadPlugins() { var allPlugins = PluginManager.AllPlugins; - foreach (var plugin in allPlugins.OrderBy(p => p.Metadata.Name)) + foreach (var plugin in allPlugins.OrderBy(p => p.Metadata.Disabled).ThenBy(p => p.Metadata.Name)) { - Plugins.Add(new PluginItemViewModel(plugin)); + Plugins.Add(new PluginItemViewModel(plugin, _settings)); } } + + #region Display Mode + + public enum DisplayMode + { + OnOff, + Priority, + SearchDelay, + HomeOnOff + } + + public class DisplayModeItem + { + public DisplayMode Value { get; } + public string Display { get; } + + public DisplayModeItem(DisplayMode value, string display) + { + Value = value; + Display = display; + } + } + + [ObservableProperty] + private List _displayModes = new(); + + [ObservableProperty] + private DisplayModeItem? _selectedDisplayModeItem; + + partial void OnSelectedDisplayModeItemChanged(DisplayModeItem? value) + { + if (value != null) + UpdateDisplayModeFlags(value.Value); + } + + [ObservableProperty] + private bool _isOnOffSelected = true; + + [ObservableProperty] + private bool _isPrioritySelected; + + [ObservableProperty] + private bool _isSearchDelaySelected; + + [ObservableProperty] + private bool _isHomeOnOffSelected; + + private void LoadDisplayModes() + { + DisplayModes = new List + { + new(DisplayMode.OnOff, _i18n.GetTranslation("pluginDisplayOnOff")), + new(DisplayMode.Priority, _i18n.GetTranslation("pluginDisplayPriority")), + new(DisplayMode.SearchDelay, _i18n.GetTranslation("pluginDisplaySearchDelay")), + new(DisplayMode.HomeOnOff, _i18n.GetTranslation("pluginDisplayHomeOnOff")) + }; + + // Set default + SelectedDisplayModeItem = DisplayModes[0]; + } + + private void UpdateDisplayModeFlags(DisplayMode mode) + { + IsOnOffSelected = mode == DisplayMode.OnOff; + IsPrioritySelected = mode == DisplayMode.Priority; + IsSearchDelaySelected = mode == DisplayMode.SearchDelay; + IsHomeOnOffSelected = mode == DisplayMode.HomeOnOff; + } + + #endregion + + [RelayCommand] + private async Task OpenHelper(Control source) + { + var helpDialog = new ContentDialog + { + Title = _i18n.GetTranslation("flowlauncher_settings"), + Content = new StackPanel + { + Spacing = 10, + Children = + { + new TextBlock + { + Text = _i18n.GetTranslation("priority"), + FontSize = 18, + FontWeight = FontWeight.Bold, + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = _i18n.GetTranslation("priority_tips"), + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = _i18n.GetTranslation("searchDelay"), + FontSize = 18, + FontWeight = FontWeight.Bold, + Margin = new Thickness(0, 10, 0, 0), + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = _i18n.GetTranslation("searchDelayTimeTips"), + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = _i18n.GetTranslation("homeTitle"), + FontSize = 18, + FontWeight = FontWeight.Bold, + Margin = new Thickness(0, 10, 0, 0), + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = _i18n.GetTranslation("homeTips"), + TextWrapping = TextWrapping.Wrap + } + } + }, + PrimaryButtonText = _i18n.GetTranslation("commonOK"), + CloseButtonText = null + }; + + await helpDialog.ShowAsync(); + } } public partial class PluginItemViewModel : ObservableObject { private readonly PluginPair _plugin; + private readonly Settings _settings; private readonly ISettingProvider? _settingProvider; + private readonly Internationalization _i18n; - public PluginItemViewModel(PluginPair plugin) + public PluginItemViewModel(PluginPair plugin, Settings settings) { _plugin = plugin; + _settings = settings; + _i18n = Ioc.Default.GetRequiredService(); + + PluginSettingsObject = _settings.PluginSettings.GetPluginSettings(plugin.Metadata.ID); - // Check if plugin has settings - for JsonRPC plugins, also check NeedCreateSettingPanel() + // Initialize settings provider if (plugin.Plugin is ISettingProvider settingProvider) { - // JsonRPC plugins may not have settings even if they implement ISettingProvider if (plugin.Plugin is JsonRPCPluginBase jsonRpcPlugin) { if (jsonRpcPlugin.NeedCreateSettingPanel()) @@ -69,35 +221,149 @@ public partial class PluginItemViewModel : ObservableObject } } + // Initialize Avalonia settings if available if (HasSettings && _settingProvider != null) { try { - System.Console.WriteLine($"Checking Avalonia settings for {Name}"); AvaloniaSettingControl = _settingProvider.CreateSettingPanelAvalonia(); HasNativeAvaloniaSettings = AvaloniaSettingControl != null; - System.Console.WriteLine($"Avalonia settings for {Name}: {HasNativeAvaloniaSettings}"); } - catch (System.Exception ex) + catch (Exception ex) { - System.Console.WriteLine($"Failed to create Avalonia settings for {Name}: {ex}"); Flow.Launcher.Infrastructure.Logger.Log.Exception(nameof(PluginItemViewModel), $"Failed to create Avalonia settings for {Name}", ex); } } + + // Listen to metadata changes + _plugin.Metadata.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(PluginMetadata.AvgQueryTime)) + OnPropertyChanged(nameof(QueryTime)); + if (args.PropertyName == nameof(PluginMetadata.ActionKeywords)) + OnPropertyChanged(nameof(ActionKeywordsText)); + }; + + _ = LoadIconAsync(); } + public Infrastructure.UserSettings.Plugin PluginSettingsObject { get; } + + private async Task LoadIconAsync() + { + Icon = await Flow.Launcher.Avalonia.Helper.ImageLoader.LoadAsync(_plugin.Metadata.IcoPath); + } + + [ObservableProperty] + private IImage? _icon; + [ObservableProperty] private bool _hasSettings; [ObservableProperty] private bool _hasNativeAvaloniaSettings; + /// + /// True if plugin has settings but only WPF settings (no native Avalonia) + /// + public bool HasWpfOnlySettings => HasSettings && !HasNativeAvaloniaSettings; + [ObservableProperty] - private AvaloniaControl? _avaloniaSettingControl; + private Control? _avaloniaSettingControl; [ObservableProperty] private bool _isExpanded; + public string Name => _plugin.Metadata.Name; + public string Description => _plugin.Metadata.Description; + public string Author => _plugin.Metadata.Author; + public string Version => _plugin.Metadata.Version; + public string IconPath => _plugin.Metadata.IcoPath; + public string ID => _plugin.Metadata.ID; + + public string ActionKeywordsText => string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords); + + public string InitTime => $"{_plugin.Metadata.InitTime}ms"; + public string QueryTime => $"{_plugin.Metadata.AvgQueryTime}ms"; + + public bool IsDisabled + { + get => _plugin.Metadata.Disabled; + set + { + if (_plugin.Metadata.Disabled != value) + { + _plugin.Metadata.Disabled = value; + PluginSettingsObject.Disabled = value; + OnPropertyChanged(); + // Also update the inverse property for binding convenience + OnPropertyChanged(nameof(PluginState)); + } + } + } + + public bool PluginState + { + get => !IsDisabled; + set => IsDisabled = !value; + } + + public bool PluginHomeState + { + get => !_plugin.Metadata.HomeDisabled; + set + { + if (_plugin.Metadata.HomeDisabled != !value) + { + _plugin.Metadata.HomeDisabled = !value; + PluginSettingsObject.HomeDisabled = !value; + OnPropertyChanged(); + } + } + } + + public int Priority + { + get => _plugin.Metadata.Priority; + set + { + if (_plugin.Metadata.Priority != value) + { + _plugin.Metadata.Priority = value; + PluginSettingsObject.Priority = value; + OnPropertyChanged(); + } + } + } + + public double PluginSearchDelayTime + { + get => _plugin.Metadata.SearchDelayTime == null ? double.NaN : _plugin.Metadata.SearchDelayTime.Value; + set + { + if (double.IsNaN(value)) + { + _plugin.Metadata.SearchDelayTime = null; + PluginSettingsObject.SearchDelayTime = null; + } + else + { + _plugin.Metadata.SearchDelayTime = (int)value; + PluginSettingsObject.SearchDelayTime = (int)value; + } + OnPropertyChanged(); + OnPropertyChanged(nameof(SearchDelayTimeText)); + } + } + + public string SearchDelayTimeText => _plugin.Metadata.SearchDelayTime == null ? + _i18n.GetTranslation("default") : + _i18n.GetTranslation($"SearchDelayTime{_plugin.Metadata.SearchDelayTime}"); + + public bool SearchDelayEnabled => _settings.SearchQueryResultsWithDelay; + public string DefaultSearchDelay => _settings.SearchDelayTime.ToString(); + public bool HomeEnabled => _settings.ShowHomePage && PluginManager.IsHomePlugin(_plugin.Metadata.ID); + [RelayCommand] private void OpenSettings() { @@ -111,36 +377,90 @@ public partial class PluginItemViewModel : ObservableObject try { - // Create the WPF settings panel on demand + // Create the WPF settings panel and show in a standalone WPF window var settingsControl = _settingProvider.CreateSettingPanel(); if (settingsControl != null) { WpfSettingsWindow.Show(settingsControl, Name); } } - catch (System.Exception ex) + catch (Exception ex) { - // Log the error so we can diagnose issues - System.Diagnostics.Debug.WriteLine($"Failed to open settings for {Name}: {ex}"); Flow.Launcher.Infrastructure.Logger.Log.Exception(nameof(PluginItemViewModel), $"Failed to open settings for {Name}", ex); } } - public string Name => _plugin.Metadata.Name; - public string Description => _plugin.Metadata.Description; - public string Author => _plugin.Metadata.Author; - public string Version => _plugin.Metadata.Version; - public string IconPath => _plugin.Metadata.IcoPath; - - public bool IsDisabled + [RelayCommand] + private void OpenPluginDirectory() { - get => _plugin.Metadata.Disabled; - set + var directory = _plugin.Metadata.PluginDirectory; + if (!string.IsNullOrEmpty(directory)) + App.API.OpenDirectory(directory); + } + + [RelayCommand] + private void OpenSourceCodeLink() + { + if (!string.IsNullOrEmpty(_plugin.Metadata.Website)) + App.API.OpenUrl(_plugin.Metadata.Website); + } + + [RelayCommand] + private async Task OpenDeletePluginWindow() + { + // We need to implement a dialog for confirmation + var dialog = new ContentDialog { - if (_plugin.Metadata.Disabled != value) + Title = _i18n.GetTranslation("plugin_uninstall_title"), + Content = string.Format(_i18n.GetTranslation("plugin_uninstall_content"), Name), + PrimaryButtonText = _i18n.GetTranslation("yes"), + CloseButtonText = _i18n.GetTranslation("no") + }; + + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + await PluginInstaller.UninstallPluginAndCheckRestartAsync(_plugin.Metadata); + } + } + + [RelayCommand] + private async Task SetActionKeywords() + { + // Simple dialog to edit keywords + var textBox = new TextBox + { + Text = ActionKeywordsText, + AcceptsReturn = false + }; + + var dialog = new ContentDialog + { + Title = _i18n.GetTranslation("actionKeywords"), + Content = new StackPanel { - _plugin.Metadata.Disabled = value; - OnPropertyChanged(); + Spacing = 10, + Children = + { + new TextBlock { Text = _i18n.GetTranslation("actionKeywordsDescription") }, + textBox + } + }, + PrimaryButtonText = _i18n.GetTranslation("done"), + CloseButtonText = _i18n.GetTranslation("cancel") + }; + + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + var newKeywords = textBox.Text?.Split(Query.ActionKeywordSeparator, StringSplitOptions.RemoveEmptyEntries).Select(k => k.Trim()).ToList(); + if (newKeywords != null) + { + // Validate? + // For now just update + _plugin.Metadata.ActionKeywords = newKeywords; + PluginSettingsObject.ActionKeywords = newKeywords; + OnPropertyChanged(nameof(ActionKeywordsText)); } } } diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml index 6f0e2beec..db1114f97 100644 --- a/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml +++ b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml @@ -5,59 +5,165 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vm="using:Flow.Launcher.Avalonia.ViewModel.SettingPages" xmlns:i18n="using:Flow.Launcher.Avalonia.Resource" + xmlns:conv="using:Avalonia.Data.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" x:Class="Flow.Launcher.Avalonia.Views.SettingPages.PluginsSettingsPage" x:DataType="vm:PluginsSettingsViewModel"> - + + + + + + + + + + + + + + + + + + Watermark="{i18n:Localize searchplugin}" + Margin="0,0,0,10"> + + + + + - + - - - - - - - - - - - - - - - + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml.cs b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml.cs index 3355c66f1..da415efa1 100644 --- a/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml.cs +++ b/Flow.Launcher.Avalonia/Views/SettingPages/PluginsSettingsPage.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Interactivity; using Flow.Launcher.Avalonia.ViewModel.SettingPages; namespace Flow.Launcher.Avalonia.Views.SettingPages; @@ -10,4 +11,12 @@ public partial class PluginsSettingsPage : UserControl InitializeComponent(); DataContext = new PluginsSettingsViewModel(); } + + private void ClearSearchText_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is PluginsSettingsViewModel vm) + { + vm.SearchText = string.Empty; + } + } }