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.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index a4ab8de08ae..b0624f373b6 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,48 +26,40 @@ 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 List _allLoadedPlugins;
+ private static readonly ConcurrentDictionary _allInitializedPlugins = [];
+ private static readonly ConcurrentDictionary _initFailedPlugins = [];
+ 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
///
public static readonly string[] Directories =
- {
+ [
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));
- }
- }
+ #region Save & Dispose & Reload Plugin
///
/// Save json and ISavable
///
public static void Save()
{
- foreach (var pluginPair in AllPlugins)
+ foreach (var pluginPair in GetAllInitializedPlugins(false))
{
var savable = pluginPair.Plugin as ISavable;
try
@@ -85,7 +78,8 @@ public static void Save()
public static async ValueTask DisposePluginsAsync()
{
- foreach (var pluginPair in AllPlugins)
+ // 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);
}
@@ -113,49 +107,53 @@ private static async Task DisposePluginAsync(PluginPair pluginPair)
public static async Task ReloadDataAsync()
{
- await Task.WhenAll(AllPlugins.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(),
_ => Task.CompletedTask,
- }).ToArray());
+ })]);
}
+ #endregion
+
+ #region External Preview
+
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
{
- await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
+ await Task.WhenAll([.. GetAllInitializedPlugins(false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
- }).ToArray());
+ })]);
}
public static async Task CloseExternalPreviewAsync()
{
- await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
+ await Task.WhenAll([.. GetAllInitializedPlugins(false).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(AllPlugins.Select(plugin => plugin.Plugin switch
+ await Task.WhenAll([.. GetAllInitializedPlugins(false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
- }).ToArray());
+ })]);
}
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;
@@ -163,6 +161,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
@@ -171,9 +178,28 @@ static PluginManager()
DeletePythonBinding();
}
+ private static void DeletePythonBinding()
+ {
+ const string binding = "flowlauncher.py";
+ foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
+ {
+ try
+ {
+ File.Delete(Path.Combine(subDirectory, binding));
+ }
+ catch (Exception e)
+ {
+ API.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}");
+ }
+ }
+ }
+
+ #endregion
+
+ #region Load & Initialize Plugins
+
///
- /// 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 void LoadPlugins(PluginsSettings settings)
@@ -181,33 +207,12 @@ public static void LoadPlugins(PluginsSettings settings)
var metadatas = PluginConfig.Parse(Directories);
Settings = settings;
Settings.UpdatePluginSettings(metadatas);
- 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();
+ // Load plugins
+ _allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings);
- // 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
- });
- }
+ // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
+ UpdatePluginDirectory(metadatas);
}
private static void UpdatePluginDirectory(List metadatas)
@@ -238,14 +243,13 @@ private static void UpdatePluginDirectory(List metadatas)
}
///
- /// Call initialize for all plugins
+ /// Initialize all plugins asynchronously.
///
+ /// 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()
+ public static async Task InitializePluginsAsync(IResultUpdateRegister register)
{
- var failedPlugins = new ConcurrentQueue();
-
- var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
+ var initTasks = _allLoadedPlugins.Select(pair => Task.Run(async () =>
{
try
{
@@ -269,35 +273,37 @@ public static async Task InitializePluginsAsync()
{
pair.Metadata.Disabled = true;
pair.Metadata.HomeDisabled = true;
- 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 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;
}
- }));
- await Task.WhenAll(InitTasks);
+ // Register ResultsUpdated event so that plugin query can use results updated interface
+ register.RegisterResultsUpdatedEvent(pair);
- foreach (var plugin in AllPlugins)
- {
- // 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())
- {
- switch (actionKeyword)
- {
- case Query.GlobalPluginWildcardSign:
- GlobalPlugins.Add(plugin);
- break;
- default:
- NonGlobalPlugins[actionKeyword] = plugin;
- 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);
- if (failedPlugins.Any())
+ // Add plugin to lists after the plugin is initialized
+ AddPluginToLists(pair);
+ }));
+
+ await Task.WhenAll(initTasks);
+
+ 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(
@@ -310,34 +316,74 @@ public static async Task InitializePluginsAsync()
}
}
+ 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)
+ {
+ _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);
+ }
+ _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
+ }
+
+ #endregion
+
+ #region Validate & Query Plugins
+
public static ICollection ValidPluginsForQuery(Query query, bool dialogJump)
{
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 [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))];
else
- return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
+ return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))];
}
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 new List
- {
- 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)
@@ -444,6 +490,47 @@ public static async Task> QueryDialogJumpForPluginAsync(P
return results;
}
+ #endregion
+
+ #region Get Plugin List
+
+ public static List GetAllLoadedPlugins()
+ {
+ return [.. _allLoadedPlugins];
+ }
+
+ private static List GetAllInitializedPlugins(bool includeFailed)
+ {
+ if (includeFailed)
+ {
+ return [.. _allInitializedPlugins.Values];
+ }
+ else
+ {
+ return [.. _allInitializedPlugins.Values
+ .Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))];
+ }
+ }
+
+ 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))];
+ }
+
+ #endregion
+
+ #region Update Metadata & Get Plugin
+
public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query)
{
foreach (var r in results)
@@ -462,28 +549,19 @@ 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 AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
+ return GetAllLoadedPlugins().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();
- }
+ #endregion
- public static IList GetTranslationPlugins()
- {
- return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
- }
+ #region Get Context Menus
public static List GetContextMenusForPlugin(Result result)
{
@@ -514,27 +592,47 @@ 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);
}
- public static IList GetDialogJumpExplorers()
- {
- return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
- }
+ #endregion
- public static IList GetDialogJumpDialogs()
+ #region Check Initializing or Init Failed
+
+ public static bool IsInitializingOrInitFailed(string id)
{
- return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
+ // 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
+
+ #region Plugin Action Keyword
+
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 +644,11 @@ 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;
+ _nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin);
}
// Update action keywords and action keyword in plugin metadata
@@ -577,11 +675,13 @@ 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);
+ }
// Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
@@ -595,6 +695,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;
@@ -620,12 +726,15 @@ 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
- && 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);
}
- #region Public functions
+ #endregion
+
+ #region Public Functions
public static bool PluginModified(string id)
{
@@ -663,7 +772,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)
{
@@ -760,7 +869,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 = GetAllInitializedPlugins(true).Where(p => p.Metadata.ID == plugin.ID).ToList();
foreach (var pluginPair in pluginPairs)
{
await DisposePluginAsync(pluginPair);
@@ -805,12 +914,22 @@ 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();
+ {
+ _allLoadedPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
+ }
+ {
+ _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);
+ _nonGlobalPlugins.Remove(key, out var ite3);
}
}
@@ -826,5 +945,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo
}
#endregion
+
+ #endregion
}
}
diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
index 9d511297e3e..381772ea8bc 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,55 +65,57 @@ 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;
+ API.LogInfo(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
}
if (erroredPlugins.Count > 0)
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
}
}
diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
index 65652878fc8..ea647491e90 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);
+ }
+ 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;
diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index dcccaebebed..b2ad8a4ae84 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -173,6 +173,9 @@ public interface IPublicAPI
///
/// Get all loaded plugins
///
+ ///
+ /// Part of plugins may not be initialized yet
+ ///
///
List GetAllPlugins();
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index 6e053db29c8..be017fc6283 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 ----------------------------------------------------");
@@ -204,32 +207,25 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();
- var imageLoadertask = ImageLoader.InitializeAsync();
-
- AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
-
- PluginManager.LoadPlugins(_settings.PluginSettings);
-
- // Register ResultsUpdated event after all plugins are loaded
- Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent();
+ 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 PluginManager.InitializePluginsAsync();
-
- // Update plugin titles after plugins are initialized with their api instances
- Internationalization.UpdatePluginMetadataTranslations();
-
- await imageLoadertask;
+ await imageLoaderTask;
_mainWindow = new MainWindow();
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();
+ 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();
@@ -237,19 +233,40 @@ 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();
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);
+
+ PluginManager.LoadPlugins(_settings.PluginSettings);
+
+ 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
+ API.SaveAppAllSettings();
+
+ API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------");
+ });
});
}
diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index 8eb41e032fa..a146e0b4b71 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -474,7 +474,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 e0ed105cff9..7d5359378d7 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;
@@ -251,7 +250,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.GetAllLoadedPlugins();
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..5ab3bd08757 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
@@ -114,8 +114,11 @@ public string FilterText
}
}
- private IList? _pluginViewModels;
- public IList PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins
+ private List? _pluginViewModels;
+ // 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
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index 045ff46cc9e..bc61efdcaf0 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()
@@ -439,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('\\'));
@@ -1328,7 +1326,10 @@ private static List GetHistoryItems(IEnumerable historyItem
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
{
- _updateSource?.Cancel();
+ if (_updateSource != null)
+ {
+ await _updateSource.CancelAsync();
+ }
App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>");
@@ -1574,7 +1575,7 @@ private async Task ConstructQueryAsync(string queryText, IEnumerable ConstructQueryAsync(string queryText, IEnumerable builtInShortcuts,
@@ -1907,7 +1908,10 @@ public async Task SetupDialogJumpAsync(nint handle)
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// Cancel the previous Dialog Jump task
- _dialogJumpSource?.Cancel();
+ if (_dialogJumpSource != null)
+ {
+ await _dialogJumpSource.CancelAsync();
+ }
// Create a new cancellation token source
_dialogJumpSource = new CancellationTokenSource();
diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs
index ea222d02374..acd3902abaf 100644
--- a/Flow.Launcher/ViewModel/PluginViewModel.cs
+++ b/Flow.Launcher/ViewModel/PluginViewModel.cs
@@ -120,8 +120,12 @@ public double PluginSearchDelayTime
private Control _bottomPart2;
public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null;
- public bool HasSettingControl => PluginPair.Plugin is ISettingProvider &&
- (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel());
+ public bool HasSettingControl =>
+ !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
+
public Control SettingControl
=> IsExpanded
? _settingControl