Migrate Program plugin settings to Avalonia and fix scroll issues

- Update ISettingProvider to support CreateSettingPanelAvalonia
- Create native Avalonia settings UI for Program Plugin (ProgramSetting.axaml)
- Implement ProgramSettingViewModel using CommunityToolkit.Mvvm
- Update PluginsSettingsViewModel to detect and load native Avalonia settings
- Replace ListBox with ItemsControl in PluginsSettingsPage to fix auto-scroll issues when expanding settings
- Align Avalonia package versions (11.2.3) across projects to fix type load errors
This commit is contained in:
Hongtao Zhang 2026-01-19 00:06:36 -08:00
parent fb9721c7f2
commit dda587493b
13 changed files with 719 additions and 38 deletions

View file

@ -7,7 +7,7 @@ using Flow.Launcher.Avalonia.Views.Controls;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Controls;
using AvaloniaControl = Avalonia.Controls.Control;
namespace Flow.Launcher.Avalonia.ViewModel.SettingPages;
@ -68,16 +68,47 @@ public partial class PluginItemViewModel : ObservableObject
HasSettings = true;
}
}
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)
{
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);
}
}
}
[ObservableProperty]
private bool _hasSettings;
[ObservableProperty]
private bool _hasNativeAvaloniaSettings;
[ObservableProperty]
private AvaloniaControl? _avaloniaSettingControl;
[ObservableProperty]
private bool _isExpanded;
[RelayCommand]
private void OpenSettings()
{
if (_settingProvider == null) return;
if (HasNativeAvaloniaSettings)
{
IsExpanded = !IsExpanded;
return;
}
try
{
// Create the WPF settings panel on demand

View file

@ -17,37 +17,50 @@
Watermark="{i18n:Localize search}"
Margin="0,0,0,20" />
<ListBox Grid.Row="2"
ItemsSource="{Binding FilteredPlugins}"
Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:PluginItemViewModel">
<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>
<ScrollViewer Grid.Row="2">
<ItemsControl ItemsSource="{Binding FilteredPlugins}"
Background="Transparent">
<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" />
<ToggleSwitch IsChecked="{Binding !IsDisabled}"
OnContent="" OffContent="" />
</StackPanel>
</Grid>
<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>
<ToggleSwitch IsChecked="{Binding !IsDisabled}"
OnContent="" OffContent="" />
</StackPanel>
</Grid>
<!-- 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>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View file

@ -61,6 +61,26 @@
"System.IO.Pipelines": "8.0.0"
}
},
"Avalonia": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "pD6woFAUfGcyEvMmrpctntU4jv4fT8752pfx1J5iRORVX3Ob0oQi8PWo0TXVaAJZiSfH0cdKTeKx0w0DzD0/mg==",
"dependencies": {
"Avalonia.BuildServices": "0.0.29",
"Avalonia.Remote.Protocol": "11.2.3",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "0.0.29",
"contentHash": "U4eJLQdoDNHXtEba7MZUCwrBErBTxFp6sUewXBOdAhU0Kwzwaa/EKFcYm8kpcysjzKtfB4S0S9n0uxKZFz/ikw=="
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "6V0aNtld48WmO8tAlWwlRlUmXYcOWv+1eJUSl1ETF+1blUe5yhcSmuWarPprO0hDk8Ta6wGfdfcrnVl2gITYcA=="
},
"Ben.Demystifier": {
"type": "Transitive",
"resolved": "0.4.1",
@ -127,6 +147,11 @@
"resolved": "2.5.192",
"contentHash": "jaJuwcgovWIZ8Zysdyf3b7b34/BrADw4v82GaEZymUhDd3ScMPrYd/cttekeDteJJPXseJxp04yTIcxiVUjTWg=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.6.3",
@ -1161,7 +1186,7 @@
"Ben.Demystifier": "[0.4.1, )",
"BitFaster.Caching": "[2.5.4, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Plugin": "[5.0.0, )",
"Flow.Launcher.Plugin": "[5.1.0, )",
"InputSimulator": "[1.0.4, )",
"MemoryPack": "[1.21.4, )",
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
@ -1176,6 +1201,7 @@
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.2.3, )",
"JetBrains.Annotations": "[2025.2.2, )"
}
}

View file

@ -121,6 +121,26 @@
"resolved": "3.1.0.3",
"contentHash": "VKcf8sUq/+LyY99WgLhOu7Q32ROEyR30/2xCCj9ADRi45wVC7kpXrYCf9vH1qirkmrIfpL8inoxAbrqAlfXxsQ=="
},
"Avalonia": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "pD6woFAUfGcyEvMmrpctntU4jv4fT8752pfx1J5iRORVX3Ob0oQi8PWo0TXVaAJZiSfH0cdKTeKx0w0DzD0/mg==",
"dependencies": {
"Avalonia.BuildServices": "0.0.29",
"Avalonia.Remote.Protocol": "11.2.3",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "0.0.29",
"contentHash": "U4eJLQdoDNHXtEba7MZUCwrBErBTxFp6sUewXBOdAhU0Kwzwaa/EKFcYm8kpcysjzKtfB4S0S9n0uxKZFz/ikw=="
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "6V0aNtld48WmO8tAlWwlRlUmXYcOWv+1eJUSl1ETF+1blUe5yhcSmuWarPprO0hDk8Ta6wGfdfcrnVl2gITYcA=="
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2025.2.2",
@ -136,6 +156,11 @@
"resolved": "1.21.4",
"contentHash": "g14EsSS85yn0lHTi0J9ivqlZMf09A2iI51fmI+0KkzIzyCbWOBWPi5mdaY7YWmXprk12aYh9u/qfWHQUYthlwg=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Transitive",
"resolved": "17.14.15",
@ -190,6 +215,7 @@
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.2.3, )",
"JetBrains.Annotations": "[2025.2.2, )"
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
@ -68,6 +68,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="Fody" Version="6.9.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -1,4 +1,5 @@
using System.Windows.Controls;
using System.Windows.Controls;
using AvaloniaControl = Avalonia.Controls.Control;
namespace Flow.Launcher.Plugin
{
@ -12,5 +13,12 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <returns></returns>
Control CreateSettingPanel();
/// <summary>
/// Create settings panel control for Avalonia version
/// </summary>
/// <returns></returns>
virtual AvaloniaControl CreateSettingPanelAvalonia() => null;
}
}

View file

@ -2,6 +2,17 @@
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"Avalonia": {
"type": "Direct",
"requested": "[11.2.3, )",
"resolved": "11.2.3",
"contentHash": "pD6woFAUfGcyEvMmrpctntU4jv4fT8752pfx1J5iRORVX3Ob0oQi8PWo0TXVaAJZiSfH0cdKTeKx0w0DzD0/mg==",
"dependencies": {
"Avalonia.BuildServices": "0.0.29",
"Avalonia.Remote.Protocol": "11.2.3",
"MicroCom.Runtime": "0.11.0"
}
},
"Fody": {
"type": "Direct",
"requested": "[6.9.3, )",
@ -44,6 +55,21 @@
"Fody": "6.6.4"
}
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "0.0.29",
"contentHash": "U4eJLQdoDNHXtEba7MZUCwrBErBTxFp6sUewXBOdAhU0Kwzwaa/EKFcYm8kpcysjzKtfB4S0S9n0uxKZFz/ikw=="
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "6V0aNtld48WmO8tAlWwlRlUmXYcOWv+1eJUSl1ETF+1blUe5yhcSmuWarPprO0hDk8Ta6wGfdfcrnVl2gITYcA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",

View file

