From 9ce911771e073011c9526f9700ccded412087ef4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 11:31:54 +0800 Subject: [PATCH 01/51] Improve code quality --- Flow.Launcher/App.xaml.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 6e053db29c8..d002ba178b0 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -181,12 +181,14 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // So set to OnExplicitShutdown to prevent the application from shutting down before main window is created Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + // Setup log level before any logging is done Log.SetLogLevel(_settings.LogLevel); // Update dynamic resources base on settings Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont); Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont); + // Initialize notification system before any notification api is called Notification.Install(); // Enable Win32 dark mode if the system is in dark mode before creating all windows @@ -195,6 +197,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // Initialize language before portable clean up since it needs translations await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + // Clean up after portability update Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate(); API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------"); From aee46e864057ca1176f6aac8aec08c550c34c790 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 12:10:06 +0800 Subject: [PATCH 02/51] Initialize quick jump earlier --- Flow.Launcher/App.xaml.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index d002ba178b0..33d6c724c36 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -233,6 +233,11 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; + // Initialize quick jump before hotkey mapper since hotkey mapper will register quick jump hotkey + // Initialize quick jump after main window is created so that it can access main window handle + DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs()); + DialogJump.SetupDialogJump(_settings.EnableDialogJump); + // Initialize hotkey mapper instantly after main window is created because // it will steal focus from main window which causes window hide HotKeyMapper.Initialize(); @@ -240,9 +245,6 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // Initialize theme for main window Ioc.Default.GetRequiredService().ChangeTheme(); - DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs()); - DialogJump.SetupDialogJump(_settings.EnableDialogJump); - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); RegisterExitEvents(); From f5f256809c057bea58840c3baf93baaba39ac0e2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 13:52:11 +0800 Subject: [PATCH 03/51] Add IResultUpdateRegister interface --- .../Plugin/IResultUpdateRegister.cs | 12 ++++ Flow.Launcher/ViewModel/MainViewModel.cs | 68 +++++++++---------- 2 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs diff --git a/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs b/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs new file mode 100644 index 00000000000..1da04bf01a6 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs @@ -0,0 +1,12 @@ +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin; + +public interface IResultUpdateRegister +{ + /// + /// Register a plugin to receive results updated event. + /// + /// + void RegisterResultsUpdatedEvent(PluginPair pair); +} diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 5eae23c2730..5816f1a851d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -27,7 +27,7 @@ namespace Flow.Launcher.ViewModel { - public partial class MainViewModel : BaseModel, ISavable, IDisposable + public partial class MainViewModel : BaseModel, ISavable, IDisposable, IResultUpdateRegister { #region Private Fields @@ -274,52 +274,50 @@ void continueAction(Task t) } } - public void RegisterResultsUpdatedEvent() + public void RegisterResultsUpdatedEvent(PluginPair pair) { - foreach (var pair in PluginManager.GetResultUpdatePlugin()) + if (pair.Plugin is not IResultUpdated plugin) return; + + plugin.ResultsUpdated += (s, e) => { - var plugin = (IResultUpdated)pair.Plugin; - plugin.ResultsUpdated += (s, e) => + if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested) { - if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested) - { - return; - } + return; + } - var token = e.Token == default ? _updateToken : e.Token; + var token = e.Token == default ? _updateToken : e.Token; - IReadOnlyList resultsCopy; - if (e.Results == null) - { - resultsCopy = _emptyResult; - } - else - { - // make a clone to avoid possible issue that plugin will also change the list and items when updating view model - resultsCopy = DeepCloneResults(e.Results, false, token); - } + IReadOnlyList resultsCopy; + if (e.Results == null) + { + resultsCopy = _emptyResult; + } + else + { + // make a clone to avoid possible issue that plugin will also change the list and items when updating view model + resultsCopy = DeepCloneResults(e.Results, false, token); + } - foreach (var result in resultsCopy) + foreach (var result in resultsCopy) + { + if (string.IsNullOrEmpty(result.BadgeIcoPath)) { - if (string.IsNullOrEmpty(result.BadgeIcoPath)) - { - result.BadgeIcoPath = pair.Metadata.IcoPath; - } + result.BadgeIcoPath = pair.Metadata.IcoPath; } + } - PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); + PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); - if (token.IsCancellationRequested) return; + if (token.IsCancellationRequested) return; - App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>"); + App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>"); - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, - token))) - { - App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); - } - }; - } + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, + token))) + { + App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); + } + }; } private async Task RegisterClockAndDateUpdateAsync() From d4e672d630f035a7157323db40fda72d2768ee15 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 13:55:47 +0800 Subject: [PATCH 04/51] Add support to update translation for one plugin --- .../Resource/Internationalization.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index d2ab2d028df..37e4966de3b 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -367,6 +367,22 @@ public static void UpdatePluginMetadataTranslations() } } + public static void UpdatePluginMetadataTranslation(PluginPair p) + { + // Update plugin metadata name & description + if (p.Plugin is not IPluginI18n pluginI18N) return; + try + { + p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); + p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); + pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); + } + } + #endregion } } From c2157e2df1625f8a86d7b22d881cfd4a0e1f8640 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 14:03:45 +0800 Subject: [PATCH 05/51] Add support for concurrent operation of explorers & dialogs adding --- .../DialogJump/DialogJump.cs | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index 65652878fc8..756aa89c1db 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -13,6 +13,7 @@ using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Accessibility; +using System.Collections.Concurrent; namespace Flow.Launcher.Infrastructure.DialogJump { @@ -64,12 +65,12 @@ public static class DialogJump private static HWND _mainWindowHandle = HWND.Null; - private static readonly Dictionary _dialogJumpExplorers = new(); + private static readonly ConcurrentDictionary _dialogJumpExplorers = new(); private static DialogJumpExplorerPair _lastExplorer = null; private static readonly object _lastExplorerLock = new(); - private static readonly Dictionary _dialogJumpDialogs = new(); + private static readonly ConcurrentDictionary _dialogJumpDialogs = new(); private static IDialogJumpDialogWindow _dialogWindow = null; private static readonly object _dialogWindowLock = new(); @@ -105,22 +106,13 @@ public static class DialogJump #region Initialize & Setup - public static void InitializeDialogJump(IList dialogJumpExplorers, - IList dialogJumpDialogs) + public static void InitializeDialogJump() { if (_initialized) return; - // Initialize Dialog Jump explorers & dialogs - _dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null); - foreach (var explorer in dialogJumpExplorers) - { - _dialogJumpExplorers.Add(explorer, null); - } - _dialogJumpDialogs.Add(WindowsDialogJumpDialog, null); - foreach (var dialog in dialogJumpDialogs) - { - _dialogJumpDialogs.Add(dialog, null); - } + // Initialize preinstalled Dialog Jump explorers & dialogs + _dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null); + _dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null); // Initialize main window handle _mainWindowHandle = Win32Helper.GetMainWindowHandle(); @@ -135,6 +127,29 @@ public static void InitializeDialogJump(IList dialogJump _initialized = true; } + public static void InitializeDialogJumpPlugin(PluginPair pair) + { + // Add Dialog Jump explorers & dialogs + if (pair.Plugin is IDialogJumpExplorer explorer) + { + var dialogJumpExplorer = new DialogJumpExplorerPair + { + Plugin = explorer, + Metadata = pair.Metadata + }; + _dialogJumpExplorers.TryAdd(dialogJumpExplorer, null); + } + else if (pair.Plugin is IDialogJumpDialog dialog) + { + var dialogJumpDialog = new DialogJumpDialogPair + { + Plugin = dialog, + Metadata = pair.Metadata + }; + _dialogJumpDialogs.TryAdd(dialogJumpDialog, null); + } + } + public static void SetupDialogJump(bool enabled) { if (enabled == _enabled) return; From 6beeecb0f740f98652271f083fb7826d8d37510a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 14:26:24 +0800 Subject: [PATCH 06/51] Use async load & initialization model --- Flow.Launcher.Core/Plugin/PluginManager.cs | 206 ++++++++++-------- Flow.Launcher/App.xaml.cs | 35 +-- Flow.Launcher/MainWindow.xaml.cs | 4 +- Flow.Launcher/PublicAPIInstance.cs | 2 +- .../SettingsPanePluginsViewModel.cs | 2 +- Flow.Launcher/ViewModel/MainViewModel.cs | 6 +- 6 files changed, 140 insertions(+), 115 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index a4ab8de08ae..51530705e09 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -8,6 +8,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; +using Flow.Launcher.Core.Resource; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; @@ -25,24 +26,21 @@ public static class PluginManager { private static readonly string ClassName = nameof(PluginManager); - public static List AllPlugins { get; private set; } - public static readonly HashSet GlobalPlugins = new(); - public static readonly Dictionary NonGlobalPlugins = new(); - // We should not initialize API in static constructor because it will create another API instance private static IPublicAPI api = null; private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - private static PluginsSettings Settings; - private static readonly ConcurrentBag ModifiedPlugins = new(); + private static readonly ConcurrentDictionary _allPlugins = []; + private static readonly ConcurrentDictionary _globalPlugins = []; + private static readonly ConcurrentDictionary _nonGlobalPlugins = []; - private static IEnumerable _contextMenuPlugins; - private static IEnumerable _homePlugins; - private static IEnumerable _resultUpdatePlugin; - private static IEnumerable _translationPlugins; + private static PluginsSettings Settings; + private static readonly ConcurrentBag ModifiedPlugins = []; - private static readonly List _dialogJumpExplorerPlugins = new(); - private static readonly List _dialogJumpDialogPlugins = new(); + private static readonly ConcurrentBag _contextMenuPlugins = []; + private static readonly ConcurrentBag _homePlugins = []; + private static readonly ConcurrentBag _translationPlugins = []; + private static readonly ConcurrentBag _externalPreviewPlugins = []; /// /// Directories that will hold Flow Launcher plugin directory @@ -61,12 +59,22 @@ private static void DeletePythonBinding() } } + public static List GetAllPlugins() + { + return [.. _allPlugins.Values]; + } + + public static Dictionary GetNonGlobalPlugins() + { + return _nonGlobalPlugins.ToDictionary(); + } + /// /// Save json and ISavable /// public static void Save() { - foreach (var pluginPair in AllPlugins) + foreach (var pluginPair in GetAllPlugins()) { var savable = pluginPair.Plugin as ISavable; try @@ -85,7 +93,7 @@ public static void Save() public static async ValueTask DisposePluginsAsync() { - foreach (var pluginPair in AllPlugins) + foreach (var pluginPair in GetAllPlugins()) { await DisposePluginAsync(pluginPair); } @@ -113,7 +121,7 @@ private static async Task DisposePluginAsync(PluginPair pluginPair) public static async Task ReloadDataAsync() { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch { IReloadable p => Task.Run(p.ReloadData), IAsyncReloadable p => p.ReloadDataAsync(), @@ -123,7 +131,7 @@ public static async Task ReloadDataAsync() public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, @@ -132,7 +140,7 @@ public static async Task OpenExternalPreviewAsync(string path, bool sendFailToas public static async Task CloseExternalPreviewAsync() { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.ClosePreviewAsync(), _ => Task.CompletedTask, @@ -141,7 +149,7 @@ public static async Task CloseExternalPreviewAsync() public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, @@ -150,12 +158,12 @@ public static async Task SwitchExternalPreviewAsync(string path, bool sendFailTo public static bool UseExternalPreview() { - return GetPluginsForInterface().Any(x => !x.Metadata.Disabled); + return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled); } public static bool AllowAlwaysPreview() { - var plugin = GetPluginsForInterface().FirstOrDefault(x => !x.Metadata.Disabled); + var plugin = GetExternalPreviewPlugins().FirstOrDefault(x => !x.Metadata.Disabled); if (plugin is null) return false; @@ -176,38 +184,19 @@ static PluginManager() /// todo happlebao The API should be removed /// /// - public static void LoadPlugins(PluginsSettings settings) + public static List LoadPlugins(PluginsSettings settings) { var metadatas = PluginConfig.Parse(Directories); Settings = settings; Settings.UpdatePluginSettings(metadatas); - AllPlugins = PluginsLoader.Plugins(metadatas, Settings); + + // Load plugins + var allPlugins = PluginsLoader.Plugins(metadatas, Settings); + // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins UpdatePluginDirectory(metadatas); - // Initialize plugin enumerable after all plugins are initialized - _contextMenuPlugins = GetPluginsForInterface(); - _homePlugins = GetPluginsForInterface(); - _resultUpdatePlugin = GetPluginsForInterface(); - _translationPlugins = GetPluginsForInterface(); - - // Initialize Dialog Jump plugin pairs - foreach (var pair in GetPluginsForInterface()) - { - _dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair - { - Plugin = (IDialogJumpExplorer)pair.Plugin, - Metadata = pair.Metadata - }); - } - foreach (var pair in GetPluginsForInterface()) - { - _dialogJumpDialogPlugins.Add(new DialogJumpDialogPair - { - Plugin = (IDialogJumpDialog)pair.Plugin, - Metadata = pair.Metadata - }); - } + return allPlugins; } private static void UpdatePluginDirectory(List metadatas) @@ -241,11 +230,11 @@ private static void UpdatePluginDirectory(List metadatas) /// Call initialize for all plugins /// /// return the list of failed to init plugins or null for none - public static async Task InitializePluginsAsync() + public static async Task InitializePluginsAsync(List allPlugins, IResultUpdateRegister register) { var failedPlugins = new ConcurrentQueue(); - var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate + var InitTasks = allPlugins.Select(pair => Task.Run(async delegate { try { @@ -255,6 +244,9 @@ public static async Task InitializePluginsAsync() pair.Metadata.InitTime += milliseconds; API.LogInfo(ClassName, $"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); + + // Add it in all plugin list + _allPlugins.TryAdd(pair.Metadata.ID, pair); } catch (Exception e) { @@ -272,30 +264,59 @@ public static async Task InitializePluginsAsync() failedPlugins.Enqueue(pair); API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); } + + // Even if the plugin cannot be initialized, we still need to add it in all plugin list + _allPlugins.TryAdd(pair.Metadata.ID, pair); + return; } - })); - await Task.WhenAll(InitTasks); + // Initialize plugin lists after the plugin is initialized + if (pair.Plugin is IContextMenu) + { + _contextMenuPlugins.Add(pair); + } + if (pair.Plugin is IAsyncHomeQuery) + { + _homePlugins.Add(pair); + } + if (pair.Plugin is IPluginI18n) + { + _translationPlugins.Add(pair); + } + if (pair.Plugin is IAsyncExternalPreview) + { + _externalPreviewPlugins.Add(pair); + } - foreach (var plugin in AllPlugins) - { + // Register ResultsUpdated event so that plugin query can use results updated interface + register.RegisterResultsUpdatedEvent(pair); + + // Register plugin's action keywords so that plugins can be queried in results // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin // has multiple global and action keywords because we will only add them here once. - foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct()) + foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct()) { switch (actionKeyword) { case Query.GlobalPluginWildcardSign: - GlobalPlugins.Add(plugin); + _globalPlugins.TryAdd(pair.Metadata.ID, pair); break; default: - NonGlobalPlugins[actionKeyword] = plugin; + _nonGlobalPlugins.TryAdd(actionKeyword, pair); break; } } - } - if (failedPlugins.Any()) + // Update plugin metadata translation after the plugin is initialized with IPublicAPI instance + Internationalization.UpdatePluginMetadataTranslation(pair); + + // Add plugin to Dialog Jump plugin list after the plugin is initialized + DialogJump.InitializeDialogJumpPlugin(pair); + })); + + await Task.WhenAll(InitTasks); + + if (!failedPlugins.IsEmpty) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); API.ShowMsg( @@ -315,12 +336,12 @@ public static ICollection ValidPluginsForQuery(Query query, bool dia if (query is null) return Array.Empty(); - if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) + if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) { if (dialogJump) - return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList(); + return _globalPlugins.Values.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList(); else - return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return _globalPlugins.Values.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } if (dialogJump && plugin.Plugin is not IAsyncDialogJump) @@ -329,10 +350,10 @@ public static ICollection ValidPluginsForQuery(Query query, bool dia if (API.PluginModified(plugin.Metadata.ID)) return Array.Empty(); - return new List - { + return + [ plugin - }; + ]; } public static ICollection ValidPluginsForHomeQuery() @@ -466,18 +487,7 @@ public static void UpdatePluginMetadata(IReadOnlyList results, PluginMet /// public static PluginPair GetPluginForId(string id) { - return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id); - } - - private static IEnumerable GetPluginsForInterface() where T : IFeatures - { - // Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup - return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty(); - } - - public static IList GetResultUpdatePlugin() - { - return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return GetAllPlugins().FirstOrDefault(o => o.Metadata.ID == id); } public static IList GetTranslationPlugins() @@ -519,14 +529,9 @@ public static bool IsHomePlugin(string id) return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id); } - public static IList GetDialogJumpExplorers() + private static List GetExternalPreviewPlugins() { - return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); - } - - public static IList GetDialogJumpDialogs() - { - return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } public static bool ActionKeywordRegistered(string actionKeyword) @@ -534,7 +539,7 @@ public static bool ActionKeywordRegistered(string actionKeyword) // this method is only checking for action keywords (defined as not '*') registration // hence the actionKeyword != Query.GlobalPluginWildcardSign logic return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + && _nonGlobalPlugins.ContainsKey(actionKeyword); } /// @@ -546,11 +551,18 @@ public static void AddActionKeyword(string id, string newActionKeyword) var plugin = GetPluginForId(id); if (newActionKeyword == Query.GlobalPluginWildcardSign) { - GlobalPlugins.Add(plugin); + _globalPlugins.TryAdd(id, plugin); } else { - NonGlobalPlugins[newActionKeyword] = plugin; + if (_nonGlobalPlugins.TryGetValue(newActionKeyword, out var item)) + { + _nonGlobalPlugins.TryUpdate(newActionKeyword, plugin, item); + } + else + { + _nonGlobalPlugins.TryAdd(newActionKeyword, plugin); + } } // Update action keywords and action keyword in plugin metadata @@ -577,11 +589,19 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword) plugin.Metadata.ActionKeywords .Count(x => x == Query.GlobalPluginWildcardSign) == 1) { - GlobalPlugins.Remove(plugin); + _globalPlugins.TryRemove(id, out _); } if (oldActionkeyword != Query.GlobalPluginWildcardSign) - NonGlobalPlugins.Remove(oldActionkeyword); + { + _nonGlobalPlugins.TryRemove(oldActionkeyword, out var item); + // If the removed item is not the same as the plugin being removed, + // we should add it back to non-global plugins + if (item.Metadata.ID != id) + { + _nonGlobalPlugins.TryAdd(oldActionkeyword, item); + } + } // Update action keywords and action keyword in plugin metadata plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); @@ -620,7 +640,7 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) if (!Version.TryParse(newMetadata.Version, out var newVersion)) return true; // If version is not valid, we assume it is lesser than any existing version - return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID + return GetAllPlugins().Any(x => x.Metadata.ID == newMetadata.ID && Version.TryParse(x.Metadata.Version, out var version) && newVersion <= version); } @@ -760,7 +780,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo // If we want to remove plugin from AllPlugins, // we need to dispose them so that they can release file handles // which can help FL to delete the plugin settings & cache folders successfully - var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID); + var pluginPairs = GetAllPlugins().Where(p => p.Metadata.ID == plugin.ID).ToList(); foreach (var pluginPair in pluginPairs) { await DisposePluginAsync(pluginPair); @@ -805,12 +825,12 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); } Settings.RemovePluginSettings(plugin.ID); - AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); - GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID); - var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); + _allPlugins.TryRemove(plugin.ID, out var item); + _globalPlugins.TryRemove(plugin.ID, out var item1); + var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); foreach (var key in keysToRemove) { - NonGlobalPlugins.Remove(key); + _nonGlobalPlugins.Remove(key, out var item2); } } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 33d6c724c36..82f49958cc5 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -209,23 +209,11 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => var imageLoadertask = ImageLoader.InitializeAsync(); - AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); - - PluginManager.LoadPlugins(_settings.PluginSettings); - - // Register ResultsUpdated event after all plugins are loaded - Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); - Http.Proxy = _settings.Proxy; // Initialize plugin manifest before initializing plugins so that they can use the manifest instantly await API.UpdatePluginManifestAsync(); - await PluginManager.InitializePluginsAsync(); - - // Update plugin titles after plugins are initialized with their api instances - Internationalization.UpdatePluginMetadataTranslations(); - await imageLoadertask; _mainWindow = new MainWindow(); @@ -235,7 +223,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // Initialize quick jump before hotkey mapper since hotkey mapper will register quick jump hotkey // Initialize quick jump after main window is created so that it can access main window handle - DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs()); + DialogJump.InitializeDialogJump(); DialogJump.SetupDialogJump(_settings.EnableDialogJump); // Initialize hotkey mapper instantly after main window is created because @@ -251,10 +239,27 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => AutoStartup(); AutoUpdates(); - AutoPluginUpdates(); API.SaveAppAllSettings(); - API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------"); + API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------"); + + _ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => + { + API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------"); + + AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); + + var allPlugins = PluginManager.LoadPlugins(_settings.PluginSettings); + + await PluginManager.InitializePluginsAsync(allPlugins, _mainVM); + + AutoPluginUpdates(); + + // Save all settings since we possibly update the plugin environment paths + API.SaveAppAllSettings(); + + API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------"); + }); }); } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2ddce81900e..2c72332189f 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Media; @@ -476,7 +476,7 @@ private void OnKeyDown(object sender, KeyEventArgs e) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) { var queryWithoutActionKeyword = - QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search; if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) { diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index d865a087b77..dc64aefa922 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -251,7 +251,7 @@ private static async Task RetryActionOnSTAThreadAsync(Action action, public string GetTranslation(string key) => Internationalization.GetTranslation(key); - public List GetAllPlugins() => PluginManager.AllPlugins.ToList(); + public List GetAllPlugins() => PluginManager.GetAllPlugins(); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index 3e1294bc2d2..9e20d7a4a1b 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -115,7 +115,7 @@ public string FilterText } private IList? _pluginViewModels; - public IList PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins + public IList PluginViewModels => _pluginViewModels ??= PluginManager.GetAllPlugins() .OrderBy(plugin => plugin.Metadata.Disabled) .ThenBy(plugin => plugin.Metadata.Name) .Select(plugin => new PluginViewModel diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 5816f1a851d..0b491175ff8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -437,7 +437,7 @@ private void LoadContextMenu() [RelayCommand] private void Backspace(object index) { - var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); + var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.GetNonGlobalPlugins()); // GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\')); @@ -1573,7 +1573,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable ConstructQueryAsync(string queryText, IEnumerable builtInShortcuts, From 59a7a2c807bbb161139296ad93551ebbb1224fd9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 15:59:27 +0800 Subject: [PATCH 07/51] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 35 ++++++++++------------ 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 51530705e09..60e832f697e 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -46,9 +46,9 @@ public static class PluginManager /// Directories that will hold Flow Launcher plugin directory /// public static readonly string[] Directories = - { + [ Constant.PreinstalledDirectory, DataLocation.PluginsDirectory - }; + ]; private static void DeletePythonBinding() { @@ -121,39 +121,39 @@ private static async Task DisposePluginAsync(PluginPair pluginPair) public static async Task ReloadDataAsync() { - await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch { IReloadable p => Task.Run(p.ReloadData), IAsyncReloadable p => p.ReloadDataAsync(), _ => Task.CompletedTask, - }).ToArray()); + })]); } public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, - }).ToArray()); + })]); } public static async Task CloseExternalPreviewAsync() { - await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.ClosePreviewAsync(), _ => Task.CompletedTask, - }).ToArray()); + })]); } public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll(GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, - }).ToArray()); + })]); } public static bool UseExternalPreview() @@ -339,9 +339,9 @@ public static ICollection ValidPluginsForQuery(Query query, bool dia if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) { if (dialogJump) - return _globalPlugins.Values.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList(); + return [.. _globalPlugins.Values.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; else - return _globalPlugins.Values.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return [.. _globalPlugins.Values.Where(p => !PluginModified(p.Metadata.ID))]; } if (dialogJump && plugin.Plugin is not IAsyncDialogJump) @@ -350,15 +350,12 @@ public static ICollection ValidPluginsForQuery(Query query, bool dia if (API.PluginModified(plugin.Metadata.ID)) return Array.Empty(); - return - [ - plugin - ]; + return [plugin]; } public static ICollection ValidPluginsForHomeQuery() { - return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))]; } public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) @@ -492,7 +489,7 @@ public static PluginPair GetPluginForId(string id) public static IList GetTranslationPlugins() { - return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))]; } public static List GetContextMenusForPlugin(Result result) @@ -531,7 +528,7 @@ public static bool IsHomePlugin(string id) private static List GetExternalPreviewPlugins() { - return _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))]; } public static bool ActionKeywordRegistered(string actionKeyword) From 52bb909f0bdd404376d510b498854688e0df94c8 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:00:42 +0800 Subject: [PATCH 08/51] Add plugin to all plugin list later --- Flow.Launcher.Core/Plugin/PluginManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 60e832f697e..8e03171caf0 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -244,9 +244,6 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe pair.Metadata.InitTime += milliseconds; API.LogInfo(ClassName, $"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); - - // Add it in all plugin list - _allPlugins.TryAdd(pair.Metadata.ID, pair); } catch (Exception e) { @@ -312,6 +309,9 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe // Add plugin to Dialog Jump plugin list after the plugin is initialized DialogJump.InitializeDialogJumpPlugin(pair); + + // Add plugin to all plugin list + _allPlugins.TryAdd(pair.Metadata.ID, pair); })); await Task.WhenAll(InitTasks); From 0a01d85f4051f298d83b22fc024c6f0474e464b6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:16:21 +0800 Subject: [PATCH 09/51] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0b491175ff8..2299e4e9857 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1327,7 +1327,7 @@ private static List GetHistoryItems(IEnumerable historyItem private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { - _updateSource?.Cancel(); + await _updateSource?.CancelAsync(); App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>"); @@ -1906,7 +1906,7 @@ public async Task SetupDialogJumpAsync(nint handle) if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { // Cancel the previous Dialog Jump task - _dialogJumpSource?.Cancel(); + await _dialogJumpSource?.CancelAsync(); // Create a new cancellation token source _dialogJumpSource = new CancellationTokenSource(); From c3a598464f695e9e5b3ad8fda6827aeb555bb03e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:20:09 +0800 Subject: [PATCH 10/51] Fix code comments --- Flow.Launcher.Core/Plugin/PluginManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 8e03171caf0..bb6b8022e3f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -180,10 +180,10 @@ static PluginManager() } /// - /// because InitializePlugins needs API, so LoadPlugins needs to be called first - /// todo happlebao The API should be removed + /// Load plugins from the directories specified in Directories. /// /// + /// public static List LoadPlugins(PluginsSettings settings) { var metadatas = PluginConfig.Parse(Directories); From 35c8e39beb7ea054309c323ad5286a089222e32d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:23:40 +0800 Subject: [PATCH 11/51] Improve code comments --- Flow.Launcher.Core/Plugin/PluginManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index bb6b8022e3f..89b4e65e46e 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -227,8 +227,10 @@ private static void UpdatePluginDirectory(List metadatas) } /// - /// Call initialize for all plugins + /// Initialize all plugins asynchronously. /// + /// List of all plugins to initialize. + /// The register to register results updated event for each plugin. /// return the list of failed to init plugins or null for none public static async Task InitializePluginsAsync(List allPlugins, IResultUpdateRegister register) { From 1d3ab39dca4919b96052d943b4ac3eccee0cdf54 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:24:23 +0800 Subject: [PATCH 12/51] Use () => instead of delegate --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 89b4e65e46e..fe73a763b77 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -236,7 +236,7 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe { var failedPlugins = new ConcurrentQueue(); - var InitTasks = allPlugins.Select(pair => Task.Run(async delegate + var InitTasks = allPlugins.Select(pair => Task.Run(async () => { try { From 445a14278b2f95d684cbc9c89044d6e9b831584f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:25:26 +0800 Subject: [PATCH 13/51] Add code comments --- Flow.Launcher.Core/Plugin/PluginManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index fe73a763b77..96dce620efa 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -264,7 +264,8 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); } - // Even if the plugin cannot be initialized, we still need to add it in all plugin list + // Even if the plugin cannot be initialized, we still need to add it in all plugin list so that + // we can remove the plugin from Plugin or Store page or Plugin Manager plugin. _allPlugins.TryAdd(pair.Metadata.ID, pair); return; } From de568140d238ab2a74f72f783ebca6e3738b56e2 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:27:11 +0800 Subject: [PATCH 14/51] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 43 ++++++++++++---------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 96dce620efa..5edf43a336a 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -270,24 +270,6 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe return; } - // Initialize plugin lists after the plugin is initialized - if (pair.Plugin is IContextMenu) - { - _contextMenuPlugins.Add(pair); - } - if (pair.Plugin is IAsyncHomeQuery) - { - _homePlugins.Add(pair); - } - if (pair.Plugin is IPluginI18n) - { - _translationPlugins.Add(pair); - } - if (pair.Plugin is IAsyncExternalPreview) - { - _externalPreviewPlugins.Add(pair); - } - // Register ResultsUpdated event so that plugin query can use results updated interface register.RegisterResultsUpdatedEvent(pair); @@ -313,8 +295,8 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe // Add plugin to Dialog Jump plugin list after the plugin is initialized DialogJump.InitializeDialogJumpPlugin(pair); - // Add plugin to all plugin list - _allPlugins.TryAdd(pair.Metadata.ID, pair); + // Add plugin to lists after the plugin is initialized + AddPluginToLists(pair); })); await Task.WhenAll(InitTasks); @@ -334,6 +316,27 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe } } + private static void AddPluginToLists(PluginPair pair) + { + if (pair.Plugin is IContextMenu) + { + _contextMenuPlugins.Add(pair); + } + if (pair.Plugin is IAsyncHomeQuery) + { + _homePlugins.Add(pair); + } + if (pair.Plugin is IPluginI18n) + { + _translationPlugins.Add(pair); + } + if (pair.Plugin is IAsyncExternalPreview) + { + _externalPreviewPlugins.Add(pair); + } + _allPlugins.TryAdd(pair.Metadata.ID, pair); + } + public static ICollection ValidPluginsForQuery(Query query, bool dialogJump) { if (query is null) From 55164ef60ffd9b10bcae9b2c500c4a139f692b83 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:29:01 +0800 Subject: [PATCH 15/51] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 37 ++++++++++++---------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 5edf43a336a..e985be51859 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -273,25 +273,12 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe // Register ResultsUpdated event so that plugin query can use results updated interface register.RegisterResultsUpdatedEvent(pair); - // Register plugin's action keywords so that plugins can be queried in results - // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin - // has multiple global and action keywords because we will only add them here once. - foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct()) - { - switch (actionKeyword) - { - case Query.GlobalPluginWildcardSign: - _globalPlugins.TryAdd(pair.Metadata.ID, pair); - break; - default: - _nonGlobalPlugins.TryAdd(actionKeyword, pair); - break; - } - } - // Update plugin metadata translation after the plugin is initialized with IPublicAPI instance Internationalization.UpdatePluginMetadataTranslation(pair); + // Register plugin action keywords so that plugins can be queried in results + RegisterPluginActionKeywords(pair); + // Add plugin to Dialog Jump plugin list after the plugin is initialized DialogJump.InitializeDialogJumpPlugin(pair); @@ -316,6 +303,24 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe } } + private static void RegisterPluginActionKeywords(PluginPair pair) + { + // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin + // has multiple global and action keywords because we will only add them here once. + foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct()) + { + switch (actionKeyword) + { + case Query.GlobalPluginWildcardSign: + _globalPlugins.TryAdd(pair.Metadata.ID, pair); + break; + default: + _nonGlobalPlugins.TryAdd(actionKeyword, pair); + break; + } + } + } + private static void AddPluginToLists(PluginPair pair) { if (pair.Plugin is IContextMenu) From cc681839402d5f5a7f5a16044adddbed98cbe35d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:30:39 +0800 Subject: [PATCH 16/51] Change variable name --- Flow.Launcher.Core/Plugin/PluginManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index e985be51859..aece1e008c2 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -236,7 +236,7 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe { var failedPlugins = new ConcurrentQueue(); - var InitTasks = allPlugins.Select(pair => Task.Run(async () => + var initTasks = allPlugins.Select(pair => Task.Run(async () => { try { @@ -286,7 +286,7 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe AddPluginToLists(pair); })); - await Task.WhenAll(InitTasks); + await Task.WhenAll(initTasks); if (!failedPlugins.IsEmpty) { From 50924e418921df3c6ea23dbe19bcdd17d36523ba Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:45:34 +0800 Subject: [PATCH 17/51] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 109 +++++++++++++++------ 1 file changed, 81 insertions(+), 28 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index aece1e008c2..745b96ff34f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -50,24 +50,7 @@ public static class PluginManager Constant.PreinstalledDirectory, DataLocation.PluginsDirectory ]; - private static void DeletePythonBinding() - { - const string binding = "flowlauncher.py"; - foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory)) - { - File.Delete(Path.Combine(subDirectory, binding)); - } - } - - public static List GetAllPlugins() - { - return [.. _allPlugins.Values]; - } - - public static Dictionary GetNonGlobalPlugins() - { - return _nonGlobalPlugins.ToDictionary(); - } + #region Save & Dispose & Reload Plugin /// /// Save json and ISavable @@ -129,6 +112,10 @@ public static async Task ReloadDataAsync() })]); } + #endregion + + #region External Preview + public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) { await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch @@ -171,6 +158,15 @@ public static bool AllowAlwaysPreview() return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview(); } + private static IList GetExternalPreviewPlugins() + { + return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))]; + } + + #endregion + + #region Constructor + static PluginManager() { // validate user directory @@ -179,6 +175,19 @@ static PluginManager() DeletePythonBinding(); } + private static void DeletePythonBinding() + { + const string binding = "flowlauncher.py"; + foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory)) + { + File.Delete(Path.Combine(subDirectory, binding)); + } + } + + #endregion + + #region Load & Initialize Plugins + /// /// Load plugins from the directories specified in Directories. /// @@ -342,6 +351,10 @@ private static void AddPluginToLists(PluginPair pair) _allPlugins.TryAdd(pair.Metadata.ID, pair); } + #endregion + + #region Validate & Query Plugins + public static ICollection ValidPluginsForQuery(Query query, bool dialogJump) { if (query is null) @@ -350,9 +363,9 @@ public static ICollection ValidPluginsForQuery(Query query, bool dia if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) { if (dialogJump) - return [.. _globalPlugins.Values.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; + return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; else - return [.. _globalPlugins.Values.Where(p => !PluginModified(p.Metadata.ID))]; + return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))]; } if (dialogJump && plugin.Plugin is not IAsyncDialogJump) @@ -473,6 +486,10 @@ public static async Task> QueryDialogJumpForPluginAsync(P return results; } + #endregion + + #region Update Metadata & Get Plugin + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) @@ -498,12 +515,35 @@ public static PluginPair GetPluginForId(string id) return GetAllPlugins().FirstOrDefault(o => o.Metadata.ID == id); } - public static IList GetTranslationPlugins() + #endregion + + #region Get Plugin List + + public static List GetAllPlugins() + { + return [.. _allPlugins.Values]; + } + + public static List GetGlobalPlugins() + { + return [.. _globalPlugins.Values]; + } + + public static Dictionary GetNonGlobalPlugins() + { + return _nonGlobalPlugins.ToDictionary(); + } + + public static List GetTranslationPlugins() { return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))]; } - public static List GetContextMenusForPlugin(Result result) + #endregion + + #region Get Context Menus + + public static IList GetContextMenusForPlugin(Result result) { var results = new List(); var pluginPair = _contextMenuPlugins.Where(p => !PluginModified(p.Metadata.ID)).FirstOrDefault(o => o.Metadata.ID == result.PluginID); @@ -532,15 +572,18 @@ public static List GetContextMenusForPlugin(Result result) return results; } + #endregion + + #region Check Home Plugin + public static bool IsHomePlugin(string id) { return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id); } - private static List GetExternalPreviewPlugins() - { - return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))]; - } + #endregion + + #region Plugin Action Keyword public static bool ActionKeywordRegistered(string actionKeyword) { @@ -623,6 +666,12 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword) } } + #endregion + + #region Plugin Install & Uninstall & Update + + #region Private Functions + private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath) { var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length; @@ -653,7 +702,9 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) && newVersion <= version); } - #region Public functions + #endregion + + #region Public Functions public static bool PluginModified(string id) { @@ -691,7 +742,7 @@ public static async Task UninstallPluginAsync(PluginMetadata plugin, bool #endregion - #region Internal functions + #region Internal Functions internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified) { @@ -854,5 +905,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo } #endregion + + #endregion } } From 8b60d26f5e72b2268f9377cb726d3d24b521780e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:56:14 +0800 Subject: [PATCH 18/51] Fix build issue --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 745b96ff34f..81528b8d6c1 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -543,7 +543,7 @@ public static List GetTranslationPlugins() #region Get Context Menus - public static IList GetContextMenusForPlugin(Result result) + public static List GetContextMenusForPlugin(Result result) { var results = new List(); var pluginPair = _contextMenuPlugins.Where(p => !PluginModified(p.Metadata.ID)).FirstOrDefault(o => o.Metadata.ID == result.PluginID); From 566572b013da3d51d2005cf48dd7134d5e75a284 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 16:57:35 +0800 Subject: [PATCH 19/51] Use api function & rename function --- Flow.Launcher.Core/Plugin/PluginManager.cs | 20 +++++++++---------- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 2 +- Flow.Launcher/PublicAPIInstance.cs | 2 +- .../SettingsPanePluginsViewModel.cs | 5 +++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 81528b8d6c1..fdd4c7fed58 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -57,7 +57,7 @@ public static class PluginManager /// public static void Save() { - foreach (var pluginPair in GetAllPlugins()) + foreach (var pluginPair in GetAllInitializedPlugins()) { var savable = pluginPair.Plugin as ISavable; try @@ -76,7 +76,7 @@ public static void Save() public static async ValueTask DisposePluginsAsync() { - foreach (var pluginPair in GetAllPlugins()) + foreach (var pluginPair in GetAllInitializedPlugins()) { await DisposePluginAsync(pluginPair); } @@ -104,7 +104,7 @@ private static async Task DisposePluginAsync(PluginPair pluginPair) public static async Task ReloadDataAsync() { - await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch { IReloadable p => Task.Run(p.ReloadData), IAsyncReloadable p => p.ReloadDataAsync(), @@ -118,7 +118,7 @@ public static async Task ReloadDataAsync() public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, @@ -127,7 +127,7 @@ public static async Task OpenExternalPreviewAsync(string path, bool sendFailToas public static async Task CloseExternalPreviewAsync() { - await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.ClosePreviewAsync(), _ => Task.CompletedTask, @@ -136,7 +136,7 @@ public static async Task CloseExternalPreviewAsync() public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll([.. GetAllPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, @@ -512,14 +512,14 @@ public static void UpdatePluginMetadata(IReadOnlyList results, PluginMet /// public static PluginPair GetPluginForId(string id) { - return GetAllPlugins().FirstOrDefault(o => o.Metadata.ID == id); + return GetAllInitializedPlugins().FirstOrDefault(o => o.Metadata.ID == id); } #endregion #region Get Plugin List - public static List GetAllPlugins() + public static List GetAllInitializedPlugins() { return [.. _allPlugins.Values]; } @@ -697,7 +697,7 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) if (!Version.TryParse(newMetadata.Version, out var newVersion)) return true; // If version is not valid, we assume it is lesser than any existing version - return GetAllPlugins().Any(x => x.Metadata.ID == newMetadata.ID + return GetAllInitializedPlugins().Any(x => x.Metadata.ID == newMetadata.ID && Version.TryParse(x.Metadata.Version, out var version) && newVersion <= version); } @@ -839,7 +839,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo // If we want to remove plugin from AllPlugins, // we need to dispose them so that they can release file handles // which can help FL to delete the plugin settings & cache folders successfully - var pluginPairs = GetAllPlugins().Where(p => p.Metadata.ID == plugin.ID).ToList(); + var pluginPairs = GetAllInitializedPlugins().Where(p => p.Metadata.ID == plugin.ID).ToList(); foreach (var pluginPair in pluginPairs) { await DisposePluginAsync(pluginPair); diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index cfa813d3f2b..4cefe4bc63f 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -171,7 +171,7 @@ public interface IPublicAPI string GetTranslation(string key); /// - /// Get all loaded plugins + /// Get all initialized plugins /// /// List GetAllPlugins(); diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index dc64aefa922..bae953bbc34 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -251,7 +251,7 @@ private static async Task RetryActionOnSTAThreadAsync(Action action, public string GetTranslation(string key) => Internationalization.GetTranslation(key); - public List GetAllPlugins() => PluginManager.GetAllPlugins(); + public List GetAllPlugins() => PluginManager.GetAllInitializedPlugins(); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index 9e20d7a4a1b..8a84a7749f8 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -114,8 +114,9 @@ public string FilterText } } - private IList? _pluginViewModels; - public IList PluginViewModels => _pluginViewModels ??= PluginManager.GetAllPlugins() + private List? _pluginViewModels; + // Get all initialized plugins and ignore those that are not initialized + public List PluginViewModels => _pluginViewModels ??= App.API.GetAllPlugins() .OrderBy(plugin => plugin.Metadata.Disabled) .ThenBy(plugin => plugin.Metadata.Name) .Select(plugin => new PluginViewModel From 11e05f73d99a9df4ecb09daad49a4f836530a9d3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:00:19 +0800 Subject: [PATCH 20/51] Add all loaded plugins --- Flow.Launcher.Core/Plugin/PluginManager.cs | 28 ++++++++++++---------- Flow.Launcher/App.xaml.cs | 4 ++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index fdd4c7fed58..bce2b1fe6e5 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -30,7 +30,8 @@ public static class PluginManager private static IPublicAPI api = null; private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - private static readonly ConcurrentDictionary _allPlugins = []; + private static List _allLoadedPlugins; + private static readonly ConcurrentDictionary _allInitializedPlugins = []; private static readonly ConcurrentDictionary _globalPlugins = []; private static readonly ConcurrentDictionary _nonGlobalPlugins = []; @@ -192,20 +193,17 @@ private static void DeletePythonBinding() /// Load plugins from the directories specified in Directories. /// /// - /// - public static List LoadPlugins(PluginsSettings settings) + public static void LoadPlugins(PluginsSettings settings) { var metadatas = PluginConfig.Parse(Directories); Settings = settings; Settings.UpdatePluginSettings(metadatas); // Load plugins - var allPlugins = PluginsLoader.Plugins(metadatas, Settings); + _allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings); // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins UpdatePluginDirectory(metadatas); - - return allPlugins; } private static void UpdatePluginDirectory(List metadatas) @@ -238,14 +236,13 @@ private static void UpdatePluginDirectory(List metadatas) /// /// Initialize all plugins asynchronously. /// - /// List of all plugins to initialize. /// The register to register results updated event for each plugin. /// return the list of failed to init plugins or null for none - public static async Task InitializePluginsAsync(List allPlugins, IResultUpdateRegister register) + public static async Task InitializePluginsAsync(IResultUpdateRegister register) { var failedPlugins = new ConcurrentQueue(); - var initTasks = allPlugins.Select(pair => Task.Run(async () => + var initTasks = _allLoadedPlugins.Select(pair => Task.Run(async () => { try { @@ -275,7 +272,7 @@ public static async Task InitializePluginsAsync(List allPlugins, IRe // Even if the plugin cannot be initialized, we still need to add it in all plugin list so that // we can remove the plugin from Plugin or Store page or Plugin Manager plugin. - _allPlugins.TryAdd(pair.Metadata.ID, pair); + _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair); return; } @@ -348,7 +345,7 @@ private static void AddPluginToLists(PluginPair pair) { _externalPreviewPlugins.Add(pair); } - _allPlugins.TryAdd(pair.Metadata.ID, pair); + _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair); } #endregion @@ -519,9 +516,14 @@ public static PluginPair GetPluginForId(string id) #region Get Plugin List + public static List GetAllLoadedPlugins() + { + return [.. _allLoadedPlugins]; + } + public static List GetAllInitializedPlugins() { - return [.. _allPlugins.Values]; + return [.. _allInitializedPlugins.Values]; } public static List GetGlobalPlugins() @@ -884,7 +886,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); } Settings.RemovePluginSettings(plugin.ID); - _allPlugins.TryRemove(plugin.ID, out var item); + _allInitializedPlugins.TryRemove(plugin.ID, out var item); _globalPlugins.TryRemove(plugin.ID, out var item1); var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); foreach (var key in keysToRemove) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 82f49958cc5..90783370046 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -249,9 +249,9 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); - var allPlugins = PluginManager.LoadPlugins(_settings.PluginSettings); + PluginManager.LoadPlugins(_settings.PluginSettings); - await PluginManager.InitializePluginsAsync(allPlugins, _mainVM); + await PluginManager.InitializePluginsAsync(_mainVM); AutoPluginUpdates(); From fe3339f645c49ea5b3a7c19b9f9e70c4d80ced95 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:02:19 +0800 Subject: [PATCH 21/51] Get initialized plugins for plugin page --- .../SettingPages/ViewModels/SettingsPanePluginsViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index 8a84a7749f8..5254cdfde28 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -116,7 +116,7 @@ public string FilterText private List? _pluginViewModels; // Get all initialized plugins and ignore those that are not initialized - public List PluginViewModels => _pluginViewModels ??= App.API.GetAllPlugins() + public List PluginViewModels => _pluginViewModels ??= PluginManager.GetAllInitializedPlugins() .OrderBy(plugin => plugin.Metadata.Disabled) .ThenBy(plugin => plugin.Metadata.Name) .Select(plugin => new PluginViewModel From 6e0f2fc4ced1830e593e6c44fa3251881dfe4931 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:03:21 +0800 Subject: [PATCH 22/51] Return loaded plugins --- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 5 ++++- Flow.Launcher/PublicAPIInstance.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 4cefe4bc63f..269635d5e32 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -171,8 +171,11 @@ public interface IPublicAPI string GetTranslation(string key); /// - /// Get all initialized plugins + /// Get all loaded plugins /// + /// + /// Part of plugins may not be initialized yet + /// /// List GetAllPlugins(); diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index bae953bbc34..69ca0c01dbc 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -251,7 +251,7 @@ private static async Task RetryActionOnSTAThreadAsync(Action action, public string GetTranslation(string key) => Internationalization.GetTranslation(key); - public List GetAllPlugins() => PluginManager.GetAllInitializedPlugins(); + public List GetAllPlugins() => PluginManager.GetAllLoadedPlugins(); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); From 6d994166417e23402246cf32cc5a44449f9ec3bd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:14:03 +0800 Subject: [PATCH 23/51] Search in loaded plugins --- Flow.Launcher.Core/Plugin/PluginManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index bce2b1fe6e5..d6d4c5eae22 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -505,11 +505,14 @@ public static void UpdatePluginMetadata(IReadOnlyList results, PluginMet /// /// get specified plugin, return null if not found /// + /// + /// Plugin may not be initialized, so do not use its plugin model to execute any commands + /// /// /// public static PluginPair GetPluginForId(string id) { - return GetAllInitializedPlugins().FirstOrDefault(o => o.Metadata.ID == id); + return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id); } #endregion From 9149e3f201c9155c22405c47760692af384a8c51 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:16:36 +0800 Subject: [PATCH 24/51] Mark initializing plugins as modified --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index d6d4c5eae22..5ea97841829 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -713,7 +713,7 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) public static bool PluginModified(string id) { - return ModifiedPlugins.Contains(id); + return ModifiedPlugins.Contains(id) && _allInitializedPlugins.ContainsKey(id); } public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 269635d5e32..456341a7140 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -534,7 +534,8 @@ public interface IPublicAPI /// /// Check if the plugin has been modified. - /// If this plugin is updated, installed or uninstalled and users do not restart the app, + /// If this plugin is initializing, it will be marked as modified. + /// Or if this plugin is updated, installed or uninstalled and users do not restart the app, /// it will be marked as modified /// /// Plugin id From 8d03fcee2edeb6cb964e9acd0e71744aa0ae68a5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:17:11 +0800 Subject: [PATCH 25/51] Remove error codes --- Flow.Launcher.Core/Plugin/PluginManager.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 5ea97841829..635690ce2e1 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -651,12 +651,6 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword) if (oldActionkeyword != Query.GlobalPluginWildcardSign) { _nonGlobalPlugins.TryRemove(oldActionkeyword, out var item); - // If the removed item is not the same as the plugin being removed, - // we should add it back to non-global plugins - if (item.Metadata.ID != id) - { - _nonGlobalPlugins.TryAdd(oldActionkeyword, item); - } } // Update action keywords and action keyword in plugin metadata From 3bd76b2dfd73b0e95753fc3df617635eb4a51ae9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:17:49 +0800 Subject: [PATCH 26/51] Rename variable --- Flow.Launcher/App.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 90783370046..7bef6ae97ca 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -207,14 +207,14 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => RegisterDispatcherUnhandledException(); RegisterTaskSchedulerUnhandledException(); - var imageLoadertask = ImageLoader.InitializeAsync(); + var imageLoaderTask = ImageLoader.InitializeAsync(); Http.Proxy = _settings.Proxy; // Initialize plugin manifest before initializing plugins so that they can use the manifest instantly await API.UpdatePluginManifestAsync(); - await imageLoadertask; + await imageLoaderTask; _mainWindow = new MainWindow(); From 7f797b1039ced8e71b24322ce11afe9b336fd58e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:22:17 +0800 Subject: [PATCH 27/51] Add code comments --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 635690ce2e1..4e2ed0762cf 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -707,6 +707,8 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) public static bool PluginModified(string id) { + // We should consider initializing plugin as modified since it cannot be installed/uninstalled/updated and + // we cannot call any plugin methods return ModifiedPlugins.Contains(id) && _allInitializedPlugins.ContainsKey(id); } From fc01ddbb1ceb8b0a4ef0213da8db731fc5e1d91c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:27:21 +0800 Subject: [PATCH 28/51] Save init failed plugins --- Flow.Launcher.Core/Plugin/PluginManager.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 4e2ed0762cf..893e36434ea 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -31,6 +31,7 @@ public static class PluginManager private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); private static List _allLoadedPlugins; + private static readonly ConcurrentDictionary _initFailedPlugins = []; private static readonly ConcurrentDictionary _allInitializedPlugins = []; private static readonly ConcurrentDictionary _globalPlugins = []; private static readonly ConcurrentDictionary _nonGlobalPlugins = []; @@ -240,8 +241,6 @@ private static void UpdatePluginDirectory(List metadatas) /// return the list of failed to init plugins or null for none public static async Task InitializePluginsAsync(IResultUpdateRegister register) { - var failedPlugins = new ConcurrentQueue(); - var initTasks = _allLoadedPlugins.Select(pair => Task.Run(async () => { try @@ -266,7 +265,7 @@ public static async Task InitializePluginsAsync(IResultUpdateRegister register) { pair.Metadata.Disabled = true; pair.Metadata.HomeDisabled = true; - failedPlugins.Enqueue(pair); + _initFailedPlugins.TryAdd(pair.Metadata.ID, pair); API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); } @@ -294,9 +293,9 @@ public static async Task InitializePluginsAsync(IResultUpdateRegister register) await Task.WhenAll(initTasks); - if (!failedPlugins.IsEmpty) + if (!_initFailedPlugins.IsEmpty) { - var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); + var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name)); API.ShowMsg( API.GetTranslation("failedToInitializePluginsTitle"), string.Format( From b348debc72690854ce88a6a29585e73a2dfd24bb Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:27:37 +0800 Subject: [PATCH 29/51] Code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 893e36434ea..3dc07c5ec6b 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -31,8 +31,8 @@ public static class PluginManager private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); private static List _allLoadedPlugins; - private static readonly ConcurrentDictionary _initFailedPlugins = []; private static readonly ConcurrentDictionary _allInitializedPlugins = []; + private static readonly ConcurrentDictionary _initFailedPlugins = []; private static readonly ConcurrentDictionary _globalPlugins = []; private static readonly ConcurrentDictionary _nonGlobalPlugins = []; From d4a19537479233d0058cc9e4c7b4988a741c6656 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:28:20 +0800 Subject: [PATCH 30/51] Add init failed plugins --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3dc07c5ec6b..7f9a4a5390b 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -265,13 +265,13 @@ public static async Task InitializePluginsAsync(IResultUpdateRegister register) { pair.Metadata.Disabled = true; pair.Metadata.HomeDisabled = true; - _initFailedPlugins.TryAdd(pair.Metadata.ID, pair); API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); } // Even if the plugin cannot be initialized, we still need to add it in all plugin list so that // we can remove the plugin from Plugin or Store page or Plugin Manager plugin. _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair); + _initFailedPlugins.TryAdd(pair.Metadata.ID, pair); return; } From 324b3eb08171b19146c181ed4a2f781cd79757f1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:33:47 +0800 Subject: [PATCH 31/51] Do not call interface methods for init failed plugins --- Flow.Launcher.Core/Plugin/PluginManager.cs | 39 ++++++++++++------- .../SettingsPanePluginsViewModel.cs | 3 +- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 7f9a4a5390b..f69a4ba0a03 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -59,7 +59,7 @@ public static class PluginManager /// public static void Save() { - foreach (var pluginPair in GetAllInitializedPlugins()) + foreach (var pluginPair in GetAllInitializedPlugins(false)) { var savable = pluginPair.Plugin as ISavable; try @@ -78,7 +78,8 @@ public static void Save() public static async ValueTask DisposePluginsAsync() { - foreach (var pluginPair in GetAllInitializedPlugins()) + // Still call dispose for all plugins even if initialization failed, so that we can clean up resources + foreach (var pluginPair in GetAllInitializedPlugins(true)) { await DisposePluginAsync(pluginPair); } @@ -106,7 +107,7 @@ private static async Task DisposePluginAsync(PluginPair pluginPair) public static async Task ReloadDataAsync() { - await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(false).Select(plugin => plugin.Plugin switch { IReloadable p => Task.Run(p.ReloadData), IAsyncReloadable p => p.ReloadDataAsync(), @@ -120,7 +121,7 @@ public static async Task ReloadDataAsync() public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(false).Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, @@ -129,7 +130,7 @@ public static async Task OpenExternalPreviewAsync(string path, bool sendFailToas public static async Task CloseExternalPreviewAsync() { - await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(false).Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.ClosePreviewAsync(), _ => Task.CompletedTask, @@ -138,7 +139,7 @@ public static async Task CloseExternalPreviewAsync() public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll([.. GetAllInitializedPlugins().Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(false).Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, @@ -523,9 +524,17 @@ public static List GetAllLoadedPlugins() return [.. _allLoadedPlugins]; } - public static List GetAllInitializedPlugins() + public static List GetAllInitializedPlugins(bool containFailed) { - return [.. _allInitializedPlugins.Values]; + if (containFailed) + { + return [.. _allInitializedPlugins.Values]; + } + else + { + return [.. _allInitializedPlugins.Values + .Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))]; + } } public static List GetGlobalPlugins() @@ -695,9 +704,10 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) if (!Version.TryParse(newMetadata.Version, out var newVersion)) return true; // If version is not valid, we assume it is lesser than any existing version - return GetAllInitializedPlugins().Any(x => x.Metadata.ID == newMetadata.ID - && Version.TryParse(x.Metadata.Version, out var version) - && newVersion <= version); + // Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists + return GetAllInitializedPlugins(true).Any(x => x.Metadata.ID == newMetadata.ID + && Version.TryParse(x.Metadata.Version, out var version) + && newVersion <= version); } #endregion @@ -839,7 +849,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo // If we want to remove plugin from AllPlugins, // we need to dispose them so that they can release file handles // which can help FL to delete the plugin settings & cache folders successfully - var pluginPairs = GetAllInitializedPlugins().Where(p => p.Metadata.ID == plugin.ID).ToList(); + var pluginPairs = GetAllInitializedPlugins(true).Where(p => p.Metadata.ID == plugin.ID).ToList(); foreach (var pluginPair in pluginPairs) { await DisposePluginAsync(pluginPair); @@ -885,11 +895,12 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo } Settings.RemovePluginSettings(plugin.ID); _allInitializedPlugins.TryRemove(plugin.ID, out var item); - _globalPlugins.TryRemove(plugin.ID, out var item1); + _initFailedPlugins.TryRemove(plugin.ID, out var item1); + _globalPlugins.TryRemove(plugin.ID, out var item2); var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); foreach (var key in keysToRemove) { - _nonGlobalPlugins.Remove(key, out var item2); + _nonGlobalPlugins.Remove(key, out var item3); } } diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index 5254cdfde28..b7723276e05 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -116,7 +116,8 @@ public string FilterText private List? _pluginViewModels; // Get all initialized plugins and ignore those that are not initialized - public List PluginViewModels => _pluginViewModels ??= PluginManager.GetAllInitializedPlugins() + // Include those init failed plugins so that we can uninstall them + public List PluginViewModels => _pluginViewModels ??= PluginManager.GetAllInitializedPlugins(true) .OrderBy(plugin => plugin.Metadata.Disabled) .ThenBy(plugin => plugin.Metadata.Name) .Select(plugin => new PluginViewModel From 67c940f3a8f20351369d4516ab353a495ea8543f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:45:01 +0800 Subject: [PATCH 32/51] Do not show setting panel for init failed plugins --- Flow.Launcher.Core/Plugin/PluginManager.cs | 11 ++++++++++- Flow.Launcher/ViewModel/PluginViewModel.cs | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index f69a4ba0a03..3ba89fce6d5 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -533,7 +533,7 @@ public static List GetAllInitializedPlugins(bool containFailed) else { return [.. _allInitializedPlugins.Values - .Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))]; + .Where(p => !IsInitFailed(p.Metadata.ID))]; } } @@ -596,6 +596,15 @@ public static bool IsHomePlugin(string id) #endregion + #region Check Init Failed + + public static bool IsInitFailed(string id) + { + return _initFailedPlugins.ContainsKey(id); + } + + #endregion + #region Plugin Action Keyword public static bool ActionKeywordRegistered(string actionKeyword) diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index ea222d02374..5fc3cd0cd0d 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -121,7 +121,9 @@ public double PluginSearchDelayTime public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; public bool HasSettingControl => PluginPair.Plugin is ISettingProvider && + PluginManager.IsInitFailed(PluginPair.Metadata.ID) && // Do not show setting panel for init failed plugins (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel()); + public Control SettingControl => IsExpanded ? _settingControl From 950a4a00a6d61e8436f12a427b4de493fbcb58b4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:45:46 +0800 Subject: [PATCH 33/51] Code quality --- Flow.Launcher/ViewModel/PluginViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 5fc3cd0cd0d..cd58b4104e2 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -120,8 +120,9 @@ public double PluginSearchDelayTime private Control _bottomPart2; public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; - public bool HasSettingControl => PluginPair.Plugin is ISettingProvider && + public bool HasSettingControl => PluginManager.IsInitFailed(PluginPair.Metadata.ID) && // Do not show setting panel for init failed plugins + PluginPair.Plugin is ISettingProvider && (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel()); public Control SettingControl From 6409c193c4642d63ff3e4cc516d46cc4f0e7a0a9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:47:19 +0800 Subject: [PATCH 34/51] Add code comments --- Flow.Launcher/ViewModel/PluginViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index cd58b4104e2..a8f4ef43236 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -123,7 +123,8 @@ public double PluginSearchDelayTime public bool HasSettingControl => PluginManager.IsInitFailed(PluginPair.Metadata.ID) && // Do not show setting panel for init failed plugins PluginPair.Plugin is ISettingProvider && - (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel()); + (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || // Is not JsonRPC plugin + jsonRPCPluginBase.NeedCreateSettingPanel()); // Is JsonRPC plugin and need to create setting panel public Control SettingControl => IsExpanded From a9a705f118e7832f8d49cfb71b78aa82bc0e893a Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:48:17 +0800 Subject: [PATCH 35/51] Fix logic Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher/ViewModel/PluginViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index a8f4ef43236..4247a518484 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -121,7 +121,7 @@ public double PluginSearchDelayTime public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; public bool HasSettingControl => - PluginManager.IsInitFailed(PluginPair.Metadata.ID) && // Do not show setting panel for init failed plugins + !PluginManager.IsInitFailed(PluginPair.Metadata.ID) && // Do not show setting panel for init failed plugins PluginPair.Plugin is ISettingProvider && (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || // Is not JsonRPC plugin jsonRPCPluginBase.NeedCreateSettingPanel()); // Is JsonRPC plugin and need to create setting panel From 59e4fb82b97047bce46799d37d9df1e29e25a2b4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:49:24 +0800 Subject: [PATCH 36/51] Fix logic & Add code comments --- Flow.Launcher.Core/Plugin/PluginManager.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3ba89fce6d5..c69af23277a 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -725,9 +725,10 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) public static bool PluginModified(string id) { - // We should consider initializing plugin as modified since it cannot be installed/uninstalled/updated and - // we cannot call any plugin methods - return ModifiedPlugins.Contains(id) && _allInitializedPlugins.ContainsKey(id); + return ModifiedPlugins.Contains(id) || + // We should consider initializing plugin as modified since it cannot be installed/uninstalled/updated and + // we cannot call any plugin methods + _allInitializedPlugins.ContainsKey(id); } public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) From 067a51775e7222a65532f7c8eeee93e00e9d90de Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:54:20 +0800 Subject: [PATCH 37/51] Fix null exception --- Flow.Launcher/ViewModel/MainViewModel.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2299e4e9857..a7fb6ae4424 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1327,7 +1327,10 @@ private static List GetHistoryItems(IEnumerable historyItem private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { - await _updateSource?.CancelAsync(); + if (_updateSource != null) + { + await _updateSource.CancelAsync(); + } App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>"); @@ -1906,7 +1909,10 @@ public async Task SetupDialogJumpAsync(nint handle) if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) { // Cancel the previous Dialog Jump task - await _dialogJumpSource?.CancelAsync(); + if (_dialogJumpSource != null) + { + await _dialogJumpSource.CancelAsync(); + } // Create a new cancellation token source _dialogJumpSource = new CancellationTokenSource(); From 63f86613c373373dacefeea9f7fb430e836442e6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:55:06 +0800 Subject: [PATCH 38/51] Use internal PluginModified method instead of API.PluginModified --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index c69af23277a..d94ec8d1d35 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -368,7 +368,7 @@ public static ICollection ValidPluginsForQuery(Query query, bool dia if (dialogJump && plugin.Plugin is not IAsyncDialogJump) return Array.Empty(); - if (API.PluginModified(plugin.Metadata.ID)) + if (PluginModified(plugin.Metadata.ID)) return Array.Empty(); return [plugin]; From f808469285749ad7c22af03c3e30618da520d2e6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 17:59:28 +0800 Subject: [PATCH 39/51] Fix logic & Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 21 +++++++++++++++++---- Flow.Launcher/ViewModel/PluginViewModel.cs | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index d94ec8d1d35..d5acfcb5572 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -533,7 +533,7 @@ public static List GetAllInitializedPlugins(bool containFailed) else { return [.. _allInitializedPlugins.Values - .Where(p => !IsInitFailed(p.Metadata.ID))]; + .Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))]; } } @@ -596,11 +596,24 @@ public static bool IsHomePlugin(string id) #endregion - #region Check Init Failed + #region Check Initializing or Init Failed - public static bool IsInitFailed(string id) + public static bool IsInitializingOrInitFailed(string id) { - return _initFailedPlugins.ContainsKey(id); + // Id does not exist in loaded plugins + if (!_allLoadedPlugins.Any(x => x.Metadata.ID == id)) return false; + + // Plugin initialized already + if (_allInitializedPlugins.ContainsKey(id)) + { + // Check if the plugin initialization failed + return _initFailedPlugins.ContainsKey(id); + } + // Plugin is still initializing + else + { + return true; + } } #endregion diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 4247a518484..acd3902abaf 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -121,7 +121,7 @@ public double PluginSearchDelayTime public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; public bool HasSettingControl => - !PluginManager.IsInitFailed(PluginPair.Metadata.ID) && // Do not show setting panel for init failed plugins + !PluginManager.IsInitializingOrInitFailed(PluginPair.Metadata.ID) && // Do not show setting panel for initializing or init failed plugins PluginPair.Plugin is ISettingProvider && (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || // Is not JsonRPC plugin jsonRPCPluginBase.NeedCreateSettingPanel()); // Is JsonRPC plugin and need to create setting panel From bef1feea0a3f7f15d5a32514b02aa6374371e17c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 18:05:22 +0800 Subject: [PATCH 40/51] Fix plugin page logic --- .../ViewModels/SettingsPanePluginsViewModel.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index b7723276e05..5ab3bd08757 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -115,9 +115,10 @@ public string FilterText } private List? _pluginViewModels; - // Get all initialized plugins and ignore those that are not initialized - // Include those init failed plugins so that we can uninstall them - public List PluginViewModels => _pluginViewModels ??= PluginManager.GetAllInitializedPlugins(true) + // Get all plugins: Initializing & Initialized & Init failed plugins + // Include init failed ones so that we can uninstall them + // Include initializing ones so that we can change related settings like action keywords, etc. + public List PluginViewModels => _pluginViewModels ??= App.API.GetAllPlugins() .OrderBy(plugin => plugin.Metadata.Disabled) .ThenBy(plugin => plugin.Metadata.Name) .Select(plugin => new PluginViewModel From 566bd04e3fb0ec35c9541c2f1a1b957a07fe3a6e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 18:06:05 +0800 Subject: [PATCH 41/51] Support two types in one class --- Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index 756aa89c1db..ea647491e90 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -139,7 +139,7 @@ public static void InitializeDialogJumpPlugin(PluginPair pair) }; _dialogJumpExplorers.TryAdd(dialogJumpExplorer, null); } - else if (pair.Plugin is IDialogJumpDialog dialog) + if (pair.Plugin is IDialogJumpDialog dialog) { var dialogJumpDialog = new DialogJumpDialogPair { From 269d21a4c02202c2c48f8c96c579590474f9057d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 18:11:16 +0800 Subject: [PATCH 42/51] Change plugin modified logic --- Flow.Launcher.Core/Plugin/PluginManager.cs | 5 +---- Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index d5acfcb5572..122542d23d8 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -738,10 +738,7 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) public static bool PluginModified(string id) { - return ModifiedPlugins.Contains(id) || - // We should consider initializing plugin as modified since it cannot be installed/uninstalled/updated and - // we cannot call any plugin methods - _allInitializedPlugins.ContainsKey(id); + return ModifiedPlugins.Contains(id); } public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 456341a7140..269635d5e32 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -534,8 +534,7 @@ public interface IPublicAPI /// /// Check if the plugin has been modified. - /// If this plugin is initializing, it will be marked as modified. - /// Or if this plugin is updated, installed or uninstalled and users do not restart the app, + /// If this plugin is updated, installed or uninstalled and users do not restart the app, /// it will be marked as modified /// /// Plugin id From 0f8553b45d164556be8052c38c261e17280c43d3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 19:39:42 +0800 Subject: [PATCH 43/51] Refresh home page after plugins are initialized --- Flow.Launcher/App.xaml.cs | 7 +++++++ Flow.Launcher/PublicAPIInstance.cs | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7bef6ae97ca..be017fc6283 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -253,6 +253,13 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => await PluginManager.InitializePluginsAsync(_mainVM); + // Refresh home page after plugins are initialized because users may open main window during plugin initialization + // And home page is created without full plugin list + if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText)) + { + _mainVM.QueryResults(); + } + AutoPluginUpdates(); // Save all settings since we possibly update the plugin environment paths diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 69ca0c01dbc..40a582fb9c4 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Runtime.CompilerServices; using System.Threading; From e0240784b502405d4b39de5daaa66b6b1fe38496 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 21:11:48 +0800 Subject: [PATCH 44/51] Improve code quality --- Flow.Launcher.Core/Plugin/PluginsLoader.cs | 80 +++++++++++----------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 9d511297e3e..8b8a14723bd 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -55,7 +55,7 @@ public static List Plugins(List metadatas, PluginsSe return plugins; } - private static IEnumerable DotNetPlugins(List source) + private static List DotNetPlugins(List source) { var erroredPlugins = new List(); @@ -65,54 +65,54 @@ private static IEnumerable DotNetPlugins(List source foreach (var metadata in metadatas) { var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () => - { - Assembly assembly = null; - IAsyncPlugin plugin = null; + { + Assembly assembly = null; + IAsyncPlugin plugin = null; - try - { - var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); - assembly = assemblyLoader.LoadAssemblyAndDependencies(); + try + { + var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); + assembly = assemblyLoader.LoadAssemblyAndDependencies(); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, - typeof(IAsyncPlugin)); + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, + typeof(IAsyncPlugin)); - plugin = Activator.CreateInstance(type) as IAsyncPlugin; + plugin = Activator.CreateInstance(type) as IAsyncPlugin; - metadata.AssemblyName = assembly.GetName().Name; - } + metadata.AssemblyName = assembly.GetName().Name; + } #if DEBUG - catch (Exception) - { - throw; - } + catch (Exception) + { + throw; + } #else - catch (Exception e) when (assembly == null) - { - Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e); - } - catch (InvalidOperationException e) - { - Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); - } - catch (ReflectionTypeLoadException e) - { - Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); - } - catch (Exception e) - { - Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e); - } + catch (Exception e) when (assembly == null) + { + Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e); + } + catch (InvalidOperationException e) + { + Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); + } + catch (ReflectionTypeLoadException e) + { + Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); + } + catch (Exception e) + { + Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e); + } #endif - if (plugin == null) - { - erroredPlugins.Add(metadata.Name); - return; - } + if (plugin == null) + { + erroredPlugins.Add(metadata.Name); + return; + } - plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); - }); + plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); + }); metadata.InitTime += milliseconds; } From 3221f930c4240af5748ffee1d233f9fda64d2114 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 21 Jul 2025 21:17:30 +0800 Subject: [PATCH 45/51] Add info log message for plugin constructors --- Flow.Launcher.Core/Plugin/PluginsLoader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 8b8a14723bd..381772ea8bc 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -113,7 +113,9 @@ private static List DotNetPlugins(List source) plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); }); + metadata.InitTime += milliseconds; + API.LogInfo(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>"); } if (erroredPlugins.Count > 0) From 93af07931681588e68312907e7c1ab0427938b4a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 12 Aug 2025 17:49:22 +0800 Subject: [PATCH 46/51] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 68 +++++++++++----------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 122542d23d8..2823ad132a7 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -485,38 +485,6 @@ public static async Task> QueryDialogJumpForPluginAsync(P #endregion - #region Update Metadata & Get Plugin - - public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) - { - foreach (var r in results) - { - r.PluginDirectory = metadata.PluginDirectory; - r.PluginID = metadata.ID; - r.OriginQuery = query; - - // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions - // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level - if (metadata.ActionKeywords.Count == 1) - r.ActionKeywordAssigned = query.ActionKeyword; - } - } - - /// - /// get specified plugin, return null if not found - /// - /// - /// Plugin may not be initialized, so do not use its plugin model to execute any commands - /// - /// - /// - public static PluginPair GetPluginForId(string id) - { - return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id); - } - - #endregion - #region Get Plugin List public static List GetAllLoadedPlugins() @@ -524,9 +492,9 @@ public static List GetAllLoadedPlugins() return [.. _allLoadedPlugins]; } - public static List GetAllInitializedPlugins(bool containFailed) + public static List GetAllInitializedPlugins(bool includeFailed) { - if (containFailed) + if (includeFailed) { return [.. _allInitializedPlugins.Values]; } @@ -554,6 +522,38 @@ public static List GetTranslationPlugins() #endregion + #region Update Metadata & Get Plugin + + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) + { + foreach (var r in results) + { + r.PluginDirectory = metadata.PluginDirectory; + r.PluginID = metadata.ID; + r.OriginQuery = query; + + // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions + // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level + if (metadata.ActionKeywords.Count == 1) + r.ActionKeywordAssigned = query.ActionKeyword; + } + } + + /// + /// get specified plugin, return null if not found + /// + /// + /// Plugin may not be initialized, so do not use its plugin model to execute any commands + /// + /// + /// + public static PluginPair GetPluginForId(string id) + { + return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id); + } + + #endregion + #region Get Context Menus public static List GetContextMenusForPlugin(Result result) From f08466245a6dec8751c16527fc0061f2bc2dbda0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 12 Aug 2025 17:53:53 +0800 Subject: [PATCH 47/51] Set GetAllInitializedPlugins to private --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 2823ad132a7..22a3e8c06d6 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -492,7 +492,7 @@ public static List GetAllLoadedPlugins() return [.. _allLoadedPlugins]; } - public static List GetAllInitializedPlugins(bool includeFailed) + private static List GetAllInitializedPlugins(bool includeFailed) { if (includeFailed) { From 31c8e850cdef89995b68ca61e83f473527e6575a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 12 Aug 2025 18:03:23 +0800 Subject: [PATCH 48/51] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 22a3e8c06d6..ac104c0113f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -914,13 +914,19 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); } Settings.RemovePluginSettings(plugin.ID); - _allInitializedPlugins.TryRemove(plugin.ID, out var item); - _initFailedPlugins.TryRemove(plugin.ID, out var item1); - _globalPlugins.TryRemove(plugin.ID, out var item2); + { + _allInitializedPlugins.TryRemove(plugin.ID, out var _); + } + { + _initFailedPlugins.TryRemove(plugin.ID, out var _); + } + { + _globalPlugins.TryRemove(plugin.ID, out var _); + } var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); foreach (var key in keysToRemove) { - _nonGlobalPlugins.Remove(key, out var item3); + _nonGlobalPlugins.Remove(key, out var ite3); } } From 9221435bad450eede47ab030b5d96024b5b8ba27 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 12 Aug 2025 18:07:10 +0800 Subject: [PATCH 49/51] Remove plugin from _allLoadedPlugins when one plugin is uninstalled --- Flow.Launcher.Core/Plugin/PluginManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index ac104c0113f..d13e4c3b08d 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -914,6 +914,9 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); } Settings.RemovePluginSettings(plugin.ID); + { + _allLoadedPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); + } { _allInitializedPlugins.TryRemove(plugin.ID, out var _); } From 04bd9ddc2c1dbd96e65dc99dbfe4edc1173551be Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 12 Aug 2025 18:09:59 +0800 Subject: [PATCH 50/51] Verify File.Delete exception handling Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Flow.Launcher.Core/Plugin/PluginManager.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index d13e4c3b08d..37b2660dc66 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -183,7 +183,14 @@ private static void DeletePythonBinding() const string binding = "flowlauncher.py"; foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory)) { - File.Delete(Path.Combine(subDirectory, binding)); + try + { + File.Delete(Path.Combine(subDirectory, binding)); + } + catch (Exception e) + { + API.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}"); + } } } From fb8daa4ed90f0ef340e00f1b912453521a5e6fe3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 12 Aug 2025 18:12:15 +0800 Subject: [PATCH 51/51] Potential race condition in action keyword management --- Flow.Launcher.Core/Plugin/PluginManager.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 37b2660dc66..b0624f373b6 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -648,14 +648,7 @@ public static void AddActionKeyword(string id, string newActionKeyword) } else { - if (_nonGlobalPlugins.TryGetValue(newActionKeyword, out var item)) - { - _nonGlobalPlugins.TryUpdate(newActionKeyword, plugin, item); - } - else - { - _nonGlobalPlugins.TryAdd(newActionKeyword, plugin); - } + _nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin); } // Update action keywords and action keyword in plugin metadata