Implement comprehensive Plugins Settings page in Avalonia

- Add plugin search with filtering across name, description, and action keywords
- Implement display mode switcher (On/Off, Priority, Search Delay, Home On/Off)
- Add plugin management controls (enable/disable, priority, search delays, home visibility)
- Integrate both native Avalonia settings and WPF fallback support
- Add action keywords editing dialog
- Include plugin directory access, source code links, and uninstall functionality
- Add help dialog explaining priority, search delay, and home features
- Improve UI with FluentAvalonia controls and proper layout
- Load plugin icons asynchronously and display plugin metrics (init time, query time)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Hongtao Zhang 2026-01-26 14:19:30 -08:00
parent d9bfe4995f
commit 0c2f026d01
3 changed files with 508 additions and 73 deletions

View file

@ -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<Settings>();
_i18n = Ioc.Default.GetRequiredService<Internationalization>();
LoadDisplayModes();
LoadPlugins();
}
@ -27,33 +42,170 @@ public partial class PluginsSettingsViewModel : ObservableObject
public IEnumerable<PluginItemViewModel> 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<DisplayModeItem> _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<DisplayModeItem>
{
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<Internationalization>();
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;
/// <summary>
/// True if plugin has settings but only WPF settings (no native Avalonia)
/// </summary>
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));
}
}
}

View file

