mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into graceful_shutdown
This commit is contained in:
commit
334a27fc28
8 changed files with 252 additions and 26 deletions
|
|
@ -205,9 +205,6 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
|
|
||||||
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
|
|
||||||
|
|
||||||
if (failedPlugins.Any())
|
if (failedPlugins.Any())
|
||||||
{
|
{
|
||||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource
|
||||||
return DefaultLanguageCode;
|
return DefaultLanguageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
|
private void AddPluginLanguageDirectories()
|
||||||
{
|
{
|
||||||
foreach (var plugin in plugins)
|
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||||
{
|
{
|
||||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||||
var dir = Path.GetDirectoryName(location);
|
var dir = Path.GetDirectoryName(location);
|
||||||
|
|
@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource
|
||||||
_oldResources.Clear();
|
_oldResources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize language. Will change app language and plugin language based on settings.
|
||||||
|
/// </summary>
|
||||||
|
public async Task InitializeLanguageAsync()
|
||||||
|
{
|
||||||
|
// Get actual language
|
||||||
|
var languageCode = _settings.Language;
|
||||||
|
if (languageCode == Constant.SystemLanguageCode)
|
||||||
|
{
|
||||||
|
languageCode = SystemLanguageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get language by language code and change language
|
||||||
|
var language = GetLanguageByLanguageCode(languageCode);
|
||||||
|
|
||||||
|
// Add plugin language directories first so that we can load language files from plugins
|
||||||
|
AddPluginLanguageDirectories();
|
||||||
|
|
||||||
|
// Change language
|
||||||
|
await ChangeLanguageAsync(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change language during runtime. Will change app language and plugin language & save settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="languageCode"></param>
|
||||||
public void ChangeLanguage(string languageCode)
|
public void ChangeLanguage(string languageCode)
|
||||||
{
|
{
|
||||||
languageCode = languageCode.NonNull();
|
languageCode = languageCode.NonNull();
|
||||||
|
|
@ -110,7 +136,12 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
// Get language by language code and change language
|
// Get language by language code and change language
|
||||||
var language = GetLanguageByLanguageCode(languageCode);
|
var language = GetLanguageByLanguageCode(languageCode);
|
||||||
ChangeLanguage(language, isSystem);
|
|
||||||
|
// Change language
|
||||||
|
_ = ChangeLanguageAsync(language);
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Language GetLanguageByLanguageCode(string languageCode)
|
private Language GetLanguageByLanguageCode(string languageCode)
|
||||||
|
|
@ -128,26 +159,22 @@ namespace Flow.Launcher.Core.Resource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ChangeLanguage(Language language, bool isSystem)
|
private async Task ChangeLanguageAsync(Language language)
|
||||||
{
|
{
|
||||||
language = language.NonNull();
|
// Remove old language files and load language
|
||||||
|
|
||||||
RemoveOldLanguageFiles();
|
RemoveOldLanguageFiles();
|
||||||
if (language != AvailableLanguages.English)
|
if (language != AvailableLanguages.English)
|
||||||
{
|
{
|
||||||
LoadLanguage(language);
|
LoadLanguage(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Culture of main thread
|
// Culture of main thread
|
||||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||||
|
|
||||||
// Raise event after culture is set
|
// Raise event for plugins after culture is set
|
||||||
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
await Task.Run(UpdatePluginMetadataTranslations);
|
||||||
_ = Task.Run(() =>
|
|
||||||
{
|
|
||||||
UpdatePluginMetadataTranslations();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,16 @@ GetMonitorInfo
|
||||||
MONITORINFOEXW
|
MONITORINFOEXW
|
||||||
|
|
||||||
WM_ENTERSIZEMOVE
|
WM_ENTERSIZEMOVE
|
||||||
WM_EXITSIZEMOVE
|
WM_EXITSIZEMOVE
|
||||||
|
|
||||||
|
GetKeyboardLayout
|
||||||
|
GetWindowThreadProcessId
|
||||||
|
ActivateKeyboardLayout
|
||||||
|
GetKeyboardLayoutList
|
||||||
|
PostMessage
|
||||||
|
WM_INPUTLANGCHANGEREQUEST
|
||||||
|
INPUTLANGCHANGE_FORWARD
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD1
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD2
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD3
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD4
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Microsoft.Win32;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
using Windows.Win32.Graphics.Dwm;
|
using Windows.Win32.Graphics.Dwm;
|
||||||
|
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||||
using Windows.Win32.UI.WindowsAndMessaging;
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Point = System.Windows.Point;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
|
|
@ -63,7 +67,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="window"></param>
|
/// <param name="window"></param>
|
||||||
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
||||||
|
|
@ -317,5 +321,172 @@ namespace Flow.Launcher.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Keyboard Layout
|
||||||
|
|
||||||
|
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
|
||||||
|
private const string EnglishLanguageTag = "en";
|
||||||
|
|
||||||
|
private static readonly string[] ImeLanguageTags =
|
||||||
|
{
|
||||||
|
"zh", // Chinese
|
||||||
|
"ja", // Japanese
|
||||||
|
"ko", // Korean
|
||||||
|
};
|
||||||
|
|
||||||
|
private const uint KeyboardLayoutLoWord = 0xFFFF;
|
||||||
|
|
||||||
|
// Store the previous keyboard layout
|
||||||
|
private static HKL _previousLayout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switches the keyboard layout to English if available.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="backupPrevious">If true, the current keyboard layout will be stored for later restoration.</param>
|
||||||
|
/// <exception cref="Win32Exception">Thrown when there's an error getting the window thread process ID.</exception>
|
||||||
|
public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious)
|
||||||
|
{
|
||||||
|
// Find an installed English layout
|
||||||
|
var enHKL = FindEnglishKeyboardLayout();
|
||||||
|
|
||||||
|
// No installed English layout found
|
||||||
|
if (enHKL == HKL.Null) return;
|
||||||
|
|
||||||
|
// Get the current foreground window
|
||||||
|
var hwnd = PInvoke.GetForegroundWindow();
|
||||||
|
if (hwnd == HWND.Null) return;
|
||||||
|
|
||||||
|
// Get the current foreground window thread ID
|
||||||
|
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
|
||||||
|
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||||
|
|
||||||
|
// If the current layout has an IME mode, disable it without switching to another layout.
|
||||||
|
// This is needed because for languages with IME mode, Flow Launcher just temporarily disables
|
||||||
|
// the IME mode instead of switching to another layout.
|
||||||
|
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
|
||||||
|
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
|
||||||
|
foreach (var langTag in ImeLanguageTags)
|
||||||
|
{
|
||||||
|
if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup current keyboard layout
|
||||||
|
if (backupPrevious) _previousLayout = currentLayout;
|
||||||
|
|
||||||
|
// Switch to English layout
|
||||||
|
PInvoke.ActivateKeyboardLayout(enHKL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the previously backed-up keyboard layout.
|
||||||
|
/// If it wasn't backed up or has already been restored, this method does nothing.
|
||||||
|
/// </summary>
|
||||||
|
public static void RestorePreviousKeyboardLayout()
|
||||||
|
{
|
||||||
|
if (_previousLayout == HKL.Null) return;
|
||||||
|
|
||||||
|
var hwnd = PInvoke.GetForegroundWindow();
|
||||||
|
if (hwnd == HWND.Null) return;
|
||||||
|
|
||||||
|
PInvoke.PostMessage(
|
||||||
|
hwnd,
|
||||||
|
PInvoke.WM_INPUTLANGCHANGEREQUEST,
|
||||||
|
PInvoke.INPUTLANGCHANGE_FORWARD,
|
||||||
|
_previousLayout.Value
|
||||||
|
);
|
||||||
|
|
||||||
|
_previousLayout = HKL.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an installed English keyboard layout.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Win32Exception"></exception>
|
||||||
|
private static unsafe HKL FindEnglishKeyboardLayout()
|
||||||
|
{
|
||||||
|
// Get the number of keyboard layouts
|
||||||
|
int count = PInvoke.GetKeyboardLayoutList(0, null);
|
||||||
|
if (count <= 0) return HKL.Null;
|
||||||
|
|
||||||
|
// Get all keyboard layouts
|
||||||
|
var handles = new HKL[count];
|
||||||
|
fixed (HKL* h = handles)
|
||||||
|
{
|
||||||
|
var result = PInvoke.GetKeyboardLayoutList(count, h);
|
||||||
|
if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for any English keyboard layout
|
||||||
|
foreach (var hkl in handles)
|
||||||
|
{
|
||||||
|
// The lower word contains the language identifier
|
||||||
|
var langId = (uint)hkl.Value & KeyboardLayoutLoWord;
|
||||||
|
var langTag = GetLanguageTag(langId);
|
||||||
|
|
||||||
|
// Check if it's an English layout
|
||||||
|
if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return hkl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HKL.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the
|
||||||
|
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
|
||||||
|
/// BCP 47 language tag
|
||||||
|
/// </see>
|
||||||
|
/// of the current input language.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Edited from: https://github.com/dotnet/winforms
|
||||||
|
/// </remarks>
|
||||||
|
private static string GetLanguageTag(uint langId)
|
||||||
|
{
|
||||||
|
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
|
||||||
|
// transient value.
|
||||||
|
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
|
||||||
|
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
|
||||||
|
//
|
||||||
|
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
|
||||||
|
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
|
||||||
|
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
|
||||||
|
//
|
||||||
|
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
|
||||||
|
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
|
||||||
|
//
|
||||||
|
// NOTE: this logic may break in future versions of Windows since it is not documented.
|
||||||
|
if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1
|
||||||
|
or PInvoke.LOCALE_TRANSIENT_KEYBOARD2
|
||||||
|
or PInvoke.LOCALE_TRANSIENT_KEYBOARD3
|
||||||
|
or PInvoke.LOCALE_TRANSIENT_KEYBOARD4)
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath);
|
||||||
|
if (key?.GetValue("Languages") is string[] languages)
|
||||||
|
{
|
||||||
|
foreach (string language in languages)
|
||||||
|
{
|
||||||
|
using var subKey = key.OpenSubKey(language);
|
||||||
|
if (subKey?.GetValue("TransientLangId") is int transientLangId
|
||||||
|
&& transientLangId == langId)
|
||||||
|
{
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CultureInfo.GetCultureInfo((int)langId).Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,14 +152,19 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||||
|
|
||||||
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
|
||||||
Ioc.Default.GetRequiredService<Internationalization>().ChangeLanguage(_settings.Language);
|
|
||||||
|
|
||||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
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;
|
||||||
|
|
||||||
await PluginManager.InitializePluginsAsync();
|
await PluginManager.InitializePluginsAsync();
|
||||||
|
|
||||||
|
// Change language after all plugins are initialized because we need to update plugin title based on their api
|
||||||
|
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
||||||
|
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
|
||||||
|
|
||||||
await imageLoadertask;
|
await imageLoadertask;
|
||||||
|
|
||||||
_mainWindow = new MainWindow();
|
_mainWindow = new MainWindow();
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,9 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
_settings.FirstLaunch = false;
|
_settings.FirstLaunch = false;
|
||||||
App.API.SaveAppAllSettings();
|
App.API.SaveAppAllSettings();
|
||||||
|
/* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */
|
||||||
|
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
|
||||||
|
_settings.BackdropType = BackdropTypes.Acrylic;
|
||||||
var WelcomeWindow = new WelcomeWindow();
|
var WelcomeWindow = new WelcomeWindow();
|
||||||
WelcomeWindow.Show();
|
WelcomeWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +149,9 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
// Since the default main window visibility is visible, so we need set focus during startup
|
// Since the default main window visibility is visible, so we need set focus during startup
|
||||||
QueryTextBox.Focus();
|
QueryTextBox.Focus();
|
||||||
|
// Set the initial state of the QueryTextBoxCursorMovedToEnd property
|
||||||
|
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
|
||||||
|
_viewModel.QueryTextCursorMovedToEnd = false;
|
||||||
_viewModel.PropertyChanged += (o, e) =>
|
_viewModel.PropertyChanged += (o, e) =>
|
||||||
{
|
{
|
||||||
switch (e.PropertyName)
|
switch (e.PropertyName)
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,6 @@ namespace Flow.Launcher.ViewModel
|
||||||
};
|
};
|
||||||
|
|
||||||
RegisterViewUpdate();
|
RegisterViewUpdate();
|
||||||
RegisterResultsUpdatedEvent();
|
|
||||||
_ = RegisterClockAndDateUpdateAsync();
|
_ = RegisterClockAndDateUpdateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,7 +212,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterResultsUpdatedEvent()
|
public void RegisterResultsUpdatedEvent()
|
||||||
{
|
{
|
||||||
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
|
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
|
||||||
{
|
{
|
||||||
|
|
@ -1373,6 +1372,11 @@ namespace Flow.Launcher.ViewModel
|
||||||
MainWindowOpacity = 1;
|
MainWindowOpacity = 1;
|
||||||
MainWindowVisibilityStatus = true;
|
MainWindowVisibilityStatus = true;
|
||||||
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
|
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
|
||||||
|
|
||||||
|
if (StartWithEnglishMode)
|
||||||
|
{
|
||||||
|
Win32Helper.SwitchToEnglishKeyboardLayout(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1441,7 +1445,12 @@ namespace Flow.Launcher.ViewModel
|
||||||
// 📌 Apply DWM Cloak (Completely hide the window)
|
// 📌 Apply DWM Cloak (Completely hide the window)
|
||||||
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
|
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StartWithEnglishMode)
|
||||||
|
{
|
||||||
|
Win32Helper.RestorePreviousKeyboardLayout();
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(50);
|
await Task.Delay(50);
|
||||||
|
|
||||||
// Update WPF properties
|
// Update WPF properties
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
|
||||||
|
|
||||||
## 📦 Plugins
|
## 📦 Plugins
|
||||||
|
|
||||||
- Support wide range of plugins. Visit [here](https://flowlauncher.com/docs/#/plugins) for our plugin portfolio.
|
- Support wide range of plugins. Visit [here](https://www.flowlauncher.com/plugins/) for our plugin portfolio.
|
||||||
- Publish your own plugin to flow! Create plugins in:
|
- Publish your own plugin to flow! Create plugins in:
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue