Merge pull request #3854 from Flow-Launcher/plugin_initialization

Asynchronous Loading & Initialization Plugin Model to Improve Window Startup Speed
This commit is contained in:
Jeremy Wu 2025-10-16 22:08:42 +11:00 committed by GitHub
commit da4b961686
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 567 additions and 258 deletions

View file

@ -0,0 +1,12 @@
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin;
public interface IResultUpdateRegister
{
/// <summary>
/// Register a plugin to receive results updated event.
/// </summary>
/// <param name="pair"></param>
void RegisterResultsUpdatedEvent(PluginPair pair);
}

View file

@ -7,6 +7,7 @@ using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
@ -24,44 +25,36 @@ namespace Flow.Launcher.Core.Plugin
{ {
private static readonly string ClassName = nameof(PluginManager); private static readonly string ClassName = nameof(PluginManager);
public static List<PluginPair> AllPlugins { get; private set; } private static readonly ConcurrentDictionary<string, PluginPair> _allLoadedPlugins = [];
public static readonly HashSet<PluginPair> GlobalPlugins = new(); private static readonly ConcurrentDictionary<string, PluginPair> _allInitializedPlugins = [];
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new(); private static readonly ConcurrentDictionary<string, PluginPair> _initFailedPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _globalPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _nonGlobalPlugins = [];
private static PluginsSettings Settings; private static PluginsSettings Settings;
private static readonly ConcurrentBag<string> ModifiedPlugins = new(); private static readonly ConcurrentBag<string> ModifiedPlugins = [];
private static IEnumerable<PluginPair> _contextMenuPlugins; private static readonly ConcurrentBag<PluginPair> _contextMenuPlugins = [];
private static IEnumerable<PluginPair> _homePlugins; private static readonly ConcurrentBag<PluginPair> _homePlugins = [];
private static IEnumerable<PluginPair> _resultUpdatePlugin; private static readonly ConcurrentBag<PluginPair> _translationPlugins = [];
private static IEnumerable<PluginPair> _translationPlugins; private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
/// <summary> /// <summary>
/// Directories that will hold Flow Launcher plugin directory /// Directories that will hold Flow Launcher plugin directory
/// </summary> /// </summary>
public static readonly string[] Directories = public static readonly string[] Directories =
{ [
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
}; ];
private static void DeletePythonBinding() #region Save & Dispose & Reload Plugin
{
const string binding = "flowlauncher.py";
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
{
File.Delete(Path.Combine(subDirectory, binding));
}
}
/// <summary> /// <summary>
/// Save json and ISavable /// Save json and ISavable
/// </summary> /// </summary>
public static void Save() public static void Save()
{ {
foreach (var pluginPair in AllPlugins) foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: false))
{ {
var savable = pluginPair.Plugin as ISavable; var savable = pluginPair.Plugin as ISavable;
try try
@ -80,7 +73,8 @@ namespace Flow.Launcher.Core.Plugin
public static async ValueTask DisposePluginsAsync() 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); await DisposePluginAsync(pluginPair);
} }
@ -108,49 +102,53 @@ namespace Flow.Launcher.Core.Plugin
public static async Task ReloadDataAsync() 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), IReloadable p => Task.Run(p.ReloadData),
IAsyncReloadable p => p.ReloadDataAsync(), IAsyncReloadable p => p.ReloadDataAsync(),
_ => Task.CompletedTask, _ => Task.CompletedTask,
}).ToArray()); })]);
} }
#endregion
#region External Preview
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) 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), IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask, _ => Task.CompletedTask,
}).ToArray()); })]);
} }
public static async Task CloseExternalPreviewAsync() 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(), IAsyncExternalPreview p => p.ClosePreviewAsync(),
_ => Task.CompletedTask, _ => Task.CompletedTask,
}).ToArray()); })]);
} }
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) 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), IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask, _ => Task.CompletedTask,
}).ToArray()); })]);
} }
public static bool UseExternalPreview() public static bool UseExternalPreview()
{ {
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled); return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled);
} }
public static bool AllowAlwaysPreview() public static bool AllowAlwaysPreview()
{ {
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled); var plugin = GetExternalPreviewPlugins().FirstOrDefault(x => !x.Metadata.Disabled);
if (plugin is null) if (plugin is null)
return false; return false;
@ -158,6 +156,15 @@ namespace Flow.Launcher.Core.Plugin
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview(); return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
} }
private static IList<PluginPair> GetExternalPreviewPlugins()
{
return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Constructor
static PluginManager() static PluginManager()
{ {
// validate user directory // validate user directory
@ -166,9 +173,28 @@ namespace Flow.Launcher.Core.Plugin
DeletePythonBinding(); 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
/// <summary> /// <summary>
/// because InitializePlugins needs API, so LoadPlugins needs to be called first /// Load plugins from the directories specified in Directories.
/// todo happlebao The API should be removed
/// </summary> /// </summary>
/// <param name="settings"></param> /// <param name="settings"></param>
public static void LoadPlugins(PluginsSettings settings) public static void LoadPlugins(PluginsSettings settings)
@ -176,33 +202,22 @@ namespace Flow.Launcher.Core.Plugin
var metadatas = PluginConfig.Parse(Directories); var metadatas = PluginConfig.Parse(Directories);
Settings = settings; Settings = settings;
Settings.UpdatePluginSettings(metadatas); 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 // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
UpdatePluginDirectory(metadatas); UpdatePluginDirectory(metadatas);
// Initialize plugin enumerable after all plugins are initialized
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
_resultUpdatePlugin = GetPluginsForInterface<IResultUpdated>();
_translationPlugins = GetPluginsForInterface<IPluginI18n>();
// Initialize Dialog Jump plugin pairs
foreach (var pair in GetPluginsForInterface<IDialogJumpExplorer>())
{
_dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair
{
Plugin = (IDialogJumpExplorer)pair.Plugin,
Metadata = pair.Metadata
});
}
foreach (var pair in GetPluginsForInterface<IDialogJumpDialog>())
{
_dialogJumpDialogPlugins.Add(new DialogJumpDialogPair
{
Plugin = (IDialogJumpDialog)pair.Plugin,
Metadata = pair.Metadata
});
}
} }
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas) private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
@ -233,15 +248,19 @@ namespace Flow.Launcher.Core.Plugin
} }
/// <summary> /// <summary>
/// Call initialize for all plugins /// Initialize all plugins asynchronously.
/// </summary> /// </summary>
/// <param name="register">The register to register results updated event for each plugin.</param>
/// <returns>return the list of failed to init plugins or null for none</returns> /// <returns>return the list of failed to init plugins or null for none</returns>
public static async Task InitializePluginsAsync() public static async Task InitializePluginsAsync(IResultUpdateRegister register)
{ {
var failedPlugins = new ConcurrentQueue<PluginPair>(); var initTasks = _allLoadedPlugins.Select(x => Task.Run(async () =>
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
{ {
var pair = x.Value;
// Register plugin action keywords so that plugins can be queried in results
RegisterPluginActionKeywords(pair);
try try
{ {
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>", 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.Disabled = true;
pair.Metadata.HomeDisabled = true; pair.Metadata.HomeDisabled = true;
failedPlugins.Enqueue(pair);
PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); 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 var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name));
// 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));
PublicApi.Instance.ShowMsg( PublicApi.Instance.ShowMsg(
Localize.failedToInitializePluginsTitle(), Localize.failedToInitializePluginsTitle(),
Localize.failedToInitializePluginsMessage(failed), 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<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump) public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
{ {
if (query is null) if (query is null)
return Array.Empty<PluginPair>(); return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
{ {
if (dialogJump) 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 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) if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
return Array.Empty<PluginPair>(); return Array.Empty<PluginPair>();
if (PublicApi.Instance.PluginModified(plugin.Metadata.ID)) if (PluginModified(plugin.Metadata.ID))
return Array.Empty<PluginPair>(); return Array.Empty<PluginPair>();
return new List<PluginPair> return [plugin];
{
plugin
};
} }
public static ICollection<PluginPair> ValidPluginsForHomeQuery() public static ICollection<PluginPair> ValidPluginsForHomeQuery()
{ {
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))];
} }
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
@ -337,6 +395,28 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<Result>(); var results = new List<Result>();
var metadata = pair.Metadata; 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 try
{ {
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@ -361,15 +441,15 @@ namespace Flow.Launcher.Core.Plugin
{ {
Result r = new() Result r = new()
{ {
Title = $"{metadata.Name}: Failed to respond!", Title = Localize.pluginFailedToRespond(metadata.Name),
SubTitle = "Select this result for more info", SubTitle = Localize.pluginFailedToRespondSubtitle(),
AutoCompleteText = query.RawQuery,
IcoPath = Constant.ErrorIcon, IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory, PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword, ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID, PluginID = metadata.ID,
OriginQuery = query, OriginQuery = query,
Action = _ => { throw new FlowPluginException(metadata, e);}, Action = _ => { throw new FlowPluginException(metadata, e);}
Score = -100
}; };
results.Add(r); results.Add(r);
} }
@ -381,6 +461,28 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<Result>(); var results = new List<Result>();
var metadata = pair.Metadata; 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 try
{ {
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@ -411,6 +513,12 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<DialogJumpResult>(); var results = new List<DialogJumpResult>();
var metadata = pair.Metadata; 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 try
{ {
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
@ -436,6 +544,52 @@ namespace Flow.Launcher.Core.Plugin
return results; return results;
} }
private static bool IsPluginInitializing(PluginMetadata metadata)
{
return !_allInitializedPlugins.ContainsKey(metadata.ID);
}
#endregion
#region Get Plugin List
public static List<PluginPair> GetAllLoadedPlugins()
{
return [.. _allLoadedPlugins.Values];
}
public static List<PluginPair> GetAllInitializedPlugins(bool includeFailed)
{
if (includeFailed)
{
return [.. _allInitializedPlugins.Values];
}
else
{
return [.. _allInitializedPlugins.Values
.Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))];
}
}
private static List<PluginPair> GetGlobalPlugins()
{
return [.. _globalPlugins.Values];
}
public static Dictionary<string, PluginPair> GetNonGlobalPlugins()
{
return _nonGlobalPlugins.ToDictionary();
}
public static List<PluginPair> GetTranslationPlugins()
{
return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Update Metadata & Get Plugin
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query) public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
{ {
foreach (var r in results) foreach (var r in results)
@ -454,28 +608,19 @@ namespace Flow.Launcher.Core.Plugin
/// <summary> /// <summary>
/// get specified plugin, return null if not found /// get specified plugin, return null if not found
/// </summary> /// </summary>
/// <remarks>
/// Plugin may not be initialized, so do not use its plugin model to execute any commands
/// </remarks>
/// <param name="id"></param> /// <param name="id"></param>
/// <returns></returns> /// <returns></returns>
public static PluginPair GetPluginForId(string id) 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<PluginPair> GetPluginsForInterface<T>() where T : IFeatures #endregion
{
// 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<PluginPair>();
}
public static IList<PluginPair> GetResultUpdatePlugin() #region Get Context Menus
{
return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static IList<PluginPair> GetTranslationPlugins()
{
return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static List<Result> GetContextMenusForPlugin(Result result) public static List<Result> GetContextMenusForPlugin(Result result)
{ {
@ -506,27 +651,82 @@ namespace Flow.Launcher.Core.Plugin
return results; return results;
} }
#endregion
#region Check Home Plugin
public static bool IsHomePlugin(string id) public static bool IsHomePlugin(string id)
{ {
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id); return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
} }
public static IList<DialogJumpExplorerPair> 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<DialogJumpDialogPair> 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) public static bool ActionKeywordRegistered(string actionKeyword)
{ {
// this method is only checking for action keywords (defined as not '*') registration // this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic // hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword); && _nonGlobalPlugins.ContainsKey(actionKeyword);
} }
/// <summary> /// <summary>
@ -538,11 +738,11 @@ namespace Flow.Launcher.Core.Plugin
var plugin = GetPluginForId(id); var plugin = GetPluginForId(id);
if (newActionKeyword == Query.GlobalPluginWildcardSign) if (newActionKeyword == Query.GlobalPluginWildcardSign)
{ {
GlobalPlugins.Add(plugin); _globalPlugins.TryAdd(id, plugin);
} }
else else
{ {
NonGlobalPlugins[newActionKeyword] = plugin; _nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin);
} }
// Update action keywords and action keyword in plugin metadata // Update action keywords and action keyword in plugin metadata
@ -569,11 +769,13 @@ namespace Flow.Launcher.Core.Plugin
plugin.Metadata.ActionKeywords plugin.Metadata.ActionKeywords
.Count(x => x == Query.GlobalPluginWildcardSign) == 1) .Count(x => x == Query.GlobalPluginWildcardSign) == 1)
{ {
GlobalPlugins.Remove(plugin); _globalPlugins.TryRemove(id, out _);
} }
if (oldActionkeyword != Query.GlobalPluginWildcardSign) if (oldActionkeyword != Query.GlobalPluginWildcardSign)
NonGlobalPlugins.Remove(oldActionkeyword); {
_nonGlobalPlugins.TryRemove(oldActionkeyword, out _);
}
// Update action keywords and action keyword in plugin metadata // Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); 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) private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
{ {
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length; var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
@ -612,12 +820,15 @@ namespace Flow.Launcher.Core.Plugin
if (!Version.TryParse(newMetadata.Version, out var newVersion)) 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 true; // If version is not valid, we assume it is lesser than any existing version
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID // Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists
&& Version.TryParse(x.Metadata.Version, out var version) return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID
&& newVersion <= version); && Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
} }
#region Public functions #endregion
#region Public Functions
public static bool PluginModified(string id) public static bool PluginModified(string id)
{ {
@ -655,7 +866,7 @@ namespace Flow.Launcher.Core.Plugin
#endregion #endregion
#region Internal functions #region Internal Functions
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified) 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, // If we want to remove plugin from AllPlugins,
// we need to dispose them so that they can release file handles // we need to dispose them so that they can release file handles
// which can help FL to delete the plugin settings & cache folders successfully // 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) foreach (var pluginPair in pluginPairs)
{ {
await DisposePluginAsync(pluginPair); await DisposePluginAsync(pluginPair);
@ -797,12 +1008,22 @@ namespace Flow.Launcher.Core.Plugin
Localize.failedToRemovePluginCacheMessage(plugin.Name)); Localize.failedToRemovePluginCacheMessage(plugin.Name));
} }
Settings.RemovePluginSettings(plugin.ID); Settings.RemovePluginSettings(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); {
GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID); _allLoadedPlugins.TryRemove(plugin.ID, out var _);
var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); }
{
_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) foreach (var key in keysToRemove)
{ {
NonGlobalPlugins.Remove(key); _nonGlobalPlugins.TryRemove(key, out var _);
} }
} }
@ -818,5 +1039,7 @@ namespace Flow.Launcher.Core.Plugin
} }
#endregion #endregion
#endregion
} }
} }

