Skip to content

Asynchronous Loading & Initialization Plugin Model to Improve Window Startup Speed #3854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 47 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
9ce9117
Improve code quality
Jack251970 Jul 21, 2025
aee46e8
Initialize quick jump earlier
Jack251970 Jul 21, 2025
f5f2568
Add IResultUpdateRegister interface
Jack251970 Jul 21, 2025
d4e672d
Add support to update translation for one plugin
Jack251970 Jul 21, 2025
c2157e2
Add support for concurrent operation of explorers & dialogs adding
Jack251970 Jul 21, 2025
6beeecb
Use async load & initialization model
Jack251970 Jul 21, 2025
59a7a2c
Improve code quality
Jack251970 Jul 21, 2025
52bb909
Add plugin to all plugin list later
Jack251970 Jul 21, 2025
0a01d85
Improve code quality
Jack251970 Jul 21, 2025
c3a5984
Fix code comments
Jack251970 Jul 21, 2025
35c8e39
Improve code comments
Jack251970 Jul 21, 2025
1d3ab39
Use () => instead of delegate
Jack251970 Jul 21, 2025
445a142
Add code comments
Jack251970 Jul 21, 2025
de56814
Improve code quality
Jack251970 Jul 21, 2025
55164ef
Improve code quality
Jack251970 Jul 21, 2025
cc68183
Change variable name
Jack251970 Jul 21, 2025
50924e4
Improve code quality
Jack251970 Jul 21, 2025
8b60d26
Fix build issue
Jack251970 Jul 21, 2025
566572b
Use api function & rename function
Jack251970 Jul 21, 2025
11e05f7
Add all loaded plugins
Jack251970 Jul 21, 2025
fe3339f
Get initialized plugins for plugin page
Jack251970 Jul 21, 2025
6e0f2fc
Return loaded plugins
Jack251970 Jul 21, 2025
6d99416
Search in loaded plugins
Jack251970 Jul 21, 2025
9149e3f
Mark initializing plugins as modified
Jack251970 Jul 21, 2025
8d03fce
Remove error codes
Jack251970 Jul 21, 2025
3bd76b2
Rename variable
Jack251970 Jul 21, 2025
7f797b1
Add code comments
Jack251970 Jul 21, 2025
fc01ddb
Save init failed plugins
Jack251970 Jul 21, 2025
b348deb
Code quality
Jack251970 Jul 21, 2025
d4a1953
Add init failed plugins
Jack251970 Jul 21, 2025
324b3eb
Do not call interface methods for init failed plugins
Jack251970 Jul 21, 2025
67c940f
Do not show setting panel for init failed plugins
Jack251970 Jul 21, 2025
950a4a0
Code quality
Jack251970 Jul 21, 2025
6409c19
Add code comments
Jack251970 Jul 21, 2025
a9a705f
Fix logic
Jack251970 Jul 21, 2025
59e4fb8
Fix logic & Add code comments
Jack251970 Jul 21, 2025
d9cc67c
Merge branch 'plugin_initialization' of https://github.com/Flow-Launc…
Jack251970 Jul 21, 2025
067a517
Fix null exception
Jack251970 Jul 21, 2025
63f8661
Use internal PluginModified method instead of API.PluginModified
Jack251970 Jul 21, 2025
f808469
Fix logic & Improve code quality
Jack251970 Jul 21, 2025
bef1fee
Fix plugin page logic
Jack251970 Jul 21, 2025
566bd04
Support two types in one class
Jack251970 Jul 21, 2025
269d21a
Change plugin modified logic
Jack251970 Jul 21, 2025
0f8553b
Refresh home page after plugins are initialized
Jack251970 Jul 21, 2025
e024078
Improve code quality
Jack251970 Jul 21, 2025
3221f93
Add info log message for plugin constructors
Jack251970 Jul 21, 2025
97fb8d6
Merge branch 'dev' into plugin_initialization
Jack251970 Jul 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
Original file line number Diff line number Diff line change
@@ -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);
}
366 changes: 239 additions & 127 deletions Flow.Launcher.Core/Plugin/PluginManager.cs

Large diffs are not rendered by default.

82 changes: 42 additions & 40 deletions Flow.Launcher.Core/Plugin/PluginsLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSe
return plugins;
}

private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
{
var erroredPlugins = new List<string>();

Expand All @@ -65,55 +65,57 @@ private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source
foreach (var metadata in metadatas)
{
var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
{
Assembly assembly = null;
IAsyncPlugin plugin = null;
{
Assembly assembly = null;
IAsyncPlugin plugin = null;

try
{
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
try
{
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();

var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));

plugin = Activator.CreateInstance(type) as IAsyncPlugin;
plugin = Activator.CreateInstance(type) as IAsyncPlugin;

metadata.AssemblyName = assembly.GetName().Name;
}
metadata.AssemblyName = assembly.GetName().Name;
}
#if DEBUG
catch (Exception)
{
throw;
}
catch (Exception)
{
throw;
}
#else
catch (Exception e) when (assembly == null)
{
Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
catch (Exception e) when (assembly == null)
{
Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
#endif

if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}

plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});

plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
metadata.InitTime += milliseconds;
API.LogInfo(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
}

if (erroredPlugins.Count > 0)
Expand Down
16 changes: 16 additions & 0 deletions Flow.Launcher.Core/Resource/Internationalization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,22 @@ public static void UpdatePluginMetadataTranslations()
}
}

public static void UpdatePluginMetadataTranslation(PluginPair p)
{
// Update plugin metadata name & description
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}

#endregion
}
}
45 changes: 30 additions & 15 deletions Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Accessibility;
using System.Collections.Concurrent;

