mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge pull request #3854 from Flow-Launcher/plugin_initialization
Asynchronous Loading & Initialization Plugin Model to Improve Window Startup Speed
This commit is contained in:
commit
da4b961686
14 changed files with 567 additions and 258 deletions
12
Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
Normal file
12
Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 ------------------------------------------------------");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue