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.Tasks;
|
||||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.DialogJump;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
|
@ -24,44 +25,36 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
private static readonly string ClassName = nameof(PluginManager);
|
||||
|
||||
public static List<PluginPair> AllPlugins { get; private set; }
|
||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
||||
private static readonly ConcurrentDictionary<string, PluginPair> _allLoadedPlugins = [];
|
||||
private static readonly ConcurrentDictionary<string, PluginPair> _allInitializedPlugins = [];
|
||||
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 readonly ConcurrentBag<string> ModifiedPlugins = new();
|
||||
private static readonly ConcurrentBag<string> ModifiedPlugins = [];
|
||||
|
||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
private static IEnumerable<PluginPair> _homePlugins;
|
||||
private static IEnumerable<PluginPair> _resultUpdatePlugin;
|
||||
private static IEnumerable<PluginPair> _translationPlugins;
|
||||
|
||||
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
|
||||
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
|
||||
private static readonly ConcurrentBag<PluginPair> _contextMenuPlugins = [];
|
||||
private static readonly ConcurrentBag<PluginPair> _homePlugins = [];
|
||||
private static readonly ConcurrentBag<PluginPair> _translationPlugins = [];
|
||||
private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];
|
||||
|
||||
/// <summary>
|
||||
/// Directories that will hold Flow Launcher plugin directory
|
||||
/// </summary>
|
||||
public static readonly string[] Directories =
|
||||
{
|
||||
[
|
||||
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
|
||||
};
|
||||
];
|
||||
|
||||
private static void DeletePythonBinding()
|
||||
{
|
||||
const string binding = "flowlauncher.py";
|
||||
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
|
||||
{
|
||||
File.Delete(Path.Combine(subDirectory, binding));
|
||||
}
|
||||
}
|
||||
#region Save & Dispose & Reload Plugin
|
||||
|
||||
/// <summary>
|
||||
/// Save json and ISavable
|
||||
/// </summary>
|
||||
public static void Save()
|
||||
{
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: false))
|
||||
{
|
||||
var savable = pluginPair.Plugin as ISavable;
|
||||
try
|
||||
|
|
@ -80,7 +73,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public static async ValueTask DisposePluginsAsync()
|
||||
{
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
// Still call dispose for all plugins even if initialization failed, so that we can clean up resources
|
||||
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: true))
|
||||
{
|
||||
await DisposePluginAsync(pluginPair);
|
||||
}
|
||||
|
|
@ -108,49 +102,53 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public static async Task ReloadDataAsync()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IReloadable p => Task.Run(p.ReloadData),
|
||||
IAsyncReloadable p => p.ReloadDataAsync(),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region External Preview
|
||||
|
||||
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
public static async Task CloseExternalPreviewAsync()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.ClosePreviewAsync(),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
public static bool UseExternalPreview()
|
||||
{
|
||||
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
|
||||
return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled);
|
||||
}
|
||||
|
||||
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)
|
||||
return false;
|
||||
|
|
@ -158,6 +156,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
|
||||
}
|
||||
|
||||
private static IList<PluginPair> GetExternalPreviewPlugins()
|
||||
{
|
||||
return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
static PluginManager()
|
||||
{
|
||||
// validate user directory
|
||||
|
|
@ -166,9 +173,28 @@ namespace Flow.Launcher.Core.Plugin
|
|||
DeletePythonBinding();
|
||||
}
|
||||
|
||||
private static void DeletePythonBinding()
|
||||
{
|
||||
const string binding = "flowlauncher.py";
|
||||
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(Path.Combine(subDirectory, binding));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load & Initialize Plugins
|
||||
|
||||
/// <summary>
|
||||
/// because InitializePlugins needs API, so LoadPlugins needs to be called first
|
||||
/// todo happlebao The API should be removed
|
||||
/// Load plugins from the directories specified in Directories.
|
||||
/// </summary>
|
||||
/// <param name="settings"></param>
|
||||
public static void LoadPlugins(PluginsSettings settings)
|
||||
|
|
@ -176,33 +202,22 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var metadatas = PluginConfig.Parse(Directories);
|
||||
Settings = settings;
|
||||
Settings.UpdatePluginSettings(metadatas);
|
||||
AllPlugins = PluginsLoader.Plugins(metadatas, Settings);
|
||||
|
||||
// Load plugins
|
||||
var allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings);
|
||||
foreach (var plugin in allLoadedPlugins)
|
||||
{
|
||||
if (plugin != null)
|
||||
{
|
||||
if (!_allLoadedPlugins.TryAdd(plugin.Metadata.ID, plugin))
|
||||
{
|
||||
PublicApi.Instance.LogError(ClassName, $"Plugin with ID {plugin.Metadata.ID} already loaded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
|
||||
UpdatePluginDirectory(metadatas);
|
||||
|
||||
// Initialize plugin enumerable after all plugins are initialized
|
||||
_contextMenuPlugins = GetPluginsForInterface<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)
|
||||
|
|
@ -233,15 +248,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call initialize for all plugins
|
||||
/// Initialize all plugins asynchronously.
|
||||
/// </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>
|
||||
public static async Task InitializePluginsAsync()
|
||||
public static async Task InitializePluginsAsync(IResultUpdateRegister register)
|
||||
{
|
||||
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
||||
|
||||
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
|
||||
var initTasks = _allLoadedPlugins.Select(x => Task.Run(async () =>
|
||||
{
|
||||
var pair = x.Value;
|
||||
|
||||
// Register plugin action keywords so that plugins can be queried in results
|
||||
RegisterPluginActionKeywords(pair);
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
|
||||
|
|
@ -264,35 +283,34 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
pair.Metadata.Disabled = true;
|
||||
pair.Metadata.HomeDisabled = true;
|
||||
failedPlugins.Enqueue(pair);
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
|
||||
}
|
||||
|
||||
// Even if the plugin cannot be initialized, we still need to add it in all plugin list so that
|
||||
// we can remove the plugin from Plugin or Store page or Plugin Manager plugin.
|
||||
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
_initFailedPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register ResultsUpdated event so that plugin query can use results updated interface
|
||||
register.RegisterResultsUpdatedEvent(pair);
|
||||
|
||||
// Update plugin metadata translation after the plugin is initialized with IPublicAPI instance
|
||||
Internationalization.UpdatePluginMetadataTranslation(pair);
|
||||
|
||||
// Add plugin to Dialog Jump plugin list after the plugin is initialized
|
||||
DialogJump.InitializeDialogJumpPlugin(pair);
|
||||
|
||||
// Add plugin to lists after the plugin is initialized
|
||||
AddPluginToLists(pair);
|
||||
}));
|
||||
|
||||
await Task.WhenAll(InitTasks);
|
||||
await Task.WhenAll(initTasks);
|
||||
|
||||
foreach (var plugin in AllPlugins)
|
||||
if (!_initFailedPlugins.IsEmpty)
|
||||
{
|
||||
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
|
||||
// has multiple global and action keywords because we will only add them here once.
|
||||
foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct())
|
||||
{
|
||||
switch (actionKeyword)
|
||||
{
|
||||
case Query.GlobalPluginWildcardSign:
|
||||
GlobalPlugins.Add(plugin);
|
||||
break;
|
||||
default:
|
||||
NonGlobalPlugins[actionKeyword] = plugin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!failedPlugins.IsEmpty)
|
||||
{
|
||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||
var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name));
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.failedToInitializePluginsTitle(),
|
||||
Localize.failedToInitializePluginsMessage(failed),
|
||||
|
|
@ -302,34 +320,74 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
private static void RegisterPluginActionKeywords(PluginPair pair)
|
||||
{
|
||||
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
|
||||
// has multiple global and action keywords because we will only add them here once.
|
||||
foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct())
|
||||
{
|
||||
switch (actionKeyword)
|
||||
{
|
||||
case Query.GlobalPluginWildcardSign:
|
||||
_globalPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
break;
|
||||
default:
|
||||
_nonGlobalPlugins.TryAdd(actionKeyword, pair);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddPluginToLists(PluginPair pair)
|
||||
{
|
||||
if (pair.Plugin is IContextMenu)
|
||||
{
|
||||
_contextMenuPlugins.Add(pair);
|
||||
}
|
||||
if (pair.Plugin is IAsyncHomeQuery)
|
||||
{
|
||||
_homePlugins.Add(pair);
|
||||
}
|
||||
if (pair.Plugin is IPluginI18n)
|
||||
{
|
||||
_translationPlugins.Add(pair);
|
||||
}
|
||||
if (pair.Plugin is IAsyncExternalPreview)
|
||||
{
|
||||
_externalPreviewPlugins.Add(pair);
|
||||
}
|
||||
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate & Query Plugins
|
||||
|
||||
public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
|
||||
{
|
||||
if (query is null)
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
|
||||
if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
|
||||
{
|
||||
if (dialogJump)
|
||||
return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList();
|
||||
return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))];
|
||||
else
|
||||
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))];
|
||||
}
|
||||
|
||||
if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
if (PublicApi.Instance.PluginModified(plugin.Metadata.ID))
|
||||
if (PluginModified(plugin.Metadata.ID))
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
return new List<PluginPair>
|
||||
{
|
||||
plugin
|
||||
};
|
||||
return [plugin];
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -337,6 +395,28 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var results = new List<Result>();
|
||||
var metadata = pair.Metadata;
|
||||
|
||||
if (IsPluginInitializing(metadata))
|
||||
{
|
||||
Result r = new()
|
||||
{
|
||||
Title = Localize.pluginStillInitializing(metadata.Name),
|
||||
SubTitle = Localize.pluginStillInitializingSubtitle(),
|
||||
AutoCompleteText = query.RawQuery,
|
||||
IcoPath = metadata.IcoPath,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ =>
|
||||
{
|
||||
PublicApi.Instance.ReQuery();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
results.Add(r);
|
||||
return results;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
|
|
@ -361,15 +441,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
Result r = new()
|
||||
{
|
||||
Title = $"{metadata.Name}: Failed to respond!",
|
||||
SubTitle = "Select this result for more info",
|
||||
Title = Localize.pluginFailedToRespond(metadata.Name),
|
||||
SubTitle = Localize.pluginFailedToRespondSubtitle(),
|
||||
AutoCompleteText = query.RawQuery,
|
||||
IcoPath = Constant.ErrorIcon,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ => { throw new FlowPluginException(metadata, e);},
|
||||
Score = -100
|
||||
Action = _ => { throw new FlowPluginException(metadata, e);}
|
||||
};
|
||||
results.Add(r);
|
||||
}
|
||||
|
|
@ -381,6 +461,28 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var results = new List<Result>();
|
||||
var metadata = pair.Metadata;
|
||||
|
||||
if (IsPluginInitializing(metadata))
|
||||
{
|
||||
Result r = new()
|
||||
{
|
||||
Title = Localize.pluginStillInitializing(metadata.Name),
|
||||
SubTitle = Localize.pluginStillInitializingSubtitle(),
|
||||
AutoCompleteText = query.RawQuery,
|
||||
IcoPath = metadata.IcoPath,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ =>
|
||||
{
|
||||
PublicApi.Instance.ReQuery();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
results.Add(r);
|
||||
return results;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
|
|
@ -411,6 +513,12 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var results = new List<DialogJumpResult>();
|
||||
var metadata = pair.Metadata;
|
||||
|
||||
if (IsPluginInitializing(metadata))
|
||||
{
|
||||
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
|
|
@ -436,6 +544,52 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return results;
|
||||
}
|
||||
|
||||
private static bool IsPluginInitializing(PluginMetadata metadata)
|
||||
{
|
||||
return !_allInitializedPlugins.ContainsKey(metadata.ID);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Plugin List
|
||||
|
||||
public static List<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)
|
||||
{
|
||||
foreach (var r in results)
|
||||
|
|
@ -454,28 +608,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// <summary>
|
||||
/// get specified plugin, return null if not found
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Plugin may not be initialized, so do not use its plugin model to execute any commands
|
||||
/// </remarks>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
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
|
||||
{
|
||||
// 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>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static IList<PluginPair> GetResultUpdatePlugin()
|
||||
{
|
||||
return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
}
|
||||
|
||||
public static IList<PluginPair> GetTranslationPlugins()
|
||||
{
|
||||
return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
}
|
||||
#region Get Context Menus
|
||||
|
||||
public static List<Result> GetContextMenusForPlugin(Result result)
|
||||
{
|
||||
|
|
@ -506,27 +651,82 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return results;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Check Home Plugin
|
||||
|
||||
public static bool IsHomePlugin(string id)
|
||||
{
|
||||
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
|
||||
}
|
||||
|
||||
public static IList<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)
|
||||
{
|
||||
// this method is only checking for action keywords (defined as not '*') registration
|
||||
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
|
||||
return actionKeyword != Query.GlobalPluginWildcardSign
|
||||
&& NonGlobalPlugins.ContainsKey(actionKeyword);
|
||||
&& _nonGlobalPlugins.ContainsKey(actionKeyword);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -538,11 +738,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var plugin = GetPluginForId(id);
|
||||
if (newActionKeyword == Query.GlobalPluginWildcardSign)
|
||||
{
|
||||
GlobalPlugins.Add(plugin);
|
||||
_globalPlugins.TryAdd(id, plugin);
|
||||
}
|
||||
else
|
||||
{
|
||||
NonGlobalPlugins[newActionKeyword] = plugin;
|
||||
_nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin);
|
||||
}
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
|
|
@ -569,11 +769,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
plugin.Metadata.ActionKeywords
|
||||
.Count(x => x == Query.GlobalPluginWildcardSign) == 1)
|
||||
{
|
||||
GlobalPlugins.Remove(plugin);
|
||||
_globalPlugins.TryRemove(id, out _);
|
||||
}
|
||||
|
||||
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
|
||||
NonGlobalPlugins.Remove(oldActionkeyword);
|
||||
{
|
||||
_nonGlobalPlugins.TryRemove(oldActionkeyword, out _);
|
||||
}
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
|
||||
|
|
@ -587,6 +789,12 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Plugin Install & Uninstall & Update
|
||||
|
||||
#region Private Functions
|
||||
|
||||
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
|
||||
{
|
||||
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
|
||||
|
|
@ -612,12 +820,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (!Version.TryParse(newMetadata.Version, out var newVersion))
|
||||
return true; // If version is not valid, we assume it is lesser than any existing version
|
||||
|
||||
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
|
||||
&& Version.TryParse(x.Metadata.Version, out var version)
|
||||
&& newVersion <= version);
|
||||
// Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists
|
||||
return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID
|
||||
&& Version.TryParse(x.Metadata.Version, out var version)
|
||||
&& newVersion <= version);
|
||||
}
|
||||
|
||||
#region Public functions
|
||||
#endregion
|
||||
|
||||
#region Public Functions
|
||||
|
||||
public static bool PluginModified(string id)
|
||||
{
|
||||
|
|
@ -655,7 +866,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
#endregion
|
||||
|
||||
#region Internal functions
|
||||
#region Internal Functions
|
||||
|
||||
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
|
||||
{
|
||||
|
|
@ -752,7 +963,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
// If we want to remove plugin from AllPlugins,
|
||||
// we need to dispose them so that they can release file handles
|
||||
// which can help FL to delete the plugin settings & cache folders successfully
|
||||
var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID);
|
||||
var pluginPairs = GetAllInitializedPlugins(includeFailed: true).Where(p => p.Metadata.ID == plugin.ID).ToList();
|
||||
foreach (var pluginPair in pluginPairs)
|
||||
{
|
||||
await DisposePluginAsync(pluginPair);
|
||||
|
|
@ -797,12 +1008,22 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Localize.failedToRemovePluginCacheMessage(plugin.Name));
|
||||
}
|
||||
Settings.RemovePluginSettings(plugin.ID);
|
||||
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||
GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID);
|
||||
var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
|
||||
{
|
||||
_allLoadedPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
{
|
||||
_allInitializedPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
{
|
||||
_initFailedPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
{
|
||||
_globalPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
NonGlobalPlugins.Remove(key);
|
||||
_nonGlobalPlugins.TryRemove(key, out var _);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -818,5 +1039,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
{
|
||||
var erroredPlugins = new List<string>();
|
||||
|
||||
|
|
@ -58,55 +58,57 @@ namespace Flow.Launcher.Core.Plugin
|
|||
foreach (var metadata in metadatas)
|
||||
{
|
||||
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
|
||||
{
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
|
||||
try
|
||||
{
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||
typeof(IAsyncPlugin));
|
||||
|
||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||
typeof(IAsyncPlugin));
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
catch (Exception e) when (assembly == null)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e) when (assembly == null)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
erroredPlugins.Add(metadata.Name);
|
||||
return;
|
||||
}
|
||||
if (plugin == null)
|
||||
{
|
||||
erroredPlugins.Add(metadata.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
metadata.InitTime += milliseconds;
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
|
||||
}
|
||||
|
||||
if (erroredPlugins.Count > 0)
|
||||
|
|
|
|||
|
|
@ -377,6 +377,22 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
}
|
||||
|
||||
public static void UpdatePluginMetadataTranslation(PluginPair p)
|
||||
{
|
||||
// Update plugin metadata name & description
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using NHotkey;
|
|||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Accessibility;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.DialogJump
|
||||
{
|
||||
|
|
@ -60,12 +61,12 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
|
||||
private static HWND _mainWindowHandle = HWND.Null;
|
||||
|
||||
private static readonly Dictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
|
||||
private static readonly ConcurrentDictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
|
||||
|
||||
private static DialogJumpExplorerPair _lastExplorer = null;
|
||||
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 readonly Lock _dialogWindowLock = new();
|
||||
|
|
@ -101,22 +102,13 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
|
||||
#region Initialize & Setup
|
||||
|
||||
public static void InitializeDialogJump(IList<DialogJumpExplorerPair> dialogJumpExplorers,
|
||||
IList<DialogJumpDialogPair> dialogJumpDialogs)
|
||||
public static void InitializeDialogJump()
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
// Initialize Dialog Jump explorers & dialogs
|
||||
_dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null);
|
||||
foreach (var explorer in dialogJumpExplorers)
|
||||
{
|
||||
_dialogJumpExplorers.Add(explorer, null);
|
||||
}
|
||||
_dialogJumpDialogs.Add(WindowsDialogJumpDialog, null);
|
||||
foreach (var dialog in dialogJumpDialogs)
|
||||
{
|
||||
_dialogJumpDialogs.Add(dialog, null);
|
||||
}
|
||||
// Initialize preinstalled Dialog Jump explorers & dialogs
|
||||
_dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null);
|
||||
_dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null);
|
||||
|
||||
// Initialize main window handle
|
||||
_mainWindowHandle = Win32Helper.GetMainWindowHandle();
|
||||
|
|
@ -131,6 +123,29 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
_initialized = true;
|
||||
}
|
||||
|
||||
public static void InitializeDialogJumpPlugin(PluginPair pair)
|
||||
{
|
||||
// Add Dialog Jump explorers & dialogs
|
||||
if (pair.Plugin is IDialogJumpExplorer explorer)
|
||||
{
|
||||
var dialogJumpExplorer = new DialogJumpExplorerPair
|
||||
{
|
||||
Plugin = explorer,
|
||||
Metadata = pair.Metadata
|
||||
};
|
||||
_dialogJumpExplorers.TryAdd(dialogJumpExplorer, null);
|
||||
}
|
||||
if (pair.Plugin is IDialogJumpDialog dialog)
|
||||
{
|
||||
var dialogJumpDialog = new DialogJumpDialogPair
|
||||
{
|
||||
Plugin = dialog,
|
||||
Metadata = pair.Metadata
|
||||
};
|
||||
_dialogJumpDialogs.TryAdd(dialogJumpDialog, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetupDialogJump(bool enabled)
|
||||
{
|
||||
if (enabled == _enabled) return;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
|
|
@ -173,9 +173,21 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Get all loaded plugins
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will also return any plugins not fully initialized yet
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
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>
|
||||
/// Registers a callback function for global keyboard events.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -187,12 +187,14 @@ namespace Flow.Launcher
|
|||
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
|
||||
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
|
||||
// Setup log level before any logging is done
|
||||
Log.SetLogLevel(_settings.LogLevel);
|
||||
|
||||
// Update dynamic resources base on settings
|
||||
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
|
||||
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
|
||||
|
||||
// Initialize notification system before any notification api is called
|
||||
Notification.Install();
|
||||
|
||||
// Enable Win32 dark mode if the system is in dark mode before creating all windows
|
||||
|
|
@ -201,6 +203,7 @@ namespace Flow.Launcher
|
|||
// Initialize language before portable clean up since it needs translations
|
||||
await _internationalization.InitializeLanguageAsync();
|
||||
|
||||
// Clean up after portability update
|
||||
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
|
||||
|
||||
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
|
||||
|
|
@ -210,32 +213,25 @@ namespace Flow.Launcher
|
|||
RegisterDispatcherUnhandledException();
|
||||
RegisterTaskSchedulerUnhandledException();
|
||||
|
||||
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
// Register ResultsUpdated event after all plugins are loaded
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
|
||||
var imageLoaderTask = ImageLoader.InitializeAsync();
|
||||
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
|
||||
await API.UpdatePluginManifestAsync();
|
||||
|
||||
await PluginManager.InitializePluginsAsync();
|
||||
|
||||
// Update plugin titles after plugins are initialized with their api instances
|
||||
Internationalization.UpdatePluginMetadataTranslations();
|
||||
|
||||
await imageLoadertask;
|
||||
await imageLoaderTask;
|
||||
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
// Initialize Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey
|
||||
// Initialize Dialog Jump after main window is created so that it can access main window handle
|
||||
DialogJump.InitializeDialogJump();
|
||||
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
|
||||
|
||||
// Initialize hotkey mapper instantly after main window is created because
|
||||
// it will steal focus from main window which causes window hide
|
||||
HotKeyMapper.Initialize();
|
||||
|
|
@ -243,19 +239,40 @@ namespace Flow.Launcher
|
|||
// Initialize theme for main window
|
||||
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||
|
||||
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
|
||||
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
RegisterExitEvents();
|
||||
|
||||
AutoStartup();
|
||||
AutoUpdates();
|
||||
AutoPluginUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
|
||||
API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------");
|
||||
|
||||
_ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
|
||||
{
|
||||
API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------");
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
await PluginManager.InitializePluginsAsync(_mainVM);
|
||||
|
||||
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
|
||||
// And home page is created without full plugin list
|
||||
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
|
||||
{
|
||||
_mainVM.QueryResults();
|
||||
}
|
||||
|
||||
AutoPluginUpdates();
|
||||
|
||||
// Save all settings since we possibly update the plugin environment paths
|
||||
API.SaveAppAllSettings();
|
||||
|
||||
API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public static class ResultHelper
|
|||
{
|
||||
var plugin = PluginManager.GetPluginForId(pluginId);
|
||||
if (plugin == null) return null;
|
||||
var query = QueryBuilder.Build(rawQuery, PluginManager.NonGlobalPlugins);
|
||||
var query = QueryBuilder.Build(rawQuery, PluginManager.GetNonGlobalPlugins());
|
||||
if (query == null) return null;
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@
|
|||
<system:String x:Key="PositionReset">Position Reset</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="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 -->
|
||||
<system:String x:Key="flowlauncher_settings">Settings</system:String>
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@ namespace Flow.Launcher
|
|||
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
|
||||
{
|
||||
var queryWithoutActionKeyword =
|
||||
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search;
|
||||
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
|
||||
|
||||
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using System.Collections.Specialized;
|
|||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -248,7 +247,10 @@ namespace Flow.Launcher
|
|||
|
||||
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) =>
|
||||
StringMatcher.FuzzySearch(query, stringToCompare);
|
||||
|
|
|
|||
|
|
@ -114,8 +114,11 @@ public partial class SettingsPanePluginsViewModel : BaseModel
|
|||
}
|
||||
}
|
||||
|
||||
private IList<PluginViewModel>? _pluginViewModels;
|
||||
public IList<PluginViewModel> PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins
|
||||
private List<PluginViewModel>? _pluginViewModels;
|
||||
// Get all plugins: Initializing & Initialized & Init failed plugins
|
||||
// Include init failed ones so that we can uninstall them
|
||||
// Include initializing ones so that we can change related settings like action keywords, etc.
|
||||
public List<PluginViewModel> PluginViewModels => _pluginViewModels ??= App.API.GetAllPlugins()
|
||||
.OrderBy(plugin => plugin.Metadata.Disabled)
|
||||
.ThenBy(plugin => plugin.Metadata.Name)
|
||||
.Select(plugin => new PluginViewModel
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ using Microsoft.VisualStudio.Threading;
|
|||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
public partial class MainViewModel : BaseModel, ISavable, IDisposable
|
||||
public partial class MainViewModel : BaseModel, ISavable, IDisposable, IResultUpdateRegister
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
|
|
@ -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;
|
||||
plugin.ResultsUpdated += (s, e) =>
|
||||
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested)
|
||||
{
|
||||
if (e.Query.RawQuery != QueryText || e.Token.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
|
||||
var token = e.Token == default ? _updateToken : e.Token;
|
||||
|
||||
IReadOnlyList<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 (e.Results == null)
|
||||
{
|
||||
resultsCopy = _emptyResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
|
||||
resultsCopy = DeepCloneResults(e.Results, false, token);
|
||||
}
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
foreach (var result in resultsCopy)
|
||||
{
|
||||
if (string.IsNullOrEmpty(result.BadgeIcoPath))
|
||||
{
|
||||
result.BadgeIcoPath = pair.Metadata.IcoPath;
|
||||
}
|
||||
}
|
||||
App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>");
|
||||
|
||||
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
|
||||
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
App.API.LogDebug(ClassName, $"Update results for plugin <{pair.Metadata.Name}>");
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
|
||||
token)))
|
||||
{
|
||||
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
|
||||
}
|
||||
};
|
||||
}
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
|
||||
token)))
|
||||
{
|
||||
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task RegisterClockAndDateUpdateAsync()
|
||||
|
|
@ -444,7 +442,7 @@ namespace Flow.Launcher.ViewModel
|
|||
[RelayCommand]
|
||||
private void Backspace(object index)
|
||||
{
|
||||
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
|
||||
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.GetNonGlobalPlugins());
|
||||
|
||||
// GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string
|
||||
var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\'));
|
||||
|
|
@ -1627,7 +1625,7 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
if (string.IsNullOrWhiteSpace(queryText))
|
||||
{
|
||||
return QueryBuilder.Build(string.Empty, PluginManager.NonGlobalPlugins);
|
||||
return QueryBuilder.Build(string.Empty, PluginManager.GetNonGlobalPlugins());
|
||||
}
|
||||
|
||||
var queryBuilder = new StringBuilder(queryText);
|
||||
|
|
@ -1647,7 +1645,7 @@ namespace Flow.Launcher.ViewModel
|
|||
// Applying builtin shortcuts
|
||||
await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp);
|
||||
|
||||
return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
|
||||
return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.GetNonGlobalPlugins());
|
||||
}
|
||||
|
||||
private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts,
|
||||
|
|
|
|||
|
|
@ -131,8 +131,13 @@ namespace Flow.Launcher.ViewModel
|
|||
private Control _bottomPart2;
|
||||
public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null;
|
||||
|
||||
public bool HasSettingControl => PluginPair.Plugin is ISettingProvider &&
|
||||
(PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel());
|
||||
public bool HasSettingControl =>
|
||||
// Here we do not check if the plugin is initialized successfully
|
||||
// So we can let users change settings for initializing or initialization failed plugins
|
||||
PluginPair.Plugin is ISettingProvider &&
|
||||
(PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || // Is not JsonRPC plugin
|
||||
jsonRPCPluginBase.NeedCreateSettingPanel()); // Is JsonRPC plugin and need to create setting panel
|
||||
|
||||
public Control SettingControl
|
||||
=> IsExpanded
|
||||
? _settingControl
|
||||
|
|
|
|||
Loading…
Reference in a new issue