View file

@ -48,7 +48,7 @@ namespace Flow.Launcher.Core.Plugin
return plugins; return plugins;
} }
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source) private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
{ {
var erroredPlugins = new List<string>(); var erroredPlugins = new List<string>();
@ -58,55 +58,57 @@ namespace Flow.Launcher.Core.Plugin
foreach (var metadata in metadatas) foreach (var metadata in metadatas)
{ {
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () => var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
{
Assembly assembly = null;
IAsyncPlugin plugin = null;
try
{ {
Assembly assembly = null; var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
IAsyncPlugin plugin = null; assembly = assemblyLoader.LoadAssemblyAndDependencies();
try var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
{ typeof(IAsyncPlugin));
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, plugin = Activator.CreateInstance(type) as IAsyncPlugin;
typeof(IAsyncPlugin));
plugin = Activator.CreateInstance(type) as IAsyncPlugin; metadata.AssemblyName = assembly.GetName().Name;
}
metadata.AssemblyName = assembly.GetName().Name;
}
#if DEBUG #if DEBUG
catch (Exception) catch (Exception)
{ {
throw; throw;
} }
#else #else
catch (Exception e) when (assembly == null) catch (Exception e) when (assembly == null)
{ {
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e); PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
} }
catch (InvalidOperationException e) catch (InvalidOperationException e)
{ {
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
} }
catch (ReflectionTypeLoadException e) catch (ReflectionTypeLoadException e)
{ {
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
} }
catch (Exception e) catch (Exception e)
{ {
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e); PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
} }
#endif #endif
if (plugin == null) if (plugin == null)
{ {
erroredPlugins.Add(metadata.Name); erroredPlugins.Add(metadata.Name);
return; return;
} }
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
metadata.InitTime += milliseconds; metadata.InitTime += milliseconds;
PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
} }
if (erroredPlugins.Count > 0) if (erroredPlugins.Count > 0)

