mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
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:
parent
d9bfe4995f
commit
0c2f026d01
3 changed files with 508 additions and 73 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue