diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index c22699b0e31..a52075c414f 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -53,6 +53,20 @@ jobs:
- name: Perform post_build tasks
shell: powershell
run: .\Scripts\post_build.ps1
+ - name: Upload Avalonia Setup
+ uses: actions/upload-artifact@v7
+ with:
+ name: Avalonia Setup
+ path: |
+ Output\Packages\Avalonia\Flow-Launcher-Avalonia-Setup.exe
+ compression-level: 0
+ - name: Upload Avalonia Portable Version
+ uses: actions/upload-artifact@v7
+ with:
+ name: Avalonia Portable Version
+ path: |
+ Output\Packages\Avalonia\Flow-Launcher-Avalonia-Portable.zip
+ compression-level: 0
- name: Upload Plugin Nupkg
uses: actions/upload-artifact@v7
with:
diff --git a/Flow.Launcher.Avalonia/App.axaml b/Flow.Launcher.Avalonia/App.axaml
new file mode 100644
index 00000000000..2d23fa5158c
--- /dev/null
+++ b/Flow.Launcher.Avalonia/App.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ avares://Flow.Launcher.Avalonia/Resources#Segoe Fluent Icons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Avalonia/App.axaml.cs b/Flow.Launcher.Avalonia/App.axaml.cs
new file mode 100644
index 00000000000..9f84e512592
--- /dev/null
+++ b/Flow.Launcher.Avalonia/App.axaml.cs
@@ -0,0 +1,228 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Avalonia.Helper;
+using Flow.Launcher.Avalonia.Resource;
+using Flow.Launcher.Avalonia.Views.Dialogs;
+using Flow.Launcher.Avalonia.ViewModel;
+using Flow.Launcher.Avalonia.Views.SettingPages;
+using Flow.Launcher.Core;
+using Flow.Launcher.Core.Configuration;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Infrastructure.Storage;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+
+namespace Flow.Launcher.Avalonia;
+
+public partial class App : Application
+{
+ private static readonly string ClassName = nameof(App);
+ private Settings? _settings;
+ private MainViewModel? _mainVM;
+ private MainWindow? _mainWindow;
+
+ public static IPublicAPI? API { get; private set; }
+
+ public override void Initialize()
+ {
+ // Configure DI before loading XAML so markup extensions can access services
+ LoadSettings();
+ ConfigureDI();
+
+ AvaloniaXamlLoader.Load(this);
+
+ // Inject translations into Application.Resources for DynamicResource bindings in plugins
+ var i18n = Ioc.Default.GetRequiredService();
+ i18n.InjectIntoApplicationResources();
+
+ #if DEBUG
+ this.AttachDeveloperTools();
+ #endif
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ API = Ioc.Default.GetRequiredService();
+ _mainVM = Ioc.Default.GetRequiredService();
+
+ _mainWindow = new MainWindow();
+ // desktop.MainWindow = _mainWindow; // Prevent auto-show on startup
+ desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+ // Initialize hotkeys after window is created
+ HotKeyMapper.Initialize();
+
+ AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
+ TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
+ Dispatcher.UIThread.UnhandledException += OnUiUnhandledException;
+
+ AutoStartup();
+
+ Dispatcher.UIThread.Post(async () => await InitializePluginsAsync(), DispatcherPriority.Background);
+
+ // Cleanup on exit
+ desktop.Exit += (_, _) =>
+ {
+ HotKeyMapper.Shutdown();
+ AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
+ TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
+ Dispatcher.UIThread.UnhandledException -= OnUiUnhandledException;
+ };
+ }
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ ///
+ /// Check startup only for Release.
+ ///
+ [Conditional("RELEASE")]
+ private void AutoStartup()
+ {
+ if (_settings?.StartFlowLauncherOnSystemStartup != true)
+ {
+ return;
+ }
+
+ try
+ {
+ Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup);
+ }
+ catch (Exception e)
+ {
+ _settings.StartFlowLauncherOnSystemStartup = false;
+ _settings.Save();
+ API?.ShowMsgError(Translator.GetString("setAutoStartFailed"), e.Message);
+ }
+ }
+
+ private void TrayIcon_OnClicked(object? sender, EventArgs e)
+ {
+ _mainVM?.ToggleFlowLauncher();
+ }
+
+ private void MenuShow_OnClick(object? sender, EventArgs e)
+ {
+ _mainVM?.Show();
+ }
+
+ private void MenuSettings_OnClick(object? sender, EventArgs e)
+ {
+ var settingsWindow = new SettingsWindow();
+ settingsWindow.Show();
+ }
+
+ private void MenuExit_OnClick(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.Shutdown();
+ }
+ }
+
+ private void LoadSettings()
+ {
+ try
+ {
+ var storage = new FlowLauncherJsonStorage();
+ _settings = storage.Load();
+ _settings.SetStorage(storage);
+ }
+ catch (Exception e)
+ {
+ Log.Exception(ClassName, "Settings load failed", e);
+ var storage = new FlowLauncherJsonStorage();
+ _settings = new Settings
+ {
+ WindowSize = 580, WindowHeightSize = 42, QueryBoxFontSize = 24,
+ ItemHeightSize = 50, ResultItemFontSize = 14, ResultSubItemFontSize = 12, MaxResultsToShow = 6
+ };
+ _settings.SetStorage(storage);
+ }
+ }
+
+ private void ConfigureDI()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton(_settings!);
+ services.AddSingleton(sp => new Updater(sp.GetRequiredService(), Constant.GitHub));
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(sp => new AvaloniaPublicAPI(
+ sp.GetRequiredService(),
+ () => sp.GetRequiredService(),
+ sp.GetRequiredService()));
+ Ioc.Default.ConfigureServices(services.BuildServiceProvider());
+ }
+
+ private async Task InitializePluginsAsync()
+ {
+ try
+ {
+ Log.Info(ClassName, "Loading plugins...");
+ PluginManager.LoadPlugins(_settings!.PluginSettings);
+ Log.Info(ClassName, $"Loaded {PluginManager.GetAllLoadedPlugins().Count} plugins");
+
+ await PluginManager.InitializePluginsAsync(_mainVM!);
+ Log.Info(ClassName, "Plugins initialized");
+
+ // Update plugin translations after they are initialized
+ var i18n = Ioc.Default.GetRequiredService();
+ i18n.UpdatePluginMetadataTranslations();
+
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = _mainWindow;
+ }
+
+ _mainVM?.OnPluginsReady();
+ }
+ catch (Exception e) { Log.Exception(ClassName, "Plugin init failed", e); }
+ }
+
+ private void OnUiUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ Log.Exception(ClassName, "Unhandled UI exception", e.Exception);
+ ShowReportWindow(e.Exception);
+ e.Handled = true;
+ }
+
+ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ if (e.ExceptionObject is Exception exception)
+ {
+ Log.Exception(ClassName, "Unhandled exception", exception);
+ ShowReportWindow(exception);
+ }
+ }
+
+ private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
+ {
+ Log.Exception(ClassName, "Unobserved task exception occurred.", e.Exception);
+ e.SetObserved();
+ }
+
+ private static void ShowReportWindow(Exception exception)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ var window = new ReportWindow(exception);
+ window.Show();
+ window.Activate();
+ });
+ }
+}
diff --git a/Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs b/Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
new file mode 100644
index 00000000000..14ff41b3986
--- /dev/null
+++ b/Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
@@ -0,0 +1,530 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Specialized;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using Flow.Launcher.Avalonia.Views.Dialogs;
+using Flow.Launcher.Core;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Infrastructure.Storage;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedModels;
+using Flow.Launcher.Plugin.SharedCommands;
+using Flow.Launcher.Avalonia.Views.Controls;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Core.ExternalPlugins;
+using Flow.Launcher.Avalonia.ViewModel;
+using Flow.Launcher.Avalonia.Resource;
+using CommunityToolkit.Mvvm.DependencyInjection;
+
+namespace Flow.Launcher.Avalonia;
+
+///
+/// IPublicAPI implementation for the Avalonia host.
+///
+public class AvaloniaPublicAPI : IPublicAPI
+{
+ private readonly Settings _settings;
+ private readonly Func _getMainViewModel;
+ private readonly Internationalization _i18n;
+ private readonly object _saveSettingsLock = new();
+ private readonly ConcurrentDictionary _pluginJsonStorages = new();
+ private readonly ConcurrentDictionary<(string, string, Type), ISavable> _pluginBinaryStorages = new();
+ private readonly object _globalKeyboardHandlersLock = new();
+ private readonly List> _globalKeyboardHandlers = new();
+ private Flow.Launcher.Core.Resource.Theme? _theme;
+ private bool _gameModeStatus;
+
+ public AvaloniaPublicAPI(Settings settings, Func getMainViewModel, Internationalization i18n)
+ {
+ _settings = settings;
+ _getMainViewModel = getMainViewModel;
+ _i18n = i18n;
+ Flow.Launcher.Infrastructure.Hotkey.GlobalHotkey.hookedKeyboardCallback = KListenerHookedKeyboardCallback;
+ }
+
+#pragma warning disable CS0067
+ public event VisibilityChangedEventHandler? VisibilityChanged;
+ public event ActualApplicationThemeChangedEventHandler? ActualApplicationThemeChanged;
+#pragma warning restore CS0067
+ public event EventHandler StringMatcherBehaviorChanged
+ {
+ add => _settings.StringMatcherBehaviorChanged += value;
+ remove => _settings.StringMatcherBehaviorChanged -= value;
+ }
+
+
+ // Essential for plugins
+ public void ChangeQuery(string query, bool requery = false)
+ {
+ var mainViewModel = _getMainViewModel();
+ if (requery && string.Equals(mainViewModel.QueryText, query, StringComparison.Ordinal))
+ {
+ ReQuery();
+ return;
+ }
+
+ mainViewModel.QueryText = query;
+ }
+
+ public string GetTranslation(string key) => _i18n.GetTranslation(key);
+
+ public List GetAllPlugins() => PluginManager.GetAllLoadedPlugins();
+ public List GetAllInitializedPlugins(bool includeFailed) => PluginManager.GetAllInitializedPlugins(includeFailed);
+ public MatchResult FuzzySearch(string query, string stringToCompare) =>
+ Ioc.Default.GetRequiredService().FuzzyMatch(query, stringToCompare);
+
+ // Logging
+ public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") => Log.Debug(className, message, methodName);
+ public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") => Log.Info(className, message, methodName);
+ public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") => Log.Warn(className, message, methodName);
+ public void LogError(string className, string message, [CallerMemberName] string methodName = "") => Log.Error(className, message, methodName);
+ public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName);
+
+ // Shell/URL operations
+ public void ShellRun(string cmd, string filename = "cmd.exe") =>
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filename, Arguments = $"/c {cmd}", UseShellExecute = true });
+ public void OpenUrl(string url, bool? inPrivate = null) => OpenUri(new Uri(url), inPrivate);
+ public void OpenUrl(Uri url, bool? inPrivate = null) => OpenUri(url, inPrivate);
+ public void OpenWebUrl(string url, bool? inPrivate = null) => OpenUri(new Uri(url), inPrivate, true);
+ public void OpenWebUrl(Uri url, bool? inPrivate = null) => OpenUri(url, inPrivate, true);
+ public void OpenAppUri(Uri appUri) => OpenUri(appUri);
+ public void OpenAppUri(string appUri) => OpenUri(new Uri(appUri));
+
+ public void OpenDirectory(string DirectoryPath, string? FileNameOrFilePath = null)
+ {
+ try
+ {
+ var explorerInfo = _settings.CustomExplorer;
+ var explorerPath = explorerInfo.Path.Trim().ToLowerInvariant();
+ var targetPath = FileNameOrFilePath is null
+ ? DirectoryPath
+ : Path.IsPathRooted(FileNameOrFilePath)
+ ? FileNameOrFilePath
+ : Path.Combine(DirectoryPath, FileNameOrFilePath);
+
+ if (Path.GetFileNameWithoutExtension(explorerPath) == "explorer")
+ {
+ if (FileNameOrFilePath is null)
+ {
+ using var explorer = new Process();
+ explorer.StartInfo = new ProcessStartInfo
+ {
+ FileName = DirectoryPath,
+ UseShellExecute = true
+ };
+ explorer.Start();
+ }
+ else
+ {
+ Win32Helper.OpenFolderAndSelectFile(targetPath);
+ }
+ }
+ else
+ {
+ using var explorer = new Process();
+ explorer.StartInfo = new ProcessStartInfo
+ {
+ FileName = explorerInfo.Path.Replace("%d", DirectoryPath),
+ UseShellExecute = true,
+ Arguments = FileNameOrFilePath is null
+ ? explorerInfo.DirectoryArgument.Replace("%d", DirectoryPath)
+ : explorerInfo.FileArgument
+ .Replace("%d", DirectoryPath)
+ .Replace("%f", targetPath)
+ };
+ explorer.Start();
+ }
+ }
+ catch (COMException ex) when (ex.ErrorCode == unchecked((int)0x80004004))
+ {
+ }
+ catch (Win32Exception ex) when (ex.NativeErrorCode == 2)
+ {
+ LogException(nameof(AvaloniaPublicAPI), "File manager not found", ex);
+ ShowMsgError(GetTranslation("fileManagerNotFoundTitle"), GetTranslation("fileManagerNotFound"));
+ }
+ catch (Exception ex)
+ {
+ LogException(nameof(AvaloniaPublicAPI), "Failed to open folder", ex);
+ ShowMsgError(GetTranslation("errorTitle"), GetTranslation("folderOpenError"));
+ }
+ }
+
+ private void OpenUri(Uri uri, bool? inPrivate = null, bool forceBrowser = false)
+ {
+ if (uri.IsFile && !uri.LocalPath.FileOrLocationExists())
+ {
+ ShowMsgError(GetTranslation("errorTitle"), string.Format(GetTranslation("fileNotFoundError"), uri.LocalPath));
+ return;
+ }
+
+ if (forceBrowser || uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
+ {
+ var browserInfo = _settings.CustomBrowser;
+ var path = browserInfo.Path == "*" ? string.Empty : browserInfo.Path;
+
+ try
+ {
+ if (browserInfo.OpenInTab)
+ {
+ uri.AbsoluteUri.OpenInBrowserTab(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg, browserInfo.ExtraArgs);
+ }
+ else
+ {
+ uri.AbsoluteUri.OpenInBrowserWindow(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg, browserInfo.ExtraArgs);
+ }
+ }
+ catch (Exception e)
+ {
+ var tabOrWindow = browserInfo.OpenInTab ? "tab" : "window";
+ var includesExtraArgs = string.IsNullOrWhiteSpace(browserInfo.ExtraArgs)
+ ? string.Empty
+ : ", [including omitted Extra Args]";
+ LogException(nameof(AvaloniaPublicAPI), $"Failed to open URL in browser {tabOrWindow}: {path}, {inPrivate ?? browserInfo.EnablePrivate}, {browserInfo.PrivateArg}{includesExtraArgs}", e);
+ ShowMsgError(GetTranslation("errorTitle"), GetTranslation("browserOpenError"));
+ }
+ }
+ else
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = uri.AbsoluteUri,
+ UseShellExecute = true
+ })?.Dispose();
+ }
+ catch (Exception e)
+ {
+ LogException(nameof(AvaloniaPublicAPI), $"Failed to open: {uri.AbsoluteUri}", e);
+ ShowMsgError(GetTranslation("errorTitle"), e.Message);
+ }
+ }
+ }
+
+ // Clipboard
+ public async void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return;
+ }
+
+ var isFile = File.Exists(text);
+ if (directCopy && (isFile || Directory.Exists(text)))
+ {
+ var exception = await RetryActionOnStaThreadAsync(() =>
+ {
+ var paths = new StringCollection { text };
+ Clipboard.SetFileDropList(paths);
+ });
+
+ if (exception == null)
+ {
+ if (showDefaultNotification)
+ {
+ ShowMsg($"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}", GetTranslation("completedSuccessfully"));
+ }
+ }
+ else
+ {
+ LogException(nameof(AvaloniaPublicAPI), "Failed to copy file/folder to clipboard", exception);
+ ShowMsgError(GetTranslation("failedToCopy"));
+ }
+
+ return;
+ }
+
+ var textException = await RetryActionOnStaThreadAsync(() => Clipboard.SetText(text));
+ if (textException == null)
+ {
+ if (showDefaultNotification)
+ {
+ ShowMsg($"{GetTranslation("copy")} {GetTranslation("textTitle")}", GetTranslation("completedSuccessfully"));
+ }
+ }
+ else
+ {
+ LogException(nameof(AvaloniaPublicAPI), "Failed to copy text to clipboard", textException);
+ ShowMsgError(GetTranslation("failedToCopy"));
+ }
+ }
+
+ private static async Task RetryActionOnStaThreadAsync(Action action, int retryCount = 6, int retryDelay = 150)
+ {
+ for (var i = 0; i < retryCount; i++)
+ {
+ try
+ {
+ await Win32Helper.StartSTATaskAsync(action).ConfigureAwait(false);
+ return null;
+ }
+ catch (Exception e)
+ {
+ if (i == retryCount - 1)
+ {
+ return e;
+ }
+
+ await Task.Delay(retryDelay).ConfigureAwait(false);
+ }
+ }
+
+ return null;
+ }
+
+ // HTTP (delegate to Infrastructure)
+ public Task HttpGetStringAsync(string url, CancellationToken token = default) => Infrastructure.Http.Http.GetAsync(url, token);
+ public Task HttpGetStreamAsync(string url, CancellationToken token = default) => Infrastructure.Http.Http.GetStreamAsync(url, token);
+ public Task HttpDownloadAsync(string url, string filePath, Action? reportProgress = null, CancellationToken token = default) =>
+ Infrastructure.Http.Http.DownloadAsync(url, filePath, reportProgress, token);
+
+ // Plugin management
+ public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword);
+ public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
+ public bool ActionKeywordAssigned(string actionKeyword) => PluginManager.ActionKeywordRegistered(actionKeyword);
+ public bool PluginModified(string id) => PluginManager.PluginModified(id);
+
+ // Paths
+ public string GetDataDirectory() => DataLocation.DataDirectory();
+ public string GetLogDirectory() => Log.CurrentLogDirectory;
+
+ public void RestartApp()
+ {
+ HideMainWindow();
+ SaveAppAllSettings();
+
+ var exePath = Environment.ProcessPath;
+ if (!string.IsNullOrWhiteSpace(exePath))
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = exePath,
+ UseShellExecute = true
+ });
+ }
+
+ if (global::Avalonia.Application.Current?.ApplicationLifetime is global::Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.Shutdown();
+ }
+ }
+
+ public void SaveAppAllSettings()
+ {
+ lock (_saveSettingsLock)
+ {
+ _settings.Save();
+ PluginManager.Save();
+ _getMainViewModel().Save();
+ SavePluginSettings();
+ SavePluginCaches();
+ }
+ }
+
+ public void SavePluginSettings()
+ {
+ foreach (var savable in _pluginJsonStorages.Values)
+ {
+ savable.Save();
+ }
+ }
+
+ public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();
+ public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService().UpdateAppAsync(false);
+ public void ShowMsgError(string title, string subTitle = "") => ShowMsg(title, subTitle, Constant.ErrorIcon, true);
+ public void ShowMsgErrorWithButton(string title, string buttonText, Action buttonAction, string subTitle = "") =>
+ ShowMsgWithButton(title, buttonText, buttonAction, subTitle, Constant.ErrorIcon, true);
+ public void ShowMainWindow() => _getMainViewModel()?.Show();
+ public void FocusQueryTextBox() =>
+ _getMainViewModel()?.RequestQueryTextFocus(new QueryTextFocusRequest(false, false, QueryTextFocusMode.CaretAtEnd));
+ public void HideMainWindow() => _getMainViewModel()?.RequestHide();
+ public bool IsMainWindowVisible() => _getMainViewModel()?.MainWindowVisibility ?? false;
+ public void ShowMsg(string title, string subTitle = "", string iconPath = "") =>
+ ShowMsg(title, subTitle, iconPath, true);
+ public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
+ {
+ NotificationWindow.ShowNotification(title, subTitle, iconPath);
+ }
+ public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle = "", string iconPath = "") =>
+ ShowMsgWithButton(title, buttonText, buttonAction, subTitle, iconPath, true);
+ public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
+ {
+ NotificationWindow.ShowNotification(title, subTitle, iconPath, buttonText, buttonAction);
+ }
+ public void OpenSettingDialog() => _getMainViewModel()?.OpenSettings();
+ public bool OpenPluginSettingsWindow(string pluginId)
+ {
+ try
+ {
+ var plugin = PluginManager.GetPluginForId(pluginId);
+ if (plugin?.Plugin is not ISettingProvider settingProvider)
+ {
+ return false;
+ }
+
+ if (plugin.Plugin is JsonRPCPluginBase jsonRpcPlugin && !jsonRpcPlugin.NeedCreateSettingPanel())
+ {
+ return false;
+ }
+
+ if (settingProvider.CreateSettingPanelAvalonia() != null)
+ {
+ OpenSettingDialog();
+ return true;
+ }
+
+ var settingsControl = settingProvider.CreateSettingPanel();
+ if (settingsControl == null)
+ {
+ return false;
+ }
+
+ WpfSettingsWindow.Show(settingsControl, plugin.Metadata.Name);
+ return true;
+ }
+ catch (Exception e)
+ {
+ Log.Exception(nameof(AvaloniaPublicAPI), $"Failed to open plugin settings window for plugin id '{pluginId}'", e);
+ return false;
+ }
+ }
+ public void RegisterGlobalKeyboardCallback(Func callback)
+ {
+ lock (_globalKeyboardHandlersLock)
+ {
+ _globalKeyboardHandlers.Add(callback);
+ }
+ }
+
+ public void RemoveGlobalKeyboardCallback(Func callback)
+ {
+ lock (_globalKeyboardHandlersLock)
+ {
+ _globalKeyboardHandlers.Remove(callback);
+ }
+ }
+
+ public T LoadSettingJsonStorage() where T : new()
+ {
+ var type = typeof(T);
+ var storage = (PluginJsonStorage)_pluginJsonStorages.GetOrAdd(type, _ => new PluginJsonStorage());
+ return storage.Load();
+ }
+
+ public void SaveSettingJsonStorage() where T : new()
+ {
+ var type = typeof(T);
+ var storage = (PluginJsonStorage)_pluginJsonStorages.GetOrAdd(type, _ => new PluginJsonStorage());
+ storage.Save();
+ }
+
+ public void ToggleGameMode() => _gameModeStatus = !_gameModeStatus;
+ public void SetGameMode(bool value) => _gameModeStatus = value;
+ public bool IsGameModeOn() => _gameModeStatus;
+ public void ReQuery(bool reselect = true) => _getMainViewModel().ReQuery(reselect);
+ public void BackToQueryResults() => _getMainViewModel().BackToQueryResults();
+ public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK)
+ {
+ if (System.Windows.Application.Current?.Dispatcher != null)
+ {
+ return System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ System.Windows.MessageBox.Show(messageBoxText, caption, button, icon, defaultResult));
+ }
+
+ return System.Windows.MessageBox.Show(messageBoxText, caption, button, icon, defaultResult);
+ }
+ public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action? cancelProgress = null) =>
+ ProgressBoxWindow.ShowAsync(caption, reportProgressAsync, cancelProgress);
+ public void StartLoadingBar() => _getMainViewModel().IsQueryRunning = true;
+ public void StopLoadingBar() => _getMainViewModel().IsQueryRunning = false;
+ private Flow.Launcher.Core.Resource.Theme Theme => _theme ??= new Flow.Launcher.Core.Resource.Theme(this, _settings);
+ public List GetAvailableThemes() => Theme.GetAvailableThemes();
+ public ThemeData GetCurrentTheme() => Theme.GetCurrentTheme();
+ public bool SetCurrentTheme(ThemeData theme) => Theme.ChangeTheme(theme.FileNameWithoutExtension);
+
+ public void SavePluginCaches()
+ {
+ foreach (var savable in _pluginBinaryStorages.Values)
+ {
+ savable.Save();
+ }
+ }
+
+ public async Task LoadCacheBinaryStorageAsync(string cacheName, string cacheDirectory, T defaultData) where T : new()
+ {
+ var type = typeof(T);
+ var storage = (PluginBinaryStorage)_pluginBinaryStorages.GetOrAdd((cacheName, cacheDirectory, type),
+ _ => new PluginBinaryStorage(cacheName, cacheDirectory));
+
+ try
+ {
+ return await storage.TryLoadAsync(defaultData);
+ }
+ catch (Exception e)
+ {
+ Log.Warn(nameof(AvaloniaPublicAPI), $"Failed to load cache '{cacheName}' from '{cacheDirectory}'. Falling back to default data. {e.Message}");
+ return defaultData;
+ }
+ }
+
+ public async Task SaveCacheBinaryStorageAsync(string cacheName, string cacheDirectory) where T : new()
+ {
+ var type = typeof(T);
+ var storage = (PluginBinaryStorage)_pluginBinaryStorages.GetOrAdd((cacheName, cacheDirectory, type),
+ _ => new PluginBinaryStorage(cacheName, cacheDirectory));
+
+ await storage.SaveAsync();
+ }
+
+ public ValueTask LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true) =>
+ Flow.Launcher.Infrastructure.Image.ImageLoader.LoadAsync(path, loadFullImage, cacheImage);
+
+ public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) =>
+ PluginsManifest.UpdateManifestAsync(usePrimaryUrlOnly, token);
+ public IReadOnlyList GetPluginManifest() => PluginsManifest.UserPlugins ?? new List();
+ public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) =>
+ PluginManager.UpdatePluginAsync(pluginMetadata, plugin, zipFilePath);
+ public bool InstallPlugin(UserPlugin plugin, string zipFilePath) => PluginManager.InstallPlugin(plugin, zipFilePath);
+ public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) =>
+ PluginManager.UninstallPluginAsync(pluginMetadata, removePluginSettings);
+ public bool IsApplicationDarkTheme() =>
+ global::Avalonia.Application.Current?.ActualThemeVariant == global::Avalonia.Styling.ThemeVariant.Dark;
+
+ public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "")
+ { var sw = System.Diagnostics.Stopwatch.StartNew(); action(); sw.Stop(); LogDebug(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
+ public async Task StopwatchLogDebugAsync(string className, string message, Func action, [CallerMemberName] string methodName = "")
+ { var sw = System.Diagnostics.Stopwatch.StartNew(); await action(); sw.Stop(); LogDebug(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
+ public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = "")
+ { var sw = System.Diagnostics.Stopwatch.StartNew(); action(); sw.Stop(); LogInfo(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
+ public async Task StopwatchLogInfoAsync(string className, string message, Func action, [CallerMemberName] string methodName = "")
+ { var sw = System.Diagnostics.Stopwatch.StartNew(); await action(); sw.Stop(); LogInfo(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
+
+ private bool KListenerHookedKeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state)
+ {
+ var continueHook = true;
+ Func[] handlers;
+ lock (_globalKeyboardHandlersLock)
+ {
+ handlers = _globalKeyboardHandlers.ToArray();
+ }
+
+ foreach (var callback in handlers)
+ {
+ continueHook &= callback((int)keyEvent, vkCode, state);
+ }
+
+ return continueHook;
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Converters/BoolToIsVisibleConverter.cs b/Flow.Launcher.Avalonia/Converters/BoolToIsVisibleConverter.cs
new file mode 100644
index 00000000000..e8d0211a464
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Converters/BoolToIsVisibleConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace Flow.Launcher.Avalonia.Converters;
+
+///
+/// Converts a boolean value to IsVisible (Avalonia uses bool for visibility, not Visibility enum)
+///
+public class BoolToIsVisibleConverter : IValueConverter
+{
+ ///
+ /// If true, inverts the boolean value (true becomes false, false becomes true)
+ ///
+ public bool Invert { get; set; }
+
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return Invert ? !boolValue : boolValue;
+ }
+ return false;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return Invert ? !boolValue : boolValue;
+ }
+ return false;
+ }
+}
diff --git a/Flow.Launcher.Avalonia/Converters/CommonConverters.cs b/Flow.Launcher.Avalonia/Converters/CommonConverters.cs
new file mode 100644
index 00000000000..669b9b5a0fb
--- /dev/null
+++ b/Flow.Launcher.Avalonia/Converters/CommonConverters.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace Flow.Launcher.Avalonia.Converters;
+
+///
+/// Converts text with highlight indices to InlineCollection with bold highlights.
+/// Usage: MultiBinding with [0]=text string, [1]=List<int> of character indices to highlight.
+///
+public class HighlightTextConverter : IMultiValueConverter
+{
+ public object? Convert(IList