View file

@ -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 #endregion
#region IDisposable #region IDisposable

View file

@ -13,6 +13,7 @@ using NHotkey;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.Accessibility;
using System.Collections.Concurrent;
namespace Flow.Launcher.Infrastructure.DialogJump namespace Flow.Launcher.Infrastructure.DialogJump
{ {
@ -60,12 +61,12 @@ namespace Flow.Launcher.Infrastructure.DialogJump
private static HWND _mainWindowHandle = HWND.Null; private static HWND _mainWindowHandle = HWND.Null;
private static readonly Dictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new(); private static readonly ConcurrentDictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
private static DialogJumpExplorerPair _lastExplorer = null; private static DialogJumpExplorerPair _lastExplorer = null;
private static readonly Lock _lastExplorerLock = new(); private static readonly Lock _lastExplorerLock = new();
private static readonly Dictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new(); private static readonly ConcurrentDictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
private static IDialogJumpDialogWindow _dialogWindow = null; private static IDialogJumpDialogWindow _dialogWindow = null;
private static readonly Lock _dialogWindowLock = new(); private static readonly Lock _dialogWindowLock = new();
@ -101,22 +102,13 @@ namespace Flow.Launcher.Infrastructure.DialogJump
#region Initialize & Setup #region Initialize & Setup
public static void InitializeDialogJump(IList<DialogJumpExplorerPair> dialogJumpExplorers, public static void InitializeDialogJump()
IList<DialogJumpDialogPair> dialogJumpDialogs)
{ {
if (_initialized) return; if (_initialized) return;
// Initialize Dialog Jump explorers & dialogs // Initialize preinstalled Dialog Jump explorers & dialogs
_dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null); _dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null);
foreach (var explorer in dialogJumpExplorers) _dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null);
{
_dialogJumpExplorers.Add(explorer, null);
}
_dialogJumpDialogs.Add(WindowsDialogJumpDialog, null);
foreach (var dialog in dialogJumpDialogs)
{
_dialogJumpDialogs.Add(dialog, null);
}
// Initialize main window handle // Initialize main window handle
_mainWindowHandle = Win32Helper.GetMainWindowHandle(); _mainWindowHandle = Win32Helper.GetMainWindowHandle();
@ -131,6 +123,29 @@ namespace Flow.Launcher.Infrastructure.DialogJump
_initialized = true; _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) public static void SetupDialogJump(bool enabled)
{ {
if (enabled == _enabled) return; if (enabled == _enabled) return;

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
@ -173,9 +173,21 @@ namespace Flow.Launcher.Plugin
/// <summary> /// <summary>
/// Get all loaded plugins /// Get all loaded plugins
/// </summary> /// </summary>
/// <remarks>
/// Will also return any plugins not fully initialized yet
/// </remarks>
/// <returns></returns> /// <returns></returns>
List<PluginPair> GetAllPlugins(); List<PluginPair> GetAllPlugins();
/// <summary>
/// Get all initialized plugins
/// </summary>
/// <param name="includeFailed">
/// Whether to include plugins that failed to initialize
/// </param>
/// <returns></returns>
List<PluginPair> GetAllInitializedPlugins(bool includeFailed);
/// <summary> /// <summary>
/// Registers a callback function for global keyboard events. /// Registers a callback function for global keyboard events.
/// </summary> /// </summary>

View file

@ -187,12 +187,14 @@ namespace Flow.Launcher
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created // So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
// Setup log level before any logging is done
Log.SetLogLevel(_settings.LogLevel); Log.SetLogLevel(_settings.LogLevel);
// Update dynamic resources base on settings // Update dynamic resources base on settings
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont); Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont); Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
// Initialize notification system before any notification api is called
Notification.Install(); Notification.Install();
// Enable Win32 dark mode if the system is in dark mode before creating all windows // Enable Win32 dark mode if the system is in dark mode before creating all windows
@ -201,6 +203,7 @@ namespace Flow.Launcher
// Initialize language before portable clean up since it needs translations // Initialize language before portable clean up since it needs translations
await _internationalization.InitializeLanguageAsync(); await _internationalization.InitializeLanguageAsync();
// Clean up after portability update
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate(); Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------"); API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
@ -210,32 +213,25 @@ namespace Flow.Launcher
RegisterDispatcherUnhandledException(); RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException(); RegisterTaskSchedulerUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync(); var imageLoaderTask = ImageLoader.InitializeAsync();
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
PluginManager.LoadPlugins(_settings.PluginSettings);
// Register ResultsUpdated event after all plugins are loaded
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
Http.Proxy = _settings.Proxy; Http.Proxy = _settings.Proxy;
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly // Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
await API.UpdatePluginManifestAsync(); await API.UpdatePluginManifestAsync();
await PluginManager.InitializePluginsAsync(); await imageLoaderTask;
// Update plugin titles after plugins are initialized with their api instances
Internationalization.UpdatePluginMetadataTranslations();
await imageLoadertask;
_mainWindow = new MainWindow(); _mainWindow = new MainWindow();
Current.MainWindow = _mainWindow; Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher; 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 // Initialize hotkey mapper instantly after main window is created because
// it will steal focus from main window which causes window hide // it will steal focus from main window which causes window hide
HotKeyMapper.Initialize(); HotKeyMapper.Initialize();
@ -243,19 +239,40 @@ namespace Flow.Launcher
// Initialize theme for main window // Initialize theme for main window
Ioc.Default.GetRequiredService<Theme>().ChangeTheme(); Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
RegisterExitEvents(); RegisterExitEvents();
AutoStartup(); AutoStartup();
AutoUpdates(); AutoUpdates();
AutoPluginUpdates();
API.SaveAppAllSettings(); 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 ------------------------------------------------------");
});
}); });
} }

