feat(avalonia): add Avalonia settings views for 8 plugins

- Add i18n injection to Application.Resources for DynamicResource bindings
- Create Avalonia settings views for plugins:
  - Explorer (ExplorerSettings + QuickAccessLinkSettings dialog)
  - BrowserBookmark (SettingsControl + CustomBrowserSettingWindow)
  - Calculator (CalculatorSettings)
  - ProcessKiller (SettingsControl)
  - PluginsManager (PluginsManagerSettings)
  - WebSearch (SettingsControl)
  - Shell (ShellSetting + converter)
  - Program (ProgramSetting)
- Add IsAvalonia detection pattern for dual-framework dialog support
- Update 11 plugin .csproj files with CopyToAvaloniaOutput targets
- Add System.Threading.Tasks using for async RelayCommand support
This commit is contained in:
Hongtao Zhang 2026-01-26 15:30:33 -08:00
parent 0c2f026d01
commit c0d17672af
41 changed files with 1810 additions and 29 deletions

View file

@ -36,6 +36,10 @@ public partial class App : Application
ConfigureDI();
AvaloniaXamlLoader.Load(this);
// Inject translations into Application.Resources for DynamicResource bindings in plugins
var i18n = Ioc.Default.GetRequiredService<Internationalization>();
i18n.InjectIntoApplicationResources();
}
public override void OnFrameworkInitializationCompleted()

View file

@ -201,6 +201,37 @@ public class Internationalization
Log.Debug(ClassName, $"Parsed {count} strings from {filePath}");
}
/// <summary>
/// Inject all translations into Avalonia's Application.Resources so {DynamicResource} works.
/// This enables plugin settings to use {DynamicResource key} for localization.
/// Must be called after Application is initialized.
/// </summary>
public void InjectIntoApplicationResources()
{
try
{
var app = Application.Current;
if (app == null)
{
Log.Warn(ClassName, "Cannot inject resources: Application.Current is null");
return;
}
var count = 0;
foreach (var kvp in _translations)
{
app.Resources[kvp.Key] = kvp.Value;
count++;
}
Log.Info(ClassName, $"Injected {count} translations into Application.Resources");
}
catch (Exception e)
{
Log.Exception(ClassName, "Failed to inject translations into Application.Resources", e);
}
}
/// <summary>
/// Get a translated string by key.
/// </summary>
@ -208,12 +239,10 @@ public class Internationalization
{
if (_translations.TryGetValue(key, out var translation))
{
Log.Debug(ClassName, $"Translation found for '{key}': '{translation}'");
return translation;
}
Log.Warn(ClassName, $"Translation not found for key: {key}");
Log.Debug(ClassName, $"Available keys (first 20): {string.Join(", ", _translations.Keys.Take(20))}");
return $"[{key}]";
}
@ -260,6 +289,10 @@ public class Internationalization
}
ChangeCultureInfo(languageCode);
// Re-inject into Application.Resources for DynamicResource bindings
InjectIntoApplicationResources();
Log.Info(ClassName, $"Language changed to: {languageCode}");
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -108,6 +108,19 @@
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageReference Include="Svg.Skia" Version="3.2.1" />
<PackageReference Include="SkiaSharp" Version="3.119.1" />
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -10,6 +10,7 @@ using Flow.Launcher.Plugin.BrowserBookmark.Commands;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.BrowserBookmark.Views;
using Flow.Launcher.Plugin.SharedCommands;
using AvaloniaControl = Avalonia.Controls.Control;
namespace Flow.Launcher.Plugin.BrowserBookmark;
@ -225,6 +226,11 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
return new SettingsControl(_settings);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
return new Views.Avalonia.SettingsControl(_settings);
}
public List<Result> LoadContextMenus(Result selectedResult)
{
return new List<Result>()

View file

@ -0,0 +1,46 @@
<Window 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:models="using:Flow.Launcher.Plugin.BrowserBookmark.Models"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="400"
x:Class="Flow.Launcher.Plugin.BrowserBookmark.Views.Avalonia.CustomBrowserSettingWindow"
Title="{DynamicResource flowlauncher_plugin_browserbookmark_bookmarkDataSetting}"
Width="550" SizeToContent="Height" CanResize="False"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="20">
<StackPanel Grid.Row="0" Spacing="15">
<TextBlock Text="{DynamicResource flowlauncher_plugin_browserbookmark_bookmarkDataSetting}"
FontSize="20" FontWeight="SemiBold" />
<TextBlock Text="{DynamicResource flowlauncher_plugin_browserbookmark_guideMessage01}" TextWrapping="Wrap" />
<TextBlock Text="{DynamicResource flowlauncher_plugin_browserbookmark_guideMessage02}" TextWrapping="Wrap" />
<TextBlock Text="{DynamicResource flowlauncher_plugin_browserbookmark_guideMessage03}" TextWrapping="Wrap" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" Margin="0,10,0,0">
<TextBlock Grid.Row="0" Grid.Column="0" Text="{DynamicResource flowlauncher_plugin_browserbookmark_browserName}" VerticalAlignment="Center" Margin="0,0,10,10"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Name}" Margin="0,0,0,10"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="{DynamicResource flowlauncher_plugin_browserbookmark_browserEngine}" VerticalAlignment="Center" Margin="0,0,10,10"/>
<ComboBox Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding AllBrowserTypes}"
SelectedValue="{Binding BrowserType}"
SelectedValueBinding="{Binding Value}"
DisplayMemberBinding="{Binding Display}"
HorizontalAlignment="Stretch" Margin="0,0,0,10"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="{DynamicResource flowlauncher_plugin_browserbookmark_browserBookmarkDataDirectory}" VerticalAlignment="Center" Margin="0,0,10,0"/>
<Grid Grid.Row="2" Grid.Column="1" ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding DataDirectoryPath}" Margin="0,0,5,0"/>
<Button Grid.Column="1" Content="{DynamicResource flowlauncher_plugin_browserbookmark_browseBrowserBookmark}" Click="OnBrowseClick"/>
</Grid>
</Grid>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10" Margin="0,20,0,0">
<Button Content="{DynamicResource cancel}" Click="OnCancelClick"/>
<Button Content="{DynamicResource done}" Click="OnDoneClick" Classes="accent"/>
</StackPanel>
</Grid>
</Window>