@ -163,6 +163,26 @@
"resolved": "6.3.0.90",
"contentHash": "WVTb5MxwGqKdeasd3nG5udlV4t6OpvkFanziwI133K0/QJ5FvZmfzRQgpAjGTJhQfIA8GP7AzKQ3sTY9JOFk8Q=="
},
"Avalonia": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "pD6woFAUfGcyEvMmrpctntU4jv4fT8752pfx1J5iRORVX3Ob0oQi8PWo0TXVaAJZiSfH0cdKTeKx0w0DzD0/mg==",
"dependencies": {
"Avalonia.BuildServices": "0.0.29",
"Avalonia.Remote.Protocol": "11.2.3",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "0.0.29",
"contentHash": "U4eJLQdoDNHXtEba7MZUCwrBErBTxFp6sUewXBOdAhU0Kwzwaa/EKFcYm8kpcysjzKtfB4S0S9n0uxKZFz/ikw=="
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "6V0aNtld48WmO8tAlWwlRlUmXYcOWv+1eJUSl1ETF+1blUe5yhcSmuWarPprO0hDk8Ta6wGfdfcrnVl2gITYcA=="
},
"Ben.Demystifier": {
"type": "Transitive",
"resolved": "0.4.1",
@ -252,6 +272,11 @@
"resolved": "3.4.5",
"contentHash": "R/d2VSdbRQHDWfPf5sjvMOWrWvxh/CswdMDKODppHTjsDLIkLQbQrnjmtAaVqu0qgUf8KFlVzEfxy3GIVoCK9g=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "9.0.9",
@ -1599,7 +1624,7 @@
"Droplex": "[1.7.0, )",
"FSharp.Core": "[9.0.303, )",
"Flow.Launcher.Infrastructure": "[1.0.0, )",
"Flow.Launcher.Plugin": "[5.0.0, )",
"Flow.Launcher.Plugin": "[5.1.0, )",
"Meziantou.Framework.Win32.Jobs": "[3.4.5, )",
"Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )",
"SemanticVersioning": "[3.0.0, )",
@ -1613,7 +1638,7 @@
"Ben.Demystifier": "[0.4.1, )",
"BitFaster.Caching": "[2.5.4, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Plugin": "[5.0.0, )",
"Flow.Launcher.Plugin": "[5.1.0, )",
"InputSimulator": "[1.0.4, )",
"MemoryPack": "[1.21.4, )",
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
@ -1628,6 +1653,7 @@
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.2.3, )",
"JetBrains.Annotations": "[2025.2.2, )"
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -62,6 +62,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="MemoryPack" Version="1.21.4" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -12,11 +12,13 @@ using Flow.Launcher.Plugin.Program.Views.Models;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.Extensions.Caching.Memory;
using Path = System.IO.Path;
using AvaloniaControl = Avalonia.Controls.Control;
namespace Flow.Launcher.Plugin.Program
{
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable
{
private static readonly string ClassName = nameof(Main);
private const string Win32CacheName = "Win32";
@ -417,6 +419,14 @@ namespace Flow.Launcher.Plugin.Program
return new ProgramSetting(Context, _settings);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
System.Console.WriteLine("Program plugin: CreateSettingPanelAvalonia called!");
Context.API.LogInfo(ClassName, "Creating Avalonia setting panel");
return new Views.Avalonia.ProgramSetting(Context, _settings);
}
public string GetTranslatedPluginTitle()
{
return Context.API.GetTranslation("flowlauncher_plugin_program_plugin_name");

View file

@ -0,0 +1,337 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Plugin.Program.Views.Models;
using Flow.Launcher.Plugin.Program.Views.Commands;
using Flow.Launcher.Plugin.Program.Programs;
using System.IO;
using System.Windows;
using Flow.Launcher.Plugin.Program.Views;
using Avalonia.Data.Converters;
using Avalonia.Media;
using System.Globalization;
namespace Flow.Launcher.Plugin.Program.ViewModels;
public partial class ProgramSettingViewModel : ObservableObject
{
public static IValueConverter EnabledToColorConverter { get; } = new FuncValueConverter<bool, IBrush>(enabled =>
enabled ? Brushes.Green : Brushes.Red);
public static IValueConverter IsNotNullConverter { get; } = new FuncValueConverter<object?, bool>(obj => obj != null);
private readonly PluginInitContext _context;
private readonly Settings _settings;
[ObservableProperty]
private bool _enableUWP;
[ObservableProperty]
private bool _enableStartMenuSource;
[ObservableProperty]
private bool _enableRegistrySource;
[ObservableProperty]
private bool _enablePATHSource;
[ObservableProperty]
private bool _hideAppsPath;
[ObservableProperty]
private bool _hideUninstallers;
[ObservableProperty]
private bool _enableDescription;
[ObservableProperty]
private bool _hideDuplicatedWindowsApp;
public bool ShowUWPCheckbox => UWPPackage.SupportUWP();
[ObservableProperty]
private ObservableCollection<ProgramSource> _programSources = new();
[ObservableProperty]
private ProgramSource? _selectedProgramSource;
[ObservableProperty]
private bool _isIndexing;
[ObservableProperty]
private string _customSuffixes;
[ObservableProperty]
private string _customProtocols;
public ProgramSettingViewModel(PluginInitContext context, Settings settings)
{
_context = context;
_settings = settings;
_enableUWP = _settings.EnableUWP;
_enableStartMenuSource = _settings.EnableStartMenuSource;
_enableRegistrySource = _settings.EnableRegistrySource;
_enablePATHSource = _settings.EnablePathSource;
_hideAppsPath = _settings.HideAppsPath;
_hideUninstallers = _settings.HideUninstallers;
_enableDescription = _settings.EnableDescription;
_hideDuplicatedWindowsApp = _settings.HideDuplicatedWindowsApp;
_customSuffixes = string.Join(Settings.SuffixSeparator, _settings.CustomSuffixes);
_customProtocols = string.Join(Settings.SuffixSeparator, _settings.CustomProtocols);
LoadProgramSources();
}
partial void OnCustomSuffixesChanged(string value)
{
_settings.CustomSuffixes = value.Split(Settings.SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
Main.ResetCache();
}
partial void OnCustomProtocolsChanged(string value)
{
_settings.CustomProtocols = value.Split(Settings.SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
Main.ResetCache();
}
public bool AppRefMS
{
get => _settings.BuiltinSuffixesStatus["appref-ms"];
set
{
_settings.BuiltinSuffixesStatus["appref-ms"] = value;
OnPropertyChanged();
Main.ResetCache();
}
}
public bool Exe
{
get => _settings.BuiltinSuffixesStatus["exe"];
set
{
_settings.BuiltinSuffixesStatus["exe"] = value;
OnPropertyChanged();
Main.ResetCache();
}
}
public bool Lnk
{
get => _settings.BuiltinSuffixesStatus["lnk"];
set
{
_settings.BuiltinSuffixesStatus["lnk"] = value;
OnPropertyChanged();
Main.ResetCache();
}
}
public bool Steam
{
get => _settings.BuiltinProtocolsStatus["steam"];
set
{
_settings.BuiltinProtocolsStatus["steam"] = value;
OnPropertyChanged();
Main.ResetCache();
}
}
public bool Epic
{
get => _settings.BuiltinProtocolsStatus["epic"];
set
{
_settings.BuiltinProtocolsStatus["epic"] = value;
OnPropertyChanged();
Main.ResetCache();
}
}
public bool Http
{
get => _settings.BuiltinProtocolsStatus["http"];
set
{
_settings.BuiltinProtocolsStatus["http"] = value;
OnPropertyChanged();
Main.ResetCache();
}
}
public bool UseCustomSuffixes
{
get => _settings.UseCustomSuffixes;
set
{
_settings.UseCustomSuffixes = value;
OnPropertyChanged();
Main.ResetCache();
}
}
public bool UseCustomProtocols
{
get => _settings.UseCustomProtocols;
set
{
_settings.UseCustomProtocols = value;
OnPropertyChanged();
Main.ResetCache();
}
}
private void LoadProgramSources()
{
// For Avalonia, we want to maintain the same display list if possible,
// but for now let's just use a fresh one from settings
var sources = ProgramSettingDisplay.LoadProgramSources();
ProgramSources = new ObservableCollection<ProgramSource>(sources);
// We need to set the static list for ProgramSettingDisplay to work
ProgramSetting.ProgramSettingDisplayList = sources;
}
[RelayCommand]
private async Task Reindex()
{
IsIndexing = true;
await Main.IndexProgramsAsync();
IsIndexing = false;
}
[RelayCommand]
private async Task LoadAllPrograms()
{
await ProgramSettingDisplay.DisplayAllProgramsAsync();
// Refresh the observable collection
ProgramSources = new ObservableCollection<ProgramSource>(ProgramSetting.ProgramSettingDisplayList);
}
[RelayCommand]
private async Task ToggleStatus()
{
if (SelectedProgramSource == null) return;
var status = !SelectedProgramSource.Enabled;
await ProgramSettingDisplay.SetProgramSourcesStatusAsync(new List<ProgramSource> { SelectedProgramSource }, status);
if (status)
ProgramSettingDisplay.RemoveDisabledFromSettings();
else
ProgramSettingDisplay.StoreDisabledInSettings();
if (await new List<ProgramSource> { SelectedProgramSource }.IsReindexRequiredAsync())
await Reindex();
// Trigger UI update for the item
OnPropertyChanged(nameof(ProgramSources));
}
[RelayCommand]
private async Task DeleteSource()
{
if (SelectedProgramSource == null) return;
// Check if it's user added
if (!_settings.ProgramSources.Any(x => x.UniqueIdentifier == SelectedProgramSource.UniqueIdentifier))
{
_context.API.ShowMsgBox(_context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source_select_user_added"));
return;
}
if (_context.API.ShowMsgBox(_context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source"),
string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{
return;
}
var toDelete = SelectedProgramSource;
_settings.ProgramSources.RemoveAll(x => x.UniqueIdentifier == toDelete.UniqueIdentifier);
ProgramSetting.ProgramSettingDisplayList.Remove(toDelete);
ProgramSources.Remove(toDelete);
if (await new List<ProgramSource> { toDelete }.IsReindexRequiredAsync())
await Reindex();
}
[RelayCommand]
private async Task AddSource()
{
var dialog = new System.Windows.Forms.FolderBrowserDialog();
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
var path = dialog.SelectedPath;
if (ProgramSources.Any(x => x.UniqueIdentifier.Equals(path, StringComparison.OrdinalIgnoreCase)))
{
_context.API.ShowMsgBox(_context.API.GetTranslation("flowlauncher_plugin_program_duplicate_program_source"));
return;
}
var source = new ProgramSource(path, true);
_settings.ProgramSources.Add(source);
ProgramSetting.ProgramSettingDisplayList.Add(source);
ProgramSources.Add(source);
await Reindex();
}
}
partial void OnEnableUWPChanged(bool value)
{
_settings.EnableUWP = value;
_ = Reindex();
}
partial void OnEnableStartMenuSourceChanged(bool value)
{
_settings.EnableStartMenuSource = value;
_ = Reindex();
}
partial void OnEnableRegistrySourceChanged(bool value)
{
_settings.EnableRegistrySource = value;
_ = Reindex();
}
partial void OnEnablePATHSourceChanged(bool value)
{
_settings.EnablePathSource = value;
_ = Reindex();
}
partial void OnHideAppsPathChanged(bool value)
{
_settings.HideAppsPath = value;
Main.ResetCache();
}
partial void OnHideUninstallersChanged(bool value)
{
_settings.HideUninstallers = value;
Main.ResetCache();
}
partial void OnEnableDescriptionChanged(bool value)
{
_settings.EnableDescription = value;
Main.ResetCache();
}
partial void OnHideDuplicatedWindowsAppChanged(bool value)
{
_settings.HideDuplicatedWindowsApp = value;
Main.ResetCache();
}
}

View file

@ -0,0 +1,150 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="using:Flow.Launcher.Plugin.Program.ViewModels"
xmlns:models="using:Flow.Launcher.Plugin.Program.Views.Models"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Flow.Launcher.Plugin.Program.Views.Avalonia.ProgramSetting"
x:DataType="vm:ProgramSettingViewModel">
<StackPanel Spacing="10" Margin="0,0,10,0">
<ui:SettingsExpander Header="Index Sources"
IconSource="Library"
Description="Configure which sources to scan for programs">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="Start Menu">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding EnableStartMenuSource}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Registry">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding EnableRegistrySource}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="PATH">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding EnablePATHSource}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="UWP Apps" IsVisible="{Binding ShowUWPCheckbox}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding EnableUWP}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<ui:SettingsExpander Header="Program Options"
IconSource="Settings"
Description="General behavior for program search">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="Hide App Path">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding HideAppsPath}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Hide Uninstallers">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding HideUninstallers}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Enable Description">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding EnableDescription}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Hide Duplicated Windows App">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding HideDuplicatedWindowsApp}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<ui:SettingsExpander Header="Advanced Options"
IconSource="Repair"
Description="Configure file suffixes and protocols">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="Executable Suffixes">
<StackPanel Orientation="Horizontal" Spacing="10">
<CheckBox Content="appref-ms" IsChecked="{Binding AppRefMS}" />
<CheckBox Content="exe" IsChecked="{Binding Exe}" />
<CheckBox Content="lnk" IsChecked="{Binding Lnk}" />
</StackPanel>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Custom Suffixes">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="5">
<CheckBox IsChecked="{Binding UseCustomSuffixes}" />
<TextBox Text="{Binding CustomSuffixes}" Width="200" IsEnabled="{Binding UseCustomSuffixes}" Watermark="e.g. py;bat" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Protocols">
<StackPanel Orientation="Horizontal" Spacing="10">
<CheckBox Content="Steam" IsChecked="{Binding Steam}" />
<CheckBox Content="Epic" IsChecked="{Binding Epic}" />
<CheckBox Content="Http" IsChecked="{Binding Http}" />
</StackPanel>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Custom Protocols">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="5">
<CheckBox IsChecked="{Binding UseCustomProtocols}" />
<TextBox Text="{Binding CustomProtocols}" Width="200" IsEnabled="{Binding UseCustomProtocols}" Watermark="e.g. obsidian;vscode" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<ui:SettingsExpander Header="Program Sources"
IconSource="Folder"
Description="Manage custom indexing locations">
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button Content="Reindex" Command="{Binding ReindexCommand}" />
<Button Content="Load All" Command="{Binding LoadAllProgramsCommand}" />
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsIndexing}" VerticalAlignment="Center" Width="80" />
</StackPanel>
</ui:SettingsExpander.Footer>
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*" MinHeight="250">
<ListBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding ProgramSources}"
SelectedItem="{Binding SelectedProgramSource}"
Background="{DynamicResource CardBackgroundFillColorDefault}"
BorderBrush="{DynamicResource ControlStrokeColorDefault}"
BorderThickness="1"
CornerRadius="4">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:ProgramSource">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center" Margin="5">
<TextBlock Text="{Binding Name}" FontWeight="Medium" />
<TextBlock Text="{Binding Location}" FontSize="12" Foreground="Gray" TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock Grid.Column="1"
Text="{Binding Enabled}"
VerticalAlignment="Center"
Margin="10,0"
Foreground="{Binding Enabled, Converter={x:Static vm:ProgramSettingViewModel.EnabledToColorConverter}}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Spacing="5" HorizontalAlignment="Right" Margin="0,0,0,5">
<Button Content="Add" Command="{Binding AddSourceCommand}" />
<Button Content="Toggle Status" Command="{Binding ToggleStatusCommand}" IsEnabled="{Binding SelectedProgramSource, Converter={x:Static vm:ProgramSettingViewModel.IsNotNullConverter}}" />
<Button Content="Delete" Command="{Binding DeleteSourceCommand}" IsEnabled="{Binding SelectedProgramSource, Converter={x:Static vm:ProgramSettingViewModel.IsNotNullConverter}}" />
</StackPanel>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Flow.Launcher.Plugin.Program.ViewModels;
namespace Flow.Launcher.Plugin.Program.Views.Avalonia;
public partial class ProgramSetting : UserControl
{
public ProgramSetting()
{
InitializeComponent();
}
public ProgramSetting(PluginInitContext context, Settings settings)
{
DataContext = new ProgramSettingViewModel(context, settings);
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}