diff --git a/Directory.Build.props b/Directory.Build.props
index a5545af12..b8c1d13ea 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
true
-
+
false
\ No newline at end of file
diff --git a/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs b/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
new file mode 100644
index 000000000..1da04bf01
--- /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 1d69eb587..d3a8003c1 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -7,6 +7,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Core.ExternalPlugins;
+using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
@@ -24,44 +25,36 @@ namespace Flow.Launcher.Core.Plugin
{
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();
+ private static readonly ConcurrentDictionary _allLoadedPlugins = [];
+ private static readonly ConcurrentDictionary _allInitializedPlugins = [];
+ private static readonly ConcurrentDictionary _initFailedPlugins = [];
+ private static readonly ConcurrentDictionary _globalPlugins = [];
+ private static readonly ConcurrentDictionary _nonGlobalPlugins = [];
private static PluginsSettings Settings;
- private static readonly ConcurrentBag ModifiedPlugins = new();
+ private static readonly ConcurrentBag ModifiedPlugins = [];
- private static IEnumerable _contextMenuPlugins;
- private static IEnumerable _homePlugins;
- private static IEnumerable _resultUpdatePlugin;
- private static IEnumerable _translationPlugins;
-
- 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(includeFailed: false))
{
var savable = pluginPair.Plugin as ISavable;
try
@@ -80,7 +73,8 @@ namespace Flow.Launcher.Core.Plugin
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(includeFailed: true))
{
await DisposePluginAsync(pluginPair);
}
@@ -108,49 +102,53 @@ namespace Flow.Launcher.Core.Plugin
public static async Task ReloadDataAsync()
{
- await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
+ await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: 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(includeFailed: 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(includeFailed: 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(includeFailed: 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;
@@ -158,6 +156,15 @@ namespace Flow.Launcher.Core.Plugin
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
@@ -166,9 +173,28 @@ namespace Flow.Launcher.Core.Plugin
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)
+ {
+ PublicApi.Instance.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)
@@ -176,33 +202,22 @@ namespace Flow.Launcher.Core.Plugin
var metadatas = PluginConfig.Parse(Directories);
Settings = settings;
Settings.UpdatePluginSettings(metadatas);
- AllPlugins = PluginsLoader.Plugins(metadatas, Settings);
+
+ // Load plugins
+ var allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings);
+ foreach (var plugin in allLoadedPlugins)
+ {
+ if (plugin != null)
+ {
+ if (!_allLoadedPlugins.TryAdd(plugin.Metadata.ID, plugin))
+ {
+ PublicApi.Instance.LogError(ClassName, $"Plugin with ID {plugin.Metadata.ID} already loaded");
+ }
+ }
+ }
+
// 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
- });
- }
}
private static void UpdatePluginDirectory(List metadatas)
@@ -233,15 +248,19 @@ namespace Flow.Launcher.Core.Plugin
}
///
- /// 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(x => Task.Run(async () =>
{
+ var pair = x.Value;
+
+ // Register plugin action keywords so that plugins can be queried in results
+ RegisterPluginActionKeywords(pair);
+
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
@@ -264,35 +283,34 @@ namespace Flow.Launcher.Core.Plugin
{
pair.Metadata.Disabled = true;
pair.Metadata.HomeDisabled = true;
- failedPlugins.Enqueue(pair);
PublicApi.Instance.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;
}
+
+ // Register ResultsUpdated event so that plugin query can use results updated interface
+ register.RegisterResultsUpdatedEvent(pair);
+
+ // 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);
+
+ // Add plugin to lists after the plugin is initialized
+ AddPluginToLists(pair);
}));
- await Task.WhenAll(InitTasks);
+ await Task.WhenAll(initTasks);
- foreach (var plugin in AllPlugins)
+ if (!_initFailedPlugins.IsEmpty)
{
- // 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;
- }
- }
- }
-
- if (!failedPlugins.IsEmpty)
- {
- var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
+ var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name));
PublicApi.Instance.ShowMsg(
Localize.failedToInitializePluginsTitle(),
Localize.failedToInitializePluginsMessage(failed),
@@ -302,34 +320,74 @@ namespace Flow.Launcher.Core.Plugin
}
}
+ 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 (PublicApi.Instance.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)
@@ -337,6 +395,28 @@ namespace Flow.Launcher.Core.Plugin
var results = new List();
var metadata = pair.Metadata;
+ if (IsPluginInitializing(metadata))
+ {
+ Result r = new()
+ {
+ Title = Localize.pluginStillInitializing(metadata.Name),
+ SubTitle = Localize.pluginStillInitializingSubtitle(),
+ AutoCompleteText = query.RawQuery,
+ IcoPath = metadata.IcoPath,
+ PluginDirectory = metadata.PluginDirectory,
+ ActionKeywordAssigned = query.ActionKeyword,
+ PluginID = metadata.ID,
+ OriginQuery = query,
+ Action = _ =>
+ {
+ PublicApi.Instance.ReQuery();
+ return false;
+ }
+ };
+ results.Add(r);
+ return results;
+ }
+
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@@ -361,15 +441,15 @@ namespace Flow.Launcher.Core.Plugin
{
Result r = new()
{
- Title = $"{metadata.Name}: Failed to respond!",
- SubTitle = "Select this result for more info",
+ Title = Localize.pluginFailedToRespond(metadata.Name),
+ SubTitle = Localize.pluginFailedToRespondSubtitle(),
+ AutoCompleteText = query.RawQuery,
IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
OriginQuery = query,
- Action = _ => { throw new FlowPluginException(metadata, e);},
- Score = -100
+ Action = _ => { throw new FlowPluginException(metadata, e);}
};
results.Add(r);
}
@@ -381,6 +461,28 @@ namespace Flow.Launcher.Core.Plugin
var results = new List();
var metadata = pair.Metadata;
+ if (IsPluginInitializing(metadata))
+ {
+ Result r = new()
+ {
+ Title = Localize.pluginStillInitializing(metadata.Name),
+ SubTitle = Localize.pluginStillInitializingSubtitle(),
+ AutoCompleteText = query.RawQuery,
+ IcoPath = metadata.IcoPath,
+ PluginDirectory = metadata.PluginDirectory,
+ ActionKeywordAssigned = query.ActionKeyword,
+ PluginID = metadata.ID,
+ OriginQuery = query,
+ Action = _ =>
+ {
+ PublicApi.Instance.ReQuery();
+ return false;
+ }
+ };
+ results.Add(r);
+ return results;
+ }
+
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@@ -411,6 +513,12 @@ namespace Flow.Launcher.Core.Plugin
var results = new List();
var metadata = pair.Metadata;
+ if (IsPluginInitializing(metadata))
+ {
+ // null will be fine since the results will only be added into queue if the token hasn't been cancelled
+ return null;
+ }
+
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@@ -436,6 +544,52 @@ namespace Flow.Launcher.Core.Plugin
return results;
}
+ private static bool IsPluginInitializing(PluginMetadata metadata)
+ {
+ return !_allInitializedPlugins.ContainsKey(metadata.ID);
+ }
+
+ #endregion
+
+ #region Get Plugin List
+
+ public static List GetAllLoadedPlugins()
+ {
+ return [.. _allLoadedPlugins.Values];
+ }
+
+ public static List GetAllInitializedPlugins(bool includeFailed)
+ {
+ if (includeFailed)
+ {
+ return [.. _allInitializedPlugins.Values];
+ }
+ else
+ {
+ return [.. _allInitializedPlugins.Values
+ .Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))];
+ }
+ }
+
+ private 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)
@@ -454,28 +608,19 @@ namespace Flow.Launcher.Core.Plugin
///
/// 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();
- }
+ #endregion
- public static IList GetResultUpdatePlugin()
- {
- return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
- }
-
- public static IList GetTranslationPlugins()
- {
- return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
- }
+ #region Get Context Menus
public static List GetContextMenusForPlugin(Result result)
{
@@ -506,27 +651,82 @@ namespace Flow.Launcher.Core.Plugin
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()
+ #endregion
+
+ #region Check Initializing & Init Failed
+
+ public static bool IsInitializingOrInitFailed(string id)
{
- return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
+ // Id does not exist in loaded plugins
+ if (!_allLoadedPlugins.ContainsKey(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;
+ }
}
- public static IList GetDialogJumpDialogs()
+ public static bool IsInitializing(string id)
{
- return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
+ // Id does not exist in loaded plugins
+ if (!_allLoadedPlugins.ContainsKey(id)) return false;
+
+ // Plugin initialized already
+ if (_allInitializedPlugins.ContainsKey(id))
+ {
+ return false;
+ }
+ // Plugin is still initializing
+ else
+ {
+ return true;
+ }
}
+ public static bool IsInitializationFailed(string id)
+ {
+ // Id does not exist in loaded plugins
+ if (!_allLoadedPlugins.ContainsKey(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 false;
+ }
+ }
+
+ #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);
}
///
@@ -538,11 +738,11 @@ namespace Flow.Launcher.Core.Plugin
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
@@ -569,11 +769,13 @@ namespace Flow.Launcher.Core.Plugin
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 _);
+ }
// Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
@@ -587,6 +789,12 @@ namespace Flow.Launcher.Core.Plugin
}
}
+ #endregion
+
+ #region Plugin Install & Uninstall & Update
+
+ #region Private Functions
+
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
{
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
@@ -612,12 +820,15 @@ namespace Flow.Launcher.Core.Plugin
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(includeFailed: 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)
{
@@ -655,7 +866,7 @@ namespace Flow.Launcher.Core.Plugin
#endregion
- #region Internal functions
+ #region Internal Functions
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
{
@@ -752,7 +963,7 @@ namespace Flow.Launcher.Core.Plugin
// 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(includeFailed: true).Where(p => p.Metadata.ID == plugin.ID).ToList();
foreach (var pluginPair in pluginPairs)
{
await DisposePluginAsync(pluginPair);
@@ -797,12 +1008,22 @@ namespace Flow.Launcher.Core.Plugin
Localize.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.TryRemove(plugin.ID, out var _);
+ }
+ {
+ _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.TryRemove(key, out var _);
}
}
@@ -818,5 +1039,7 @@ namespace Flow.Launcher.Core.Plugin
}
#endregion
+
+ #endregion
}
}
diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
index a8a4fba3a..119dd83ba 100644
--- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
@@ -48,7 +48,7 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
- private static IEnumerable DotNetPlugins(List source)
+ private static List DotNetPlugins(List source)
{
var erroredPlugins = new List();
@@ -58,55 +58,57 @@ namespace Flow.Launcher.Core.Plugin
foreach (var metadata in metadatas)
{
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
+ {
+ Assembly assembly = null;
+ IAsyncPlugin plugin = null;
+
+ try
{
- Assembly assembly = null;
- IAsyncPlugin plugin = null;
+ 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)
- {
- PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
- }
- catch (InvalidOperationException e)
- {
- PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
- }
- catch (ReflectionTypeLoadException e)
- {
- PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
- }
- catch (Exception e)
- {
- PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
- }
+ catch (Exception e) when (assembly == null)
+ {
+ PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
+ }
+ catch (InvalidOperationException e)
+ {
+ PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
+ }
+ catch (ReflectionTypeLoadException e)
+ {
+ PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
+ }
+ catch (Exception e)
+ {
+ PublicApi.Instance.LogException(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;
+ PublicApi.Instance.LogDebug(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 6f373746e..7505dca62 100644
--- a/Flow.Launcher.Core/Resource/Internationalization.cs
+++ b/Flow.Launcher.Core/Resource/Internationalization.cs
@@ -377,6 +377,22 @@ namespace Flow.Launcher.Core.Resource
}
}
+ 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)
+ {
+ PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
+ }
+ }
+
#endregion
#region IDisposable
diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
index 9035a541d..4a3e6474e 100644
--- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
+++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
@@ -13,6 +13,7 @@ using NHotkey;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Accessibility;
+using System.Collections.Concurrent;
namespace Flow.Launcher.Infrastructure.DialogJump
{
@@ -60,12 +61,12 @@ namespace Flow.Launcher.Infrastructure.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 Lock _lastExplorerLock = new();
- private static readonly Dictionary _dialogJumpDialogs = new();
+ private static readonly ConcurrentDictionary _dialogJumpDialogs = new();
private static IDialogJumpDialogWindow _dialogWindow = null;
private static readonly Lock _dialogWindowLock = new();
@@ -101,22 +102,13 @@ namespace Flow.Launcher.Infrastructure.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();
@@ -131,6 +123,29 @@ namespace Flow.Launcher.Infrastructure.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;
@@ -828,9 +843,25 @@ namespace Flow.Launcher.Infrastructure.DialogJump
return true;
}
// file: URI paths
- var localPath = path.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
- ? new Uri(path).LocalPath
- : path;
+ string localPath;
+ if (path.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
+ {
+ // Try to create a URI from the path
+ if (Uri.TryCreate(path, UriKind.Absolute, out var uri))
+ {
+ localPath = uri.LocalPath;
+ }
+ else
+ {
+ // If URI creation fails, treat it as a regular path
+ // by removing the "file:" prefix
+ localPath = path.Substring(5);
+ }
+ }
+ else
+ {
+ localPath = path;
+ }
// Is folder?
var isFolder = Directory.Exists(localPath);
// Is file?
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 7b4e22773..7cff670be 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
+using Flow.Launcher.Localization.Attributes;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@@ -515,6 +516,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
[JsonConverter(typeof(JsonStringEnumConverter))]
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
+ private HistoryStyle _historyStyle = HistoryStyle.Query;
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public HistoryStyle HistoryStyle
+ {
+ get => _historyStyle;
+ set
+ {
+ if (_historyStyle != value)
+ {
+ _historyStyle = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
[JsonConverter(typeof(JsonStringEnumConverter))]
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
public int CustomAnimationLength { get; set; } = 360;
@@ -697,4 +713,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
FullPathOpen,
Directory
}
+
+ [EnumLocalize]
+ public enum HistoryStyle
+ {
+ [EnumLocalizeKey(nameof(Localize.queryHistory))]
+ Query,
+
+ [EnumLocalizeKey(nameof(Localize.executedHistory))]
+ LastOpened
+ }
}
diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index a4cdeb3bf..b12df74ea 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -182,9 +182,21 @@ namespace Flow.Launcher.Plugin
///
/// Get all loaded plugins
///
+ ///
+ /// Will also return any plugins not fully initialized yet
+ ///
///
List GetAllPlugins();
+ ///
+ /// Get all initialized plugins
+ ///
+ ///
+ /// Whether to include plugins that failed to initialize
+ ///
+ ///
+ List GetAllInitializedPlugins(bool includeFailed);
+
///
/// Registers a callback function for global keyboard events.
///
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index b0fb40110..7443d7b5b 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -196,12 +196,14 @@ namespace Flow.Launcher
// 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
@@ -210,6 +212,7 @@ namespace Flow.Launcher
// Initialize language before portable clean up since it needs translations
await _internationalization.InitializeLanguageAsync();
+ // Clean up after portability update
Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate();
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
@@ -219,32 +222,25 @@ namespace Flow.Launcher
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 Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey
+ // Initialize Dialog 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();
@@ -252,19 +248,40 @@ namespace Flow.Launcher
// 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/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index 8c7670426..576bf6f2f 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -185,7 +185,7 @@
-
+
diff --git a/Flow.Launcher/Helper/ResultHelper.cs b/Flow.Launcher/Helper/ResultHelper.cs
new file mode 100644
index 000000000..5f9a69f28
--- /dev/null
+++ b/Flow.Launcher/Helper/ResultHelper.cs
@@ -0,0 +1,45 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Storage;
+
+namespace Flow.Launcher.Helper;
+
+#nullable enable
+
+public static class ResultHelper
+{
+ public static async Task PopulateResultsAsync(LastOpenedHistoryItem item)
+ {
+ return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
+ }
+
+ public static async Task PopulateResultsAsync(string pluginId, string rawQuery, string title, string subTitle, string recordKey)
+ {
+ var plugin = PluginManager.GetPluginForId(pluginId);
+ if (plugin == null) return null;
+ var query = QueryBuilder.Build(rawQuery, PluginManager.GetNonGlobalPlugins());
+ if (query == null) return null;
+ try
+ {
+ var freshResults = await plugin.Plugin.QueryAsync(query, CancellationToken.None);
+ // Try to match by record key first if it is valid, otherwise fall back to title + subtitle match
+ if (string.IsNullOrEmpty(recordKey))
+ {
+ return freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
+ }
+ else
+ {
+ return freshResults?.FirstOrDefault(r => r.RecordKey == recordKey) ??
+ freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
+ }
+ }
+ catch (System.Exception e)
+ {
+ App.API.LogException(nameof(ResultHelper), $"Failed to query results for {plugin.Metadata.Name}", e);
+ return null;
+ }
+ }
+}
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 6fd42e84e..fc3ab0a9c 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -66,6 +66,10 @@
Position Reset
Reset search window position
Type here to search
+ {0}: This plugin is still initializing...
+ Select this result to requery
+ {0}: Failed to respond!
+ Select this result for more info
(Admin)
@@ -167,6 +171,10 @@
Show home page results when query text is empty.
Show History Results in Home Page
Maximum History Results Shown in Home Page
+ History Style
+ Choose the type of history to show in the History and Home Page
+ Query history
+ Last opened history
This can only be edited if plugin supports Home feature and Home Page is enabled.
Show Search Window at Foremost
Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.
diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index bb4878066..d6e5f7cdb 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -322,6 +322,7 @@ namespace Flow.Launcher
break;
case nameof(Settings.ShowHomePage):
case nameof(Settings.ShowHistoryResultsForHomePage):
+ case nameof(Settings.HistoryStyle):
if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
@@ -476,7 +477,7 @@ namespace Flow.Launcher
&& 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))
{
@@ -863,7 +864,7 @@ namespace Flow.Launcher
public void UpdatePosition()
{
- // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
+ // Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
@@ -887,7 +888,7 @@ namespace Flow.Launcher
private void InitializePosition()
{
- // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
+ // Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
InitializePositionInner();
InitializePositionInner();
return;
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index 94454e43c..9205864ee 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -6,7 +6,6 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
-using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -253,7 +252,10 @@ namespace Flow.Launcher
public string GetTranslation(string key) => Internationalization.GetTranslation(key);
- public List GetAllPlugins() => PluginManager.AllPlugins.ToList();
+ public List GetAllPlugins() => PluginManager.GetAllLoadedPlugins();
+
+ public List GetAllInitializedPlugins(bool includeFailed) =>
+ PluginManager.GetAllInitializedPlugins(includeFailed);
public MatchResult FuzzySearch(string query, string stringToCompare) =>
StringMatcher.FuzzySearch(query, stringToCompare);
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
index e33a92851..88f6fa929 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
@@ -210,6 +210,8 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
public List LastQueryModes { get; } =
DropdownDataGeneric.GetValues("LastQuery");
+ public List HistoryStyles { get; } = HistoryStyleLocalized.GetValues();
+
public bool EnableDialogJump
{
get => Settings.EnableDialogJump;
@@ -276,6 +278,7 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
DropdownDataGeneric.UpdateLabels(SearchWindowAligns);
DropdownDataGeneric.UpdateLabels(SearchPrecisionScores);
DropdownDataGeneric.UpdateLabels(LastQueryModes);
+ HistoryStyleLocalized.UpdateLabels(HistoryStyles);
DropdownDataGeneric.UpdateLabels(DoublePinyinSchemas);
DropdownDataGeneric.UpdateLabels(DialogJumpWindowPositions);
DropdownDataGeneric.UpdateLabels(DialogJumpResultBehaviours);
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
index f35d4a81b..2d418ee7e 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
@@ -114,8 +114,11 @@ public partial class SettingsPanePluginsViewModel : BaseModel
}
}
- 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/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
index d25863402..bf284b1a6 100644
--- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
@@ -259,6 +259,22 @@
SelectedValuePath="Value" />
+
+
+
+
+
+
+
+
Items { get; private set; } = new List();
+#pragma warning disable CS0618 // Type or member is obsolete
+ public List Items { get; private set; } = [];
+#pragma warning restore CS0618 // Type or member is obsolete
- private int _maxHistory = 300;
+ [JsonInclude]
+ public List LastOpenedHistoryItems { get; private set; } = [];
- public void Add(string query)
+ private readonly int _maxHistory = 300;
+
+ public void PopulateHistoryFromLegacyHistory()
{
- if (string.IsNullOrEmpty(query)) return;
- if (Items.Count > _maxHistory)
+ if (Items.Count == 0) return;
+ // Migrate old history items to new LastOpenedHistoryItems
+ foreach (var item in Items)
{
- Items.RemoveAt(0);
+ LastOpenedHistoryItems.Add(new LastOpenedHistoryItem
+ {
+ Query = item.Query,
+ ExecutedDateTime = item.ExecutedDateTime
+ });
+ }
+ Items.Clear();
+ }
+
+ public void Add(Result result)
+ {
+ if (string.IsNullOrEmpty(result.OriginQuery.RawQuery)) return;
+ if (string.IsNullOrEmpty(result.PluginID)) return;
+
+ // Maintain the max history limit
+ if (LastOpenedHistoryItems.Count > _maxHistory)
+ {
+ LastOpenedHistoryItems.RemoveAt(0);
}
- if (Items.Count > 0 && Items.Last().Query == query)
+ // If the last item is the same as the current result, just update the timestamp
+ if (LastOpenedHistoryItems.Count > 0 &&
+ LastOpenedHistoryItems.Last().Equals(result))
{
- Items.Last().ExecutedDateTime = DateTime.Now;
+ LastOpenedHistoryItems.Last().ExecutedDateTime = DateTime.Now;
}
else
{
- Items.Add(new HistoryItem
+ LastOpenedHistoryItems.Add(new LastOpenedHistoryItem
{
- Query = query,
+ Title = result.Title,
+ SubTitle = result.SubTitle,
+ PluginID = result.PluginID,
+ Query = result.OriginQuery.RawQuery,
+ RecordKey = result.RecordKey,
ExecutedDateTime = DateTime.Now
});
}
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index ec19e2e8b..f0f4b257a 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -8,16 +8,17 @@ using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Windows;
-using System.Windows.Input;
using System.Windows.Controls;
+using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
+using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@@ -28,7 +29,7 @@ using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.ViewModel
{
- public partial class MainViewModel : BaseModel, ISavable, IDisposable
+ public partial class MainViewModel : BaseModel, ISavable, IDisposable, IResultUpdateRegister
{
#region Private Fields
@@ -151,6 +152,7 @@ namespace Flow.Launcher.ViewModel
_userSelectedRecordStorage = new FlowLauncherJsonStorage();
_topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
_history = _historyItemsStorage.Load();
+ _history.PopulateHistoryFromLegacyHistory();
_userSelectedRecord = _userSelectedRecordStorage.Load();
ContextMenu = new ResultsViewModel(Settings, this)
@@ -275,52 +277,50 @@ namespace Flow.Launcher.ViewModel
}
}
- 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;
+ }
+
+ 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);
+ }
+
+ foreach (var result in resultsCopy)
+ {
+ if (string.IsNullOrEmpty(result.BadgeIcoPath))
{
- return;
+ result.BadgeIcoPath = pair.Metadata.IcoPath;
}
+ }
- var token = e.Token == default ? _updateToken : e.Token;
+ PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
- 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);
- }
+ if (token.IsCancellationRequested) return;
- foreach (var result in resultsCopy)
- {
- if (string.IsNullOrEmpty(result.BadgeIcoPath))
- {
- result.BadgeIcoPath = pair.Metadata.IcoPath;
- }
- }
+ App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>");
- PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
-
- if (token.IsCancellationRequested) return;
-
- 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()
@@ -352,7 +352,7 @@ namespace Flow.Launcher.ViewModel
if (QueryResultsSelected())
{
SelectedResults = History;
- History.SelectedIndex = _history.Items.Count - 1;
+ History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1;
}
else
{
@@ -380,10 +380,11 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
public void ReverseHistory()
{
- if (_history.Items.Count > 0)
+ var historyItems = _history.LastOpenedHistoryItems;
+ if (historyItems.Count > 0)
{
- ChangeQueryText(_history.Items[^lastHistoryIndex].Query);
- if (lastHistoryIndex < _history.Items.Count)
+ ChangeQueryText(historyItems[^lastHistoryIndex].Query);
+ if (lastHistoryIndex < historyItems.Count)
{
lastHistoryIndex++;
}
@@ -393,9 +394,10 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
public void ForwardHistory()
{
- if (_history.Items.Count > 0)
+ var historyItems = _history.LastOpenedHistoryItems;
+ if (historyItems.Count > 0)
{
- ChangeQueryText(_history.Items[^lastHistoryIndex].Query);
+ ChangeQueryText(historyItems[^lastHistoryIndex].Query);
if (lastHistoryIndex > 1)
{
lastHistoryIndex--;
@@ -440,7 +442,7 @@ namespace Flow.Launcher.ViewModel
[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('\\'));
@@ -487,6 +489,8 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private async Task OpenResultAsync(string index)
{
+ // Must check query results selected before executing the action
+ var queryResultsSelected = QueryResultsSelected();
var results = SelectedResults;
if (index is not null)
{
@@ -527,10 +531,12 @@ namespace Flow.Launcher.ViewModel
}
}
- if (QueryResultsSelected())
+ // Record user selected result for result ranking
+ _userSelectedRecord.Add(result);
+ // Add item to history only if it is from results but not context menu or history
+ if (queryResultsSelected)
{
- _userSelectedRecord.Add(result);
- _history.Add(result.OriginQuery.RawQuery);
+ _history.Add(result);
lastHistoryIndex = 1;
}
}
@@ -559,7 +565,7 @@ namespace Flow.Launcher.ViewModel
resultsCopy.Add(resultCopy);
}
}
-
+
return resultsCopy;
}
@@ -606,10 +612,11 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void SelectPrevItem()
{
+ var historyItems = _history.LastOpenedHistoryItems;
if (QueryResultsSelected() // Results selected
&& string.IsNullOrEmpty(QueryText) // No input
&& Results.Visibility != Visibility.Visible // No items in result list, e.g. when home page is off and no query text is entered, therefore the view is collapsed.
- && _history.Items.Count > 0) // Have history items
+ && historyItems.Count > 0) // Have history items
{
lastHistoryIndex = 1;
ReverseHistory();
@@ -908,7 +915,7 @@ namespace Flow.Launcher.ViewModel
private string _placeholderText;
public string PlaceholderText
{
- get => string.IsNullOrEmpty(_placeholderText) ? Localize.queryTextBoxPlaceholder(): _placeholderText;
+ get => string.IsNullOrEmpty(_placeholderText) ? Localize.queryTextBoxPlaceholder() : _placeholderText;
set
{
_placeholderText = value;
@@ -1150,6 +1157,7 @@ namespace Flow.Launcher.ViewModel
HideInternalPreview();
_ = OpenExternalPreviewAsync(path);
}
+
break;
case true
when !CanExternalPreviewSelectedResult(out var _):
@@ -1263,18 +1271,18 @@ namespace Flow.Launcher.ViewModel
var filtered = results.Select(x => x.Clone()).Where
(
r =>
- {
- var match = App.API.FuzzySearch(query, r.Title);
- if (!match.IsSearchPrecisionScoreMet())
- {
- match = App.API.FuzzySearch(query, r.SubTitle);
- }
+ {
+ var match = App.API.FuzzySearch(query, r.Title);
+ if (!match.IsSearchPrecisionScoreMet())
+ {
+ match = App.API.FuzzySearch(query, r.SubTitle);
+ }
- if (!match.IsSearchPrecisionScoreMet()) return false;
+ if (!match.IsSearchPrecisionScoreMet()) return false;
- r.Score = match.Score;
- return true;
- }).ToList();
+ r.Score = match.Score;
+ return true;
+ }).ToList();
ContextMenu.AddResults(filtered, id);
}
else
@@ -1290,7 +1298,7 @@ namespace Flow.Launcher.ViewModel
var query = QueryText.ToLower().Trim();
History.Clear();
- var results = GetHistoryItems(_history.Items);
+ var results = GetHistoryItems(_history.LastOpenedHistoryItems);
if (!string.IsNullOrEmpty(query))
{
@@ -1307,26 +1315,64 @@ namespace Flow.Launcher.ViewModel
}
}
- private static List GetHistoryItems(IEnumerable historyItems)
+ private List GetHistoryItems(IEnumerable historyItems)
{
var results = new List();
- foreach (var h in historyItems)
+ if (Settings.HistoryStyle == HistoryStyle.Query)
{
- var result = new Result
+ foreach (var h in historyItems)
{
- Title = Localize.executeQuery(h.Query),
- SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
- IcoPath = Constant.HistoryIcon,
- OriginQuery = new Query { RawQuery = h.Query },
- Action = _ =>
+ var result = new Result
{
- App.API.BackToQueryResults();
- App.API.ChangeQuery(h.Query);
- return false;
- },
- Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
- };
- results.Add(result);
+ Title = Localize.executeQuery(h.Query),
+ SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
+ IcoPath = Constant.HistoryIcon,
+ OriginQuery = new Query { RawQuery = h.Query },
+ Action = _ =>
+ {
+ App.API.BackToQueryResults();
+ App.API.ChangeQuery(h.Query);
+ return false;
+ },
+ Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
+ };
+ results.Add(result);
+ }
+ }
+ else
+ {
+ foreach (var h in historyItems)
+ {
+ var result = new Result
+ {
+ Title = string.IsNullOrEmpty(h.Title) ? // Old migrated history items have no title
+ Localize.executeQuery(h.Query) :
+ h.Title,
+ SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime),
+ IcoPath = Constant.HistoryIcon,
+ OriginQuery = new Query { RawQuery = h.Query },
+ AsyncAction = async c =>
+ {
+ var reflectResult = await ResultHelper.PopulateResultsAsync(h);
+ if (reflectResult != null)
+ {
+ // Record the user selected record for result ranking
+ _userSelectedRecord.Add(reflectResult);
+
+ // Since some actions may need to hide the Flow window to execute
+ // So let us populate the results of them
+ return await reflectResult.ExecuteAsync(c);
+ }
+
+ // If we cannot get the result, fallback to re-query
+ App.API.BackToQueryResults();
+ App.API.ChangeQuery(h.Query);
+ return false;
+ },
+ Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
+ };
+ results.Add(result);
+ }
}
return results;
}
@@ -1558,7 +1604,7 @@ namespace Flow.Launcher.ViewModel
void QueryHistoryTask(CancellationToken token)
{
// Select last history results and revert its order to make sure last history results are on top
- var historyItems = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse();
+ var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse();
var results = GetHistoryItems(historyItems);
@@ -1579,7 +1625,7 @@ namespace Flow.Launcher.ViewModel
{
if (string.IsNullOrWhiteSpace(queryText))
{
- return QueryBuilder.Build(string.Empty, PluginManager.NonGlobalPlugins);
+ return QueryBuilder.Build(string.Empty, PluginManager.GetNonGlobalPlugins());
}
var queryBuilder = new StringBuilder(queryText);
@@ -1599,7 +1645,7 @@ namespace Flow.Launcher.ViewModel
// Applying builtin shortcuts
await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp);
- return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
+ return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.GetNonGlobalPlugins());
}
private async Task BuildQueryAsync(IEnumerable builtInShortcuts,
diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs
index bf7720651..810a238a7 100644
--- a/Flow.Launcher/ViewModel/PluginViewModel.cs
+++ b/Flow.Launcher/ViewModel/PluginViewModel.cs
@@ -131,8 +131,13 @@ namespace Flow.Launcher.ViewModel
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 =>
+ // Here we do not check if the plugin is initialized successfully
+ // So we can let users change settings for initializing or initialization 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
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
index 8392a0dbe..8f0dfb4f5 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
@@ -105,7 +105,7 @@
-
+