View file

@ -0,0 +1,58 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using System.Collections.Generic;
using System.Linq;
namespace Flow.Launcher.Plugin.BrowserBookmark.Views.Avalonia;
public partial class CustomBrowserSettingWindow : Window
{
public CustomBrowserSettingWindow(CustomBrowser browser)
{
InitializeComponent();
DataContext = browser;
}
public CustomBrowserSettingWindow()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void OnCancelClick(object sender, RoutedEventArgs e)
{
Close();
}
private void OnDoneClick(object sender, RoutedEventArgs e)
{
Close();
}
private async void OnBrowseClick(object sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Bookmark Data Directory",
AllowMultiple = false
});
if (folders.Count > 0)
{
if (DataContext is CustomBrowser browser)
{
browser.DataDirectoryPath = folders[0].Path.LocalPath;
}
}
}
}

View file

@ -0,0 +1,81 @@
<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:local="using:Flow.Launcher.Plugin.BrowserBookmark.Views.Avalonia"
xmlns:models="using:Flow.Launcher.Plugin.BrowserBookmark.Models"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="Flow.Launcher.Plugin.BrowserBookmark.Views.Avalonia.SettingsControl"
x:DataType="local:SettingsViewModel">
<StackPanel Spacing="10" Margin="0,0,10,0">
<ui:SettingsExpander Header="{DynamicResource flowlauncher_plugin_browserbookmark_loadBrowserFrom}"
IconSource="Globe"
Description="Select which browsers to load bookmarks from"
IsExpanded="True">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="Chrome">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding LoadChromeBookmark}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Edge">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding LoadEdgeBookmark}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Firefox">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding LoadFirefoxBookmark}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{DynamicResource flowlauncher_plugin_browserbookmark_others}"
IconSource="Library"
Description="Manage custom Chromium-based browsers">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem>
<Grid RowDefinitions="*,Auto">
<ListBox Grid.Row="0"
ItemsSource="{Binding Settings.CustomChromiumBrowsers}"
SelectedItem="{Binding SelectedCustomBrowser}"
Height="200"
Background="{DynamicResource CardBackgroundFillColorDefault}"
BorderBrush="{DynamicResource ControlStrokeColorDefault}"
BorderThickness="1"
CornerRadius="4">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:CustomBrowser">
<Grid ColumnDefinitions="2*,*,3*">
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center" Margin="10,5"/>
<TextBlock Grid.Column="1" Text="{Binding BrowserType}" VerticalAlignment="Center" Margin="5" Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
<TextBlock Grid.Column="2" Text="{Binding DataDirectoryPath}" VerticalAlignment="Center" Margin="5" Foreground="{DynamicResource TextFillColorSecondaryBrush}" TextTrimming="CharacterEllipsis"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="10" Margin="0,10,0,0" HorizontalAlignment="Right">
<Button Content="{DynamicResource flowlauncher_plugin_browserbookmark_addBrowserBookmark}" Command="{Binding NewCustomBrowserCommand}" />
<Button Content="{DynamicResource flowlauncher_plugin_browserbookmark_editBrowserBookmark}" Command="{Binding EditCustomBrowserCommand}" />
<Button Content="{DynamicResource flowlauncher_plugin_browserbookmark_removeBrowserBookmark}" Command="{Binding DeleteCustomBrowserCommand}" />
</StackPanel>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<ui:SettingsExpander Header="Options" IconSource="Settings">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_browserbookmark_enable_favicons}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.EnableFavicons}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,130 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using System;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin.BrowserBookmark.Views.Avalonia;
public partial class SettingsControl : UserControl
{
public SettingsControl(Settings settings)
{
InitializeComponent();
DataContext = new SettingsViewModel(settings, EditBrowser);
}
public SettingsControl()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private async Task EditBrowser(CustomBrowser browser)
{
var window = new CustomBrowserSettingWindow(browser);
if (VisualRoot is Window parentWindow)
{
await window.ShowDialog(parentWindow);
}
}
}
public partial class SettingsViewModel : ObservableObject
{
private readonly Settings _settings;
private readonly Func<CustomBrowser, Task> _editBrowserAction;
public SettingsViewModel(Settings settings, Func<CustomBrowser, Task> editBrowserAction)
{
_settings = settings;
_editBrowserAction = editBrowserAction;
}
public Settings Settings => _settings;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(EditCustomBrowserCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteCustomBrowserCommand))]
private CustomBrowser _selectedCustomBrowser;
public bool LoadChromeBookmark
{
get => _settings.LoadChromeBookmark;
set
{
if (_settings.LoadChromeBookmark != value)
{
_settings.LoadChromeBookmark = value;
OnPropertyChanged();
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
}
public bool LoadFirefoxBookmark
{
get => _settings.LoadFirefoxBookmark;
set
{
if (_settings.LoadFirefoxBookmark != value)
{
_settings.LoadFirefoxBookmark = value;
OnPropertyChanged();
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
}
public bool LoadEdgeBookmark
{
get => _settings.LoadEdgeBookmark;
set
{
if (_settings.LoadEdgeBookmark != value)
{
_settings.LoadEdgeBookmark = value;
OnPropertyChanged();
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
}
[RelayCommand]
private async Task NewCustomBrowser()
{
var newBrowser = new CustomBrowser();
await _editBrowserAction(newBrowser);
if (!string.IsNullOrEmpty(newBrowser.Name) && !string.IsNullOrEmpty(newBrowser.DataDirectoryPath))
{
_settings.CustomChromiumBrowsers.Add(newBrowser);
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
[RelayCommand(CanExecute = nameof(CanEditOrDelete))]
private async Task EditCustomBrowser()
{
if (SelectedCustomBrowser is null) return;
await _editBrowserAction(SelectedCustomBrowser);
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
[RelayCommand(CanExecute = nameof(CanEditOrDelete))]
private void DeleteCustomBrowser()
{
if (SelectedCustomBrowser is null) return;
_settings.CustomChromiumBrowsers.Remove(SelectedCustomBrowser);
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
private bool CanEditOrDelete() => SelectedCustomBrowser != null;
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -65,6 +65,19 @@
<ItemGroup>
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="Mages" Version="3.0.0" />
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,10 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows.Controls;
using AvaloniaControl = Avalonia.Controls.Control;
using Flow.Launcher.Plugin.Calculator.ViewModels;
using Flow.Launcher.Plugin.Calculator.Views;
using Mages.Core;
@ -424,6 +425,11 @@ namespace Flow.Launcher.Plugin.Calculator
return new CalculatorSettings(_settings);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
return new Views.Avalonia.CalculatorSettings(_viewModel);
}
public void OnCultureInfoChanged(CultureInfo newCulture)
{
DecimalSeparatorLocalized.UpdateLabels(_viewModel.AllDecimalSeparator);

View file

@ -0,0 +1,43 @@
<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.Calculator.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flow.Launcher.Plugin.Calculator.Views.Avalonia.CalculatorSettings"
x:DataType="vm:SettingsViewModel">
<StackPanel Spacing="10" Margin="0,0,10,0">
<ui:SettingsExpander Header="{DynamicResource flowlauncher_plugin_calculator_plugin_name}"
IconSource="Calculator"
Description="{DynamicResource flowlauncher_plugin_calculator_plugin_description}"
IsExpanded="True">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_calculator_output_decimal_separator}">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding AllDecimalSeparator}"
SelectedValue="{Binding SelectedDecimalSeparator, Mode=TwoWay}"
DisplayMemberBinding="{Binding Display}"
SelectedValueBinding="{Binding Value}"
MinWidth="150" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_calculator_max_decimal_places}">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding MaxDecimalPlacesRange}"
SelectedItem="{Binding Settings.MaxDecimalPlaces}"
MinWidth="150" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_calculator_show_error_message}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.ShowErrorMessage}" />
</ui:SettingsExpanderItem.Footer>
</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.Calculator.ViewModels;
namespace Flow.Launcher.Plugin.Calculator.Views.Avalonia;
public partial class CalculatorSettings : UserControl
{
public CalculatorSettings()
{
InitializeComponent();
}
public CalculatorSettings(SettingsViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View file

@ -46,6 +46,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Droplex" Version="1.7.0" />
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
@ -57,5 +59,16 @@
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.ViewModels;
@ -12,6 +12,7 @@ using System.Windows.Controls;
using Flow.Launcher.Plugin.Explorer.Exceptions;
using System.Linq;
using System.Globalization;
using AvaloniaControl = Avalonia.Controls.Control;
namespace Flow.Launcher.Plugin.Explorer
{
@ -34,6 +35,11 @@ namespace Flow.Launcher.Plugin.Explorer
return new ExplorerSettings(viewModel);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
return new Views.Avalonia.ExplorerSettings(viewModel);
}
public Task InitAsync(PluginInitContext context)
{
Context = context;

View file

@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
@ -15,11 +16,18 @@ using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Views;
using AvaloniaApp = Avalonia.Application;
using AvaloniaQuickAccessLinkSettings = Flow.Launcher.Plugin.Explorer.Views.Avalonia.QuickAccessLinkSettings;
namespace Flow.Launcher.Plugin.Explorer.ViewModels
{
public partial class SettingsViewModel : BaseModel
{
/// <summary>
/// Detects if we're running in an Avalonia application context
/// </summary>
private static bool IsAvalonia => AvaloniaApp.Current != null;
public Settings Settings { get; set; }
internal PluginInitContext Context { get; set; }
@ -394,7 +402,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
}
[RelayCommand]
private void EditQuickAccessLink()
private async Task EditQuickAccessLinkAsync()
{
var selectedLink = SelectedQuickAccessLink;
var collection = Settings.QuickAccessLinks;
@ -405,20 +413,50 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
return;
}
var quickAccessLinkSettings = new QuickAccessLinkSettings(collection, SelectedQuickAccessLink);
if (quickAccessLinkSettings.ShowDialog() == true)
if (IsAvalonia)
{
Save();
var dialog = new AvaloniaQuickAccessLinkSettings(collection, selectedLink);
var mainWindow = AvaloniaApp.Current?.ApplicationLifetime is global::Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop
? desktop.MainWindow
: null;
var result = await dialog.ShowDialog<bool?>(mainWindow!);
if (result == true)
{
Save();
}
}
else
{
var quickAccessLinkSettings = new QuickAccessLinkSettings(collection, SelectedQuickAccessLink);
if (quickAccessLinkSettings.ShowDialog() == true)
{
Save();
}
}
}
[RelayCommand]
private void AddQuickAccessLink()
private async Task AddQuickAccessLinkAsync()
{
var quickAccessLinkSettings = new QuickAccessLinkSettings(Settings.QuickAccessLinks);
if (quickAccessLinkSettings.ShowDialog() == true)
if (IsAvalonia)
{
Save();
var dialog = new AvaloniaQuickAccessLinkSettings(Settings.QuickAccessLinks);
var mainWindow = AvaloniaApp.Current?.ApplicationLifetime is global::Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop
? desktop.MainWindow
: null;
var result = await dialog.ShowDialog<bool?>(mainWindow!);
if (result == true)
{
Save();
}
}
else
{
var quickAccessLinkSettings = new QuickAccessLinkSettings(Settings.QuickAccessLinks);
if (quickAccessLinkSettings.ShowDialog() == true)
{
Save();
}
}
}

View file

@ -0,0 +1,320 @@
<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.Explorer.ViewModels"
xmlns:qa="using:Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1200"
x:Class="Flow.Launcher.Plugin.Explorer.Views.Avalonia.ExplorerSettings"
x:DataType="vm:SettingsViewModel">
<ScrollViewer>
<StackPanel Spacing="10" Margin="10">
<!-- General Settings -->
<ui:SettingsExpander Header="{DynamicResource plugin_explorer_generalsetting_header}"
IconSource="Settings"
IsExpanded="True">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_use_location_as_working_dir}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.UseLocationAsWorkingDir}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_default_open_in_file_manager}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.DefaultOpenFolderInFileManager}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_display_more_info_in_tooltip}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.DisplayMoreInformationInToolTip}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_file_editor_path}">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBox Text="{Binding FileEditorPath}" Width="200" />
<Button Content="{DynamicResource select}" Click="BtnOpenFileEditorPath_OnClick" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_folder_editor_path}">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBox Text="{Binding FolderEditorPath}" Width="200" />
<Button Content="{DynamicResource select}" Click="BtnOpenFolderEditorPath_OnClick" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_shell_path}">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBox Text="{Binding ShellPath}" Width="200" />
<Button Content="{DynamicResource select}" Click="BtnOpenShellPath_OnClick" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_Index_Search_Engine}">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding IndexSearchEngines}"
SelectedItem="{Binding SelectedIndexSearchEngine}"
DisplayMemberBinding="{Binding Description}"
MinWidth="150"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_Content_Search_Engine}">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding ContentIndexSearchEngines}"
SelectedItem="{Binding SelectedContentSearchEngine}"
DisplayMemberBinding="{Binding Description}"
MinWidth="150"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_Directory_Recursive_Search_Engine}">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding PathEnumerationEngines}"
SelectedItem="{Binding SelectedPathEnumerationEngine}"
DisplayMemberBinding="{Binding Description}"
MinWidth="150"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_Excluded_File_Types}"
Description="{DynamicResource plugin_explorer_Excluded_File_Types_Tooltip}">
<ui:SettingsExpanderItem.Footer>
<TextBox Text="{Binding ExcludedFileTypes}" Width="200" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_Maximum_Results}"
Description="{DynamicResource plugin_explorer_Maximum_Results_Tooltip}">
<ui:SettingsExpanderItem.Footer>
<TextBox Text="{Binding MaxResult}" Width="100" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Button Content="{DynamicResource plugin_explorer_Open_Window_Index_Option}"
Click="btnOpenIndexingOptions_Click" />
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<!-- Native Context Menu -->
<ui:SettingsExpander Header="{DynamicResource plugin_explorer_native_context_menu_header}"
IconSource="Menu"
IsExpanded="False">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_native_context_menu_display_context_menu}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding ShowWindowsContextMenu}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_native_context_menu_include_patterns_guide}">
<ui:SettingsExpanderItem.Footer>
<TextBox Text="{Binding WindowsContextMenuIncludedItems}"
AcceptsReturn="True"
Height="100"
Width="300"
TextWrapping="Wrap" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_native_context_menu_exclude_patterns_guide}">
<ui:SettingsExpanderItem.Footer>
<TextBox Text="{Binding WindowsContextMenuExcludedItems}"
AcceptsReturn="True"
Height="100"
Width="300"
TextWrapping="Wrap" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<!-- Preview Panel -->
<ui:SettingsExpander Header="{DynamicResource plugin_explorer_previewpanel_setting_header}"
IconSource="PreviewLink"
IsExpanded="False">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_previewpanel_file_info_label}">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="10">
<CheckBox Content="{DynamicResource plugin_explorer_previewpanel_display_file_size_checkbox}"
IsChecked="{Binding ShowFileSizeInPreviewPanel}" />
<CheckBox Content="{DynamicResource plugin_explorer_previewpanel_display_file_creation_checkbox}"
IsChecked="{Binding ShowCreatedDateInPreviewPanel}" />
<CheckBox Content="{DynamicResource plugin_explorer_previewpanel_display_file_modification_checkbox}"
IsChecked="{Binding ShowModifiedDateInPreviewPanel}" />
<CheckBox Content="{DynamicResource plugin_explorer_previewpanel_display_file_age_checkbox}"
IsChecked="{Binding ShowFileAgeInPreviewPanel}" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_previewpanel_date_and_time_format_label}"
IsVisible="{Binding ShowPreviewPanelDateTimeChoices}">
<ui:SettingsExpanderItem.Footer>
<StackPanel Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<ComboBox ItemsSource="{Binding DateFormatList}"
SelectedItem="{Binding PreviewPanelDateFormat}"
MinWidth="150"/>
<TextBlock Text="{Binding PreviewPanelDateFormatDemo}" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<ComboBox ItemsSource="{Binding TimeFormatList}"
SelectedItem="{Binding PreviewPanelTimeFormat}"
MinWidth="150"/>
<TextBlock Text="{Binding PreviewPanelTimeFormatDemo}" VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<!-- Everything Settings -->
<ui:SettingsExpander Header="{DynamicResource plugin_explorer_everything_setting_header}"
IconSource="Find"
IsExpanded="False">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_everything_search_fullpath}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.EverythingSearchFullPath}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_everything_enable_run_count}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.EverythingEnableRunCount}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_everything_sort_option}">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding AllEverythingSortOptions}"
SelectedValue="{Binding SelectedEverythingSortOption}"
SelectedValueBinding="{Binding Value}"
DisplayMemberBinding="{Binding Display}"
MinWidth="150"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_explorer_everything_installed_path}">
<ui:SettingsExpanderItem.Footer>
<TextBox Text="{Binding EverythingInstalledPath}" Width="200" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem IsVisible="{Binding FastSortWarningVisibility}">
<TextBlock Text="{Binding SortOptionWarningMessage}" Foreground="Orange" TextWrapping="Wrap" />
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<!-- Manage Action Keywords -->
<ui:SettingsExpander Header="{DynamicResource plugin_explorer_manageactionkeywords_header}"
IconSource="Keyboard"
IsExpanded="False">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem>
<StackPanel Spacing="10">
<ListBox ItemsSource="{Binding ActionKeywordsModels}"
SelectedItem="{Binding SelectedActionKeyword}"
Height="150">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*,*,*">
<TextBlock Grid.Column="0" Text="{Binding LocalizedDescription}" />
<TextBlock Grid.Column="1" Text="{Binding Keyword}" />
<TextBlock Grid.Column="2" Text="{Binding Enabled}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="{DynamicResource plugin_explorer_edit}"
Command="{Binding EditActionKeywordCommand}"
HorizontalAlignment="Right"/>
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<!-- Quick Access Links -->
<ui:SettingsExpander Header="{DynamicResource plugin_explorer_quickaccesslinks_header}"
IconSource="Star"
IsExpanded="False">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem>
<StackPanel Spacing="10">
<ListBox ItemsSource="{Binding Settings.QuickAccessLinks}"
SelectedItem="{Binding SelectedQuickAccessLink}"
Height="200">
<ListBox.ItemTemplate>
<DataTemplate DataType="qa:AccessLink">
<Grid ColumnDefinitions="*,2*">
<TextBlock Grid.Column="0" Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Grid.Column="1" Text="{Binding Path}" TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
<Button Content="{DynamicResource plugin_explorer_delete}"
Command="{Binding RemoveLinkCommand}"
CommandParameter="QuickAccessLink"/>
<Button Content="{DynamicResource plugin_explorer_edit}"
Command="{Binding EditQuickAccessLinkCommand}"/>
<Button Content="{DynamicResource plugin_explorer_add}"
Command="{Binding AddQuickAccessLinkCommand}"/>
</StackPanel>
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<!-- Index Search Excluded Paths -->
<ui:SettingsExpander Header="{DynamicResource plugin_explorer_indexsearchexcludedpaths_header}"
IconSource="Block"
IsExpanded="False">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem>
<StackPanel Spacing="10">
<ListBox ItemsSource="{Binding Settings.IndexSearchExcludedSubdirectoryPaths}"
SelectedItem="{Binding SelectedIndexSearchExcludedPath}"
Height="200">
<ListBox.ItemTemplate>
<DataTemplate DataType="qa:AccessLink">
<TextBlock Text="{Binding Path}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
<Button Content="{DynamicResource plugin_explorer_delete}"
Command="{Binding RemoveLinkCommand}"
CommandParameter="IndexSearchExcludedPaths"/>
<Button Content="{DynamicResource plugin_explorer_edit}"
Command="{Binding EditIndexSearchExcludePathsCommand}"/>
<Button Content="{DynamicResource plugin_explorer_add}"
Command="{Binding AddIndexSearchExcludePathsCommand}"/>
</StackPanel>
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View file

@ -0,0 +1,85 @@
#nullable enable
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using System;
namespace Flow.Launcher.Plugin.Explorer.Views.Avalonia
{
public partial class ExplorerSettings : UserControl
{
public ExplorerSettings()
{
InitializeComponent();
}
public ExplorerSettings(SettingsViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void btnOpenIndexingOptions_Click(object sender, RoutedEventArgs e)
{
SettingsViewModel.OpenWindowsIndexingOptions();
}
private async void BtnOpenFileEditorPath_OnClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not SettingsViewModel vm) return;
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
AllowMultiple = false
});
if (files.Count > 0)
{
vm.FileEditorPath = files[0].Path.LocalPath;
}
}
private async void BtnOpenFolderEditorPath_OnClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not SettingsViewModel vm) return;
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
AllowMultiple = false
});
if (folders.Count > 0)
{
vm.FolderEditorPath = folders[0].Path.LocalPath;
}
}
private async void BtnOpenShellPath_OnClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not SettingsViewModel vm) return;
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
AllowMultiple = false
});
if (files.Count > 0)
{
vm.ShellPath = files[0].Path.LocalPath;
}
}
}
}

View file

@ -0,0 +1,86 @@
<Window 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"
mc:Ignorable="d"
x:Class="Flow.Launcher.Plugin.Explorer.Views.Avalonia.QuickAccessLinkSettings"
Title="{DynamicResource plugin_explorer_manage_quick_access_links_header}"
Width="450"
Height="280"
WindowStartupLocation="CenterOwner"
CanResize="False"
ShowInTaskbar="False">
<Grid RowDefinitions="*,Auto">
<!-- Main Content -->
<StackPanel Grid.Row="0" Margin="24,20,24,10" Spacing="16">
<!-- Header -->
<TextBlock Text="{DynamicResource plugin_explorer_manage_quick_access_links_header}"
FontSize="20"
FontWeight="SemiBold" />
<!-- Form Fields -->
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,Auto,Auto">
<!-- Name -->
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource plugin_explorer_name}"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
Text="{Binding SelectedName, Mode=TwoWay}"
Margin="0,0,0,10" />
<!-- Type -->
<TextBlock Grid.Row="1" Grid.Column="0"
Text="{DynamicResource plugin_explorer_type}"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<StackPanel Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
Orientation="Horizontal" Spacing="20"
Margin="0,0,0,10">
<RadioButton Content="{DynamicResource plugin_explorer_file}"
GroupName="PathType"
IsChecked="{Binding IsFileSelected, Mode=TwoWay}" />
<RadioButton Content="{DynamicResource plugin_explorer_folder}"
GroupName="PathType"
IsChecked="{Binding IsFolderSelected, Mode=TwoWay}" />
</StackPanel>
<!-- Path -->
<TextBlock Grid.Row="2" Grid.Column="0"
Text="{DynamicResource plugin_explorer_path}"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding SelectedPath, Mode=TwoWay}"
IsReadOnly="True"
Margin="0,0,10,0" />
<Button Grid.Row="2" Grid.Column="2"
Content="{DynamicResource select}"
Click="SelectPath_OnClick"
MinWidth="80" />
</Grid>
</StackPanel>
<!-- Button Area -->
<Border Grid.Row="1"
Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
BorderThickness="0,1,0,0"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
Padding="24,16">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="10">
<Button Content="{DynamicResource cancel}"
Click="BtnCancel_OnClick"
Width="120"
HorizontalContentAlignment="Center" />
<Button Content="{DynamicResource done}"
Click="OnDoneButtonClick"
Width="120"
HorizontalContentAlignment="Center"
Classes="accent" />
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,240 @@
#nullable enable
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
namespace Flow.Launcher.Plugin.Explorer.Views.Avalonia;
public partial class QuickAccessLinkSettings : Window, INotifyPropertyChanged
{
private static readonly string ClassName = nameof(QuickAccessLinkSettings);
private string _selectedPath = string.Empty;
public string SelectedPath
{
get => _selectedPath;
set
{
if (_selectedPath != value)
{
_selectedPath = value;
OnPropertyChanged();
if (string.IsNullOrEmpty(_selectedName))
{
SelectedName = _selectedPath.GetPathName();
}
if (!string.IsNullOrEmpty(_selectedPath))
{
_accessLinkType = GetResultType(_selectedPath);
}
}
}
}
private string _selectedName = string.Empty;
public string SelectedName
{
get => string.IsNullOrEmpty(_selectedName) ? _selectedPath.GetPathName() : _selectedName;
set
{
if (_selectedName != value)
{
_selectedName = value;
OnPropertyChanged();
}
}
}
private bool _isFileSelected;
public bool IsFileSelected
{
get => _isFileSelected;
set
{
if (_isFileSelected != value)
{
_isFileSelected = value;
OnPropertyChanged();
}
}
}
private bool _isFolderSelected = true;
public bool IsFolderSelected
{
get => _isFolderSelected;
set
{
if (_isFolderSelected != value)
{
_isFolderSelected = value;
OnPropertyChanged();
}
}
}
private bool IsEdit { get; }
private AccessLink? SelectedAccessLink { get; }
public ObservableCollection<AccessLink> QuickAccessLinks { get; }
private ResultType _accessLinkType = ResultType.Folder;
public QuickAccessLinkSettings()
{
QuickAccessLinks = new ObservableCollection<AccessLink>();
InitializeComponent();
DataContext = this;
}
public QuickAccessLinkSettings(ObservableCollection<AccessLink> quickAccessLinks)
{
IsEdit = false;
QuickAccessLinks = quickAccessLinks;
InitializeComponent();
DataContext = this;
}
public QuickAccessLinkSettings(ObservableCollection<AccessLink> quickAccessLinks, AccessLink selectedAccessLink)
{
IsEdit = true;
_selectedName = selectedAccessLink.Name;
_selectedPath = selectedAccessLink.Path;
_accessLinkType = GetResultType(_selectedPath);
_isFileSelected = selectedAccessLink.Type == ResultType.File;
_isFolderSelected = !_isFileSelected;
SelectedAccessLink = selectedAccessLink;
QuickAccessLinks = quickAccessLinks;
InitializeComponent();
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void BtnCancel_OnClick(object? sender, RoutedEventArgs e)
{
Close(false);
}
private void OnDoneButtonClick(object? sender, RoutedEventArgs e)
{
// Validate the input before proceeding
if (string.IsNullOrEmpty(SelectedName) || string.IsNullOrEmpty(SelectedPath))
{
var warning = Localize.plugin_explorer_quick_access_link_no_folder_selected();
Main.Context.API.ShowMsgBox(warning);
return;
}
// Check if the path already exists in the quick access links (when not editing)
if (!IsEdit && QuickAccessLinks.Any(x =>
x.Path.Equals(SelectedPath, StringComparison.OrdinalIgnoreCase) &&
x.Name.Equals(SelectedName, StringComparison.OrdinalIgnoreCase)))
{
var warning = Localize.plugin_explorer_quick_access_link_path_already_exists();
Main.Context.API.ShowMsgBox(warning);
return;
}
// If editing, update the existing link
if (IsEdit && SelectedAccessLink != null)
{
var index = QuickAccessLinks.IndexOf(SelectedAccessLink);
if (index >= 0)
{
var updatedLink = new AccessLink
{
Name = SelectedName,
Type = _accessLinkType,
Path = SelectedPath
};
QuickAccessLinks[index] = updatedLink;
}
Close(true);
}
else
{
// Add a new one
var newAccessLink = new AccessLink
{
Name = SelectedName,
Type = _accessLinkType,
Path = SelectedPath
};
QuickAccessLinks.Add(newAccessLink);
Close(true);
}
}
private async void SelectPath_OnClick(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel == null) return;
if (IsFileSelected)
{
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
AllowMultiple = false
});
if (files.Count > 0)
{
SelectedPath = files[0].Path.LocalPath;
_accessLinkType = ResultType.File;
}
}
else
{
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
AllowMultiple = false
});
if (folders.Count > 0)
{
SelectedPath = folders[0].Path.LocalPath;
_accessLinkType = GetResultType(SelectedPath);
}
}
}
private static ResultType GetResultType(string path)
{
if (File.Exists(path))
{
return ResultType.File;
}
if (Directory.Exists(path))
{
if (string.Equals(Path.GetPathRoot(path), path, StringComparison.OrdinalIgnoreCase))
{
return ResultType.Volume;
}
return ResultType.Folder;
}
Main.Context.API.LogError(ClassName, $"The path '{path}' does not exist or is invalid. Defaulting to Folder type.");
return ResultType.Folder;
}
public new event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -60,4 +60,15 @@
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
@ -38,5 +38,18 @@
<ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,10 +1,12 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using AvaloniaControl = Avalonia.Controls.Control;
using System.Threading.Tasks;
using System.Threading;
using Flow.Launcher.Plugin.PluginsManager.ViewModels;
using Flow.Launcher.Plugin.PluginsManager.Views;
using Flow.Launcher.Plugin.PluginsManager.Views.Avalonia;
namespace Flow.Launcher.Plugin.PluginsManager
{
@ -22,7 +24,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
public Control CreateSettingPanel()
{
return new PluginsManagerSettings(viewModel);
return new Views.PluginsManagerSettings(viewModel);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
return new Views.Avalonia.PluginsManagerSettings(viewModel);
}
public async Task InitAsync(PluginInitContext context)

View file

@ -0,0 +1,29 @@
<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.PluginsManager.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flow.Launcher.Plugin.PluginsManager.Views.Avalonia.PluginsManagerSettings"
x:DataType="vm:SettingsViewModel">
<StackPanel Spacing="10" Margin="0,0,10,0">
<ui:SettingsExpander Header="{DynamicResource plugin_pluginsmanager_plugin_name}"
IconSource="Settings"
Description="{DynamicResource plugin_pluginsmanager_plugin_description}"
IsExpanded="True">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_pluginsmanager_plugin_settings_unknown_source}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding WarnFromUnknownSource}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource plugin_pluginsmanager_plugin_settings_auto_restart}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding AutoRestartAfterChanging}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</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.PluginsManager.ViewModels;
namespace Flow.Launcher.Plugin.PluginsManager.Views.Avalonia;
public partial class PluginsManagerSettings : UserControl
{
public PluginsManagerSettings()
{
InitializeComponent();
}
internal PluginsManagerSettings(SettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -16,6 +16,11 @@
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>portable</DebugType>
@ -64,4 +69,15 @@
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,9 +1,10 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using Flow.Launcher.Plugin.ProcessKiller.ViewModels;
using Flow.Launcher.Plugin.ProcessKiller.Views;
using AvaloniaControl = Avalonia.Controls.Control;
namespace Flow.Launcher.Plugin.ProcessKiller
{
@ -217,5 +218,11 @@ namespace Flow.Launcher.Plugin.ProcessKiller
{
return new SettingsControl(_viewModel);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
return new Views.Avalonia.SettingsControl(_viewModel);
}
}
}

View file

@ -0,0 +1,29 @@
<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.ProcessKiller.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Flow.Launcher.Plugin.ProcessKiller.Views.Avalonia.SettingsControl"
x:DataType="vm:SettingsViewModel">
<StackPanel Spacing="10" Margin="0,0,10,0">
<ui:SettingsExpander Header="{DynamicResource flowlauncher_plugin_processkiller_plugin_name}"
IconSource="Settings"
Description="{DynamicResource flowlauncher_plugin_processkiller_plugin_description}"
IsExpanded="True">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_processkiller_show_window_title}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.ShowWindowTitle}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_processkiller_put_visible_window_process_top}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.PutVisibleWindowProcessesTop}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</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.ProcessKiller.ViewModels;
namespace Flow.Launcher.Plugin.ProcessKiller.Views.Avalonia;
public partial class SettingsControl : UserControl
{
public SettingsControl()
{
InitializeComponent();
}
public SettingsControl(SettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View file

@ -75,4 +75,15 @@
<PackageReference Include="NLog" Version="6.0.4" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Flow.Launcher.Plugin.Shell.Converters.Avalonia
{
public class LeaveShellOpenOrCloseShellAfterPressEnabledConverter : IMultiValueConverter
{
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
if (values == null || values.Count != 2)
return false;
if (values[0] is not bool closeShellAfterPressOrLeaveShellOpen ||
values[1] is not Shell shell)
return false;
return (!closeShellAfterPressOrLeaveShellOpen) && shell != Shell.RunCommand;
}
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -61,6 +61,19 @@
<ItemGroup>
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="InputSimulator" Version="1.0.4" NoWarn="NU1701" />
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@ -10,6 +10,7 @@ using Flow.Launcher.Plugin.Shell.Views;
using WindowsInput;
using WindowsInput.Native;
using Control = System.Windows.Controls.Control;
using AvaloniaControl = Avalonia.Controls.Control;
using Keys = System.Windows.Forms.Keys;
namespace Flow.Launcher.Plugin.Shell
@ -434,6 +435,11 @@ namespace Flow.Launcher.Plugin.Shell
return new CMDSetting(_settings);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
return new Flow.Launcher.Plugin.Shell.Views.Avalonia.ShellSetting(_settings);
}
public string GetTranslatedPluginTitle()
{
return Localize.flowlauncher_plugin_cmd_plugin_name();

View file

@ -0,0 +1,91 @@
<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.Shell.ViewModels"
xmlns:converters="using:Flow.Launcher.Plugin.Shell.Converters.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="Flow.Launcher.Plugin.Shell.Views.Avalonia.ShellSetting"
x:DataType="vm:ShellSettingViewModel">
<UserControl.Resources>
<converters:LeaveShellOpenOrCloseShellAfterPressEnabledConverter x:Key="LeaveShellOpenOrCloseShellAfterPressEnabledConverter" />
</UserControl.Resources>
<StackPanel Spacing="10" Margin="0,0,10,0">
<ui:SettingsExpander Header="{DynamicResource flowlauncher_plugin_cmd_plugin_name}"
IconSource="Code"
Description="{DynamicResource flowlauncher_plugin_cmd_plugin_description}"
IsExpanded="True">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_cmd_relace_winr}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.ReplaceWinR}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_cmd_always_run_as_administrator}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.RunAsAdministrator}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_cmd_use_windows_terminal}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding Settings.UseWindowsTerminal}" OnContent="" OffContent="" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="Shell">
<ui:SettingsExpanderItem.Footer>
<ComboBox ItemsSource="{Binding AllShells}"
SelectedValue="{Binding SelectedShell}"
DisplayMemberBinding="{Binding Display}"
SelectedValueBinding="{Binding Value}"
MinWidth="150" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_cmd_close_cmd_after_press}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding CloseShellAfterPress}" OnContent="" OffContent="">
<ToggleSwitch.IsEnabled>
<MultiBinding Converter="{StaticResource LeaveShellOpenOrCloseShellAfterPressEnabledConverter}">
<Binding Path="LeaveShellOpen" />
<Binding Path="SelectedShell" />
</MultiBinding>
</ToggleSwitch.IsEnabled>
</ToggleSwitch>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_cmd_leave_cmd_open}">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding LeaveShellOpen}" OnContent="" OffContent="">
<ToggleSwitch.IsEnabled>
<MultiBinding Converter="{StaticResource LeaveShellOpenOrCloseShellAfterPressEnabledConverter}">
<Binding Path="CloseShellAfterPress" />
<Binding Path="SelectedShell" />
</MultiBinding>
</ToggleSwitch.IsEnabled>
</ToggleSwitch>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem Content="{DynamicResource flowlauncher_plugin_cmd_history}">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="10">
<ComboBox ItemsSource="{Binding OnlyMostUsedCMDsNumbers}"
SelectedItem="{Binding SelectedOnlyMostUsedCMDsNumber}"
IsEnabled="{Binding Settings.ShowOnlyMostUsedCMDs}"
MinWidth="60" />
<ToggleSwitch IsChecked="{Binding Settings.ShowOnlyMostUsedCMDs}" OnContent="" OffContent="" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,25 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Flow.Launcher.Plugin.Shell.ViewModels;
namespace Flow.Launcher.Plugin.Shell.Views.Avalonia
{
public partial class ShellSetting : UserControl
{
public ShellSetting()
{
InitializeComponent();
}
public ShellSetting(Settings settings)
{
InitializeComponent();
DataContext = new ShellSettingViewModel(settings);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -65,4 +65,16 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -61,4 +61,15 @@
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -54,4 +54,20 @@
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.2.0" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using Flow.Launcher.Plugin.SharedCommands;
using AvaloniaControl = Avalonia.Controls.Control;
namespace Flow.Launcher.Plugin.WebSearch
{
@ -193,6 +194,11 @@ namespace Flow.Launcher.Plugin.WebSearch
return new SettingsControl(_context, _viewModel);
}
public AvaloniaControl CreateSettingPanelAvalonia()
{
return new Views.Avalonia.SettingsControl(_context, _viewModel);
}
#endregion
public string GetTranslatedPluginTitle()

View file

@ -0,0 +1,65 @@
<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.WebSearch"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="Flow.Launcher.Plugin.WebSearch.Views.Avalonia.SettingsControl"
x:DataType="vm:SettingsViewModel">
<StackPanel Spacing="10" Margin="0,0,10,0">
<ui:SettingsExpander Header="{DynamicResource flowlauncher_plugin_websearch_plugin_name}"
IconSource="Globe"
Description="{DynamicResource flowlauncher_plugin_websearch_plugin_description}"
IsExpanded="True">
<ui:SettingsExpander.Items>
<ui:SettingsExpanderItem>
<Grid RowDefinitions="*,Auto" MinHeight="300">
<ListBox Grid.Row="0"
ItemsSource="{Binding Settings.SearchSources}"
SelectedItem="{Binding Settings.SelectedSearchSource}"
Background="{DynamicResource CardBackgroundFillColorDefault}"
BorderBrush="{DynamicResource ControlStrokeColorDefault}"
BorderThickness="1"
CornerRadius="4"
Margin="0,0,0,10">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SearchSource">
<Grid ColumnDefinitions="Auto,120,*,80,80" Margin="0,5">
<Image Grid.Column="0" Width="20" Height="20" Source="{Binding IconPath}" Margin="0,0,10,0"/>
<TextBlock Grid.Column="1" Text="{Binding ActionKeyword}" VerticalAlignment="Center" FontWeight="SemiBold"/>
<TextBlock Grid.Column="2" Text="{Binding Title}" VerticalAlignment="Center"/>
<CheckBox Grid.Column="3" IsChecked="{Binding Enabled}" IsEnabled="False" HorizontalAlignment="Center"/>
<CheckBox Grid.Column="4" IsChecked="{Binding IsPrivateMode}" IsEnabled="False" HorizontalAlignment="Center"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
<Button Content="{DynamicResource flowlauncher_plugin_websearch_add}" Click="OnAddSearchSourceClick"/>
<Button Content="{DynamicResource flowlauncher_plugin_websearch_edit}" Click="OnEditSearchSourceClick"/>
<Button Content="{DynamicResource flowlauncher_plugin_websearch_delete}" Click="OnDeleteSearchSourceClick"/>
</StackPanel>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion_provider}"
IconSource="Lightbulb"
Description="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion}">
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="10">
<ComboBox ItemsSource="{Binding Settings.Suggestions}"
SelectedItem="{Binding Settings.SelectedSuggestion}"
IsEnabled="{Binding Settings.EnableSuggestion}"
MinWidth="150"/>
<ToggleSwitch IsChecked="{Binding Settings.EnableSuggestion}"
OnContent="" OffContent=""/>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,87 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;
using System;
namespace Flow.Launcher.Plugin.WebSearch.Views.Avalonia
{
public partial class SettingsControl : UserControl
{
private readonly PluginInitContext _context;
private readonly SettingsViewModel _viewModel;
public SettingsControl(PluginInitContext context, SettingsViewModel viewModel)
{
InitializeComponent();
_context = context;
_viewModel = viewModel;
DataContext = viewModel;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private async void OnAddSearchSourceClick(object sender, RoutedEventArgs e)
{
var dialog = new ContentDialog
{
Title = "Add Search Source",
Content = "Adding search sources is not yet implemented in the Avalonia version.",
CloseButtonText = "OK"
};
if (VisualRoot is TopLevel topLevel)
{
await dialog.ShowAsync(topLevel);
}
}
private async void OnEditSearchSourceClick(object sender, RoutedEventArgs e)
{
var dialog = new ContentDialog
{
Title = "Edit Search Source",
Content = "Editing search sources is not yet implemented in the Avalonia version.",
CloseButtonText = "OK"
};
if (VisualRoot is TopLevel topLevel)
{
await dialog.ShowAsync(topLevel);
}
}
private async void OnDeleteSearchSourceClick(object sender, RoutedEventArgs e)
{
if (_viewModel.Settings.SelectedSearchSource != null)
{
var selected = _viewModel.Settings.SelectedSearchSource;
var warning = _context.API.GetTranslation("flowlauncher_plugin_websearch_delete_warning");
var formatted = string.Format(warning, selected.Title);
var dialog = new ContentDialog
{
Title = "Delete Search Source",
Content = formatted,
PrimaryButtonText = "Yes",
CloseButtonText = "No"
};
if (VisualRoot is TopLevel topLevel)
{
var result = await dialog.ShowAsync(topLevel);
if (result == ContentDialogResult.Primary)
{
var id = _context.CurrentPluginMetadata.ID;
_context.API.RemoveActionKeyword(id, selected.ActionKeyword);
_viewModel.Settings.SearchSources.Remove(selected);
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
@ -69,4 +69,15 @@
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<!-- Copy plugin output to Avalonia build directory -->
<Target Name="CopyToAvaloniaOutput" AfterTargets="Build">
<PropertyGroup>
<AvaloniaPluginDir>..\..\Output\$(Configuration)\Avalonia\Plugins\$(AssemblyName)\</AvaloniaPluginDir>
</PropertyGroup>
<ItemGroup>
<PluginFiles Include="$(OutputPath)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(AvaloniaPluginDir)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>