namespace Flow.Launcher.Infrastructure.DialogJump
{
Expand Down Expand Up @@ -64,12 +65,12 @@

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 object _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 object _dialogWindowLock = new();
Expand Down Expand Up @@ -105,22 +106,13 @@

#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();
Expand All @@ -135,6 +127,29 @@
_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;
Expand Down Expand Up @@ -162,22 +177,22 @@
}
if (!_locationChangeHook.IsNull)
{
PInvoke.UnhookWinEvent(_locationChangeHook);

Check warning on line 180 in Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`PInvoke` is not a recognized word. (unrecognized-spelling)
_locationChangeHook = HWINEVENTHOOK.Null;
}
if (!_destroyChangeHook.IsNull)
{
PInvoke.UnhookWinEvent(_destroyChangeHook);

Check warning on line 185 in Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`PInvoke` is not a recognized word. (unrecognized-spelling)
_destroyChangeHook = HWINEVENTHOOK.Null;
}
if (!_hideChangeHook.IsNull)
{
PInvoke.UnhookWinEvent(_hideChangeHook);

Check warning on line 190 in Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`PInvoke` is not a recognized word. (unrecognized-spelling)
_hideChangeHook = HWINEVENTHOOK.Null;
}
if (!_dialogEndChangeHook.IsNull)
{
PInvoke.UnhookWinEvent(_dialogEndChangeHook);

Check warning on line 195 in Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`PInvoke` is not a recognized word. (unrecognized-spelling)
_dialogEndChangeHook = HWINEVENTHOOK.Null;
}

Expand Down Expand Up @@ -318,7 +333,7 @@
if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified
explorer.Metadata.Disabled) continue; // Plugin is disabled

var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd);

Check warning on line 336 in Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
if (explorerWindow != null)
{
_dialogJumpExplorers[explorer] = explorerWindow;
Expand Down
3 changes: 3 additions & 0 deletions Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ public interface IPublicAPI
/// <summary>
/// Get all loaded plugins
/// </summary>
/// <remarks>
/// Part of plugins may not be initialized yet
/// </remarks>
/// <returns></returns>
List<PluginPair> GetAllPlugins();

Expand Down
55 changes: 36 additions & 19 deletions Flow.Launcher/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,14 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;

// Setup log level before any logging is done
Log.SetLogLevel(_settings.LogLevel);

// Update dynamic resources base on settings
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);

// Initialize notification system before any notification api is called
Notification.Install();

// Enable Win32 dark mode if the system is in dark mode before creating all windows
Expand All @@ -195,6 +197,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
// Initialize language before portable clean up since it needs translations
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();

// Clean up after portability update
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();

API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
Expand All @@ -204,52 +207,66 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();

var imageLoadertask = ImageLoader.InitializeAsync();

AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);

PluginManager.LoadPlugins(_settings.PluginSettings);

// Register ResultsUpdated event after all plugins are loaded
Ioc.Default.GetRequiredService<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 quick jump before hotkey mapper since hotkey mapper will register quick jump hotkey
// Initialize quick jump after main window is created so that it can access main window handle
DialogJump.InitializeDialogJump();
DialogJump.SetupDialogJump(_settings.EnableDialogJump);

// Initialize hotkey mapper instantly after main window is created because
// it will steal focus from main window which causes window hide
HotKeyMapper.Initialize();

// 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 ------------------------------------------------------");
});
});
}

Expand Down
2 changes: 1 addition & 1 deletion Flow.Launcher/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
private MediaPlayer animationSoundWMP;
private SoundPlayer animationSoundWPF;

// Window WndProc

Check warning on line 68 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
private HwndSource _hwndSource;
private int _initialWidth;
private int _initialHeight;
Expand Down Expand Up @@ -113,7 +113,7 @@
{
var handle = Win32Helper.GetWindowHandle(this, true);
_hwndSource = HwndSource.FromHwnd(handle);
_hwndSource.AddHook(WndProc);

Check warning on line 116 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this);
}
Expand Down Expand Up @@ -371,7 +371,7 @@
{
try
{
_hwndSource.RemoveHook(WndProc);

Check warning on line 374 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
}
catch (Exception)
{
Expand Down Expand Up @@ -474,7 +474,7 @@
&& 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))
{
Expand Down Expand Up @@ -773,8 +773,8 @@
Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")",
Icon = openIcon
};
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };

Check warning on line 776 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`gamemode` is not a recognized word. (unrecognized-spelling)
var gamemode = new MenuItem

Check warning on line 777 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`gamemode` is not a recognized word. (unrecognized-spelling)
{
Header = App.API.GetTranslation("GameMode"),
Icon = gamemodeIcon
Expand Down Expand Up @@ -820,7 +820,7 @@

public void UpdatePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910

Check failure on line 823 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`work around` matches a line_forbidden.patterns entry: `\bwork[- ]arounds?\b`. (forbidden-pattern)
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
Expand All @@ -844,7 +844,7 @@

private void InitializePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910

Check failure on line 847 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`work around` matches a line_forbidden.patterns entry: `\bwork[- ]arounds?\b`. (forbidden-pattern)
InitializePositionInner();
InitializePositionInner();
return;
Expand Down
3 changes: 1 addition & 2 deletions Flow.Launcher/PublicAPIInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
Expand Down Expand Up @@ -251,7 +250,7 @@ private static async Task<Exception> RetryActionOnSTAThreadAsync(Action action,

public string GetTranslation(string key) => Internationalization.GetTranslation(key);

public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
public List<PluginPair> GetAllPlugins() => PluginManager.GetAllLoadedPlugins();

public MatchResult FuzzySearch(string query, string stringToCompare) =>
StringMatcher.FuzzySearch(query, stringToCompare);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ public string FilterText
}
}

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
Expand Down
Loading
Loading