View file

@ -20,7 +20,7 @@ public static class ResultHelper
{ {
var plugin = PluginManager.GetPluginForId(pluginId); var plugin = PluginManager.GetPluginForId(pluginId);
if (plugin == null) return null; if (plugin == null) return null;
var query = QueryBuilder.Build(rawQuery, PluginManager.NonGlobalPlugins); var query = QueryBuilder.Build(rawQuery, PluginManager.GetNonGlobalPlugins());
if (query == null) return null; if (query == null) return null;
try try
{ {

View file

@ -66,6 +66,10 @@
<system:String x:Key="PositionReset">Position Reset</system:String> <system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String> <system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String> <system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<system:String x:Key="pluginStillInitializing">{0}: This plugin is still initializing...</system:String>
<system:String x:Key="pluginStillInitializingSubtitle">Select this result to requery</system:String>
<system:String x:Key="pluginFailedToRespond">{0}: Failed to respond!</system:String>
<system:String x:Key="pluginFailedToRespondSubtitle">Select this result for more info</system:String>
<!-- Setting General --> <!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Settings</system:String> <system:String x:Key="flowlauncher_settings">Settings</system:String>

View file

@ -477,7 +477,7 @@ namespace Flow.Launcher
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{ {
var queryWithoutActionKeyword = var queryWithoutActionKeyword =
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
{ {

View file

@ -5,7 +5,6 @@ using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -248,7 +247,10 @@ namespace Flow.Launcher
public string GetTranslation(string key) => Internationalization.GetTranslation(key); public string GetTranslation(string key) => Internationalization.GetTranslation(key);
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList(); public List<PluginPair> GetAllPlugins() => PluginManager.GetAllLoadedPlugins();
public List<PluginPair> GetAllInitializedPlugins(bool includeFailed) =>
PluginManager.GetAllInitializedPlugins(includeFailed);
public MatchResult FuzzySearch(string query, string stringToCompare) => public MatchResult FuzzySearch(string query, string stringToCompare) =>
StringMatcher.FuzzySearch(query, stringToCompare); StringMatcher.FuzzySearch(query, stringToCompare);

View file

@ -114,8 +114,11 @@ public partial class SettingsPanePluginsViewModel : BaseModel
} }
} }
private IList<PluginViewModel>? _pluginViewModels; private List<PluginViewModel>? _pluginViewModels;
public IList<PluginViewModel> PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins // 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<PluginViewModel> PluginViewModels => _pluginViewModels ??= App.API.GetAllPlugins()
.OrderBy(plugin => plugin.Metadata.Disabled) .OrderBy(plugin => plugin.Metadata.Disabled)
.ThenBy(plugin => plugin.Metadata.Name) .ThenBy(plugin => plugin.Metadata.Name)
.Select(plugin => new PluginViewModel .Select(plugin => new PluginViewModel

View file

@ -29,7 +29,7 @@ using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
public partial class MainViewModel : BaseModel, ISavable, IDisposable public partial class MainViewModel : BaseModel, ISavable, IDisposable, IResultUpdateRegister
{ {
#region Private Fields #region Private Fields
@ -277,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; if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested)
plugin.ResultsUpdated += (s, e) =>
{ {
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested) return;
}
var token = e.Token == default ? _updateToken : e.Token;
IReadOnlyList<Result> 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<Result> resultsCopy; if (token.IsCancellationRequested) return;
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) App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>");
{
if (string.IsNullOrEmpty(result.BadgeIcoPath))
{
result.BadgeIcoPath = pair.Metadata.IcoPath;
}
}
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
token)))
if (token.IsCancellationRequested) return; {
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
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");
}
};
}
} }
private async Task RegisterClockAndDateUpdateAsync() private async Task RegisterClockAndDateUpdateAsync()
@ -444,7 +442,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void Backspace(object index) 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 // GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string
var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\')); var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\'));
@ -1627,7 +1625,7 @@ namespace Flow.Launcher.ViewModel
{ {
if (string.IsNullOrWhiteSpace(queryText)) if (string.IsNullOrWhiteSpace(queryText))
{ {
return QueryBuilder.Build(string.Empty, PluginManager.NonGlobalPlugins); return QueryBuilder.Build(string.Empty, PluginManager.GetNonGlobalPlugins());
} }
var queryBuilder = new StringBuilder(queryText); var queryBuilder = new StringBuilder(queryText);
@ -1647,7 +1645,7 @@ namespace Flow.Launcher.ViewModel
// Applying builtin shortcuts // Applying builtin shortcuts
await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp); 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<BaseBuiltinShortcutModel> builtInShortcuts, private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts,

View file

@ -131,8 +131,13 @@ namespace Flow.Launcher.ViewModel
private Control _bottomPart2; private Control _bottomPart2;
public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null;
public bool HasSettingControl => PluginPair.Plugin is ISettingProvider && public bool HasSettingControl =>
(PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel()); // 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 public Control SettingControl
=> IsExpanded => IsExpanded
? _settingControl ? _settingControl