@ -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">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock Grid.Row="0" Text="{i18n:Localize plugin}" FontSize="28" FontWeight="SemiBold" Margin="0,0,0,10" />
<!-- Header -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,10">
<TextBlock Text="{i18n:Localize plugins}" FontSize="28" FontWeight="SemiBold" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
<TextBlock Text="{i18n:Localize FilterComboboxLabel}" VerticalAlignment="Center" Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
<ComboBox ItemsSource="{Binding DisplayModes}"
SelectedItem="{Binding SelectedDisplayModeItem, Mode=TwoWay}"
MinWidth="150">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:PluginsSettingsViewModel+DisplayModeItem">
<TextBlock Text="{Binding Display}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Command="{Binding OpenHelperCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
ToolTip.Tip="{i18n:Localize flowlauncher_settings}">
<ui:SymbolIcon Symbol="Help" FontSize="16"/>
</Button>
</StackPanel>
</Grid>
<!-- Search Box -->
<TextBox Grid.Row="1"
x:Name="SearchTextBox"
Text="{Binding SearchText, Mode=TwoWay}"
Watermark="{i18n:Localize search}"
Margin="0,0,0,20" />
Watermark="{i18n:Localize searchplugin}"
Margin="0,0,0,10">
<TextBox.InnerRightContent>
<Button Theme="{StaticResource TransparentButton}"
IsVisible="{Binding SearchText, Converter={x:Static conv:StringConverters.IsNotNullOrEmpty}}"
Click="ClearSearchText_Click">
<ui:SymbolIcon Symbol="Dismiss" FontSize="12"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<!-- Plugin List -->
<ScrollViewer Grid.Row="2">
<ItemsControl ItemsSource="{Binding FilteredPlugins}"
Background="Transparent">
<ItemsControl ItemsSource="{Binding FilteredPlugins}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PluginItemViewModel">
<StackPanel Spacing="5" Margin="0,0,10,0">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,5">
<Image Grid.Column="0" Source="{Binding IconPath}" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Center" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Version}" Foreground="Gray" FontSize="12" VerticalAlignment="Bottom" />
</StackPanel>
<TextBlock Text="{Binding Description}" Foreground="Gray" FontSize="12" TextWrapping="Wrap" />
<TextBlock Text="{Binding Author}" Foreground="LightGray" FontSize="11" />
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<Button Command="{Binding OpenSettingsCommand}"
IsVisible="{Binding HasSettings}"
ToolTip.Tip="Settings">
<ui:SymbolIcon Symbol="Settings" />
</Button>
<Expander HorizontalAlignment="Stretch" Margin="0,0,10,10" IsExpanded="{Binding IsExpanded}">
<Expander.Styles>
<Style Selector="Expander">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<Style Selector="Expander /template/ ToggleButton#ExpanderHeader">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</Expander.Styles>
<Expander.Header>
<Grid ColumnDefinitions="Auto,*,Auto" HorizontalAlignment="Stretch">
<!-- Icon -->
<Image Grid.Column="0" Source="{Binding Icon}" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Center"
RenderOptions.BitmapInterpolationMode="HighQuality" />
<ToggleSwitch IsChecked="{Binding !IsDisabled}"
OnContent="" OffContent="" />
</StackPanel>
</Grid>
<!-- Info -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Description}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" FontSize="12" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" MaxLines="1"/>
</StackPanel>
<!-- Conditional Controls -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="10,0,0,0" HorizontalAlignment="Right">
<!-- On/Off -->
<ToggleSwitch IsChecked="{Binding PluginState}"
OnContent="" OffContent=""
IsVisible="{Binding $parent[UserControl].((vm:PluginsSettingsViewModel)DataContext).IsOnOffSelected, FallbackValue=True}"/>
<!-- Priority -->
<StackPanel Orientation="Horizontal" Spacing="10"
IsVisible="{Binding $parent[UserControl].((vm:PluginsSettingsViewModel)DataContext).IsPrioritySelected, FallbackValue=False}">
<TextBlock Text="{i18n:Localize priority}" VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding Priority}" SpinButtonPlacementMode="Inline" Width="100" SmallChange="1" LargeChange="10"/>
</StackPanel>
<!-- Search Delay -->
<StackPanel Orientation="Horizontal" Spacing="10"
IsVisible="{Binding $parent[UserControl].((vm:PluginsSettingsViewModel)DataContext).IsSearchDelaySelected, FallbackValue=False}">
<TextBlock Text="{i18n:Localize searchDelay}" VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding PluginSearchDelayTime}" SpinButtonPlacementMode="Inline" Width="100" Minimum="0" SmallChange="10" LargeChange="100"/>
</StackPanel>
<!-- Home On/Off -->
<ToggleSwitch IsChecked="{Binding PluginHomeState}"
OnContent="" OffContent=""
IsVisible="{Binding $parent[UserControl].((vm:PluginsSettingsViewModel)DataContext).IsHomeOnOffSelected, FallbackValue=False}"/>
</StackPanel>
</Grid>
</Expander.Header>
<!-- Native Avalonia Settings Panel -->
<Border IsVisible="{Binding IsExpanded}"
Background="{DynamicResource SolidBackgroundFillColorBase}"
BorderBrush="{DynamicResource ControlElevationBorderBrush}"
BorderThickness="1"
CornerRadius="4"
Padding="10"
Margin="48,0,0,10">
<ContentControl Content="{Binding AvaloniaSettingControl}" />
</Border>
</StackPanel>
<Expander.Content>
<StackPanel Spacing="15">
<!-- Action Keywords -->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="5">
<TextBlock Text="{i18n:Localize actionKeywords}" FontWeight="Bold"/>
<TextBlock Text="{Binding ActionKeywordsText}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
</StackPanel>
<Button Grid.Column="1" Content="{i18n:Localize edit}" Command="{Binding SetActionKeywordsCommand}"/>
</Grid>
<Separator/>
<!-- Settings Panel -->
<ContentControl Content="{Binding AvaloniaSettingControl}"
IsVisible="{Binding HasNativeAvaloniaSettings}"
HorizontalContentAlignment="Stretch"/>
<StackPanel Orientation="Horizontal" Spacing="10"
IsVisible="{Binding HasWpfOnlySettings}">
<ui:SymbolIcon Symbol="Open" FontSize="14" Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
<TextBlock Text="{i18n:Localize plugin_settings_open_in_wpf_window}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
FontStyle="Italic"
VerticalAlignment="Center"/>
<Button Command="{Binding OpenSettingsCommand}"
Content="{i18n:Localize openSettings}"/>
</StackPanel>
<Separator/>
<!-- Footer -->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Orientation="Horizontal" Spacing="15" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="5">
<ui:SymbolIcon Symbol="Contact" FontSize="14"/>
<TextBlock Text="{Binding Author}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<ui:SymbolIcon Symbol="Tag" FontSize="14"/>
<TextBlock Text="{Binding Version}" />
</StackPanel>
<TextBlock Text="{Binding InitTime}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" FontSize="12" VerticalAlignment="Center"/>
<TextBlock Text="{Binding QueryTime}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" FontSize="12" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
<Button Command="{Binding OpenSourceCodeLinkCommand}" ToolTip.Tip="{i18n:Localize website}">
<ui:SymbolIcon Symbol="Globe"/>
</Button>
<Button Command="{Binding OpenPluginDirectoryCommand}" ToolTip.Tip="{i18n:Localize plugin_open_plugin_directory}">
<ui:SymbolIcon Symbol="Folder"/>
</Button>
<Button Command="{Binding OpenDeletePluginWindowCommand}" ToolTip.Tip="{i18n:Localize plugin_uninstall}">
<ui:SymbolIcon Symbol="Delete"/>
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Expander.Content>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

View file

@ -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;
}
}
}