diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d9b39eb89..da4231f74 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,7 +8,8 @@ updates:
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
- interval: "weekly"
+ interval: "daily"
+ open-pull-requests-limit: 3
ignore:
- dependency-name: "squirrel-windows"
reviewers:
diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs
index d7c73fb46..069154364 100644
--- a/Flow.Launcher.Core/Configuration/Portable.cs
+++ b/Flow.Launcher.Core/Configuration/Portable.cs
@@ -9,11 +9,15 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using System.Linq;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Configuration
{
public class Portable : IPortable
{
+ private readonly IPublicAPI API = Ioc.Default.GetRequiredService();
+
///
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
///
@@ -40,7 +44,7 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.PortableDataPath);
- MessageBoxEx.Show("Flow Launcher needs to restart to finish disabling portable mode, " +
+ API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
"after the restart your portable data profile will be deleted and roaming data profile kept");
UpdateManager.RestartApp(Constant.ApplicationFileName);
@@ -64,7 +68,7 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.RoamingDataPath);
- MessageBoxEx.Show("Flow Launcher needs to restart to finish enabling portable mode, " +
+ API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
"after the restart your roaming data profile will be deleted and portable data profile kept");
UpdateManager.RestartApp(Constant.ApplicationFileName);
@@ -95,13 +99,13 @@ namespace Flow.Launcher.Core.Configuration
public void MoveUserDataFolder(string fromLocation, string toLocation)
{
- FilesFolders.CopyAll(fromLocation, toLocation, MessageBoxEx.Show);
+ FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
VerifyUserDataAfterMove(fromLocation, toLocation);
}
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
{
- FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, MessageBoxEx.Show);
+ FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
}
public void CreateShortcuts()
@@ -157,13 +161,13 @@ namespace Flow.Launcher.Core.Configuration
// delete it and prompt the user to pick the portable data location
if (File.Exists(roamingDataDeleteFilePath))
{
- FilesFolders.RemoveFolderIfExists(roamingDataDir, MessageBoxEx.Show);
+ FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
- if (MessageBoxEx.Show("Flow Launcher has detected you enabled portable mode, " +
+ if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " +
"would you like to move it to a different location?", string.Empty,
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
- FilesFolders.OpenPath(Constant.RootDirectory, MessageBoxEx.Show);
+ FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
Environment.Exit(0);
}
@@ -172,9 +176,9 @@ namespace Flow.Launcher.Core.Configuration
// delete it and notify the user about it.
else if (File.Exists(portableDataDeleteFilePath))
{
- FilesFolders.RemoveFolderIfExists(portableDataDir, MessageBoxEx.Show);
+ FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
- MessageBoxEx.Show("Flow Launcher has detected you disabled portable mode, " +
+ API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
"the relevant shortcuts and uninstaller entry have been created");
}
}
@@ -186,7 +190,7 @@ namespace Flow.Launcher.Core.Configuration
if (roamingLocationExists && portableLocationExists)
{
- MessageBoxEx.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " +
+ API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " +
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
index 6d41e2383..451df6147 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
@@ -8,11 +8,14 @@ using System.Linq;
using System.Windows;
using System.Windows.Forms;
using Flow.Launcher.Core.Resource;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
public abstract class AbstractPluginEnvironment
{
+ protected readonly IPublicAPI API = Ioc.Default.GetRequiredService();
+
internal abstract string Language { get; }
internal abstract string EnvName { get; }
@@ -25,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal virtual string FileDialogFilter => string.Empty;
- internal abstract string PluginsSettingsFilePath { get; set; }
+ internal abstract string PluginsSettingsFilePath { get; set; }
internal List PluginMetadataList;
@@ -57,7 +60,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
EnvName,
Environment.NewLine
);
- if (MessageBoxEx.Show(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
+ if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{
var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
string selectedFile;
@@ -82,7 +85,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
else
{
- MessageBoxEx.Show(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
+ API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
Log.Error("PluginsLoader",
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");
@@ -98,7 +101,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
if (expectedPath == currentPath)
return;
- FilesFolders.RemoveFolderIfExists(installedDirPath, MessageBoxEx.Show);
+ FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
InstallEnvironment();
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs
index 96c29646e..607c19062 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs
@@ -28,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override void InstallEnvironment()
{
- FilesFolders.RemoveFolderIfExists(InstallPath, MessageBoxEx.Show);
+ FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and
// uses Python plugin they need to custom install and use v3.8.9
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs
index 0d6f109e0..399f7cc03 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs
@@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override void InstallEnvironment()
{
- FilesFolders.RemoveFolderIfExists(InstallPath, MessageBoxEx.Show);
+ FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs
index 582a4407c..e8cb72e11 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs
@@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override void InstallEnvironment()
{
- FilesFolders.RemoveFolderIfExists(InstallPath, MessageBoxEx.Show);
+ FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
index 63f21c1d6..ac8abcdcc 100644
--- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
@@ -1,4 +1,4 @@
-using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Infrastructure.Logger;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -21,7 +21,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
public static List UserPlugins { get; private set; }
- public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
+ public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
{
try
{
@@ -31,8 +31,14 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
- UserPlugins = results;
- lastFetchedAt = DateTime.Now;
+ // If the results are empty, we shouldn't update the manifest because the results are invalid.
+ if (results.Count != 0)
+ {
+ UserPlugins = results;
+ lastFetchedAt = DateTime.Now;
+
+ return true;
+ }
}
}
catch (Exception e)
@@ -43,6 +49,8 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
manifestUpdateLock.Release();
}
+
+ return false;
}
}
}
diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
index 8aeca4699..e9f199d00 100644
--- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj
+++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
@@ -54,11 +54,11 @@
-
+
-
+
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
index f6e5e5879..7248c6259 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
@@ -34,7 +34,7 @@ namespace Flow.Launcher.Core.Plugin
/// Represent the plugin that using JsonPRC
/// every JsonRPC plugin should has its own plugin instance
///
- internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
+ public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
{
protected PluginInitContext Context;
public const string JsonRPC = "JsonRPC";
@@ -44,8 +44,10 @@ namespace Flow.Launcher.Core.Plugin
private string SettingConfigurationPath =>
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
- private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory,
- Context.CurrentPluginMetadata.Name, "Settings.json");
+ private string SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory,
+ Context.CurrentPluginMetadata.Name);
+
+ private string SettingPath => Path.Combine(SettingDirectory, "Settings.json");
public abstract List LoadContextMenus(Result selectedResult);
@@ -155,9 +157,22 @@ namespace Flow.Launcher.Core.Plugin
Settings?.Save();
}
+ public bool NeedCreateSettingPanel()
+ {
+ return Settings.NeedCreateSettingPanel();
+ }
+
public Control CreateSettingPanel()
{
return Settings.CreateSettingPanel();
}
+
+ public void DeletePluginSettingsDirectory()
+ {
+ if (Directory.Exists(SettingDirectory))
+ {
+ Directory.Delete(SettingDirectory, true);
+ }
+ }
}
}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
index 50eb30998..8412ba7e8 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
@@ -109,10 +109,15 @@ namespace Flow.Launcher.Core.Plugin
_storage.Save();
}
+ public bool NeedCreateSettingPanel()
+ {
+ // If there are no settings or the settings configuration is empty, return null
+ return Settings != null && Configuration != null && Configuration.Body.Count != 0;
+ }
+
public Control CreateSettingPanel()
{
- if (Settings == null || Settings.Count == 0)
- return new();
+ // No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true
var settingWindow = new UserControl();
var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
index 5a6633525..abe563c14 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
@@ -26,54 +26,33 @@ namespace Flow.Launcher.Core.Plugin
protected override async Task ExecuteResultAsync(JsonRPCResult result)
{
- try
- {
- var res = await RPC.InvokeAsync(result.JsonRPCAction.Method,
- argument: result.JsonRPCAction.Parameters);
+ var res = await RPC.InvokeAsync(result.JsonRPCAction.Method,
+ argument: result.JsonRPCAction.Parameters);
- return res.Hide;
- }
- catch
- {
- return false;
- }
+ return res.Hide;
}
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
public override List LoadContextMenus(Result selectedResult)
{
- try
- {
- var res = JTF.Run(() => RPC.InvokeWithCancellationAsync("context_menu",
- new object[] { selectedResult.ContextData }));
+ var res = JTF.Run(() => RPC.InvokeWithCancellationAsync("context_menu",
+ new object[] { selectedResult.ContextData }));
- var results = ParseResults(res);
+ var results = ParseResults(res);
- return results;
- }
- catch
- {
- return new List();
- }
+ return results;
}
public override async Task> QueryAsync(Query query, CancellationToken token)
{
- try
- {
- var res = await RPC.InvokeWithCancellationAsync("query",
- new object[] { query, Settings.Inner },
- token);
+ var res = await RPC.InvokeWithCancellationAsync("query",
+ new object[] { query, Settings.Inner },
+ token);
- var results = ParseResults(res);
+ var results = ParseResults(res);
- return results;
- }
- catch
- {
- return new List();
- }
+ return results;
}
@@ -133,10 +112,15 @@ namespace Flow.Launcher.Core.Plugin
RPC.StartListening();
}
- public virtual Task ReloadDataAsync()
+ public virtual async Task ReloadDataAsync()
{
- SetupJsonRPC();
- return Task.CompletedTask;
+ try
+ {
+ await RPC.InvokeAsync("reload_data", Context);
+ }
+ catch (RemoteMethodNotFoundException e)
+ {
+ }
}
public virtual async ValueTask DisposeAsync()
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
index b8bfee591..8df2ce9ed 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
@@ -121,10 +120,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
return _api.HttpGetStreamAsync(url, token);
}
- public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
+ public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null,
CancellationToken token = default)
{
- return _api.HttpDownloadAsync(url, filePath, token);
+ return _api.HttpDownloadAsync(url, filePath, reportProgress, token);
}
public void AddActionKeyword(string pluginId, string newActionKeyword)
@@ -162,16 +161,29 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
}
-
public void OpenUrl(string url, bool? inPrivate = null)
{
_api.OpenUrl(url, inPrivate);
}
-
public void OpenAppUri(string appUri)
{
_api.OpenAppUri(appUri);
}
+
+ public void BackToQueryResults()
+ {
+ _api.BackToQueryResults();
+ }
+
+ public void StartLoadingBar()
+ {
+ _api.StartLoadingBar();
+ }
+
+ public void StopLoadingBar()
+ {
+ _api.StopLoadingBar();
+ }
}
}
diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index 5c4eaa1da..09711051e 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -14,6 +14,7 @@ using ISavable = Flow.Launcher.Plugin.ISavable;
using Flow.Launcher.Plugin.SharedCommands;
using System.Text.Json;
using Flow.Launcher.Core.Resource;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Plugin
{
@@ -28,7 +29,9 @@ namespace Flow.Launcher.Core.Plugin
public static readonly HashSet GlobalPlugins = new();
public static readonly Dictionary NonGlobalPlugins = new();
- public static IPublicAPI API { private set; get; }
+ // We should not initialize API in static constructor because it will create another API instance
+ private static IPublicAPI api = null;
+ private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService();
private static PluginsSettings Settings;
private static List _metadatas;
@@ -158,9 +161,8 @@ namespace Flow.Launcher.Core.Plugin
/// Call initialize for all plugins
///
/// return the list of failed to init plugins or null for none
- public static async Task InitializePluginsAsync(IPublicAPI api)
+ public static async Task InitializePluginsAsync()
{
- API = api;
var failedPlugins = new ConcurrentQueue();
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
@@ -204,15 +206,15 @@ namespace Flow.Launcher.Core.Plugin
}
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface());
- InternationalizationManager.Instance.ChangeLanguage(InternationalizationManager.Instance.Settings.Language);
+ InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService().Language);
if (failedPlugins.Any())
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
API.ShowMsg(
- InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsTitle"),
+ API.GetTranslation("failedToInitializePluginsTitle"),
string.Format(
- InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsMessage"),
+ API.GetTranslation("failedToInitializePluginsMessage"),
failed
),
"",
@@ -281,7 +283,7 @@ namespace Flow.Launcher.Core.Plugin
return results;
}
- public static void UpdatePluginMetadata(List results, PluginMetadata metadata, Query query)
+ public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query)
{
foreach (var r in results)
{
@@ -439,7 +441,7 @@ namespace Flow.Launcher.Core.Plugin
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{
InstallPlugin(newVersion, zipFilePath, checkModified:false);
- UninstallPlugin(existingVersion, removeSettings:false, checkModified:false);
+ UninstallPlugin(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
_modifiedPlugins.Add(existingVersion.ID);
}
@@ -454,9 +456,9 @@ namespace Flow.Launcher.Core.Plugin
///
/// Uninstall a plugin.
///
- public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true)
+ public static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
{
- UninstallPlugin(plugin, removeSettings, true);
+ UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, true);
}
#endregion
@@ -519,9 +521,17 @@ namespace Flow.Launcher.Core.Plugin
var newPluginPath = Path.Combine(installDirectory, folderName);
- FilesFolders.CopyAll(pluginFolderPath, newPluginPath, MessageBoxEx.Show);
+ FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s));
- Directory.Delete(tempFolderPluginPath, true);
+ try
+ {
+ if (Directory.Exists(tempFolderPluginPath))
+ Directory.Delete(tempFolderPluginPath, true);
+ }
+ catch (Exception e)
+ {
+ Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e);
+ }
if (checkModified)
{
@@ -529,14 +539,62 @@ namespace Flow.Launcher.Core.Plugin
}
}
- internal static void UninstallPlugin(PluginMetadata plugin, bool removeSettings, bool checkModified)
+ internal static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
}
- if (removeSettings)
+ if (removePluginSettings)
+ {
+ if (AllowedLanguage.IsDotNet(plugin.Language)) // for the plugin in .NET, we can use assembly loader
+ {
+ var assemblyLoader = new PluginAssemblyLoader(plugin.ExecuteFilePath);
+ var assembly = assemblyLoader.LoadAssemblyAndDependencies();
+ var assemblyName = assembly.GetName().Name;
+
+ // if user want to remove the plugin settings, we cannot call save method for the plugin json storage instance of this plugin
+ // so we need to remove it from the api instance
+ var method = API.GetType().GetMethod("RemovePluginSettings");
+ var pluginJsonStorage = method?.Invoke(API, new object[] { assemblyName });
+
+ // if there exists a json storage for current plugin, we need to delete the directory path
+ if (pluginJsonStorage != null)
+ {
+ var deleteMethod = pluginJsonStorage.GetType().GetMethod("DeleteDirectory");
+ try
+ {
+ deleteMethod?.Invoke(pluginJsonStorage, null);
+ }
+ catch (Exception e)
+ {
+ Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
+ API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
+ string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
+ }
+ }
+ }
+ else // the plugin with json prc interface
+ {
+ var pluginPair = AllPlugins.FirstOrDefault(p => p.Metadata.ID == plugin.ID);
+ if (pluginPair != null && pluginPair.Plugin is JsonRPCPlugin jsonRpcPlugin)
+ {
+ try
+ {
+ jsonRpcPlugin.DeletePluginSettingsDirectory();
+ }
+ catch (Exception e)
+ {
+ Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
+ API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
+ string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
+ }
+ }
+ }
+ }
+
+ if (removePluginFromSettings)
{
Settings.Plugins.Remove(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
index 7973c66ba..4827cf69d 100644
--- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
+using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins.Environments;
#pragma warning disable IDE0005
using Flow.Launcher.Infrastructure.Logger;
@@ -119,7 +120,7 @@ namespace Flow.Launcher.Core.Plugin
_ = Task.Run(() =>
{
- MessageBoxEx.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
+ Ioc.Default.GetRequiredService().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
$"Please refer to the logs for more information", "",
MessageBoxButton.OK, MessageBoxImage.Warning);
diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs
index 536e69b3d..e40b0330e 100644
--- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs
+++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs
@@ -1,4 +1,5 @@
-using System.Diagnostics;
+using System;
+using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading;
@@ -25,14 +26,13 @@ namespace Flow.Launcher.Core.Plugin
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
+ // Prevent Python from writing .py[co] files.
+ // Because .pyc contains location infos which will prevent python portable.
+ _startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
-
-
- //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
- _startInfo.ArgumentList.Add("-B");
}
protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
@@ -50,10 +50,53 @@ namespace Flow.Launcher.Core.Plugin
// TODO: Async Action
return Execute(_startInfo);
}
+
public override async Task InitAsync(PluginInitContext context)
{
- _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
- _startInfo.ArgumentList.Add("");
+ // Run .py files via `-c `
+ if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
+ {
+ var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
+ var libDirectory = Path.Combine(rootDirectory, "lib");
+ var libPyWin32Directory = Path.Combine(libDirectory, "win32");
+ var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
+ var pluginDirectory = Path.Combine(rootDirectory, "plugin");
+
+ // This makes it easier for plugin authors to import their own modules.
+ // They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
+ // Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
+ // This code sets sys.path for the plugin author and then runs the .py file via runpy.
+ _startInfo.ArgumentList.Add("-c");
+ _startInfo.ArgumentList.Add(
+ $"""
+ import sys
+ sys.path.append(r'{rootDirectory}')
+ sys.path.append(r'{libDirectory}')
+ sys.path.append(r'{libPyWin32LibDirectory}')
+ sys.path.append(r'{libPyWin32Directory}')
+ sys.path.append(r'{pluginDirectory}')
+
+ import runpy
+ runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__')
+ """
+ );
+ // Plugins always expect the JSON data to be in the third argument
+ // (we're always setting it as _startInfo.ArgumentList[2] = ...).
+ _startInfo.ArgumentList.Add("");
+ }
+ // Run .pyz files as is
+ else
+ {
+ // No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now,
+ // but the plugins still expect data to be sent as the third argument, so we're keeping
+ // the flag here, even though it's not necessary anymore.
+ _startInfo.ArgumentList.Add("-B");
+ _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
+ // Plugins always expect the JSON data to be in the third argument
+ // (we're always setting it as _startInfo.ArgumentList[2] = ...).
+ _startInfo.ArgumentList.Add("");
+ }
+
await base.InitAsync(context);
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
}
diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs
index 5c36e0eea..8a9e1ff44 100644
--- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs
+++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs
@@ -26,14 +26,45 @@ namespace Flow.Launcher.Core.Plugin
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
-
- //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
- StartInfo.ArgumentList.Add("-B");
+ StartInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
}
public override async Task InitAsync(PluginInitContext context)
{
- StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
+ // Run .py files via `-c `
+ if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
+ {
+ var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
+ var libDirectory = Path.Combine(rootDirectory, "lib");
+ var libPyWin32Directory = Path.Combine(libDirectory, "win32");
+ var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
+ var pluginDirectory = Path.Combine(rootDirectory, "plugin");
+ var filePath = context.CurrentPluginMetadata.ExecuteFilePath;
+
+ // This makes it easier for plugin authors to import their own modules.
+ // They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
+ // Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
+ // This code sets sys.path for the plugin author and then runs the .py file via runpy.
+ StartInfo.ArgumentList.Add("-c");
+ StartInfo.ArgumentList.Add(
+ $"""
+ import sys
+ sys.path.append(r'{rootDirectory}')
+ sys.path.append(r'{libDirectory}')
+ sys.path.append(r'{libPyWin32LibDirectory}')
+ sys.path.append(r'{libPyWin32Directory}')
+ sys.path.append(r'{pluginDirectory}')
+
+ import runpy
+ runpy.run_path(r'{filePath}', None, '__main__')
+ """
+ );
+ }
+ // Run .pyz files as is
+ else
+ {
+ StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
+ }
await base.InitAsync(context);
}
diff --git a/Flow.Launcher.Core/Resource/AvailableLanguages.cs b/Flow.Launcher.Core/Resource/AvailableLanguages.cs
index c385cd8e8..ecaecf646 100644
--- a/Flow.Launcher.Core/Resource/AvailableLanguages.cs
+++ b/Flow.Launcher.Core/Resource/AvailableLanguages.cs
@@ -30,7 +30,6 @@ namespace Flow.Launcher.Core.Resource
public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt");
public static Language Hebrew = new Language("he", "עברית");
-
public static List GetAvailableLanguages()
{
List languages = new List
@@ -63,5 +62,38 @@ namespace Flow.Launcher.Core.Resource
};
return languages;
}
+
+ public static string GetSystemTranslation(string languageCode)
+ {
+ return languageCode switch
+ {
+ "en" => "System",
+ "zh-cn" => "系统",
+ "zh-tw" => "系統",
+ "uk-UA" => "Система",
+ "ru" => "Система",
+ "fr" => "Système",
+ "ja" => "システム",
+ "nl" => "Systeem",
+ "pl" => "System",
+ "da" => "System",
+ "de" => "System",
+ "ko" => "시스템",
+ "sr" => "Систем",
+ "pt-pt" => "Sistema",
+ "pt-br" => "Sistema",
+ "es" => "Sistema",
+ "es-419" => "Sistema",
+ "it" => "Sistema",
+ "nb-NO" => "System",
+ "sk" => "Systém",
+ "tr" => "Sistem",
+ "cs" => "Systém",
+ "ar" => "النظام",
+ "vi-vn" => "Hệ thống",
+ "he" => "מערכת",
+ _ => "System",
+ };
+ }
}
}
diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs
index 1505e84f8..e2a66656a 100644
--- a/Flow.Launcher.Core/Resource/Internationalization.cs
+++ b/Flow.Launcher.Core/Resource/Internationalization.cs
@@ -11,30 +11,61 @@ using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using System.Globalization;
using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Resource
{
public class Internationalization
{
- public Settings Settings { get; set; }
private const string Folder = "Languages";
+ private const string DefaultLanguageCode = "en";
private const string DefaultFile = "en.xaml";
private const string Extension = ".xaml";
+ private readonly Settings _settings;
private readonly List _languageDirectories = new List();
private readonly List _oldResources = new List();
+ private readonly string SystemLanguageCode;
- public Internationalization()
+ public Internationalization(Settings settings)
{
+ _settings = settings;
AddFlowLauncherLanguageDirectory();
+ SystemLanguageCode = GetSystemLanguageCodeAtStartup();
}
-
private void AddFlowLauncherLanguageDirectory()
{
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
_languageDirectories.Add(directory);
}
+ private static string GetSystemLanguageCodeAtStartup()
+ {
+ var availableLanguages = AvailableLanguages.GetAvailableLanguages();
+
+ // Retrieve the language identifiers for the current culture.
+ // ChangeLanguage method overrides the CultureInfo.CurrentCulture, so this needs to
+ // be called at startup in order to get the correct lang code of system.
+ var currentCulture = CultureInfo.CurrentCulture;
+ var twoLetterCode = currentCulture.TwoLetterISOLanguageName;
+ var threeLetterCode = currentCulture.ThreeLetterISOLanguageName;
+ var fullName = currentCulture.Name;
+
+ // Try to find a match in the available languages list
+ foreach (var language in availableLanguages)
+ {
+ var languageCode = language.LanguageCode;
+
+ if (string.Equals(languageCode, twoLetterCode, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase))
+ {
+ return languageCode;
+ }
+ }
+
+ return DefaultLanguageCode;
+ }
internal void AddPluginLanguageDirectories(IEnumerable plugins)
{
@@ -68,8 +99,18 @@ namespace Flow.Launcher.Core.Resource
public void ChangeLanguage(string languageCode)
{
languageCode = languageCode.NonNull();
- Language language = GetLanguageByLanguageCode(languageCode);
- ChangeLanguage(language);
+
+ // Get actual language if language code is system
+ var isSystem = false;
+ if (languageCode == Constant.SystemLanguageCode)
+ {
+ languageCode = SystemLanguageCode;
+ isSystem = true;
+ }
+
+ // Get language by language code and change language
+ var language = GetLanguageByLanguageCode(languageCode);
+ ChangeLanguage(language, isSystem);
}
private Language GetLanguageByLanguageCode(string languageCode)
@@ -87,11 +128,10 @@ namespace Flow.Launcher.Core.Resource
}
}
- public void ChangeLanguage(Language language)
+ private void ChangeLanguage(Language language, bool isSystem)
{
language = language.NonNull();
-
RemoveOldLanguageFiles();
if (language != AvailableLanguages.English)
{
@@ -103,7 +143,7 @@ namespace Flow.Launcher.Core.Resource
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
// Raise event after culture is set
- Settings.Language = language.LanguageCode;
+ _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
_ = Task.Run(() =>
{
UpdatePluginMetadataTranslations();
@@ -114,7 +154,7 @@ namespace Flow.Launcher.Core.Resource
{
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
- if (Settings.ShouldUsePinyin)
+ if (_settings.ShouldUsePinyin)
return false;
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
@@ -124,7 +164,7 @@ namespace Flow.Launcher.Core.Resource
// "Do you want to search with pinyin?"
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
- if (MessageBoxEx.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
+ if (Ioc.Default.GetRequiredService().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
return false;
return true;
@@ -167,7 +207,9 @@ namespace Flow.Launcher.Core.Resource
public List LoadAvailableLanguages()
{
- return AvailableLanguages.GetAvailableLanguages();
+ var list = AvailableLanguages.GetAvailableLanguages();
+ list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
+ return list;
}
public string GetTranslation(string key)
diff --git a/Flow.Launcher.Core/Resource/InternationalizationManager.cs b/Flow.Launcher.Core/Resource/InternationalizationManager.cs
index 3d87626e6..5d718466c 100644
--- a/Flow.Launcher.Core/Resource/InternationalizationManager.cs
+++ b/Flow.Launcher.Core/Resource/InternationalizationManager.cs
@@ -1,26 +1,12 @@
-namespace Flow.Launcher.Core.Resource
+using System;
+using CommunityToolkit.Mvvm.DependencyInjection;
+
+namespace Flow.Launcher.Core.Resource
{
+ [Obsolete("InternationalizationManager.Instance is obsolete. Use Ioc.Default.GetRequiredService() instead.")]
public static class InternationalizationManager
{
- private static Internationalization instance;
- private static object syncObject = new object();
-
public static Internationalization Instance
- {
- get
- {
- if (instance == null)
- {
- lock (syncObject)
- {
- if (instance == null)
- {
- instance = new Internationalization();
- }
- }
- }
- return instance;
- }
- }
+ => Ioc.Default.GetRequiredService();
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs
index 0d8c0d901..cda125a39 100644
--- a/Flow.Launcher.Core/Resource/Theme.cs
+++ b/Flow.Launcher.Core/Resource/Theme.cs
@@ -3,16 +3,16 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
-using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
-using System.Windows.Interop;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Effects;
+using System.Windows.Shell;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Resource
{
@@ -24,21 +24,27 @@ namespace Flow.Launcher.Core.Resource
private const int ShadowExtraMargin = 32;
- private readonly List _themeDirectories = new List();
+ private readonly IPublicAPI _api;
+ private readonly Settings _settings;
+ private readonly List _themeDirectories = new();
private ResourceDictionary _oldResource;
private string _oldTheme;
- public Settings Settings { get; set; }
private const string Folder = Constant.Themes;
private const string Extension = ".xaml";
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
+ public string CurrentTheme => _settings.Theme;
+
public bool BlurEnabled { get; set; }
private double mainWindowWidth;
- public Theme()
+ public Theme(IPublicAPI publicAPI, Settings settings)
{
+ _api = publicAPI;
+ _settings = settings;
+
_themeDirectories.Add(DirectoryPath);
_themeDirectories.Add(UserDirectoryPath);
MakeSureThemeDirectoriesExist();
@@ -89,7 +95,7 @@ namespace Flow.Launcher.Core.Resource
// to things like fonts
UpdateResourceDictionary(GetResourceDictionary(theme));
- Settings.Theme = theme;
+ _settings.Theme = theme;
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
@@ -98,19 +104,19 @@ namespace Flow.Launcher.Core.Resource
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
- BlurEnabled = IsBlurTheme();
+ BlurEnabled = Win32Helper.IsBlurTheme();
- if (Settings.UseDropShadowEffect && !BlurEnabled)
+ if (_settings.UseDropShadowEffect && !BlurEnabled)
AddDropShadowEffectToCurrentTheme();
- SetBlurForWindow();
+ Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
}
catch (DirectoryNotFoundException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
if (theme != defaultTheme)
{
- MessageBoxEx.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
+ _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
ChangeTheme(defaultTheme);
}
return false;
@@ -120,7 +126,7 @@ namespace Flow.Launcher.Core.Resource
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
if (theme != defaultTheme)
{
- MessageBoxEx.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
+ _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
ChangeTheme(defaultTheme);
}
return false;
@@ -148,7 +154,7 @@ namespace Flow.Launcher.Core.Resource
return dict;
}
- private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(Settings.Theme);
+ private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme);
public ResourceDictionary GetResourceDictionary(string theme)
{
@@ -157,10 +163,10 @@ namespace Flow.Launcher.Core.Resource
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
{
- var fontFamily = new FontFamily(Settings.QueryBoxFont);
- var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle);
- var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight);
- var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch);
+ var fontFamily = new FontFamily(_settings.QueryBoxFont);
+ var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
+ var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
+ var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
@@ -185,10 +191,10 @@ namespace Flow.Launcher.Core.Resource
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
{
- Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont));
- Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle));
- Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultFontWeight));
- Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch));
+ Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultFont));
+ Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle));
+ Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight));
+ Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch));
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
Array.ForEach(
@@ -200,10 +206,10 @@ namespace Flow.Launcher.Core.Resource
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
{
- Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultSubFont));
- Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultSubFontStyle));
- Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultSubFontWeight));
- Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultSubFontStretch));
+ Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultSubFont));
+ Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle));
+ Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight));
+ Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch));
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
Array.ForEach(
@@ -213,7 +219,7 @@ namespace Flow.Launcher.Core.Resource
/* Ignore Theme Window Width and use setting */
var windowStyle = dict["WindowStyle"] as Style;
- var width = Settings.WindowSize;
+ var width = _settings.WindowSize;
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
mainWindowWidth = (double)width;
return dict;
@@ -221,7 +227,7 @@ namespace Flow.Launcher.Core.Resource
private ResourceDictionary GetCurrentResourceDictionary( )
{
- return GetResourceDictionary(Settings.Theme);
+ return GetResourceDictionary(_settings.Theme);
}
public List LoadAvailableThemes()
@@ -308,12 +314,15 @@ namespace Flow.Launcher.Core.Resource
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
if (marginSetter == null)
{
+ var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin);
marginSetter = new Setter()
{
Property = Border.MarginProperty,
- Value = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin),
+ Value = margin,
};
windowBorderStyle.Setters.Add(marginSetter);
+
+ SetResizeBoarderThickness(margin);
}
else
{
@@ -324,6 +333,8 @@ namespace Flow.Launcher.Core.Resource
baseMargin.Right + ShadowExtraMargin,
baseMargin.Bottom + ShadowExtraMargin);
marginSetter.Value = newMargin;
+
+ SetResizeBoarderThickness(newMargin);
}
windowBorderStyle.Setters.Add(effectSetter);
@@ -354,101 +365,36 @@ namespace Flow.Launcher.Core.Resource
marginSetter.Value = newMargin;
}
+ SetResizeBoarderThickness(null);
+
UpdateResourceDictionary(dict);
}
- #region Blur Handling
- /*
- Found on https://github.com/riverar/sample-win10-aeroglass
- */
- private enum AccentState
+ // because adding drop shadow effect will change the margin of the window,
+ // we need to update the window chrome thickness to correct set the resize border
+ private static void SetResizeBoarderThickness(Thickness? effectMargin)
{
- ACCENT_DISABLED = 0,
- ACCENT_ENABLE_GRADIENT = 1,
- ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
- ACCENT_ENABLE_BLURBEHIND = 3,
- ACCENT_INVALID_STATE = 4
- }
-
- [StructLayout(LayoutKind.Sequential)]
- private struct AccentPolicy
- {
- public AccentState AccentState;
- public int AccentFlags;
- public int GradientColor;
- public int AnimationId;
- }
-
- [StructLayout(LayoutKind.Sequential)]
- private struct WindowCompositionAttributeData
- {
- public WindowCompositionAttribute Attribute;
- public IntPtr Data;
- public int SizeOfData;
- }
-
- private enum WindowCompositionAttribute
- {
- WCA_ACCENT_POLICY = 19
- }
- [DllImport("user32.dll")]
- private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
-
- ///
- /// Sets the blur for a window via SetWindowCompositionAttribute
- ///
- public void SetBlurForWindow()
- {
- if (BlurEnabled)
+ var window = Application.Current.MainWindow;
+ if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome)
{
- SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
- }
- else
- {
- SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
+ Thickness thickness;
+ if (effectMargin == null)
+ {
+ thickness = SystemParameters.WindowResizeBorderThickness;
+ }
+ else
+ {
+ thickness = new Thickness(
+ effectMargin.Value.Left + SystemParameters.WindowResizeBorderThickness.Left,
+ effectMargin.Value.Top + SystemParameters.WindowResizeBorderThickness.Top,
+ effectMargin.Value.Right + SystemParameters.WindowResizeBorderThickness.Right,
+ effectMargin.Value.Bottom + SystemParameters.WindowResizeBorderThickness.Bottom);
+ }
+
+ windowChrome.ResizeBorderThickness = thickness;
}
}
- private bool IsBlurTheme()
- {
- if (Environment.OSVersion.Version >= new Version(6, 2))
- {
- var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
-
- if (resource is bool)
- return (bool)resource;
-
- return false;
- }
-
- return false;
- }
-
- private void SetWindowAccent(Window w, AccentState state)
- {
- var windowHelper = new WindowInteropHelper(w);
-
- windowHelper.EnsureHandle();
-
- var accent = new AccentPolicy { AccentState = state };
- var accentStructSize = Marshal.SizeOf(accent);
-
- var accentPtr = Marshal.AllocHGlobal(accentStructSize);
- Marshal.StructureToPtr(accent, accentPtr, false);
-
- var data = new WindowCompositionAttributeData
- {
- Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
- SizeOfData = accentStructSize,
- Data = accentPtr
- };
-
- SetWindowCompositionAttribute(windowHelper.Handle, ref data);
-
- Marshal.FreeHGlobal(accentPtr);
- }
- #endregion
-
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
}
}
diff --git a/Flow.Launcher.Core/Resource/ThemeManager.cs b/Flow.Launcher.Core/Resource/ThemeManager.cs
index 71f9acaa5..3cbe8319a 100644
--- a/Flow.Launcher.Core/Resource/ThemeManager.cs
+++ b/Flow.Launcher.Core/Resource/ThemeManager.cs
@@ -1,26 +1,12 @@
-namespace Flow.Launcher.Core.Resource
+using System;
+using CommunityToolkit.Mvvm.DependencyInjection;
+
+namespace Flow.Launcher.Core.Resource
{
+ [Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService() instead.")]
public class ThemeManager
{
- private static Theme instance;
- private static object syncObject = new object();
-
public static Theme Instance
- {
- get
- {
- if (instance == null)
- {
- lock (syncObject)
- {
- if (instance == null)
- {
- instance = new Theme();
- }
- }
- }
- return instance;
- }
- }
+ => Ioc.Default.GetRequiredService();
}
}
diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs
index b92d86568..9a77ece32 100644
--- a/Flow.Launcher.Core/Updater.cs
+++ b/Flow.Launcher.Core/Updater.cs
@@ -22,23 +22,26 @@ namespace Flow.Launcher.Core
{
public class Updater
{
- public string GitHubRepository { get; }
+ public string GitHubRepository { get; init; }
- public Updater(string gitHubRepository)
+ private readonly IPublicAPI _api;
+
+ public Updater(IPublicAPI publicAPI, string gitHubRepository)
{
+ _api = publicAPI;
GitHubRepository = gitHubRepository;
}
private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1);
- public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
+ public async Task UpdateAppAsync(bool silentUpdate = true)
{
await UpdateLock.WaitAsync().ConfigureAwait(false);
try
{
if (!silentUpdate)
- api.ShowMsg(api.GetTranslation("pleaseWait"),
- api.GetTranslation("update_flowlauncher_update_check"));
+ _api.ShowMsg(_api.GetTranslation("pleaseWait"),
+ _api.GetTranslation("update_flowlauncher_update_check"));
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
@@ -53,13 +56,13 @@ namespace Flow.Launcher.Core
if (newReleaseVersion <= currentVersion)
{
if (!silentUpdate)
- MessageBoxEx.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
+ _api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
return;
}
if (!silentUpdate)
- api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"),
- api.GetTranslation("update_flowlauncher_updating"));
+ _api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
+ _api.GetTranslation("update_flowlauncher_updating"));
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
@@ -68,9 +71,9 @@ namespace Flow.Launcher.Core
if (DataLocation.PortableDataLocationInUse())
{
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
- FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, MessageBoxEx.Show);
- if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, MessageBoxEx.Show))
- MessageBoxEx.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
+ FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
+ if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
+ _api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
DataLocation.PortableDataPath,
targetDestination));
}
@@ -83,7 +86,7 @@ namespace Flow.Launcher.Core
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
- if (MessageBoxEx.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+ if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
@@ -96,8 +99,8 @@ namespace Flow.Launcher.Core
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
if (!silentUpdate)
- api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
- api.GetTranslation("update_flowlauncher_check_connection"));
+ _api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
+ _api.GetTranslation("update_flowlauncher_check_connection"));
}
finally
{
diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs
index 2889e5ec7..c86ed4324 100644
--- a/Flow.Launcher.Infrastructure/Constant.cs
+++ b/Flow.Launcher.Infrastructure/Constant.cs
@@ -52,5 +52,7 @@ namespace Flow.Launcher.Infrastructure
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
public const string Docs = "https://flowlauncher.com/docs";
+
+ public const string SystemLanguageCode = "system";
}
}
diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs
index 76695a4e3..b97c096c3 100644
--- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs
+++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
+using Windows.Win32;
namespace Flow.Launcher.Infrastructure
{
@@ -15,7 +15,20 @@ namespace Flow.Launcher.Infrastructure
{
var explorerWindow = GetActiveExplorer();
string locationUrl = explorerWindow?.LocationURL;
- return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null;
+ return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
+ }
+
+ ///
+ /// Get directory path from a file path
+ ///
+ private static string GetDirectoryPath(string path)
+ {
+ if (!path.EndsWith("\\"))
+ {
+ return path + "\\";
+ }
+
+ return path;
}
///
@@ -54,10 +67,6 @@ namespace Flow.Launcher.Infrastructure
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
}
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
-
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
///
@@ -70,9 +79,9 @@ namespace Flow.Launcher.Infrastructure
var index = 0;
var numRemaining = hWnds.Count;
- EnumWindows((wnd, _) =>
+ PInvoke.EnumWindows((wnd, _) =>
{
- var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32());
+ var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
if (searchIndex != -1)
{
z[searchIndex] = index;
diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
index ec35ef9d6..b91da7114 100644
--- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
+++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
@@ -35,6 +35,10 @@
false
+
+
+
+
@@ -49,13 +53,18 @@
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs
index f847ab189..b2a140755 100644
--- a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs
+++ b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs
@@ -1,6 +1,11 @@
-using System;
+using System;
+using System.Diagnostics;
using System.Runtime.InteropServices;
using Flow.Launcher.Plugin;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.Input.KeyboardAndMouse;
+using Windows.Win32.UI.WindowsAndMessaging;
namespace Flow.Launcher.Infrastructure.Hotkey
{
@@ -10,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey
///
public unsafe class GlobalHotkey : IDisposable
{
- private static readonly IntPtr hookId;
-
-
-
+ private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback;
+ private static readonly UnhookWindowsHookExSafeHandle hookId;
+
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
internal static Func hookedKeyboardCallback;
- //Modifier key constants
- private const int VK_SHIFT = 0x10;
- private const int VK_CONTROL = 0x11;
- private const int VK_ALT = 0x12;
- private const int VK_WIN = 91;
-
static GlobalHotkey()
{
// Set the hook
- hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc);
+ hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL);
+ }
+
+ private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId)
+ {
+ using var curProcess = Process.GetCurrentProcess();
+ using var curModule = curProcess.MainModule;
+ return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0);
}
public static SpecialKeyState CheckModifiers()
{
SpecialKeyState state = new SpecialKeyState();
- if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0)
+ if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0)
{
//SHIFT is pressed
state.ShiftPressed = true;
}
- if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0)
+ if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0)
{
//CONTROL is pressed
state.CtrlPressed = true;
}
- if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0)
+ if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0)
{
//ALT is pressed
state.AltPressed = true;
}
- if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0)
+ if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 ||
+ (PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0)
{
//WIN is pressed
state.WinPressed = true;
@@ -56,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey
return state;
}
- [UnmanagedCallersOnly]
- private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
+ private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam)
{
bool continues = true;
if (nCode >= 0)
{
- if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN ||
- wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP ||
- wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN ||
- wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP)
+ if (wParam.Value == (int)KeyEvent.WM_KEYDOWN ||
+ wParam.Value == (int)KeyEvent.WM_KEYUP ||
+ wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN ||
+ wParam.Value == (int)KeyEvent.WM_SYSKEYUP)
{
if (hookedKeyboardCallback != null)
- continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers());
+ continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers());
}
}
if (continues)
{
- return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam);
+ return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam);
}
- return (IntPtr)(-1);
+
+ return new LRESULT(1);
}
public void Dispose()
{
- InterceptKeys.UnhookWindowsHookEx(hookId);
+ hookId.Dispose();
}
~GlobalHotkey()
@@ -90,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey
Dispose();
}
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs b/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs
deleted file mode 100644
index d33bac34c..000000000
--- a/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-
-namespace Flow.Launcher.Infrastructure.Hotkey
-{
- internal static unsafe class InterceptKeys
- {
- public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
-
- private const int WH_KEYBOARD_LL = 13;
-
- public static IntPtr SetHook(delegate* unmanaged proc)
- {
- using (Process curProcess = Process.GetCurrentProcess())
- using (ProcessModule curModule = curProcess.MainModule)
- {
- return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
- }
- }
-
- [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- public static extern IntPtr SetWindowsHookEx(int idHook, delegate* unmanaged lpfn, IntPtr hMod, uint dwThreadId);
-
- [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool UnhookWindowsHookEx(IntPtr hhk);
-
- [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam);
-
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- public static extern IntPtr GetModuleHandle(string lpModuleName);
-
- [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
- public static extern short GetKeyState(int keyCode);
- }
-}
\ No newline at end of file
diff --git a/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs b/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs
index 15e306883..95bb25837 100644
--- a/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs
+++ b/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs
@@ -1,3 +1,5 @@
+using Windows.Win32;
+
namespace Flow.Launcher.Infrastructure.Hotkey
{
public enum KeyEvent
@@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey
///
/// Key down
///
- WM_KEYDOWN = 256,
+ WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN,
///
/// Key up
///
- WM_KEYUP = 257,
+ WM_KEYUP = (int)PInvoke.WM_KEYUP,
///
/// System key up
///
- WM_SYSKEYUP = 261,
+ WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP,
///
/// System key down
///
- WM_SYSKEYDOWN = 260
+ WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs
index 14b8eef4e..030aff7cf 100644
--- a/Flow.Launcher.Infrastructure/Http/Http.cs
+++ b/Flow.Launcher.Infrastructure/Http/Http.cs
@@ -8,6 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
using System;
using System.Threading;
using Flow.Launcher.Plugin;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Infrastructure.Http
{
@@ -17,8 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http
private static HttpClient client = new HttpClient();
- public static IPublicAPI API { get; set; }
-
static Http()
{
// need to be added so it would work on a win10 machine
@@ -78,20 +77,55 @@ namespace Flow.Launcher.Infrastructure.Http
}
catch (UriFormatException e)
{
- API.ShowMsg("Please try again", "Unable to parse Http Proxy");
+ Ioc.Default.GetRequiredService().ShowMsg("Please try again", "Unable to parse Http Proxy");
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
}
}
- public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default)
+ public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default)
{
try
{
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
+
if (response.StatusCode == HttpStatusCode.OK)
{
- await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
- await response.Content.CopyToAsync(fileStream, token);
+ var totalBytes = response.Content.Headers.ContentLength ?? -1L;
+ var canReportProgress = totalBytes != -1;
+
+ if (canReportProgress && reportProgress != null)
+ {
+ await using var contentStream = await response.Content.ReadAsStreamAsync(token);
+ await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);
+
+ var buffer = new byte[8192];
+ long totalRead = 0;
+ int read;
+ double progressValue = 0;
+
+ reportProgress(0);
+
+ while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
+ {
+ await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
+ totalRead += read;
+
+ progressValue = totalRead * 100.0 / totalBytes;
+
+ if (token.IsCancellationRequested)
+ return;
+ else
+ reportProgress(progressValue);
+ }
+
+ if (progressValue < 100)
+ reportProgress(100);
+ }
+ else
+ {
+ await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
+ await response.Content.CopyToAsync(fileStream, token);
+ }
}
else
{
diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs
index 247238bb6..b98ea50fe 100644
--- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs
+++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs
@@ -1,12 +1,19 @@
using System;
using System.Runtime.InteropServices;
using System.IO;
+using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
-using System.Windows;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.Shell;
+using Windows.Win32.Graphics.Gdi;
namespace Flow.Launcher.Infrastructure.Image
{
+ ///
+ /// Subclass of
+ ///
[Flags]
public enum ThumbnailOptions
{
@@ -22,91 +29,13 @@ namespace Flow.Launcher.Infrastructure.Image
{
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
- private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
-
- [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
- internal static extern int SHCreateItemFromParsingName(
- [MarshalAs(UnmanagedType.LPWStr)] string path,
- IntPtr pbc,
- ref Guid riid,
- [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
-
- [DllImport("gdi32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static extern bool DeleteObject(IntPtr hObject);
-
- [ComImport]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
- internal interface IShellItem
- {
- void BindToHandler(IntPtr pbc,
- [MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
- [MarshalAs(UnmanagedType.LPStruct)]Guid riid,
- out IntPtr ppv);
-
- void GetParent(out IShellItem ppsi);
- void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
- void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
- void Compare(IShellItem psi, uint hint, out int piOrder);
- };
-
- internal enum SIGDN : uint
- {
- NORMALDISPLAY = 0,
- PARENTRELATIVEPARSING = 0x80018001,
- PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
- DESKTOPABSOLUTEPARSING = 0x80028000,
- PARENTRELATIVEEDITING = 0x80031001,
- DESKTOPABSOLUTEEDITING = 0x8004c000,
- FILESYSPATH = 0x80058000,
- URL = 0x80068000
- }
-
- internal enum HResult
- {
- Ok = 0x0000,
- False = 0x0001,
- InvalidArguments = unchecked((int)0x80070057),
- OutOfMemory = unchecked((int)0x8007000E),
- NoInterface = unchecked((int)0x80004002),
- Fail = unchecked((int)0x80004005),
- ExtractionFailed = unchecked((int)0x8004B200),
- ElementNotFound = unchecked((int)0x80070490),
- TypeElementNotFound = unchecked((int)0x8002802B),
- NoObject = unchecked((int)0x800401E5),
- Win32ErrorCanceled = 1223,
- Canceled = unchecked((int)0x800704C7),
- ResourceInUse = unchecked((int)0x800700AA),
- AccessDenied = unchecked((int)0x80030005)
- }
-
- [ComImportAttribute()]
- [GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
- [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
- internal interface IShellItemImageFactory
- {
- [PreserveSig]
- HResult GetImage(
- [In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
- [In] ThumbnailOptions flags,
- [Out] out IntPtr phbm);
- }
-
- [StructLayout(LayoutKind.Sequential)]
- internal struct NativeSize
- {
- private int width;
- private int height;
-
- public int Width { set { width = value; } }
- public int Height { set { height = value; } }
- };
+ private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID;
+ private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200;
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
- IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
+ HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
try
{
@@ -115,39 +44,61 @@ namespace Flow.Launcher.Infrastructure.Image
finally
{
// delete HBitmap to avoid memory leaks
- DeleteObject(hBitmap);
+ PInvoke.DeleteObject(hBitmap);
}
}
-
- private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
- {
- IShellItem nativeShellItem;
- Guid shellItem2Guid = new Guid(IShellItem2Guid);
- int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
- if (retCode != 0)
+ private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
+ {
+ var retCode = PInvoke.SHCreateItemFromParsingName(
+ fileName,
+ null,
+ GUID_IShellItem,
+ out var nativeShellItem);
+
+ if (retCode != HRESULT.S_OK)
throw Marshal.GetExceptionForHR(retCode);
- NativeSize nativeSize = new NativeSize
+ if (nativeShellItem is not IShellItemImageFactory imageFactory)
{
- Width = width,
- Height = height
- };
-
- IntPtr hBitmap;
- HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);
-
- // if extracting image thumbnail and failed, extract shell icon
- if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed)
- {
- hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap);
+ Marshal.ReleaseComObject(nativeShellItem);
+ nativeShellItem = null;
+ throw new InvalidOperationException("Failed to get IShellItemImageFactory");
}
- Marshal.ReleaseComObject(nativeShellItem);
+ SIZE size = new SIZE
+ {
+ cx = width,
+ cy = height
+ };
- if (hr == HResult.Ok) return hBitmap;
+ HBITMAP hBitmap = default;
+ try
+ {
+ try
+ {
+ imageFactory.GetImage(size, (SIIGBF)options, &hBitmap);
+ }
+ catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly)
+ {
+ // Fallback to IconOnly if ThumbnailOnly fails
+ imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
+ }
+ catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly)
+ {
+ // Fallback to IconOnly if files cannot be found
+ imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
+ }
+ }
+ finally
+ {
+ if (nativeShellItem != null)
+ {
+ Marshal.ReleaseComObject(nativeShellItem);
+ }
+ }
- throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr));
+ return hBitmap;
}
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs
index d4bd473ac..7f847e287 100644
--- a/Flow.Launcher.Infrastructure/Logger/Log.cs
+++ b/Flow.Launcher.Infrastructure/Logger/Log.cs
@@ -48,17 +48,45 @@ namespace Flow.Launcher.Infrastructure.Logger
configuration.AddTarget("file", fileTargetASyncWrapper);
configuration.AddTarget("debug", debugTarget);
+ var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper)
+ {
+ RuleName = "file"
+ };
#if DEBUG
- var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
- var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
+ var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget)
+ {
+ RuleName = "debug"
+ };
configuration.LoggingRules.Add(debugRule);
-#else
- var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
#endif
configuration.LoggingRules.Add(fileRule);
LogManager.Configuration = configuration;
}
+ public static void SetLogLevel(LOGLEVEL level)
+ {
+ switch (level)
+ {
+ case LOGLEVEL.DEBUG:
+ UseDebugLogLevel();
+ break;
+ default:
+ UseInfoLogLevel();
+ break;
+ }
+ Info(nameof(Logger), $"Using log level: {level}.");
+ }
+
+ private static void UseDebugLogLevel()
+ {
+ LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
+ }
+
+ private static void UseInfoLogLevel()
+ {
+ LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
+ }
+
private static void LogFaultyFormat(string message)
{
var logger = LogManager.GetLogger("FaultyLogger");
@@ -206,4 +234,10 @@ namespace Flow.Launcher.Infrastructure.Logger
LogInternal(message, LogLevel.Warn);
}
}
+
+ public enum LOGLEVEL
+ {
+ DEBUG,
+ INFO
+ }
}
diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt
new file mode 100644
index 000000000..f117534a1
--- /dev/null
+++ b/Flow.Launcher.Infrastructure/NativeMethods.txt
@@ -0,0 +1,19 @@
+SHCreateItemFromParsingName
+DeleteObject
+IShellItem
+IShellItemImageFactory
+S_OK
+
+SetWindowsHookEx
+UnhookWindowsHookEx
+CallNextHookEx
+GetModuleHandle
+GetKeyState
+VIRTUAL_KEY
+
+WM_KEYDOWN
+WM_KEYUP
+WM_SYSKEYDOWN
+WM_SYSKEYUP
+
+EnumWindows
\ No newline at end of file
diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs
index 7d7235968..8eaa757be 100644
--- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs
+++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs
@@ -6,6 +6,7 @@ using System.Text;
using JetBrains.Annotations;
using Flow.Launcher.Infrastructure.UserSettings;
using ToolGood.Words.Pinyin;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Infrastructure
{
@@ -129,7 +130,12 @@ namespace Flow.Launcher.Infrastructure
private Settings _settings;
- public void Initialize([NotNull] Settings settings)
+ public PinyinAlphabet()
+ {
+ Initialize(Ioc.Default.GetRequiredService());
+ }
+
+ private void Initialize([NotNull] Settings settings)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
index 642250627..507838d94 100644
--- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
@@ -31,11 +31,12 @@ namespace Flow.Launcher.Infrastructure.Storage
protected JsonStorage()
{
}
+
public JsonStorage(string filePath)
{
FilePath = filePath;
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
-
+
Helper.ValidateDirectory(DirectoryPath);
}
@@ -97,6 +98,7 @@ namespace Flow.Launcher.Infrastructure.Storage
return default;
}
}
+
private void RestoreBackup()
{
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
@@ -179,25 +181,21 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save()
{
string serialized = JsonSerializer.Serialize(Data,
- new JsonSerializerOptions
- {
- WriteIndented = true
- });
+ new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(TempFilePath, serialized);
AtomicWriteSetting();
}
+
public async Task SaveAsync()
{
- var tempOutput = File.OpenWrite(TempFilePath);
+ await using var tempOutput = File.OpenWrite(TempFilePath);
await JsonSerializer.SerializeAsync(tempOutput, Data,
- new JsonSerializerOptions
- {
- WriteIndented = true
- });
+ new JsonSerializerOptions { WriteIndented = true });
AtomicWriteSetting();
}
+
private void AtomicWriteSetting()
{
if (!File.Exists(FilePath))
@@ -206,9 +204,9 @@ namespace Flow.Launcher.Infrastructure.Storage
}
else
{
- File.Replace(TempFilePath, FilePath, BackupFilePath);
+ var finalFilePath = new FileInfo(FilePath).LinkTarget ?? FilePath;
+ File.Replace(TempFilePath, finalFilePath, BackupFilePath);
}
}
-
}
}
diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
index abe3f55b5..bc3900da8 100644
--- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
@@ -3,14 +3,17 @@ using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Infrastructure.Storage
{
- public class PluginJsonStorage :JsonStorage where T : new()
+ public class PluginJsonStorage : JsonStorage where T : new()
{
+ // Use assembly name to check which plugin is using this storage
+ public readonly string AssemblyName;
+
public PluginJsonStorage()
{
// C# related, add python related below
var dataType = typeof(T);
- var assemblyName = dataType.Assembly.GetName().Name;
- DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName);
+ AssemblyName = dataType.Assembly.GetName().Name;
+ DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName);
Helper.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
@@ -20,6 +23,13 @@ namespace Flow.Launcher.Infrastructure.Storage
{
Data = data;
}
+
+ public void DeleteDirectory()
+ {
+ if (Directory.Exists(DirectoryPath))
+ {
+ Directory.Delete(DirectoryPath, true);
+ }
+ }
}
}
-
diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs
index bd5dbdda9..e85c5d6f4 100644
--- a/Flow.Launcher.Infrastructure/StringMatcher.cs
+++ b/Flow.Launcher.Infrastructure/StringMatcher.cs
@@ -1,28 +1,35 @@
-using Flow.Launcher.Plugin.SharedModels;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Plugin.SharedModels;
using System;
using System.Collections.Generic;
using System.Linq;
+using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Infrastructure
{
public class StringMatcher
{
- private readonly MatchOption _defaultMatchOption = new MatchOption();
+ private readonly MatchOption _defaultMatchOption = new();
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
private readonly IAlphabet _alphabet;
- public StringMatcher(IAlphabet alphabet = null)
+ public StringMatcher(IAlphabet alphabet, Settings settings)
+ {
+ _alphabet = alphabet;
+ UserSettingSearchPrecision = settings.QuerySearchPrecision;
+ }
+
+ // This is a workaround to allow unit tests to set the instance
+ public StringMatcher(IAlphabet alphabet)
{
_alphabet = alphabet;
}
- public static StringMatcher Instance { get; internal set; }
-
public static MatchResult FuzzySearch(string query, string stringToCompare)
{
- return Instance.FuzzyMatch(query, stringToCompare);
+ return Ioc.Default.GetRequiredService().FuzzyMatch(query, stringToCompare);
}
public MatchResult FuzzyMatch(string query, string stringToCompare)
@@ -241,16 +248,16 @@ namespace Flow.Launcher.Infrastructure
return false;
}
- private bool IsAcronymChar(string stringToCompare, int compareStringIndex)
+ private static bool IsAcronymChar(string stringToCompare, int compareStringIndex)
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
- private bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
+ private static bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
=> stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
// To get the index of the closest space which preceeds the first matching index
- private int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex)
+ private static int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex)
{
var closestSpaceIndex = -1;
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 0bcc9368d..93f6db111 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -1,10 +1,12 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Text.Json.Serialization;
using System.Windows;
+using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
+using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.ViewModel;
@@ -13,7 +15,25 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
public class Settings : BaseModel, IHotkeySettings
{
- private string language = "en";
+ private FlowLauncherJsonStorage _storage;
+ private StringMatcher _stringMatcher = null;
+
+ public void SetStorage(FlowLauncherJsonStorage storage)
+ {
+ _storage = storage;
+ }
+
+ public void Initialize()
+ {
+ _stringMatcher = Ioc.Default.GetRequiredService();
+ }
+
+ public void Save()
+ {
+ _storage.Save();
+ }
+
+ private string language = Constant.SystemLanguageCode;
private string _theme = Constant.DefaultTheme;
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
@@ -180,6 +200,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
};
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
///
/// when false Alphabet static service will always return empty results
@@ -198,8 +220,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
set
{
_querySearchPrecision = value;
- if (StringMatcher.Instance != null)
- StringMatcher.Instance.UserSettingSearchPrecision = value;
+ if (_stringMatcher != null)
+ _stringMatcher.UserSettingSearchPrecision = value;
}
}
@@ -238,6 +260,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool EnableUpdateLog { get; set; }
public bool StartFlowLauncherOnSystemStartup { get; set; } = false;
+ public bool UseLogonTaskForStartup { get; set; } = false;
public bool HideOnStartup { get; set; } = true;
bool _hideNotifyIcon { get; set; }
public bool HideNotifyIcon
diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs
new file mode 100644
index 000000000..867fef4f5
--- /dev/null
+++ b/Flow.Launcher.Infrastructure/Win32Helper.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows.Interop;
+using System.Windows;
+
+namespace Flow.Launcher.Infrastructure
+{
+ public static class Win32Helper
+ {
+ #region Blur Handling
+
+ /*
+ Found on https://github.com/riverar/sample-win10-aeroglass
+ */
+
+ private enum AccentState
+ {
+ ACCENT_DISABLED = 0,
+ ACCENT_ENABLE_GRADIENT = 1,
+ ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
+ ACCENT_ENABLE_BLURBEHIND = 3,
+ ACCENT_INVALID_STATE = 4
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct AccentPolicy
+ {
+ public AccentState AccentState;
+ public int AccentFlags;
+ public int GradientColor;
+ public int AnimationId;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct WindowCompositionAttributeData
+ {
+ public WindowCompositionAttribute Attribute;
+ public IntPtr Data;
+ public int SizeOfData;
+ }
+
+ private enum WindowCompositionAttribute
+ {
+ WCA_ACCENT_POLICY = 19
+ }
+
+ [DllImport("user32.dll")]
+ private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
+
+ ///
+ /// Checks if the blur theme is enabled
+ ///
+ public static bool IsBlurTheme()
+ {
+ if (Environment.OSVersion.Version >= new Version(6, 2))
+ {
+ var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
+
+ if (resource is bool b)
+ return b;
+
+ return false;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Sets the blur for a window via SetWindowCompositionAttribute
+ ///
+ public static void SetBlurForWindow(Window w, bool blur)
+ {
+ SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED);
+ }
+
+ private static void SetWindowAccent(Window w, AccentState state)
+ {
+ var windowHelper = new WindowInteropHelper(w);
+
+ windowHelper.EnsureHandle();
+
+ var accent = new AccentPolicy { AccentState = state };
+ var accentStructSize = Marshal.SizeOf(accent);
+
+ var accentPtr = Marshal.AllocHGlobal(accentStructSize);
+ Marshal.StructureToPtr(accent, accentPtr, false);
+
+ var data = new WindowCompositionAttributeData
+ {
+ Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
+ SizeOfData = accentStructSize,
+ Data = accentPtr
+ };
+
+ SetWindowCompositionAttribute(windowHelper.Handle, ref data);
+
+ Marshal.FreeHGlobal(accentPtr);
+ }
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
index 35b9af1c9..2feb21b12 100644
--- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
+++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
@@ -57,7 +57,11 @@
-
+
+
+
+
+
@@ -68,6 +72,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index a0186b7a2..4c7af4cd4 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -1,4 +1,4 @@
-using Flow.Launcher.Plugin.SharedModels;
+using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
@@ -17,7 +17,8 @@ namespace Flow.Launcher.Plugin
public interface IPublicAPI
{
///
- /// Change Flow.Launcher query
+ /// Change Flow.Launcher query.
+ /// When current results are from context menu or history, it will go back to query results before changing query.
///
/// query text
///
@@ -181,9 +182,13 @@ namespace Flow.Launcher.Plugin
///
/// URL to download file
/// path to save downloaded file
+ ///
+ /// Action to report progress. The input of the action is the progress value which is a double value between 0 and 100.
+ /// It will be called if url support range request and the reportProgress is not null.
+ ///
/// place to store file
/// Task showing the progress
- Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
+ Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default);
///
/// Add ActionKeyword for specific plugin
@@ -295,7 +300,7 @@ namespace Flow.Launcher.Plugin
///
/// Reloads the query.
- /// This method should run when selected item is from query results.
+ /// When current results are from context menu or history, it will go back to query results before requerying.
///
/// Choose the first result after reload if true; keep the last selected result if false. Default is true.
public void ReQuery(bool reselect = true);
@@ -316,5 +321,28 @@ namespace Flow.Launcher.Plugin
/// Specifies the default result of the message box.
/// Specifies which message box button is clicked by the user.
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK);
+
+ ///
+ /// Displays a standardised Flow progress box.
+ ///
+ /// The caption of the progress box.
+ ///
+ /// Time-consuming task function, whose input is the action to report progress.
+ /// The input of the action is the progress value which is a double value between 0 and 100.
+ /// If there are any exceptions, this action will be null.
+ ///
+ /// When user cancel the progress, this action will be called.
+ ///
+ public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null);
+
+ ///
+ /// Start the loading bar in main window
+ ///
+ public void StartLoadingBar();
+
+ ///
+ /// Stop the loading bar in main window
+ ///
+ public void StopLoadingBar();
}
}
diff --git a/Flow.Launcher.Plugin/NativeMethods.txt b/Flow.Launcher.Plugin/NativeMethods.txt
new file mode 100644
index 000000000..e3e2b705e
--- /dev/null
+++ b/Flow.Launcher.Plugin/NativeMethods.txt
@@ -0,0 +1,3 @@
+EnumThreadWindows
+GetWindowText
+GetWindowTextLength
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs
index b41675a1a..e182491c2 100644
--- a/Flow.Launcher.Plugin/Query.cs
+++ b/Flow.Launcher.Plugin/Query.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.Json.Serialization;
+using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin
{
@@ -9,15 +6,6 @@ namespace Flow.Launcher.Plugin
{
public Query() { }
- [Obsolete("Use the default Query constructor.")]
- public Query(string rawQuery, string search, string[] terms, string[] searchTerms, string actionKeyword = "")
- {
- Search = search;
- RawQuery = rawQuery;
- SearchTerms = searchTerms;
- ActionKeyword = actionKeyword;
- }
-
///
/// Raw query, this includes action keyword if it has
/// We didn't recommend use this property directly. You should always use Search property.
diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs
index f2050cc9f..9b16cc1cb 100644
--- a/Flow.Launcher.Plugin/Result.cs
+++ b/Flow.Launcher.Plugin/Result.cs
@@ -157,27 +157,6 @@ namespace Flow.Launcher.Plugin
}
}
- ///
- public override bool Equals(object obj)
- {
- var r = obj as Result;
-
- var equality = string.Equals(r?.Title, Title) &&
- string.Equals(r?.SubTitle, SubTitle) &&
- string.Equals(r?.AutoCompleteText, AutoCompleteText) &&
- string.Equals(r?.CopyText, CopyText) &&
- string.Equals(r?.IcoPath, IcoPath) &&
- TitleHighlightData == r.TitleHighlightData;
-
- return equality;
- }
-
- ///
- public override int GetHashCode()
- {
- return HashCode.Combine(Title, SubTitle, AutoCompleteText, CopyText, IcoPath);
- }
-
///
public override string ToString()
{
@@ -206,6 +185,16 @@ namespace Flow.Launcher.Plugin
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory,
+ ContextData = ContextData,
+ PluginID = PluginID,
+ TitleToolTip = TitleToolTip,
+ SubTitleToolTip = SubTitleToolTip,
+ PreviewPanel = PreviewPanel,
+ ProgressBar = ProgressBar,
+ ProgressBarColor = ProgressBarColor,
+ Preview = Preview,
+ AddSelectedCount = AddSelectedCount,
+ RecordKey = RecordKey
};
}
@@ -273,6 +262,14 @@ namespace Flow.Launcher.Plugin
///
public const int MaxScore = int.MaxValue;
+ ///
+ /// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records.
+ /// This can be useful when your plugin will change the Title or SubTitle of the result dynamically.
+ /// If the plugin does not specific this, FL just uses Title and SubTitle to identify this result.
+ /// Note: Because old data does not have this key, we should use null as the default value for consistency.
+ ///
+ public string RecordKey { get; set; } = null;
+
///
/// Info of the preview section of a
///
diff --git a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs
index 49f78b458..a0440e30d 100644
--- a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs
@@ -2,18 +2,15 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
using System.Threading;
+using Windows.Win32;
+using Windows.Win32.Foundation;
namespace Flow.Launcher.Plugin.SharedCommands
{
public static class ShellCommand
{
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
- [DllImport("user32.dll")] static extern bool EnumThreadWindows(uint threadId, EnumThreadDelegate lpfn, IntPtr lParam);
- [DllImport("user32.dll")] static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount);
- [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hwnd);
private static bool containsSecurityWindow;
@@ -28,6 +25,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
CheckSecurityWindow();
Thread.Sleep(25);
}
+
while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open
{
containsSecurityWindow = false;
@@ -42,24 +40,33 @@ namespace Flow.Launcher.Plugin.SharedCommands
{
ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads;
for (int i = 0; i < ptc.Count; i++)
- EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
+ PInvoke.EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
}
- private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam)
+ private static BOOL CheckSecurityThread(HWND hwnd, LPARAM lParam)
{
if (GetWindowTitle(hwnd) == "Windows Security")
containsSecurityWindow = true;
return true;
}
- private static string GetWindowTitle(IntPtr hwnd)
+ private static unsafe string GetWindowTitle(HWND hwnd)
{
- StringBuilder sb = new StringBuilder(GetWindowTextLength(hwnd) + 1);
- GetWindowText(hwnd, sb, sb.Capacity);
- return sb.ToString();
+ var capacity = PInvoke.GetWindowTextLength(hwnd) + 1;
+ int length;
+ Span buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity];
+ fixed (char* pBuffer = buffer)
+ {
+ // If the window has no title bar or text, if the title bar is empty,
+ // or if the window or control handle is invalid, the return value is zero.
+ length = PInvoke.GetWindowText(hwnd, pBuffer, capacity);
+ }
+
+ return buffer[..length].ToString();
}
- public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "", bool createNoWindow = false)
+ public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "",
+ string arguments = "", string verb = "", bool createNoWindow = false)
{
var info = new ProcessStartInfo
{
diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs
index 3dead9918..2621fc2da 100644
--- a/Flow.Launcher.Test/FilesFoldersTest.cs
+++ b/Flow.Launcher.Test/FilesFoldersTest.cs
@@ -1,5 +1,6 @@
using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
+using NUnit.Framework.Legacy;
namespace Flow.Launcher.Test
{
@@ -35,7 +36,7 @@ namespace Flow.Launcher.Test
[TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)]
public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
{
- Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path));
+ ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path));
}
// Equality
@@ -47,7 +48,7 @@ namespace Flow.Launcher.Test
[TestCase(@"c:\foo", @"c:\foo\", true)]
public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
{
- Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult));
+ ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult));
}
}
}
diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
index 8286e142e..0241a374e 100644
--- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj
+++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
@@ -49,7 +49,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs
index d7f143218..090719642 100644
--- a/Flow.Launcher.Test/FuzzyMatcherTest.cs
+++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
+using NUnit.Framework.Legacy;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@@ -21,6 +22,8 @@ namespace Flow.Launcher.Test
private const string MicrosoftSqlServerManagementStudio = "Microsoft SQL Server Management Studio";
private const string VisualStudioCode = "Visual Studio Code";
+ private readonly IAlphabet alphabet = null;
+
public List GetSearchStrings()
=> new List
{
@@ -34,7 +37,7 @@ namespace Flow.Launcher.Test
OneOneOneOne
};
- public List GetPrecisionScores()
+ public static List GetPrecisionScores()
{
var listToReturn = new List();
@@ -59,7 +62,7 @@ namespace Flow.Launcher.Test
};
var results = new List();
- var matcher = new StringMatcher();
+ var matcher = new StringMatcher(alphabet);
foreach (var str in sources)
{
results.Add(new Result
@@ -71,20 +74,20 @@ namespace Flow.Launcher.Test
results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList();
- Assert.IsTrue(results.Count == 3);
- Assert.IsTrue(results[0].Title == "Inste");
- Assert.IsTrue(results[1].Title == "Install Package");
- Assert.IsTrue(results[2].Title == "file open in browser-test");
+ ClassicAssert.IsTrue(results.Count == 3);
+ ClassicAssert.IsTrue(results[0].Title == "Inste");
+ ClassicAssert.IsTrue(results[1].Title == "Install Package");
+ ClassicAssert.IsTrue(results[2].Title == "file open in browser-test");
}
[TestCase("Chrome")]
public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString)
{
var compareString = "Can have rum only in my glass";
- var matcher = new StringMatcher();
+ var matcher = new StringMatcher(alphabet);
var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore;
- Assert.True(scoreResult == 0);
+ ClassicAssert.True(scoreResult == 0);
}
[TestCase("chr")]
@@ -97,7 +100,7 @@ namespace Flow.Launcher.Test
string searchTerm)
{
var results = new List();
- var matcher = new StringMatcher();
+ var matcher = new StringMatcher(alphabet);
foreach (var str in GetSearchStrings())
{
results.Add(new Result
@@ -125,7 +128,7 @@ namespace Flow.Launcher.Test
Debug.WriteLine("###############################################");
Debug.WriteLine("");
- Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore));
+ ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore));
}
}
@@ -147,11 +150,11 @@ namespace Flow.Launcher.Test
string queryString, string compareString, int expectedScore)
{
// When, Given
- var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
+ var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;
// Should
- Assert.AreEqual(expectedScore, rawScore,
+ ClassicAssert.AreEqual(expectedScore, rawScore,
$"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
}
@@ -181,7 +184,7 @@ namespace Flow.Launcher.Test
bool expectedPrecisionResult)
{
// When
- var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore};
+ var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore};
// Given
var matchResult = matcher.FuzzyMatch(queryString, compareString);
@@ -190,12 +193,12 @@ namespace Flow.Launcher.Test
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
Debug.WriteLine(
- $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
+ $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
Debug.WriteLine("###############################################");
Debug.WriteLine("");
// Should
- Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
+ ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
$"Query: {queryString}{Environment.NewLine} " +
$"Compare: {compareString}{Environment.NewLine}" +
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
@@ -232,7 +235,7 @@ namespace Flow.Launcher.Test
bool expectedPrecisionResult)
{
// When
- var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore};
+ var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore};
// Given
var matchResult = matcher.FuzzyMatch(queryString, compareString);
@@ -241,12 +244,12 @@ namespace Flow.Launcher.Test
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
Debug.WriteLine(
- $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
+ $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
Debug.WriteLine("###############################################");
Debug.WriteLine("");
// Should
- Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
+ ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
$"Query:{queryString}{Environment.NewLine} " +
$"Compare:{compareString}{Environment.NewLine}" +
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
@@ -260,7 +263,7 @@ namespace Flow.Launcher.Test
string queryString, string compareString1, string compareString2)
{
// When
- var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
+ var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
// Given
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
@@ -277,7 +280,7 @@ namespace Flow.Launcher.Test
Debug.WriteLine("");
// Should
- Assert.True(compareString1Result.Score > compareString2Result.Score,
+ ClassicAssert.True(compareString1Result.Score > compareString2Result.Score,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
@@ -293,7 +296,7 @@ namespace Flow.Launcher.Test
string queryString, string compareString1, string compareString2)
{
// When
- var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
+ var matcher = new StringMatcher(alphabet) { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
// Given
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
@@ -310,7 +313,7 @@ namespace Flow.Launcher.Test
Debug.WriteLine("");
// Should
- Assert.True(compareString1Result.Score > compareString2Result.Score,
+ ClassicAssert.True(compareString1Result.Score > compareString2Result.Score,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
@@ -323,7 +326,7 @@ namespace Flow.Launcher.Test
string secondName, string secondDescription, string secondExecutableName)
{
// Act
- var matcher = new StringMatcher();
+ var matcher = new StringMatcher(alphabet);
var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore;
var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore;
var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore;
@@ -336,7 +339,7 @@ namespace Flow.Launcher.Test
var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max();
// Assert
- Assert.IsTrue(firstScore > secondScore,
+ ClassicAssert.IsTrue(firstScore > secondScore,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
@@ -358,9 +361,9 @@ namespace Flow.Launcher.Test
public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString,
int desiredScore)
{
- var matcher = new StringMatcher();
+ var matcher = new StringMatcher(alphabet);
var score = matcher.FuzzyMatch(queryString, compareString).Score;
- Assert.IsTrue(score == desiredScore,
+ ClassicAssert.IsTrue(score == desiredScore,
$@"Query: ""{queryString}""
CompareString: ""{compareString}""
Score: {score}
diff --git a/Flow.Launcher.Test/HttpTest.cs b/Flow.Launcher.Test/HttpTest.cs
index e72ad7a67..4f135978a 100644
--- a/Flow.Launcher.Test/HttpTest.cs
+++ b/Flow.Launcher.Test/HttpTest.cs
@@ -1,4 +1,5 @@
-using NUnit.Framework;
+using NUnit.Framework;
+using NUnit.Framework.Legacy;
using System;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Infrastructure.Http;
@@ -16,16 +17,16 @@ namespace Flow.Launcher.Test
proxy.Enabled = true;
proxy.Server = "127.0.0.1";
- Assert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}"));
- Assert.IsNull(Http.WebProxy.Credentials);
+ ClassicAssert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}"));
+ ClassicAssert.IsNull(Http.WebProxy.Credentials);
proxy.UserName = "test";
- Assert.NotNull(Http.WebProxy.Credentials);
- Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName);
- Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, "");
+ ClassicAssert.NotNull(Http.WebProxy.Credentials);
+ ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName);
+ ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, "");
proxy.Password = "test password";
- Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password);
+ ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password);
}
}
}
diff --git a/Flow.Launcher.Test/PluginLoadTest.cs b/Flow.Launcher.Test/PluginLoadTest.cs
index d6ba48f19..2cc05f95a 100644
--- a/Flow.Launcher.Test/PluginLoadTest.cs
+++ b/Flow.Launcher.Test/PluginLoadTest.cs
@@ -1,4 +1,5 @@
using NUnit.Framework;
+using NUnit.Framework.Legacy;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
using System.Collections.Generic;
@@ -15,37 +16,37 @@ namespace Flow.Launcher.Test
// Given
var duplicateList = new List
{
- new PluginMetadata
+ new()
{
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
Version = "1.0.0"
},
- new PluginMetadata
+ new()
{
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
Version = "1.0.1"
},
- new PluginMetadata
+ new()
{
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
Version = "1.0.2"
},
- new PluginMetadata
+ new()
{
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
Version = "1.0.0"
},
- new PluginMetadata
+ new()
{
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
Version = "1.0.0"
},
- new PluginMetadata
+ new()
{
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
Version = "1.0.0"
},
- new PluginMetadata
+ new()
{
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
Version = "1.0.0"
@@ -56,11 +57,11 @@ namespace Flow.Launcher.Test
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
// Then
- Assert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2");
- Assert.True(unique.Count() == 1);
+ ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2");
+ ClassicAssert.True(unique.Count == 1);
- Assert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855"));
- Assert.True(duplicates.Count() == 6);
+ ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855"));
+ ClassicAssert.True(duplicates.Count == 6);
}
[Test]
@@ -69,12 +70,12 @@ namespace Flow.Launcher.Test
// Given
var duplicateList = new List
{
- new PluginMetadata
+ new()
{
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
Version = "1.0.0"
},
- new PluginMetadata
+ new()
{
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
Version = "1.0.0"
@@ -85,8 +86,8 @@ namespace Flow.Launcher.Test
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
// Then
- Assert.True(unique.Count() == 0);
- Assert.True(duplicates.Count() == 2);
+ ClassicAssert.True(unique.Count == 0);
+ ClassicAssert.True(duplicates.Count == 2);
}
}
}
diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
index 80cb74729..420da266d 100644
--- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -5,12 +5,10 @@ using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
+using NUnit.Framework.Legacy;
using System;
-using System.Collections.Generic;
using System.IO;
using System.Runtime.Versioning;
-using System.Threading;
-using System.Threading.Tasks;
using static Flow.Launcher.Plugin.Explorer.Search.SearchManager;
namespace Flow.Launcher.Test.Plugins
@@ -22,28 +20,6 @@ namespace Flow.Launcher.Test.Plugins
[TestFixture]
public class ExplorerTest
{
-#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature)
- private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken)
- {
- return new List();
- }
-#pragma warning restore CS1998
-
- private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token)
- {
- return new List
- {
- new Result
- {
- Title = "Result 1"
- },
- new Result
- {
- Title = "Result 2"
- }
- };
- }
-
private bool PreviousLocationExistsReturnsTrue(string dummyString) => true;
private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
@@ -57,7 +33,7 @@ namespace Flow.Launcher.Test.Plugins
var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
// Then
- Assert.IsTrue(result == expectedString,
+ ClassicAssert.IsTrue(result == expectedString,
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
$"Actual: {result}{Environment.NewLine}");
}
@@ -74,7 +50,7 @@ namespace Flow.Launcher.Test.Plugins
var queryString = queryConstructor.Directory(folderPath);
// Then
- Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
+ ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
}
@@ -94,7 +70,7 @@ namespace Flow.Launcher.Test.Plugins
var queryString = queryConstructor.Directory(folderPath, userSearchString);
// Then
- Assert.AreEqual(expectedString, queryString);
+ ClassicAssert.AreEqual(expectedString, queryString);
}
[SupportedOSPlatform("windows7.0")]
@@ -105,7 +81,7 @@ namespace Flow.Launcher.Test.Plugins
const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
// Then
- Assert.AreEqual(expectedString, resultString);
+ ClassicAssert.AreEqual(expectedString, resultString);
}
[SupportedOSPlatform("windows7.0")]
@@ -128,7 +104,7 @@ namespace Flow.Launcher.Test.Plugins
var resultString = queryConstructor.FilesAndFolders(userSearchString);
// Then
- Assert.AreEqual(expectedString, resultString);
+ ClassicAssert.AreEqual(expectedString, resultString);
}
@@ -138,13 +114,13 @@ namespace Flow.Launcher.Test.Plugins
string querySearchString, string expectedString)
{
// Given
- var queryConstructor = new QueryConstructor(new Settings());
+ _ = new QueryConstructor(new Settings());
//When
var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
// Then
- Assert.IsTrue(resultString == expectedString,
+ ClassicAssert.IsTrue(resultString == expectedString,
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
}
@@ -162,12 +138,12 @@ namespace Flow.Launcher.Test.Plugins
var resultString = queryConstructor.FileContent(userSearchString);
// Then
- Assert.IsTrue(resultString == expectedString,
+ ClassicAssert.IsTrue(resultString == expectedString,
$"Expected query string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
}
- public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
+ public static void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
{
// Given
var query = new Query
@@ -181,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
// Then
- Assert.IsTrue(result,
+ ClassicAssert.IsTrue(result,
$"Expected True for file content search. {Environment.NewLine} " +
$"Actual result was: {result}{Environment.NewLine}");
}
@@ -206,7 +182,7 @@ namespace Flow.Launcher.Test.Plugins
var result = FilesFolders.IsLocationPathString(querySearchString);
//Then
- Assert.IsTrue(result == expectedResult,
+ ClassicAssert.IsTrue(result == expectedResult,
$"Expected query search string check result is: {expectedResult} {Environment.NewLine} " +
$"Actual check result is {result} {Environment.NewLine}");
@@ -233,7 +209,7 @@ namespace Flow.Launcher.Test.Plugins
var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path);
//Then
- Assert.IsTrue(previousDirectoryPath == expectedString,
+ ClassicAssert.IsTrue(previousDirectoryPath == expectedString,
$"Expected path string: {expectedString} {Environment.NewLine} " +
$"Actual path string is {previousDirectoryPath} {Environment.NewLine}");
}
@@ -246,7 +222,7 @@ namespace Flow.Launcher.Test.Plugins
var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
//Then
- Assert.IsTrue(returnedPath == expectedString,
+ ClassicAssert.IsTrue(returnedPath == expectedString,
$"Expected path string: {expectedString} {Environment.NewLine} " +
$"Actual path string is {returnedPath} {Environment.NewLine}");
}
@@ -260,7 +236,7 @@ namespace Flow.Launcher.Test.Plugins
var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
// Then
- Assert.AreEqual(expectedString, resultString);
+ ClassicAssert.AreEqual(expectedString, resultString);
}
[SupportedOSPlatform("windows7.0")]
@@ -274,7 +250,7 @@ namespace Flow.Launcher.Test.Plugins
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
// Then
- Assert.AreEqual(expectedString, resultString);
+ ClassicAssert.AreEqual(expectedString, resultString);
}
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")]
@@ -305,7 +281,7 @@ namespace Flow.Launcher.Test.Plugins
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
// Then
- Assert.AreEqual(result, expectedResult);
+ ClassicAssert.AreEqual(result, expectedResult);
}
[TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")]
@@ -334,7 +310,7 @@ namespace Flow.Launcher.Test.Plugins
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
// Then
- Assert.AreEqual(result, expectedResult);
+ ClassicAssert.AreEqual(result, expectedResult);
}
[TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")]
@@ -366,7 +342,7 @@ namespace Flow.Launcher.Test.Plugins
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
// Then
- Assert.AreEqual(result, expectedResult);
+ ClassicAssert.AreEqual(result, expectedResult);
}
[TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")]
@@ -398,7 +374,7 @@ namespace Flow.Launcher.Test.Plugins
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
// Then
- Assert.AreEqual(result, expectedResult);
+ ClassicAssert.AreEqual(result, expectedResult);
}
[TestCase(@"c:\foo", @"c:\foo", true)]
@@ -420,7 +396,7 @@ namespace Flow.Launcher.Test.Plugins
};
// When, Then
- Assert.AreEqual(expectedResult, comparator.Equals(result1, result2));
+ ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2));
}
[TestCase(@"c:\foo\", @"c:\foo\")]
@@ -444,7 +420,7 @@ namespace Flow.Launcher.Test.Plugins
var hash2 = comparator.GetHashCode(result2);
// When, Then
- Assert.IsTrue(hash1 == hash2);
+ ClassicAssert.IsTrue(hash1 == hash2);
}
[TestCase(@"%appdata%", true)]
@@ -461,7 +437,7 @@ namespace Flow.Launcher.Test.Plugins
var result = EnvironmentVariables.HasEnvironmentVar(path);
// Then
- Assert.AreEqual(result, expectedResult);
+ ClassicAssert.AreEqual(result, expectedResult);
}
}
}
diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
index 3d05e5679..497f874e7 100644
--- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
+++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
@@ -1,12 +1,11 @@
-using NUnit.Framework;
+using NUnit.Framework;
+using NUnit.Framework.Legacy;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
using System.Threading.Tasks;
using System.IO;
using System.Threading;
using System.Text;
-using System.Text.Json;
-using System.Linq;
using System.Collections.Generic;
namespace Flow.Launcher.Test.Plugins
@@ -40,13 +39,13 @@ namespace Flow.Launcher.Test.Plugins
Search = resultText
}, default);
- Assert.IsNotNull(results);
+ ClassicAssert.IsNotNull(results);
foreach (var result in results)
{
- Assert.IsNotNull(result);
- Assert.IsNotNull(result.AsyncAction);
- Assert.IsNotNull(result.Title);
+ ClassicAssert.IsNotNull(result);
+ ClassicAssert.IsNotNull(result.AsyncAction);
+ ClassicAssert.IsNotNull(result.Title);
}
}
@@ -56,35 +55,11 @@ namespace Flow.Launcher.Test.Plugins
new JsonRPCQueryResponseModel(0, new List()),
new JsonRPCQueryResponseModel(0, new List
{
- new JsonRPCResult
+ new()
{
Title = "Test1", SubTitle = "Test2"
}
})
};
-
- [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
- public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
- {
- var camelText = JsonSerializer.Serialize(reference, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
-
- var pascalText = JsonSerializer.Serialize(reference);
-
- var results1 = await QueryAsync(new Query { Search = camelText }, default);
- var results2 = await QueryAsync(new Query { Search = pascalText }, default);
-
- Assert.IsNotNull(results1);
- Assert.IsNotNull(results2);
-
- foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result))
- {
- Assert.AreEqual(result1, result2);
- Assert.AreEqual(result1, referenceResult);
-
- Assert.IsNotNull(result1);
- Assert.IsNotNull(result1.AsyncAction);
- }
- }
-
}
}
diff --git a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs
index 7ccac5bd5..0dd1fe489 100644
--- a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs
+++ b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs
@@ -1,7 +1,8 @@
using NUnit.Framework;
+using NUnit.Framework.Legacy;
using Flow.Launcher.Plugin.Url;
-namespace Flow.Launcher.Test
+namespace Flow.Launcher.Test.Plugins
{
[TestFixture]
public class UrlPluginTest
@@ -10,23 +11,23 @@ namespace Flow.Launcher.Test
public void URLMatchTest()
{
var plugin = new Main();
- Assert.IsTrue(plugin.IsURL("http://www.google.com"));
- Assert.IsTrue(plugin.IsURL("https://www.google.com"));
- Assert.IsTrue(plugin.IsURL("http://google.com"));
- Assert.IsTrue(plugin.IsURL("www.google.com"));
- Assert.IsTrue(plugin.IsURL("google.com"));
- Assert.IsTrue(plugin.IsURL("http://localhost"));
- Assert.IsTrue(plugin.IsURL("https://localhost"));
- Assert.IsTrue(plugin.IsURL("http://localhost:80"));
- Assert.IsTrue(plugin.IsURL("https://localhost:80"));
- Assert.IsTrue(plugin.IsURL("http://110.10.10.10"));
- Assert.IsTrue(plugin.IsURL("110.10.10.10"));
- Assert.IsTrue(plugin.IsURL("ftp://110.10.10.10"));
+ ClassicAssert.IsTrue(plugin.IsURL("http://www.google.com"));
+ ClassicAssert.IsTrue(plugin.IsURL("https://www.google.com"));
+ ClassicAssert.IsTrue(plugin.IsURL("http://google.com"));
+ ClassicAssert.IsTrue(plugin.IsURL("www.google.com"));
+ ClassicAssert.IsTrue(plugin.IsURL("google.com"));
+ ClassicAssert.IsTrue(plugin.IsURL("http://localhost"));
+ ClassicAssert.IsTrue(plugin.IsURL("https://localhost"));
+ ClassicAssert.IsTrue(plugin.IsURL("http://localhost:80"));
+ ClassicAssert.IsTrue(plugin.IsURL("https://localhost:80"));
+ ClassicAssert.IsTrue(plugin.IsURL("http://110.10.10.10"));
+ ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10"));
+ ClassicAssert.IsTrue(plugin.IsURL("ftp://110.10.10.10"));
- Assert.IsFalse(plugin.IsURL("wwww"));
- Assert.IsFalse(plugin.IsURL("wwww.c"));
- Assert.IsFalse(plugin.IsURL("wwww.c"));
+ ClassicAssert.IsFalse(plugin.IsURL("wwww"));
+ ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
+ ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
}
}
}
diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs
index aa0c8da12..c8ac17748 100644
--- a/Flow.Launcher.Test/QueryBuilderTest.cs
+++ b/Flow.Launcher.Test/QueryBuilderTest.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using NUnit.Framework;
+using NUnit.Framework.Legacy;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
@@ -17,17 +18,17 @@ namespace Flow.Launcher.Test
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
- Assert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
- Assert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
- Assert.AreEqual(">", q.ActionKeyword);
+ ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
+ ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
+ ClassicAssert.AreEqual(">", q.ActionKeyword);
- Assert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match.");
+ ClassicAssert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match.");
- Assert.AreEqual("ping", q.FirstSearch);
- Assert.AreEqual("google.com", q.SecondSearch);
- Assert.AreEqual("-n", q.ThirdSearch);
+ ClassicAssert.AreEqual("ping", q.FirstSearch);
+ ClassicAssert.AreEqual("google.com", q.SecondSearch);
+ ClassicAssert.AreEqual("-n", q.ThirdSearch);
- Assert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
+ ClassicAssert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
}
[Test]
@@ -40,11 +41,11 @@ namespace Flow.Launcher.Test
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
- Assert.AreEqual("> ping google.com -n 20 -6", q.Search);
- Assert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
- Assert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
- Assert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
- Assert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
+ ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search);
+ ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
+ ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
+ ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
+ ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
}
[Test]
@@ -52,13 +53,13 @@ namespace Flow.Launcher.Test
{
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary());
- Assert.AreEqual("file.txt file2 file3", q.Search);
- Assert.AreEqual("", q.ActionKeyword);
+ ClassicAssert.AreEqual("file.txt file2 file3", q.Search);
+ ClassicAssert.AreEqual("", q.ActionKeyword);
- Assert.AreEqual("file.txt", q.FirstSearch);
- Assert.AreEqual("file2", q.SecondSearch);
- Assert.AreEqual("file3", q.ThirdSearch);
- Assert.AreEqual("file2 file3", q.SecondToEndSearch);
+ ClassicAssert.AreEqual("file.txt", q.FirstSearch);
+ ClassicAssert.AreEqual("file2", q.SecondSearch);
+ ClassicAssert.AreEqual("file3", q.ThirdSearch);
+ ClassicAssert.AreEqual("file2 file3", q.SecondToEndSearch);
}
}
}
diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs
index ba47a4ded..c3966e618 100644
--- a/Flow.Launcher/ActionKeywords.xaml.cs
+++ b/Flow.Launcher/ActionKeywords.xaml.cs
@@ -44,7 +44,7 @@ namespace Flow.Launcher
else
{
string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned");
- MessageBoxEx.Show(msg);
+ App.API.ShowMsgBox(msg);
}
}
}
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index 4d1adc6cd..ab68cf426 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -4,6 +4,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
+using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core;
using Flow.Launcher.Core.Configuration;
using Flow.Launcher.Core.ExternalPlugins.Environments;
@@ -14,24 +15,85 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher
{
public partial class App : IDisposable, ISingleInstanceApp
{
- public static PublicAPIInstance API { get; private set; }
+ public static IPublicAPI API { get; private set; }
private const string Unique = "Flow.Launcher_Unique_Application_Mutex";
private static bool _disposed;
- private Settings _settings;
- private MainViewModel _mainVM;
- private SettingWindowViewModel _settingsVM;
- private readonly Updater _updater = new Updater(Flow.Launcher.Properties.Settings.Default.GithubRepo);
- private readonly Portable _portable = new Portable();
- private readonly PinyinAlphabet _alphabet = new PinyinAlphabet();
- private StringMatcher _stringMatcher;
+ private readonly Settings _settings;
+
+ public App()
+ {
+ // Initialize settings
+ try
+ {
+ var storage = new FlowLauncherJsonStorage();
+ _settings = storage.Load();
+ _settings.SetStorage(storage);
+ _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
+ }
+ catch (Exception e)
+ {
+ ShowErrorMsgBoxAndFailFast("Cannot load setting storage, please check local data directory", e);
+ return;
+ }
+
+ // Configure the dependency injection container
+ try
+ {
+ var host = Host.CreateDefaultBuilder()
+ .UseContentRoot(AppContext.BaseDirectory)
+ .ConfigureServices(services => services
+ .AddSingleton(_ => _settings)
+ .AddSingleton(sp => new Updater(sp.GetRequiredService(), Launcher.Properties.Settings.Default.GithubRepo))
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ ).Build();
+ Ioc.Default.ConfigureServices(host.Services);
+ }
+ catch (Exception e)
+ {
+ ShowErrorMsgBoxAndFailFast("Cannot configure dependency injection container, please open new issue in Flow.Launcher", e);
+ return;
+ }
+
+ // Initialize the public API and Settings first
+ try
+ {
+ API = Ioc.Default.GetRequiredService();
+ _settings.Initialize();
+ }
+ catch (Exception e)
+ {
+ ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
+ return;
+ }
+ }
+
+ private static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
+ {
+ // Firstly show users the message
+ MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
+
+ // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
+ Environment.FailFast(message, e);
+ }
[STAThread]
public static void Main()
@@ -50,52 +112,42 @@ namespace Flow.Launcher
{
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
{
- _portable.PreStartCleanUpAfterPortabilityUpdate();
+ Log.SetLogLevel(_settings.LogLevel);
- Log.Info(
- "|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
+ Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate();
+
+ Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
+
RegisterAppDomainExceptions();
RegisterDispatcherUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync();
- _settingsVM = new SettingWindowViewModel(_updater, _portable);
- _settings = _settingsVM.Settings;
- _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
-
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
- _alphabet.Initialize(_settings);
- _stringMatcher = new StringMatcher(_alphabet);
- StringMatcher.Instance = _stringMatcher;
- _stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision;
-
- InternationalizationManager.Instance.Settings = _settings;
+ // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
PluginManager.LoadPlugins(_settings.PluginSettings);
- _mainVM = new MainViewModel(_settings);
- API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
-
- Http.API = API;
Http.Proxy = _settings.Proxy;
- await PluginManager.InitializePluginsAsync(API);
+ await PluginManager.InitializePluginsAsync();
await imageLoadertask;
- var window = new MainWindow(_settings, _mainVM);
+ var mainVM = Ioc.Default.GetRequiredService();
+ var window = new MainWindow(_settings, mainVM);
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
Current.MainWindow = window;
Current.MainWindow.Title = Constant.FlowLauncher;
- HotKeyMapper.Initialize(_mainVM);
+ HotKeyMapper.Initialize();
// main windows needs initialized before theme change because of blur settings
- ThemeManager.Instance.Settings = _settings;
+ // TODO: Clean ThemeManager.Instance in future
ThemeManager.Instance.ChangeTheme(_settings.Theme);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@@ -119,7 +171,14 @@ namespace Flow.Launcher
{
try
{
- Helper.AutoStartup.Enable();
+ if (_settings.UseLogonTaskForStartup)
+ {
+ Helper.AutoStartup.EnableViaLogonTask();
+ }
+ else
+ {
+ Helper.AutoStartup.EnableViaRegistry();
+ }
}
catch (Exception e)
{
@@ -141,11 +200,11 @@ namespace Flow.Launcher
{
// check update every 5 hours
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
- await _updater.UpdateAppAsync(API);
+ await Ioc.Default.GetRequiredService().UpdateAppAsync();
while (await timer.WaitForNextTickAsync())
// check updates on startup
- await _updater.UpdateAppAsync(API);
+ await Ioc.Default.GetRequiredService().UpdateAppAsync();
}
});
}
@@ -188,7 +247,7 @@ namespace Flow.Launcher
public void OnSecondAppStarted()
{
- _mainVM.Show();
+ Ioc.Default.GetRequiredService().Show();
}
}
}
diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml
index 068afda15..70ebb404b 100644
--- a/Flow.Launcher/CustomQueryHotkeySetting.xaml
+++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml
@@ -32,14 +32,11 @@
-
-
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
+
+
+
diff --git a/Flow.Launcher/Helper/AutoStartup.cs b/Flow.Launcher/Helper/AutoStartup.cs
index 4bff30caf..c5e20504b 100644
--- a/Flow.Launcher/Helper/AutoStartup.cs
+++ b/Flow.Launcher/Helper/AutoStartup.cs
@@ -1,18 +1,31 @@
using System;
+using System.IO;
+using System.Linq;
+using System.Security.Principal;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.Win32;
+using Microsoft.Win32.TaskScheduler;
namespace Flow.Launcher.Helper;
public class AutoStartup
{
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
+ private const string LogonTaskName = $"{Constant.FlowLauncher} Startup";
+ private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup";
public static bool IsEnabled
{
get
{
+ // Check if logon task is enabled
+ if (CheckLogonTask())
+ {
+ return true;
+ }
+
+ // Check if registry is enabled
try
{
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
@@ -28,12 +41,74 @@ public class AutoStartup
}
}
- public static void Disable()
+ private static bool CheckLogonTask()
+ {
+ using var taskService = new TaskService();
+ var task = taskService.RootFolder.AllTasks.FirstOrDefault(t => t.Name == LogonTaskName);
+ if (task != null)
+ {
+ try
+ {
+ // Check if the action is the same as the current executable path
+ var action = task.Definition.Actions.FirstOrDefault()!.ToString().Trim();
+ if (!Constant.ExecutablePath.Equals(action, StringComparison.OrdinalIgnoreCase) && !File.Exists(action))
+ {
+ UnscheduleLogonTask();
+ ScheduleLogonTask();
+ }
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ Log.Error("AutoStartup", $"Failed to check logon task: {e}");
+ }
+ }
+
+ return false;
+ }
+
+ public static void DisableViaLogonTaskAndRegistry()
+ {
+ Disable(true);
+ Disable(false);
+ }
+
+ public static void EnableViaLogonTask()
+ {
+ Enable(true);
+ }
+
+ public static void EnableViaRegistry()
+ {
+ Enable(false);
+ }
+
+ public static void ChangeToViaLogonTask()
+ {
+ Disable(false);
+ Enable(true);
+ }
+
+ public static void ChangeToViaRegistry()
+ {
+ Disable(true);
+ Enable(false);
+ }
+
+ private static void Disable(bool logonTask)
{
try
{
- using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
- key?.DeleteValue(Constant.FlowLauncher, false);
+ if (logonTask)
+ {
+ UnscheduleLogonTask();
+ }
+ else
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
+ key?.DeleteValue(Constant.FlowLauncher, false);
+ }
}
catch (Exception e)
{
@@ -42,12 +117,19 @@ public class AutoStartup
}
}
- internal static void Enable()
+ private static void Enable(bool logonTask)
{
try
{
- using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
- key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
+ if (logonTask)
+ {
+ ScheduleLogonTask();
+ }
+ else
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
+ key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
+ }
}
catch (Exception e)
{
@@ -55,4 +137,54 @@ public class AutoStartup
throw;
}
}
+
+ private static bool ScheduleLogonTask()
+ {
+ using var td = TaskService.Instance.NewTask();
+ td.RegistrationInfo.Description = LogonTaskDesc;
+ td.Triggers.Add(new LogonTrigger { UserId = WindowsIdentity.GetCurrent().Name, Delay = TimeSpan.FromSeconds(2) });
+ td.Actions.Add(Constant.ExecutablePath);
+
+ if (IsCurrentUserIsAdmin())
+ {
+ td.Principal.RunLevel = TaskRunLevel.Highest;
+ }
+
+ td.Settings.StopIfGoingOnBatteries = false;
+ td.Settings.DisallowStartIfOnBatteries = false;
+ td.Settings.ExecutionTimeLimit = TimeSpan.Zero;
+
+ try
+ {
+ TaskService.Instance.RootFolder.RegisterTaskDefinition(LogonTaskName, td);
+ return true;
+ }
+ catch (Exception e)
+ {
+ Log.Error("AutoStartup", $"Failed to schedule logon task: {e}");
+ return false;
+ }
+ }
+
+ private static bool UnscheduleLogonTask()
+ {
+ using var taskService = new TaskService();
+ try
+ {
+ taskService.RootFolder.DeleteTask(LogonTaskName);
+ return true;
+ }
+ catch (Exception e)
+ {
+ Log.Error("AutoStartup", $"Failed to unschedule logon task: {e}");
+ return false;
+ }
+ }
+
+ private static bool IsCurrentUserIsAdmin()
+ {
+ var identity = WindowsIdentity.GetCurrent();
+ var principal = new WindowsPrincipal(identity);
+ return principal.IsInRole(WindowsBuiltInRole.Administrator);
+ }
}
diff --git a/Flow.Launcher/Helper/DWMDropShadow.cs b/Flow.Launcher/Helper/DWMDropShadow.cs
index e448acd4c..58817d70e 100644
--- a/Flow.Launcher/Helper/DWMDropShadow.cs
+++ b/Flow.Launcher/Helper/DWMDropShadow.cs
@@ -1,20 +1,16 @@
using System;
-using System.Drawing.Printing;
-using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Graphics.Dwm;
+using Windows.Win32.UI.Controls;
namespace Flow.Launcher.Helper;
public class DwmDropShadow
{
- [DllImport("dwmapi.dll", PreserveSig = true)]
- private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
-
- [DllImport("dwmapi.dll")]
- private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset);
-
///
/// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven).
/// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
@@ -43,24 +39,22 @@ public class DwmDropShadow
///
/// Window to which the shadow will be applied
/// True if the method succeeded, false if not
- private static bool DropShadow(Window window)
+ private static unsafe bool DropShadow(Window window)
{
try
{
WindowInteropHelper helper = new WindowInteropHelper(window);
int val = 2;
- int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4);
+ var ret1 = PInvoke.DwmSetWindowAttribute(new (helper.Handle), DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY, &val, 4);
- if (ret1 == 0)
+ if (ret1 == HRESULT.S_OK)
{
- Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 };
- int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m);
- return ret2 == 0;
- }
- else
- {
- return false;
+ var m = new MARGINS { cyBottomHeight = 0, cxLeftWidth = 0, cxRightWidth = 0, cyTopHeight = 0 };
+ var ret2 = PInvoke.DwmExtendFrameIntoClientArea(new(helper.Handle), &m);
+ return ret2 == HRESULT.S_OK;
}
+
+ return false;
}
catch (Exception)
{
diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs
index 8b30b8be1..7b2fdfcf4 100644
--- a/Flow.Launcher/Helper/HotKeyMapper.cs
+++ b/Flow.Launcher/Helper/HotKeyMapper.cs
@@ -5,7 +5,9 @@ using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel;
-using Flow.Launcher.Core;
+using ChefKeys;
+using Flow.Launcher.Infrastructure.Logger;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Helper;
@@ -14,10 +16,10 @@ internal static class HotKeyMapper
private static Settings _settings;
private static MainViewModel _mainViewModel;
- internal static void Initialize(MainViewModel mainVM)
+ internal static void Initialize()
{
- _mainViewModel = mainVM;
- _settings = _mainViewModel.Settings;
+ _mainViewModel = Ioc.Default.GetRequiredService();
+ _settings = Ioc.Default.GetService();
SetHotkey(_settings.Hotkey, OnToggleHotkey);
LoadCustomPluginHotkey();
@@ -29,33 +31,92 @@ internal static class HotKeyMapper
_mainViewModel.ToggleFlowLauncher();
}
+ internal static void OnToggleHotkeyWithChefKeys()
+ {
+ if (!_mainViewModel.ShouldIgnoreHotkeys())
+ _mainViewModel.ToggleFlowLauncher();
+ }
+
private static void SetHotkey(string hotkeyStr, EventHandler action)
{
var hotkey = new HotkeyModel(hotkeyStr);
SetHotkey(hotkey, action);
}
+ private static void SetWithChefKeys(string hotkeyStr)
+ {
+ try
+ {
+ ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, OnToggleHotkeyWithChefKeys);
+ ChefKeysManager.Start();
+ }
+ catch (Exception e)
+ {
+ Log.Error(
+ string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
+ e.Message,
+ e.StackTrace));
+ string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
+ string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
+ MessageBoxEx.Show(errorMsg, errorMsgTitle);
+ }
+ }
+
internal static void SetHotkey(HotkeyModel hotkey, EventHandler action)
{
string hotkeyStr = hotkey.ToString();
try
{
+ if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
+ {
+ SetWithChefKeys(hotkeyStr);
+ return;
+ }
+
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
}
- catch (Exception)
+ catch (Exception e)
{
+ Log.Error(
+ string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}",
+ e.Message,
+ e.StackTrace,
+ hotkeyStr));
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
- MessageBoxEx.Show(errorMsg, errorMsgTitle);
+ App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
internal static void RemoveHotkey(string hotkeyStr)
{
- if (!string.IsNullOrEmpty(hotkeyStr))
+ try
{
- HotkeyManager.Current.Remove(hotkeyStr);
+ if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
+ {
+ RemoveWithChefKeys(hotkeyStr);
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(hotkeyStr))
+ HotkeyManager.Current.Remove(hotkeyStr);
}
+ catch (Exception e)
+ {
+ Log.Error(
+ string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
+ e.Message,
+ e.StackTrace));
+ string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("unregisterHotkeyFailed"), hotkeyStr);
+ string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
+ MessageBoxEx.Show(errorMsg, errorMsgTitle);
+ }
+ }
+
+ private static void RemoveWithChefKeys(string hotkeyStr)
+ {
+ ChefKeysManager.UnregisterHotkey(hotkeyStr);
+ ChefKeysManager.Stop();
}
internal static void LoadCustomPluginHotkey()
diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs
index 739fed378..e0e3075f6 100644
--- a/Flow.Launcher/Helper/SingleInstance.cs
+++ b/Flow.Launcher/Helper/SingleInstance.cs
@@ -1,11 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Runtime.InteropServices;
using System.IO.Pipes;
-using System.Security;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -14,172 +8,6 @@ using System.Windows;
// modified to allow single instace restart
namespace Flow.Launcher.Helper
{
- internal enum WM
- {
- NULL = 0x0000,
- CREATE = 0x0001,
- DESTROY = 0x0002,
- MOVE = 0x0003,
- SIZE = 0x0005,
- ACTIVATE = 0x0006,
- SETFOCUS = 0x0007,
- KILLFOCUS = 0x0008,
- ENABLE = 0x000A,
- SETREDRAW = 0x000B,
- SETTEXT = 0x000C,
- GETTEXT = 0x000D,
- GETTEXTLENGTH = 0x000E,
- PAINT = 0x000F,
- CLOSE = 0x0010,
- QUERYENDSESSION = 0x0011,
- QUIT = 0x0012,
- QUERYOPEN = 0x0013,
- ERASEBKGND = 0x0014,
- SYSCOLORCHANGE = 0x0015,
- SHOWWINDOW = 0x0018,
- ACTIVATEAPP = 0x001C,
- SETCURSOR = 0x0020,
- MOUSEACTIVATE = 0x0021,
- CHILDACTIVATE = 0x0022,
- QUEUESYNC = 0x0023,
- GETMINMAXINFO = 0x0024,
-
- WINDOWPOSCHANGING = 0x0046,
- WINDOWPOSCHANGED = 0x0047,
-
- CONTEXTMENU = 0x007B,
- STYLECHANGING = 0x007C,
- STYLECHANGED = 0x007D,
- DISPLAYCHANGE = 0x007E,
- GETICON = 0x007F,
- SETICON = 0x0080,
- NCCREATE = 0x0081,
- NCDESTROY = 0x0082,
- NCCALCSIZE = 0x0083,
- NCHITTEST = 0x0084,
- NCPAINT = 0x0085,
- NCACTIVATE = 0x0086,
- GETDLGCODE = 0x0087,
- SYNCPAINT = 0x0088,
- NCMOUSEMOVE = 0x00A0,
- NCLBUTTONDOWN = 0x00A1,
- NCLBUTTONUP = 0x00A2,
- NCLBUTTONDBLCLK = 0x00A3,
- NCRBUTTONDOWN = 0x00A4,
- NCRBUTTONUP = 0x00A5,
- NCRBUTTONDBLCLK = 0x00A6,
- NCMBUTTONDOWN = 0x00A7,
- NCMBUTTONUP = 0x00A8,
- NCMBUTTONDBLCLK = 0x00A9,
-
- SYSKEYDOWN = 0x0104,
- SYSKEYUP = 0x0105,
- SYSCHAR = 0x0106,
- SYSDEADCHAR = 0x0107,
- COMMAND = 0x0111,
- SYSCOMMAND = 0x0112,
-
- MOUSEMOVE = 0x0200,
- LBUTTONDOWN = 0x0201,
- LBUTTONUP = 0x0202,
- LBUTTONDBLCLK = 0x0203,
- RBUTTONDOWN = 0x0204,
- RBUTTONUP = 0x0205,
- RBUTTONDBLCLK = 0x0206,
- MBUTTONDOWN = 0x0207,
- MBUTTONUP = 0x0208,
- MBUTTONDBLCLK = 0x0209,
- MOUSEWHEEL = 0x020A,
- XBUTTONDOWN = 0x020B,
- XBUTTONUP = 0x020C,
- XBUTTONDBLCLK = 0x020D,
- MOUSEHWHEEL = 0x020E,
-
-
- CAPTURECHANGED = 0x0215,
-
- ENTERSIZEMOVE = 0x0231,
- EXITSIZEMOVE = 0x0232,
-
- IME_SETCONTEXT = 0x0281,
- IME_NOTIFY = 0x0282,
- IME_CONTROL = 0x0283,
- IME_COMPOSITIONFULL = 0x0284,
- IME_SELECT = 0x0285,
- IME_CHAR = 0x0286,
- IME_REQUEST = 0x0288,
- IME_KEYDOWN = 0x0290,
- IME_KEYUP = 0x0291,
-
- NCMOUSELEAVE = 0x02A2,
-
- DWMCOMPOSITIONCHANGED = 0x031E,
- DWMNCRENDERINGCHANGED = 0x031F,
- DWMCOLORIZATIONCOLORCHANGED = 0x0320,
- DWMWINDOWMAXIMIZEDCHANGE = 0x0321,
-
- #region Windows 7
- DWMSENDICONICTHUMBNAIL = 0x0323,
- DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326,
- #endregion
-
- USER = 0x0400,
-
- // This is the hard-coded message value used by WinForms for Shell_NotifyIcon.
- // It's relatively safe to reuse.
- TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024
- APP = 0x8000
- }
-
- [SuppressUnmanagedCodeSecurity]
- internal static class NativeMethods
- {
- ///
- /// Delegate declaration that matches WndProc signatures.
- ///
- public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled);
-
- [DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
- private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
-
-
- [DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)]
- private static extern IntPtr _LocalFree(IntPtr hMem);
-
-
- public static string[] CommandLineToArgvW(string cmdLine)
- {
- IntPtr argv = IntPtr.Zero;
- try
- {
- int numArgs = 0;
-
- argv = _CommandLineToArgvW(cmdLine, out numArgs);
- if (argv == IntPtr.Zero)
- {
- throw new Win32Exception();
- }
- var result = new string[numArgs];
-
- for (int i = 0; i < numArgs; i++)
- {
- IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
- result[i] = Marshal.PtrToStringUni(currArg);
- }
-
- return result;
- }
- finally
- {
-
- IntPtr p = _LocalFree(argv);
- // Otherwise LocalFree failed.
- // Assert.AreEqual(IntPtr.Zero, p);
- }
- }
-
- }
-
public interface ISingleInstanceApp
{
void OnSecondAppStarted();
@@ -219,10 +47,6 @@ namespace Flow.Launcher.Helper
#endregion
- #region Public Properties
-
- #endregion
-
#region Public Methods
///
@@ -264,56 +88,6 @@ namespace Flow.Launcher.Helper
#region Private Methods
- ///
- /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved.
- ///
- /// List of command line arg strings.
- private static IList GetCommandLineArgs( string uniqueApplicationName )
- {
- string[] args = null;
-
- try
- {
- // The application was not clickonce deployed, get args from standard API's
- args = Environment.GetCommandLineArgs();
- }
- catch (NotSupportedException)
- {
-
- // The application was clickonce deployed
- // Clickonce deployed apps cannot recieve traditional commandline arguments
- // As a workaround commandline arguments can be written to a shared location before
- // the app is launched and the app can obtain its commandline arguments from the
- // shared location
- string appFolderPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName);
-
- string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt");
- if (File.Exists(cmdLinePath))
- {
- try
- {
- using (TextReader reader = new StreamReader(cmdLinePath, Encoding.Unicode))
- {
- args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd());
- }
-
- File.Delete(cmdLinePath);
- }
- catch (IOException)
- {
- }
- }
- }
-
- if (args == null)
- {
- args = new string[] { };
- }
-
- return new List(args);
- }
-
///
/// Creates a remote server pipe for communication.
/// Once receives signal from client, will activate first instance.
diff --git a/Flow.Launcher/Helper/SingletonWindowOpener.cs b/Flow.Launcher/Helper/SingletonWindowOpener.cs
index b5c2d8b55..5282b61f9 100644
--- a/Flow.Launcher/Helper/SingletonWindowOpener.cs
+++ b/Flow.Launcher/Helper/SingletonWindowOpener.cs
@@ -10,16 +10,29 @@ public static class SingletonWindowOpener
{
var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.GetType() == typeof(T))
?? (T)Activator.CreateInstance(typeof(T), args);
-
+
// Fix UI bug
// Add `window.WindowState = WindowState.Normal`
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
// Not sure why this works tho
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
// https://stackoverflow.com/a/59719760/4230390
- window.WindowState = WindowState.Normal;
- window.Show();
-
+ // Ensure the window is not minimized before showing it
+ if (window.WindowState == WindowState.Minimized)
+ {
+ window.WindowState = WindowState.Normal;
+ }
+
+ // Ensure the window is visible
+ if (!window.IsVisible)
+ {
+ window.Show();
+ }
+ else
+ {
+ window.Activate(); // Bring the window to the foreground if already open
+ }
+
window.Focus();
return (T)window;
diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs
index e08e227cc..a3bd83a97 100644
--- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs
+++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs
@@ -1,33 +1,95 @@
using System;
+using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
-using System.Text;
+using System.Windows;
using System.Windows.Media;
+using System.Windows.Media.Imaging;
using Microsoft.Win32;
+using Windows.Win32;
+using Windows.Win32.UI.WindowsAndMessaging;
namespace Flow.Launcher.Helper;
public static class WallpaperPathRetrieval
{
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern Int32 SystemParametersInfo(UInt32 action,
- Int32 uParam, StringBuilder vParam, UInt32 winIni);
- private static readonly UInt32 SPI_GETDESKWALLPAPER = 0x73;
- private static int MAX_PATH = 260;
+ private static readonly int MAX_PATH = 260;
+ private static readonly int MAX_CACHE_SIZE = 3;
- public static string GetWallpaperPath()
+ private static readonly Dictionary<(string, DateTime), ImageBrush> wallpaperCache = new();
+
+ public static Brush GetWallpaperBrush()
{
- var wallpaper = new StringBuilder(MAX_PATH);
- SystemParametersInfo(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaper, 0);
+ // Invoke the method on the UI thread
+ if (!Application.Current.Dispatcher.CheckAccess())
+ {
+ return Application.Current.Dispatcher.Invoke(GetWallpaperBrush);
+ }
- var str = wallpaper.ToString();
- if (string.IsNullOrEmpty(str))
- return null;
+ try
+ {
+ var wallpaperPath = GetWallpaperPath();
+ if (wallpaperPath is not null && File.Exists(wallpaperPath))
+ {
+ // Since the wallpaper file name can be the same (TranscodedWallpaper),
+ // we need to add the last modified date to differentiate them
+ var dateModified = File.GetLastWriteTime(wallpaperPath);
+ wallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper);
+ if (cachedWallpaper != null)
+ {
+ return cachedWallpaper;
+ }
- return str;
+ // We should not dispose the memory stream since the bitmap is still in use
+ var memStream = new MemoryStream(File.ReadAllBytes(wallpaperPath));
+ var bitmap = new BitmapImage();
+ bitmap.BeginInit();
+ bitmap.StreamSource = memStream;
+ bitmap.DecodePixelWidth = 800;
+ bitmap.DecodePixelHeight = 600;
+ bitmap.EndInit();
+ bitmap.Freeze(); // Make the bitmap thread-safe
+ var wallpaperBrush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill };
+ wallpaperBrush.Freeze(); // Make the brush thread-safe
+
+ // Manage cache size
+ if (wallpaperCache.Count >= MAX_CACHE_SIZE)
+ {
+ // Remove the oldest wallpaper from the cache
+ var oldestCache = wallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault();
+ if (oldestCache != default)
+ {
+ wallpaperCache.Remove(oldestCache);
+ }
+ }
+
+ wallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush);
+ return wallpaperBrush;
+ }
+
+ var wallpaperColor = GetWallpaperColor();
+ return new SolidColorBrush(wallpaperColor);
+ }
+ catch (Exception ex)
+ {
+ App.API.LogException(nameof(WallpaperPathRetrieval), "Error retrieving wallpaper", ex);
+ return new SolidColorBrush(Colors.Transparent);
+ }
}
- public static Color GetWallpaperColor()
+ private static unsafe string GetWallpaperPath()
+ {
+ var wallpaperPtr = stackalloc char[MAX_PATH];
+ PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, (uint)MAX_PATH,
+ wallpaperPtr,
+ 0);
+ var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr);
+
+ return wallpaper.ToString();
+ }
+
+ private static Color GetWallpaperColor()
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true);
var result = key?.GetValue("Background", null);
@@ -35,13 +97,14 @@ public static class WallpaperPathRetrieval
{
try
{
- var parts = strResult.Trim().Split(new[] {' '}, 3).Select(byte.Parse).ToList();
+ var parts = strResult.Trim().Split(new[] { ' ' }, 3).Select(byte.Parse).ToList();
return Color.FromRgb(parts[0], parts[1], parts[2]);
}
catch
{
}
}
+
return Colors.Transparent;
}
}
diff --git a/Flow.Launcher/Helper/WindowsInteropHelper.cs b/Flow.Launcher/Helper/WindowsInteropHelper.cs
index 89fbec967..3e57948a5 100644
--- a/Flow.Launcher/Helper/WindowsInteropHelper.cs
+++ b/Flow.Launcher/Helper/WindowsInteropHelper.cs
@@ -1,75 +1,52 @@
using System;
+using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
-using System.Text;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows.Media;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.WindowsAndMessaging;
using Point = System.Windows.Point;
namespace Flow.Launcher.Helper;
public class WindowsInteropHelper
{
- private const int GWL_STYLE = -16; //WPF's Message code for Title Bar's Style
- private const int WS_SYSMENU = 0x80000; //WPF's Message code for System Menu
- private static IntPtr _hwnd_shell;
- private static IntPtr _hwnd_desktop;
+ private static HWND _hwnd_shell;
+ private static HWND _hwnd_desktop;
//Accessors for shell and desktop handlers
//Will set the variables once and then will return them
- private static IntPtr HWND_SHELL
+ private static HWND HWND_SHELL
{
get
{
- return _hwnd_shell != IntPtr.Zero ? _hwnd_shell : _hwnd_shell = GetShellWindow();
+ return _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow();
}
}
- private static IntPtr HWND_DESKTOP
+
+ private static HWND HWND_DESKTOP
{
get
{
- return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow();
+ return _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow();
}
}
- [DllImport("user32.dll", SetLastError = true)]
- internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
-
- [DllImport("user32.dll")]
- internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
-
- [DllImport("user32.dll")]
- internal static extern IntPtr GetForegroundWindow();
-
- [DllImport("user32.dll")]
- internal static extern IntPtr GetDesktopWindow();
-
- [DllImport("user32.dll")]
- internal static extern IntPtr GetShellWindow();
-
- [DllImport("user32.dll", SetLastError = true)]
- internal static extern int GetWindowRect(IntPtr hwnd, out RECT rc);
-
- [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
-
- [DllImport("user32.DLL")]
- public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
-
-
const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
const string WINDOW_CLASS_WINTAB = "Flip3D";
const string WINDOW_CLASS_PROGMAN = "Progman";
const string WINDOW_CLASS_WORKERW = "WorkerW";
- public static bool IsWindowFullscreen()
+ public unsafe static bool IsWindowFullscreen()
{
//get current active window
- IntPtr hWnd = GetForegroundWindow();
+ var hWnd = PInvoke.GetForegroundWindow();
- if (hWnd.Equals(IntPtr.Zero))
+ if (hWnd.Equals(HWND.Null))
{
return false;
}
@@ -80,9 +57,17 @@ public class WindowsInteropHelper
return false;
}
- StringBuilder sb = new StringBuilder(256);
- GetClassName(hWnd, sb, sb.Capacity);
- string windowClass = sb.ToString();
+ string windowClass;
+ const int capacity = 256;
+ Span buffer = stackalloc char[capacity];
+ int validLength;
+ fixed (char* pBuffer = buffer)
+ {
+ validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity);
+ }
+
+ windowClass = buffer[..validLength].ToString();
+
//for Win+Tab (Flip3D)
if (windowClass == WINDOW_CLASS_WINTAB)
@@ -90,28 +75,28 @@ public class WindowsInteropHelper
return false;
}
- RECT appBounds;
- GetWindowRect(hWnd, out appBounds);
+ PInvoke.GetWindowRect(hWnd, out var appBounds);
//for console (ConsoleWindowClass), we have to check for negative dimensions
if (windowClass == WINDOW_CLASS_CONSOLE)
{
- return appBounds.Top < 0 && appBounds.Bottom < 0;
+ return appBounds.top < 0 && appBounds.bottom < 0;
}
//for desktop (Progman or WorkerW, depends on the system), we have to check
if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW)
{
- IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null);
- hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView");
- if (!hWndDesktop.Equals(IntPtr.Zero))
+ var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
+ hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
+ if (hWndDesktop.Value != (IntPtr.Zero))
{
return false;
}
}
Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds;
- return (appBounds.Bottom - appBounds.Top) == screenBounds.Height && (appBounds.Right - appBounds.Left) == screenBounds.Width;
+ return (appBounds.bottom - appBounds.top) == screenBounds.Height &&
+ (appBounds.right - appBounds.left) == screenBounds.Width;
}
///
@@ -120,8 +105,24 @@ public class WindowsInteropHelper
///
public static void DisableControlBox(Window win)
{
- var hwnd = new WindowInteropHelper(win).Handle;
- SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
+ var hwnd = new HWND(new WindowInteropHelper(win).Handle);
+
+ var style = PInvoke.GetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
+
+ if (style == 0)
+ {
+ throw new Win32Exception(Marshal.GetLastPInvokeError());
+ }
+
+ style &= ~(int)WINDOW_STYLE.WS_SYSMENU;
+
+ var previousStyle = PInvoke.SetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE,
+ style);
+
+ if (previousStyle == 0)
+ {
+ throw new Win32Exception(Marshal.GetLastPInvokeError());
+ }
}
///
@@ -144,16 +145,67 @@ public class WindowsInteropHelper
using var src = new HwndSource(new HwndSourceParameters());
matrix = src.CompositionTarget.TransformFromDevice;
}
+
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
}
+ #region Alt Tab
- [StructLayout(LayoutKind.Sequential)]
- public struct RECT
+ private static int SetWindowLong(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong)
{
- public int Left;
- public int Top;
- public int Right;
- public int Bottom;
+ PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error
+
+ var result = PInvoke.SetWindowLong(hWnd, nIndex, dwNewLong);
+ if (result == 0 && Marshal.GetLastPInvokeError() != 0)
+ {
+ throw new Win32Exception(Marshal.GetLastPInvokeError());
+ }
+
+ return result;
}
+
+ ///
+ /// Hide windows in the Alt+Tab window list
+ ///
+ /// To hide a window
+ public static void HideFromAltTab(Window window)
+ {
+ var exStyle = GetCurrentWindowStyle(window);
+
+ // Add TOOLWINDOW style, remove APPWINDOW style
+ var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
+
+ SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
+ }
+
+ ///
+ /// Restore window display in the Alt+Tab window list.
+ ///
+ /// To restore the displayed window
+ public static void ShowInAltTab(Window window)
+ {
+ var exStyle = GetCurrentWindowStyle(window);
+
+ // Remove the TOOLWINDOW style and add the APPWINDOW style.
+ var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
+
+ SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
+ }
+
+ ///
+ /// To obtain the current overridden style of a window.
+ ///
+ /// To obtain the style dialog window
+ /// current extension style value
+ private static int GetCurrentWindowStyle(Window window)
+ {
+ var style = PInvoke.GetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
+ if (style == 0 && Marshal.GetLastPInvokeError() != 0)
+ {
+ throw new Win32Exception(Marshal.GetLastPInvokeError());
+ }
+ return style;
+ }
+
+ #endregion
}
diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs
index a42bde7c9..e1dfc1108 100644
--- a/Flow.Launcher/HotkeyControl.xaml.cs
+++ b/Flow.Launcher/HotkeyControl.xaml.cs
@@ -154,7 +154,16 @@ namespace Flow.Launcher
{
if (triggerValidate)
{
- bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture);
+ bool hotkeyAvailable = false;
+ // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157
+ if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin")
+ {
+ hotkeyAvailable = true;
+ }
+ else
+ {
+ hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture);
+ }
if (!hotkeyAvailable)
{
diff --git a/Flow.Launcher/HotkeyControlDialog.xaml.cs b/Flow.Launcher/HotkeyControlDialog.xaml.cs
index a7b99f670..a4d21a782 100644
--- a/Flow.Launcher/HotkeyControlDialog.xaml.cs
+++ b/Flow.Launcher/HotkeyControlDialog.xaml.cs
@@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
+using ChefKeys;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
@@ -33,6 +34,8 @@ public partial class HotkeyControlDialog : ContentDialog
public string ResultValue { get; private set; } = string.Empty;
public static string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none");
+ private static bool isOpenFlowHotkey;
+
public HotkeyControlDialog(string hotkey, string defaultHotkey, IHotkeySettings hotkeySettings, string windowTitle = "")
{
WindowTitle = windowTitle switch
@@ -46,6 +49,14 @@ public partial class HotkeyControlDialog : ContentDialog
SetKeysToDisplay(CurrentHotkey);
InitializeComponent();
+
+ // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157
+ isOpenFlowHotkey = _hotkeySettings.RegisteredHotkeys
+ .Any(x => x.DescriptionResourceKey == "flowlauncherHotkey"
+ && x.Hotkey.ToString() == hotkey);
+
+ ChefKeysManager.StartMenuEnableBlocking = true;
+ ChefKeysManager.Start();
}
private void Reset(object sender, RoutedEventArgs routedEventArgs)
@@ -61,12 +72,18 @@ public partial class HotkeyControlDialog : ContentDialog
private void Cancel(object sender, RoutedEventArgs routedEventArgs)
{
+ ChefKeysManager.StartMenuEnableBlocking = false;
+ ChefKeysManager.Stop();
+
ResultType = EResultType.Cancel;
Hide();
}
private void Save(object sender, RoutedEventArgs routedEventArgs)
{
+ ChefKeysManager.StartMenuEnableBlocking = false;
+ ChefKeysManager.Stop();
+
if (KeysToDisplay.Count == 1 && KeysToDisplay[0] == EmptyHotkey)
{
ResultType = EResultType.Delete;
@@ -85,6 +102,9 @@ public partial class HotkeyControlDialog : ContentDialog
//when alt is pressed, the real key should be e.SystemKey
Key key = e.Key == Key.System ? e.SystemKey : e.Key;
+ if (ChefKeysManager.StartMenuBlocked && key.ToString() == ChefKeysManager.StartMenuSimulatedKey)
+ return;
+
SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers();
var hotkeyModel = new HotkeyModel(
@@ -168,8 +188,13 @@ public partial class HotkeyControlDialog : ContentDialog
}
}
- private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
- hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
+ private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture)
+ {
+ if (isOpenFlowHotkey && (hotkey.ToString() == "LWin" || hotkey.ToString() == "RWin"))
+ return true;
+
+ return hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
+ }
private void Overwrite(object sender, RoutedEventArgs e)
{
diff --git a/Flow.Launcher/Languages/de.xaml b/Flow.Launcher/Languages/de.xaml
index 7f2e2bd8d..c7e775ae3 100644
--- a/Flow.Launcher/Languages/de.xaml
+++ b/Flow.Launcher/Languages/de.xaml
@@ -65,8 +65,8 @@
Letzte Abfrage beibehalten
Letzte Abfrage auswählen
Letzte Abfrage leeren
- Preserve Last Action Keyword
- Select Last Action Keyword
+ Letztes Aktions-Schlüsselwort beibehalten
+ Letztes Aktions-Schlüsselwort auswählen
Feste Fensterhöhe
Die Fensterhöhe ist durch Ziehen nicht anpassbar.
Maximal gezeigte Ergebnisse
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 4c465d61f..a3f87cd30 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -15,6 +15,7 @@
Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program.
+ Failed to unregister hotkey "{0}". Please try again or see log for details
Flow Launcher
Could not start {0}
Invalid Flow Launcher plugin file format
@@ -46,6 +47,8 @@
Portable Mode
Store all settings and user data in one folder (Useful when used with removable drives or cloud services).
Start Flow Launcher on system startup
+ Use logon task instead of startup entry for faster startup experience
+ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler
Error setting launch on startup
Hide Flow Launcher when focus is lost
Do not show new version notifications
@@ -128,7 +131,8 @@
Version
Website
Uninstall
-
+ Fail to remove plugin settings
+ Plugins: {0} - Fail to remove plugin settings files, please remove them manually
Plugin Store
@@ -145,8 +149,6 @@
This plugin has been updated within the last 7 days
New Update is Available
-
-
Theme
Appearance
@@ -196,7 +198,6 @@
This theme supports two(light/dark) modes.
This theme supports Blur Transparent Background.
-
Hotkey
Hotkeys
@@ -299,6 +300,9 @@
User Data Location
User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.
Open Folder
+ Log Level
+ Debug
+ Info
Select File Manager
@@ -367,6 +371,7 @@
OK
Yes
No
+ Background
Version
@@ -383,6 +388,9 @@
Report sent successfully
Failed to send report
Flow Launcher got an error
+ Please open new issue in
+ 1. Upload log file: {0}
+ 2. Copy below exception message
Please wait...
diff --git a/Flow.Launcher/Languages/es.xaml b/Flow.Launcher/Languages/es.xaml
index bf22aea48..c7595f69b 100644
--- a/Flow.Launcher/Languages/es.xaml
+++ b/Flow.Launcher/Languages/es.xaml
@@ -65,8 +65,8 @@
Mantener la última consulta
Seleccionar la última consulta
Limpiar la última consulta
- Conservar palabra clave de última acción
- Seleccionar palabra clave de última acción
+ Conservar última palabra clave de acción
+ Seleccionar última palabra clave de acción
Altura de la ventana fija
La altura de la ventana no se puede ajustar arrastrando el ratón.
Número máximo de resultados mostrados
diff --git a/Flow.Launcher/Languages/he.xaml b/Flow.Launcher/Languages/he.xaml
index eeb33da17..f3c9bb307 100644
--- a/Flow.Launcher/Languages/he.xaml
+++ b/Flow.Launcher/Languages/he.xaml
@@ -9,7 +9,7 @@
אנא בחר את קובץ ההפעלה {0}
לא ניתן להגדיר נתיב הפעלה {0}, אנא נסה שוב בהגדרות Flow (גלול עד למטה).
נכשל בהפעלת תוספים
- תוספים: {0} - נכשלים בטעינה ויהיו מושבתים, אנא צור קשר עם יוצרי התוספים לקבלת עזרה
+ תוספים: {0} - נכשלו בטעינה ויושבתו, אנא צור קשר עם יוצרי התוספים לקבלת עזרה
רישום מקש הקיצור "{0}" נכשל. ייתכן שמקש הקיצור נמצא בשימוש על ידי תוכנה אחרת. שנה למקש קיצור אחר, או צא מהתוכנה האחרת.
@@ -54,10 +54,10 @@
צג ראשי
צג מותאם אישית
Search Window Position on Monitor
- Center
- Center Top
- Left Top
- Right Top
+ מרכז
+ מרכז עליון
+ שמאל עליון
+ ימין עליון
Custom Position
שפה
Last Query Style
@@ -83,16 +83,16 @@
Please select pythonw.exe
Always Start Typing in English Mode
Temporarily change your input method to English mode when activating Flow.
- Auto Update
- Select
+ עדכון אוטומטי
+ בחר
Hide Flow Launcher on startup
Flow Launcher search window is hidden in the tray after starting up.
Hide tray icon
When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window.
Query Search Precision
Changes minimum match score required for results.
- None
- Low
+ ללא
+ נמוך
Regular
Search with Pinyin
Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese.
@@ -120,26 +120,26 @@
Priority
Change Plugin Results Priority
Plugin Directory
- by
+ מאת
Init time:
Query time:
- Version
- Website
- Uninstall
+ גרסה
+ אתר
+ הסר התקנה
חנות תוספים
- New Release
+ שחרור חדש
Recently Updated
תוספים
- Installed
+ מותקן
רענן
התקן
- Uninstall
+ הסר התקנה
עדכון
Plugin already installed
- New Version
+ גרסה חדשה
This plugin has been updated within the last 7 days
New Update is Available
@@ -164,7 +164,7 @@
Query Box Font
Result Title Font
Result Subtitle Font
- Reset
+ אפס
Customize
Window Mode
Opacity
@@ -196,9 +196,9 @@
- Hotkey
- Hotkeys
- Open Flow Launcher
+ מקש קיצור
+ מקשי קיצור
+ פתח את Flow Launcher
Enter shortcut to show/hide Flow Launcher.
Toggle Preview
Enter shortcut to show/hide preview in search window.
@@ -206,24 +206,24 @@
List of currently registered hotkeys
Open Result Modifier Key
Select a modifier key to open selected result via keyboard.
- Show Hotkey
+ הצג מקש קיצור
Show result selection hotkey with results.
Auto Complete
Runs autocomplete for the selected items.
Select Next Item
Select Previous Item
- Next Page
- Previous Page
+ הדף הבא
+ הדף הקודם
Cycle Previous Query
Cycle Next Query
Open Context Menu
Open Native Context Menu
Open Setting Window
- Copy File Path
+ העתק את נתיב הקובץ
Toggle Game Mode
Toggle History
Open Containing Folder
- Run As Admin
+ הרץ כמנהל
Refresh Search Results
Reload Plugins Data
Quick Adjust Window Width
@@ -234,13 +234,13 @@
Custom Query Shortcuts
Built-in Shortcuts
שאילתה
- Shortcut
- Expansion
- Description
+ קיצור דרך
+ הרחבה
+ תיאור
מחק
ערוך
הוסף
- None
+ ללא
אנא בחר פריט
Are you sure you want to delete {0} plugin hotkey?
Are you sure you want to delete shortcut: {0} with expansion {1}?
@@ -259,8 +259,8 @@
Enable HTTP Proxy
HTTP Server
Port
- User Name
- Password
+ שם משתמש
+ סיסמא
Test Proxy
שמור
Server field can't be empty
@@ -272,11 +272,11 @@
אודות
- Website
- GitHub
- Docs
- Version
- Icons
+ אתר אינטרנט
+ Github
+ תיעוד
+ גרסה
+ סמלים
You have activated Flow Launcher {0} times
Check for Updates
Become A Sponsor
@@ -302,8 +302,8 @@
Select File Manager
Please specify the file location of the file manager you using and add arguments as required. The "%d" represents the directory path to open for, used by the Arg for Folder field and for commands opening specific directories. The "%f" represents the file path to open for, used by the Arg for File field and for commands opening specific files.
For example, if the file manager uses a command such as "totalcmd.exe /A c:\windows" to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A "%d". Certain file managers like QTTabBar may just require a path to be supplied, in this instance use "%d" as the File Manager Path and leave the rest of the fileds blank.
- File Manager
- Profile Name
+ מנהל קבצים
+ שם פרופיל
File Manager Path
Arg For Folder
Arg For File
@@ -314,9 +314,9 @@
Browser
Browser Name
Browser Path
- New Window
- New Tab
- Private Mode
+ חלון חדש
+ כרטיסייה חדשה
+ מצב פרטיות
Change Priority
@@ -362,14 +362,14 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
שמור
Overwrite
ביטול
- Reset
+ אפס
מחק
- OK
- Yes
- No
+ אישור
+ כן
+ לא
- Version
+ גרסה
זמן
Please tell us how application crashed so we can fix it
שלח דיווח
@@ -377,21 +377,21 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
כללי
חריגים
Exception Type
- Source
+ מקור
Stack Trace
- Sending
+ שולח
Report sent successfully
Failed to send report
Flow Launcher got an error
- Please wait...
+ אנא המתן...
Checking for new update
You already have the latest Flow Launcher version
- Update found
- Updating...
+ עדכון נמצא
+ מעדכן...
Flow Launcher was not able to move your user profile data to the new update version.
Please manually move your profile data folder from {0} to {1}
@@ -405,7 +405,7 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com.
This upgrade will restart Flow Launcher
Following files will be updated
- Update files
+ עדכן קבצים
Update description
@@ -416,7 +416,7 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
Search and run all files and applications on your PC
Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.
Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard.
- Hotkeys
+ מקשי קיצור
Action Keyword and Commands
Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher.
Let's Start Flow Launcher
diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml
index f5fd729d4..10fc3583b 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -20,6 +20,7 @@
Closing="OnClosing"
Deactivated="OnDeactivated"
Icon="Images/app.png"
+ SourceInitialized="OnSourceInitialized"
Initialized="OnInitialized"
Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Loaded="OnLoaded"
@@ -37,7 +38,7 @@
mc:Ignorable="d">
-
+
diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index 0f8b8f6d7..3f1bae090 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -26,7 +26,7 @@ using System.Media;
using DataObject = System.Windows.DataObject;
using System.Windows.Media;
using System.Windows.Interop;
-using System.Runtime.InteropServices;
+using Windows.Win32;
namespace Flow.Launcher
{
@@ -34,9 +34,6 @@ namespace Flow.Launcher
{
#region Private Fields
- [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
- public static extern IntPtr SetForegroundWindow(IntPtr hwnd);
-
private readonly Storyboard _progressBarStoryboard = new Storyboard();
private bool isProgressBarStoryboardPaused;
private Settings _settings;
@@ -81,21 +78,19 @@ namespace Flow.Launcher
InitializeComponent();
}
- private const int WM_ENTERSIZEMOVE = 0x0231;
- private const int WM_EXITSIZEMOVE = 0x0232;
private int _initialWidth;
private int _initialHeight;
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
- if (msg == WM_ENTERSIZEMOVE)
+ if (msg == PInvoke.WM_ENTERSIZEMOVE)
{
_initialWidth = (int)Width;
_initialHeight = (int)Height;
handled = true;
}
- if (msg == WM_EXITSIZEMOVE)
+ if (msg == PInvoke.WM_EXITSIZEMOVE)
{
if (_initialHeight != (int)Height)
{
@@ -176,6 +171,11 @@ namespace Flow.Launcher
Environment.Exit(0);
}
+ private void OnSourceInitialized(object sender, EventArgs e)
+ {
+ WindowsInteropHelper.HideFromAltTab(this);
+ }
+
private void OnInitialized(object sender, EventArgs e)
{
}
@@ -424,7 +424,7 @@ namespace Flow.Launcher
// Get context menu handle and bring it to the foreground
if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource)
{
- _ = SetForegroundWindow(hwndSource.Handle);
+ PInvoke.SetForegroundWindow(new(hwndSource.Handle));
}
contextMenu.Focus();
@@ -438,7 +438,7 @@ namespace Flow.Launcher
if (_settings.FirstLaunch)
{
_settings.FirstLaunch = false;
- PluginManager.API.SaveAppAllSettings();
+ App.API.SaveAppAllSettings();
OpenWelcomeWindow();
}
}
@@ -692,7 +692,7 @@ namespace Flow.Launcher
screen = Screen.PrimaryScreen;
break;
case SearchWindowScreens.Focus:
- IntPtr foregroundWindowHandle = WindowsInteropHelper.GetForegroundWindow();
+ var foregroundWindowHandle = PInvoke.GetForegroundWindow().Value;
screen = Screen.FromHandle(foregroundWindowHandle);
break;
case SearchWindowScreens.Custom:
diff --git a/Flow.Launcher.Core/MessageBoxEx.xaml b/Flow.Launcher/MessageBoxEx.xaml
similarity index 98%
rename from Flow.Launcher.Core/MessageBoxEx.xaml
rename to Flow.Launcher/MessageBoxEx.xaml
index fff107a68..a0e5ffaf8 100644
--- a/Flow.Launcher.Core/MessageBoxEx.xaml
+++ b/Flow.Launcher/MessageBoxEx.xaml
@@ -1,9 +1,9 @@
diff --git a/Flow.Launcher.Core/MessageBoxEx.xaml.cs b/Flow.Launcher/MessageBoxEx.xaml.cs
similarity index 99%
rename from Flow.Launcher.Core/MessageBoxEx.xaml.cs
rename to Flow.Launcher/MessageBoxEx.xaml.cs
index a01b5f68d..e9b434fd9 100644
--- a/Flow.Launcher.Core/MessageBoxEx.xaml.cs
+++ b/Flow.Launcher/MessageBoxEx.xaml.cs
@@ -7,7 +7,7 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
-namespace Flow.Launcher.Core
+namespace Flow.Launcher
{
public partial class MessageBoxEx : Window
{
diff --git a/Flow.Launcher/NativeMethods.txt b/Flow.Launcher/NativeMethods.txt
new file mode 100644
index 000000000..88eeeca6e
--- /dev/null
+++ b/Flow.Launcher/NativeMethods.txt
@@ -0,0 +1,20 @@
+DwmSetWindowAttribute
+DwmExtendFrameIntoClientArea
+SystemParametersInfo
+SetForegroundWindow
+
+GetWindowLong
+SetWindowLong
+GetForegroundWindow
+GetDesktopWindow
+GetShellWindow
+GetWindowRect
+GetClassName
+FindWindowEx
+WINDOW_STYLE
+
+WM_ENTERSIZEMOVE
+WM_EXITSIZEMOVE
+
+SetLastError
+WINDOW_EX_STYLE
\ No newline at end of file
diff --git a/Flow.Launcher/PriorityChangeWindow.xaml b/Flow.Launcher/PriorityChangeWindow.xaml
index c917eeffc..33ed54bb4 100644
--- a/Flow.Launcher/PriorityChangeWindow.xaml
+++ b/Flow.Launcher/PriorityChangeWindow.xaml
@@ -29,14 +29,11 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Flow.Launcher/ProgressBoxEx.xaml.cs b/Flow.Launcher/ProgressBoxEx.xaml.cs
new file mode 100644
index 000000000..2395bdf34
--- /dev/null
+++ b/Flow.Launcher/ProgressBoxEx.xaml.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using Flow.Launcher.Infrastructure.Logger;
+
+namespace Flow.Launcher
+{
+ public partial class ProgressBoxEx : Window
+ {
+ private readonly Action _cancelProgress;
+
+ private ProgressBoxEx(Action cancelProgress)
+ {
+ _cancelProgress = cancelProgress;
+ InitializeComponent();
+ }
+
+ public static async Task ShowAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null)
+ {
+ ProgressBoxEx prgBox = null;
+ try
+ {
+ if (!Application.Current.Dispatcher.CheckAccess())
+ {
+ await Application.Current.Dispatcher.InvokeAsync(() =>
+ {
+ prgBox = new ProgressBoxEx(cancelProgress)
+ {
+ Title = caption
+ };
+ prgBox.TitleTextBlock.Text = caption;
+ prgBox.Show();
+ });
+ }
+ else
+ {
+ prgBox = new ProgressBoxEx(cancelProgress)
+ {
+ Title = caption
+ };
+ prgBox.TitleTextBlock.Text = caption;
+ prgBox.Show();
+ }
+
+ await reportProgressAsync(prgBox.ReportProgress).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Log.Error($"|ProgressBoxEx.Show|An error occurred: {e.Message}");
+
+ await reportProgressAsync(null).ConfigureAwait(false);
+ }
+ finally
+ {
+ if (!Application.Current.Dispatcher.CheckAccess())
+ {
+ await Application.Current.Dispatcher.InvokeAsync(() =>
+ {
+ prgBox?.Close();
+ });
+ }
+ else
+ {
+ prgBox?.Close();
+ }
+ }
+ }
+
+ private void ReportProgress(double progress)
+ {
+ if (!Application.Current.Dispatcher.CheckAccess())
+ {
+ Application.Current.Dispatcher.Invoke(() => ReportProgress(progress));
+ return;
+ }
+
+ if (progress < 0)
+ {
+ ProgressBar.Value = 0;
+ }
+ else if (progress >= 100)
+ {
+ ProgressBar.Value = 100;
+ Close();
+ }
+ else
+ {
+ ProgressBar.Value = progress;
+ }
+ }
+
+ private void KeyEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
+ {
+ ForceClose();
+ }
+
+ private void Button_Cancel(object sender, RoutedEventArgs e)
+ {
+ ForceClose();
+ }
+
+ private void Button_Minimize(object sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState.Minimized;
+ }
+
+ private void Button_Background(object sender, RoutedEventArgs e)
+ {
+ Hide();
+ }
+
+ private void ForceClose()
+ {
+ Close();
+ _cancelProgress?.Invoke();
+ }
+ }
+}
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index f4712770d..ac22170ae 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -25,23 +25,23 @@ using Flow.Launcher.Infrastructure.Storage;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Collections.Specialized;
+using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core;
+using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher
{
public class PublicAPIInstance : IPublicAPI
{
- private readonly SettingWindowViewModel _settingsVM;
+ private readonly Settings _settings;
private readonly MainViewModel _mainVM;
- private readonly PinyinAlphabet _alphabet;
#region Constructor
- public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM, PinyinAlphabet alphabet)
+ public PublicAPIInstance(Settings settings, MainViewModel mainVM)
{
- _settingsVM = settingsVM;
+ _settings = settings;
_mainVM = mainVM;
- _alphabet = alphabet;
GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback;
WebRequest.RegisterPrefix("data", new DataWebRequestFactory());
}
@@ -78,14 +78,15 @@ namespace Flow.Launcher
public event VisibilityChangedEventHandler VisibilityChanged { add => _mainVM.VisibilityChanged += value; remove => _mainVM.VisibilityChanged -= value; }
- public void CheckForNewUpdate() => _settingsVM.UpdateApp();
+ // Must use Ioc.Default.GetRequiredService() to avoid circular dependency
+ public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService().UpdateAppAsync(false);
public void SaveAppAllSettings()
{
PluginManager.Save();
_mainVM.Save();
- _settingsVM.Save();
- ImageLoader.Save();
+ _settings.Save();
+ _ = ImageLoader.Save();
}
public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();
@@ -105,7 +106,7 @@ namespace Flow.Launcher
{
Application.Current.Dispatcher.Invoke(() =>
{
- SettingWindow sw = SingletonWindowOpener.Open(this, _settingsVM);
+ SettingWindow sw = SingletonWindowOpener.Open();
});
}
@@ -126,9 +127,9 @@ namespace Flow.Launcher
if (directCopy && (isFile || Directory.Exists(stringToCopy)))
{
var paths = new StringCollection
- {
- stringToCopy
- };
+ {
+ stringToCopy
+ };
Clipboard.SetFileDropList(paths);
@@ -164,8 +165,8 @@ namespace Flow.Launcher
public Task HttpGetStreamAsync(string url, CancellationToken token = default) =>
Http.GetStreamAsync(url);
- public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
- CancellationToken token = default) => Http.DownloadAsync(url, filePath, token);
+ public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null,
+ CancellationToken token = default) => Http.DownloadAsync(url, filePath, reportProgress, token);
public void AddActionKeyword(string pluginId, string newActionKeyword) =>
PluginManager.AddActionKeyword(pluginId, newActionKeyword);
@@ -189,6 +190,23 @@ namespace Flow.Launcher
private readonly ConcurrentDictionary _pluginJsonStorages = new();
+ public object RemovePluginSettings(string assemblyName)
+ {
+ foreach (var keyValuePair in _pluginJsonStorages)
+ {
+ var key = keyValuePair.Key;
+ var value = keyValuePair.Value;
+ var name = value.GetType().GetField("AssemblyName")?.GetValue(value)?.ToString();
+ if (name == assemblyName)
+ {
+ _pluginJsonStorages.Remove(key, out var pluginJsonStorage);
+ return pluginJsonStorage;
+ }
+ }
+
+ return null;
+ }
+
///
/// Save plugin settings.
///
@@ -230,7 +248,7 @@ namespace Flow.Launcher
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
{
using var explorer = new Process();
- var explorerInfo = _settingsVM.Settings.CustomExplorer;
+ var explorerInfo = _settings.CustomExplorer;
explorer.StartInfo = new ProcessStartInfo
{
@@ -251,7 +269,7 @@ namespace Flow.Launcher
{
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
- var browserInfo = _settingsVM.Settings.CustomBrowser;
+ var browserInfo = _settings.CustomBrowser;
var path = browserInfo.Path == "*" ? "" : browserInfo.Path;
@@ -324,6 +342,8 @@ namespace Flow.Launcher
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK) =>
MessageBoxEx.Show(messageBoxText, caption, button, icon, defaultResult);
+ public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress);
+
#endregion
#region Private Methods
diff --git a/Flow.Launcher/ReportWindow.xaml b/Flow.Launcher/ReportWindow.xaml
index 50dace075..5e799e2a3 100644
--- a/Flow.Launcher/ReportWindow.xaml
+++ b/Flow.Launcher/ReportWindow.xaml
@@ -1,23 +1,82 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/ReportWindow.xaml.cs b/Flow.Launcher/ReportWindow.xaml.cs
index a535dfb3e..6fe90783e 100644
--- a/Flow.Launcher/ReportWindow.xaml.cs
+++ b/Flow.Launcher/ReportWindow.xaml.cs
@@ -43,20 +43,21 @@ namespace Flow.Launcher
var log = directory.GetFiles().OrderByDescending(f => f.LastWriteTime).First();
var websiteUrl = exception switch
- {
- FlowPluginException pluginException =>GetIssuesUrl(pluginException.Metadata.Website),
- _ => Constant.IssuesUrl
- };
-
+ {
+ FlowPluginException pluginException =>GetIssuesUrl(pluginException.Metadata.Website),
+ _ => Constant.IssuesUrl
+ };
- var paragraph = Hyperlink("Please open new issue in: ", websiteUrl);
- paragraph.Inlines.Add($"1. upload log file: {log.FullName}\n");
- paragraph.Inlines.Add($"2. copy below exception message");
+ var paragraph = Hyperlink(App.API.GetTranslation("reportWindow_please_open_issue"), websiteUrl);
+ paragraph.Inlines.Add(string.Format(App.API.GetTranslation("reportWindow_upload_log"), log.FullName));
+ paragraph.Inlines.Add("\n");
+ paragraph.Inlines.Add(App.API.GetTranslation("reportWindow_copy_below"));
ErrorTextbox.Document.Blocks.Add(paragraph);
StringBuilder content = new StringBuilder();
content.AppendLine(ErrorReporting.RuntimeInfo());
content.AppendLine(ErrorReporting.DependenciesInfo());
+ content.AppendLine();
content.AppendLine($"Date: {DateTime.Now.ToString(CultureInfo.InvariantCulture)}");
content.AppendLine("Exception:");
content.AppendLine(exception.ToString());
@@ -65,10 +66,12 @@ namespace Flow.Launcher
ErrorTextbox.Document.Blocks.Add(paragraph);
}
- private Paragraph Hyperlink(string textBeforeUrl, string url)
+ private static Paragraph Hyperlink(string textBeforeUrl, string url)
{
- var paragraph = new Paragraph();
- paragraph.Margin = new Thickness(0);
+ var paragraph = new Paragraph
+ {
+ Margin = new Thickness(0)
+ };
var link = new Hyperlink
{
@@ -79,10 +82,16 @@ namespace Flow.Launcher
link.Click += (s, e) => SearchWeb.OpenInBrowserTab(url);
paragraph.Inlines.Add(textBeforeUrl);
+ paragraph.Inlines.Add(" ");
paragraph.Inlines.Add(link);
paragraph.Inlines.Add("\n");
return paragraph;
}
+
+ private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
}
}
diff --git a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs
index 7dfb85a83..1ed5747cd 100644
--- a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs
+++ b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs
@@ -2,11 +2,10 @@
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
-using System.Windows;
-using System.Windows.Media;
using System.Windows.Navigation;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.ViewModel;
+using System.Windows.Media;
namespace Flow.Launcher.Resources.Pages
{
@@ -29,5 +28,10 @@ namespace Flow.Launcher.Resources.Pages
{
HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey);
}
+
+ public Brush PreviewBackground
+ {
+ get => WallpaperPathRetrieval.GetWallpaperBrush();
+ }
}
}
diff --git a/Flow.Launcher/SelectBrowserWindow.xaml b/Flow.Launcher/SelectBrowserWindow.xaml
index d8807dbef..c12879a04 100644
--- a/Flow.Launcher/SelectBrowserWindow.xaml
+++ b/Flow.Launcher/SelectBrowserWindow.xaml
@@ -28,14 +28,11 @@
-
-
-
-
-
-
{ }
+
+ public List LogLevels { get; } =
+ DropdownDataGeneric.GetValues("LogLevel");
+
+ public LOGLEVEL LogLevel
+ {
+ get => _settings.LogLevel;
+ set
+ {
+ if (_settings.LogLevel != value)
+ {
+ _settings.LogLevel = value;
+
+ Log.SetLogLevel(value);
+ }
+ }
+ }
+
public SettingsPaneAboutViewModel(Settings settings, Updater updater)
{
_settings = settings;
_updater = updater;
+ UpdateEnumDropdownLocalizations();
+ }
+
+ private void UpdateEnumDropdownLocalizations()
+ {
+ DropdownDataGeneric.UpdateLabels(LogLevels);
}
[RelayCommand]
@@ -62,7 +87,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
[RelayCommand]
private void AskClearLogFolderConfirmation()
{
- var confirmResult = MessageBoxEx.Show(
+ var confirmResult = App.API.ShowMsgBox(
InternationalizationManager.Instance.GetTranslation("clearlogfolderMessage"),
InternationalizationManager.Instance.GetTranslation("clearlogfolder"),
MessageBoxButton.YesNo
@@ -77,7 +102,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
[RelayCommand]
private void OpenSettingsFolder()
{
- PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Settings));
+ App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Settings));
}
[RelayCommand]
@@ -85,7 +110,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
{
string settingsFolderPath = Path.Combine(DataLocation.DataDirectory(), Constant.Settings);
string parentFolderPath = Path.GetDirectoryName(settingsFolderPath);
- PluginManager.API.OpenDirectory(parentFolderPath);
+ App.API.OpenDirectory(parentFolderPath);
}
@@ -96,7 +121,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
}
[RelayCommand]
- private Task UpdateApp() => _updater.UpdateAppAsync(App.API, false);
+ private Task UpdateApp() => _updater.UpdateAppAsync(false);
private void ClearLogFolder()
{
@@ -139,5 +164,4 @@ public partial class SettingsPaneAboutViewModel : BaseModel
return "0 B";
}
-
}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
index 3d94355e6..de4f158ad 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core;
@@ -42,9 +41,20 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
try
{
if (value)
- AutoStartup.Enable();
+ {
+ if (UseLogonTaskForStartup)
+ {
+ AutoStartup.EnableViaLogonTask();
+ }
+ else
+ {
+ AutoStartup.EnableViaRegistry();
+ }
+ }
else
- AutoStartup.Disable();
+ {
+ AutoStartup.DisableViaLogonTaskAndRegistry();
+ }
}
catch (Exception e)
{
@@ -54,6 +64,34 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
}
}
+ public bool UseLogonTaskForStartup
+ {
+ get => Settings.UseLogonTaskForStartup;
+ set
+ {
+ Settings.UseLogonTaskForStartup = value;
+
+ if (StartFlowLauncherOnSystemStartup)
+ {
+ try
+ {
+ if (UseLogonTaskForStartup)
+ {
+ AutoStartup.ChangeToViaLogonTask();
+ }
+ else
+ {
+ AutoStartup.ChangeToViaRegistry();
+ }
+ }
+ catch (Exception e)
+ {
+ Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
+ e.Message);
+ }
+ }
+ }
+ }
public List SearchWindowScreens { get; } =
DropdownDataGeneric.GetValues("SearchWindowScreen");
@@ -160,7 +198,7 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
private void UpdateApp()
{
- _ = _updater.UpdateAppAsync(App.API, false);
+ _ = _updater.UpdateAppAsync(false);
}
public bool AutoUpdates
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs
index 6d8af9a3f..b13aaefe3 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs
@@ -7,7 +7,6 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
-using Flow.Launcher.Core;
namespace Flow.Launcher.SettingPages.ViewModels;
@@ -42,11 +41,11 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
var item = SelectedCustomPluginHotkey;
if (item is null)
{
- MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return;
}
- var result = MessageBoxEx.Show(
+ var result = App.API.ShowMsgBox(
string.Format(
InternationalizationManager.Instance.GetTranslation("deleteCustomHotkeyWarning"), item.Hotkey
),
@@ -67,11 +66,11 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
var item = SelectedCustomPluginHotkey;
if (item is null)
{
- MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return;
}
- var window = new CustomQueryHotkeySetting(null, Settings);
+ var window = new CustomQueryHotkeySetting(Settings);
window.UpdateItem(item);
window.ShowDialog();
}
@@ -79,7 +78,7 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
[RelayCommand]
private void CustomHotkeyAdd()
{
- new CustomQueryHotkeySetting(null, Settings).ShowDialog();
+ new CustomQueryHotkeySetting(Settings).ShowDialog();
}
[RelayCommand]
@@ -88,11 +87,11 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
var item = SelectedCustomShortcut;
if (item is null)
{
- MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return;
}
- var result = MessageBoxEx.Show(
+ var result = App.API.ShowMsgBox(
string.Format(
InternationalizationManager.Instance.GetTranslation("deleteCustomShortcutWarning"), item.Key, item.Value
),
@@ -112,7 +111,7 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
var item = SelectedCustomShortcut;
if (item is null)
{
- MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return;
}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
index e06931011..15579a61d 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
@@ -13,8 +13,8 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
{
public string FilterText { get; set; } = string.Empty;
- public IList ExternalPlugins => PluginsManifest.UserPlugins
- .Select(p => new PluginStoreItemViewModel(p))
+ public IList ExternalPlugins =>
+ PluginsManifest.UserPlugins?.Select(p => new PluginStoreItemViewModel(p))
.OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease)
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated)
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.None)
@@ -24,8 +24,10 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
[RelayCommand]
private async Task RefreshExternalPluginsAsync()
{
- await PluginsManifest.UpdateManifestAsync();
- OnPropertyChanged(nameof(ExternalPlugins));
+ if (await PluginsManifest.UpdateManifestAsync())
+ {
+ OnPropertyChanged(nameof(ExternalPlugins));
+ }
}
public bool SatisfiesFilter(PluginStoreItemViewModel plugin)
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs
index 1c840fb27..e2f9e516c 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs
@@ -22,7 +22,7 @@ public partial class SettingsPaneProxyViewModel : BaseModel
private void OnTestProxyClicked()
{
var message = TestProxy();
- MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation(message));
+ App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation(message));
}
private string TestProxy()
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
index 8d8ccb780..ed933678d 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
@@ -5,7 +5,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows.Media;
-using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
@@ -14,7 +13,6 @@ using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using ModernWpf;
-using Flow.Launcher.Core;
using ThemeManager = Flow.Launcher.Core.Resource.ThemeManager;
using ThemeManagerForColorSchemeSwitch = ModernWpf.ThemeManager;
@@ -49,7 +47,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
{
if (ThemeManager.Instance.BlurEnabled && value)
{
- MessageBoxEx.Show(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed"));
+ App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed"));
return;
}
@@ -212,24 +210,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
public Brush PreviewBackground
{
- get
- {
- var wallpaper = WallpaperPathRetrieval.GetWallpaperPath();
- if (wallpaper is not null && File.Exists(wallpaper))
- {
- var memStream = new MemoryStream(File.ReadAllBytes(wallpaper));
- var bitmap = new BitmapImage();
- bitmap.BeginInit();
- bitmap.StreamSource = memStream;
- bitmap.DecodePixelWidth = 800;
- bitmap.DecodePixelHeight = 600;
- bitmap.EndInit();
- return new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill };
- }
-
- var wallpaperColor = WallpaperPathRetrieval.GetWallpaperColor();
- return new SolidColorBrush(wallpaperColor);
- }
+ get => WallpaperPathRetrieval.GetWallpaperBrush();
}
public ResultsViewModel PreviewResults
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
index e33fee02b..75c513411 100644
--- a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
@@ -123,6 +123,14 @@
+
+
+
+
+
+
+
+
();
+ _settings = Ioc.Default.GetRequiredService();
DataContext = viewModel;
_viewModel = viewModel;
- _api = api;
+ _updater = Ioc.Default.GetRequiredService();
+ _portable = Ioc.Default.GetRequiredService();
+ _api = Ioc.Default.GetRequiredService();
InitializePosition();
InitializeComponent();
}
@@ -34,11 +40,11 @@ public partial class SettingWindow
private void OnLoaded(object sender, RoutedEventArgs e)
{
RefreshMaximizeRestoreButton();
- // Fix (workaround) for the window freezes after lock screen (Win+L)
+ // Fix (workaround) for the window freezes after lock screen (Win+L) or sleep
// https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf
HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
HwndTarget hwndTarget = hwndSource.CompositionTarget;
- hwndTarget.RenderMode = RenderMode.Default;
+ hwndTarget.RenderMode = RenderMode.SoftwareOnly; // Must use software only render mode here
InitializePosition();
}
@@ -125,7 +131,7 @@ public partial class SettingWindow
WindowState = _settings.SettingWindowState;
}
- private bool IsPositionValid(double top, double left)
+ private static bool IsPositionValid(double top, double left)
{
foreach (var screen in Screen.AllScreens)
{
@@ -145,7 +151,7 @@ public partial class SettingWindow
var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
- var left = (dip2.X - this.ActualWidth) / 2 + dip1.X;
+ var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}
@@ -154,13 +160,13 @@ public partial class SettingWindow
var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
- var top = (dip2.Y - this.ActualHeight) / 2 + dip1.Y - 20;
+ var top = (dip2.Y - ActualHeight) / 2 + dip1.Y - 20;
return top;
}
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
- var paneData = new PaneData(_settings, _viewModel.Updater, _viewModel.Portable);
+ var paneData = new PaneData(_settings, _updater, _portable);
if (args.IsSettingsSelected)
{
ContentFrame.Navigate(typeof(SettingsPaneGeneral), paneData);
diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs
index cbd0b88fc..7f35904a5 100644
--- a/Flow.Launcher/Storage/TopMostRecord.cs
+++ b/Flow.Launcher/Storage/TopMostRecord.cs
@@ -12,6 +12,8 @@ namespace Flow.Launcher.Storage
internal bool IsTopMost(Result result)
{
+ // origin query is null when user select the context menu item directly of one item from query list
+ // in this case, we do not need to check if the result is top most
if (records.IsEmpty || result.OriginQuery == null ||
!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
{
@@ -24,24 +26,34 @@ namespace Flow.Launcher.Storage
internal void Remove(Result result)
{
+ // origin query is null when user select the context menu item directly of one item from query list
+ // in this case, we do not need to remove the record
+ if (result.OriginQuery == null)
+ {
+ return;
+ }
+
records.Remove(result.OriginQuery.RawQuery, out _);
}
internal void AddOrUpdate(Result result)
{
+ // origin query is null when user select the context menu item directly of one item from query list
+ // in this case, we do not need to add or update the record
+ if (result.OriginQuery == null)
+ {
+ return;
+ }
+
var record = new Record
{
PluginID = result.PluginID,
Title = result.Title,
- SubTitle = result.SubTitle
+ SubTitle = result.SubTitle,
+ RecordKey = result.RecordKey
};
records.AddOrUpdate(result.OriginQuery.RawQuery, record, (key, oldValue) => record);
}
-
- public void Load(Dictionary dictionary)
- {
- records = new ConcurrentDictionary(dictionary);
- }
}
public class Record
@@ -49,12 +61,21 @@ namespace Flow.Launcher.Storage
public string Title { get; set; }
public string SubTitle { get; set; }
public string PluginID { get; set; }
+ public string RecordKey { get; set; }
public bool Equals(Result r)
{
- return Title == r.Title
- && SubTitle == r.SubTitle
- && PluginID == r.PluginID;
+ if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey))
+ {
+ return Title == r.Title
+ && SubTitle == r.SubTitle
+ && PluginID == r.PluginID;
+ }
+ else
+ {
+ return RecordKey == r.RecordKey
+ && PluginID == r.PluginID;
+ }
}
}
}
diff --git a/Flow.Launcher/Storage/UserSelectedRecord.cs b/Flow.Launcher/Storage/UserSelectedRecord.cs
index d6405005d..d30dccd26 100644
--- a/Flow.Launcher/Storage/UserSelectedRecord.cs
+++ b/Flow.Launcher/Storage/UserSelectedRecord.cs
@@ -15,7 +15,6 @@ namespace Flow.Launcher.Storage
[JsonInclude, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary records { get; private set; }
-
public UserSelectedRecord()
{
recordsWithQuery = new Dictionary();
@@ -45,12 +44,21 @@ namespace Flow.Launcher.Storage
private static int GenerateResultHashCode(Result result)
{
- int hashcode = GenerateStaticHashCode(result.Title);
- return GenerateStaticHashCode(result.SubTitle, hashcode);
+ if (string.IsNullOrEmpty(result.RecordKey))
+ {
+ int hashcode = GenerateStaticHashCode(result.Title);
+ return GenerateStaticHashCode(result.SubTitle, hashcode);
+ }
+ else
+ {
+ return GenerateStaticHashCode(result.RecordKey);
+ }
}
private static int GenerateQueryAndResultHashCode(Query query, Result result)
{
+ // query is null when user select the context menu item directly of one item from query list
+ // so we only need to consider the result
if (query == null)
{
return GenerateResultHashCode(result);
@@ -58,8 +66,16 @@ namespace Flow.Launcher.Storage
int hashcode = GenerateStaticHashCode(query.ActionKeyword);
hashcode = GenerateStaticHashCode(query.Search, hashcode);
- hashcode = GenerateStaticHashCode(result.Title, hashcode);
- hashcode = GenerateStaticHashCode(result.SubTitle, hashcode);
+
+ if (string.IsNullOrEmpty(result.RecordKey))
+ {
+ hashcode = GenerateStaticHashCode(result.Title, hashcode);
+ hashcode = GenerateStaticHashCode(result.SubTitle, hashcode);
+ }
+ else
+ {
+ hashcode = GenerateStaticHashCode(result.RecordKey, hashcode);
+ }
return hashcode;
}
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index 55bc8d1b3..6b0144a03 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -25,6 +25,7 @@ using System.Windows.Input;
using System.ComponentModel;
using Flow.Launcher.Infrastructure.Image;
using System.Windows.Media;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.ViewModel
{
@@ -34,8 +35,6 @@ namespace Flow.Launcher.ViewModel
private bool _isQueryRunning;
private Query _lastQuery;
- private Result lastContextMenuResult = new Result();
- private List lastContextMenuResults = new List();
private string _queryTextBeforeLeaveResults;
private readonly FlowLauncherJsonStorage _historyItemsStorage;
@@ -58,13 +57,13 @@ namespace Flow.Launcher.ViewModel
#region Constructor
- public MainViewModel(Settings settings)
+ public MainViewModel()
{
_queryTextBeforeLeaveResults = "";
_queryText = "";
_lastQuery = new Query();
- Settings = settings;
+ Settings = Ioc.Default.GetRequiredService();
Settings.PropertyChanged += (_, args) =>
{
switch (args.PropertyName)
@@ -233,8 +232,8 @@ namespace Flow.Launcher.ViewModel
var token = e.Token == default ? _updateToken : e.Token;
- // make a copy of results to avoid plugin change the result when updating view model
- var resultsCopy = e.Results.ToList();
+ // make a clone to avoid possible issue that plugin will also change the list and items when updating view model
+ var resultsCopy = DeepCloneResults(e.Results, token);
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
@@ -281,10 +280,8 @@ namespace Flow.Launcher.ViewModel
public void ReQuery(bool reselect)
{
- if (SelectedIsFromQueryResults())
- {
- QueryResults(isReQuery: true, reSelect: reselect);
- }
+ BackToQueryResults();
+ QueryResults(isReQuery: true, reSelect: reselect);
}
[RelayCommand]
@@ -398,11 +395,15 @@ namespace Flow.Launcher.ViewModel
})
.ConfigureAwait(false);
-
if (SelectedIsFromQueryResults())
{
_userSelectedRecord.Add(result);
- _history.Add(result.OriginQuery.RawQuery);
+ // origin query is null when user select the context menu item directly of one item from query list
+ // so we don't want to add it to history
+ if (result.OriginQuery != null)
+ {
+ _history.Add(result.OriginQuery.RawQuery);
+ }
lastHistoryIndex = 1;
}
@@ -412,6 +413,22 @@ namespace Flow.Launcher.ViewModel
}
}
+ private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default)
+ {
+ var resultsCopy = new List();
+ foreach (var result in results.ToList())
+ {
+ if (token.IsCancellationRequested)
+ {
+ break;
+ }
+
+ var resultCopy = result.Clone();
+ resultsCopy.Add(resultCopy);
+ }
+ return resultsCopy;
+ }
+
#endregion
#region BasicCommands
@@ -425,7 +442,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void SelectHelp()
{
- PluginManager.API.OpenUrl("https://www.flowlauncher.com/docs/#/usage-tips");
+ App.API.OpenUrl("https://www.flowlauncher.com/docs/#/usage-tips");
}
[RelayCommand]
@@ -608,6 +625,8 @@ namespace Flow.Launcher.ViewModel
{
Application.Current.Dispatcher.Invoke(() =>
{
+ BackToQueryResults();
+
if (QueryText != queryText)
{
// re-query is done in QueryText's setter method
@@ -986,19 +1005,10 @@ namespace Flow.Launcher.ViewModel
if (selected != null) // SelectedItem returns null if selection is empty.
{
List results;
- if (selected == lastContextMenuResult)
- {
- results = lastContextMenuResults;
- }
- else
- {
- results = PluginManager.GetContextMenusForPlugin(selected);
- lastContextMenuResults = results;
- lastContextMenuResult = selected;
- results.Add(ContextMenuTopMost(selected));
- results.Add(ContextMenuPluginInfo(selected.PluginID));
- }
+ results = PluginManager.GetContextMenusForPlugin(selected);
+ results.Add(ContextMenuTopMost(selected));
+ results.Add(ContextMenuPluginInfo(selected.PluginID));
if (!string.IsNullOrEmpty(query))
{
@@ -1187,9 +1197,18 @@ namespace Flow.Launcher.ViewModel
currentCancellationToken.ThrowIfCancellationRequested();
- results ??= _emptyResult;
+ IReadOnlyList resultsCopy;
+ if (results == null)
+ {
+ resultsCopy = _emptyResult;
+ }
+ else
+ {
+ // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc.
+ resultsCopy = DeepCloneResults(results);
+ }
- if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query,
+ if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query,
currentCancellationToken, reSelect)))
{
Log.Error("MainViewModel", "Unable to add item to Result Update Queue");
@@ -1273,6 +1292,7 @@ namespace Flow.Launcher.ViewModel
{
_topMostRecord.Remove(result);
App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success"));
+ App.API.ReQuery();
return false;
}
};
@@ -1289,6 +1309,7 @@ namespace Flow.Launcher.ViewModel
{
_topMostRecord.AddOrUpdate(result);
App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success"));
+ App.API.ReQuery();
return false;
}
};
@@ -1377,8 +1398,6 @@ namespace Flow.Launcher.ViewModel
lastHistoryIndex = 1;
// Trick for no delay
MainWindowOpacity = 0;
- lastContextMenuResult = new Result();
- lastContextMenuResults = new List();
if (ExternalPreviewVisible)
CloseExternalPreview();
@@ -1476,12 +1495,31 @@ namespace Flow.Launcher.ViewModel
{
result.Score = Result.MaxScore;
}
- else if (result.Score != Result.MaxScore)
+ else
{
var priorityScore = metaResults.Metadata.Priority * 150;
- result.Score += result.AddSelectedCount ?
- _userSelectedRecord.GetSelectedCount(result) + priorityScore :
- priorityScore;
+ if (result.AddSelectedCount)
+ {
+ if ((long)result.Score + _userSelectedRecord.GetSelectedCount(result) + priorityScore > Result.MaxScore)
+ {
+ result.Score = Result.MaxScore;
+ }
+ else
+ {
+ result.Score += _userSelectedRecord.GetSelectedCount(result) + priorityScore;
+ }
+ }
+ else
+ {
+ if ((long)result.Score + priorityScore > Result.MaxScore)
+ {
+ result.Score = Result.MaxScore;
+ }
+ else
+ {
+ result.Score += priorityScore;
+ }
+ }
}
}
}
diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs
index 4ce8bd470..209a81395 100644
--- a/Flow.Launcher/ViewModel/PluginViewModel.cs
+++ b/Flow.Launcher/ViewModel/PluginViewModel.cs
@@ -90,13 +90,13 @@ namespace Flow.Launcher.ViewModel
private Control _bottomPart2;
public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null;
- public bool HasSettingControl => PluginPair.Plugin is ISettingProvider;
+ public bool HasSettingControl => PluginPair.Plugin is ISettingProvider && (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel());
public Control SettingControl
=> IsExpanded
? _settingControl
- ??= PluginPair.Plugin is not ISettingProvider settingProvider
- ? null
- : settingProvider.CreateSettingPanel()
+ ??= HasSettingControl
+ ? ((ISettingProvider)PluginPair.Plugin).CreateSettingPanel()
+ : null
: null;
private ImageSource _image = ImageLoader.MissingImage;
@@ -134,20 +134,20 @@ namespace Flow.Launcher.ViewModel
{
var directory = PluginPair.Metadata.PluginDirectory;
if (!string.IsNullOrEmpty(directory))
- PluginManager.API.OpenDirectory(directory);
+ App.API.OpenDirectory(directory);
}
[RelayCommand]
private void OpenSourceCodeLink()
{
- PluginManager.API.OpenUrl(PluginPair.Metadata.Website);
+ App.API.OpenUrl(PluginPair.Metadata.Website);
}
[RelayCommand]
private void OpenDeletePluginWindow()
{
- PluginManager.API.ChangeQuery($"{PluginManagerActionKeyword} uninstall {PluginPair.Metadata.Name}".Trim(), true);
- PluginManager.API.ShowMainWindow();
+ App.API.ChangeQuery($"{PluginManagerActionKeyword} uninstall {PluginPair.Metadata.Name}".Trim(), true);
+ App.API.ShowMainWindow();
}
public static bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
@@ -159,5 +159,4 @@ namespace Flow.Launcher.ViewModel
changeKeywordsWindow.ShowDialog();
}
}
-
}
diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs
index 107372e01..7d2b5bc93 100644
--- a/Flow.Launcher/ViewModel/ResultsViewModel.cs
+++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs
@@ -182,7 +182,7 @@ namespace Flow.Launcher.ViewModel
///
/// To avoid deadlock, this method should not called from main thread
///
- public void AddResults(IEnumerable resultsForUpdates, CancellationToken token, bool reselect = true)
+ public void AddResults(ICollection resultsForUpdates, CancellationToken token, bool reselect = true)
{
var newResults = NewResults(resultsForUpdates);
@@ -228,12 +228,12 @@ namespace Flow.Launcher.ViewModel
.ToList();
}
- private List NewResults(IEnumerable resultsForUpdates)
+ private List NewResults(ICollection resultsForUpdates)
{
if (!resultsForUpdates.Any())
return Results;
- return Results.Where(r => r != null && !resultsForUpdates.Any(u => u.ID == r.Result.PluginID))
+ return Results.Where(r => r?.Result != null && resultsForUpdates.All(u => u.ID != r.Result.PluginID))
.Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings)))
.OrderByDescending(rv => rv.Result.Score)
.ToList();
diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
index 04dd6312b..51afe533d 100644
--- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
+++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
@@ -1,66 +1,46 @@
-using Flow.Launcher.Core;
-using Flow.Launcher.Core.Configuration;
-using Flow.Launcher.Infrastructure.Storage;
-using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.ViewModel;
-public class SettingWindowViewModel : BaseModel
+public partial class SettingWindowViewModel : BaseModel
{
- private readonly FlowLauncherJsonStorage _storage;
+ private readonly Settings _settings;
- public Updater Updater { get; }
-
- public IPortable Portable { get; }
-
- public Settings Settings { get; }
-
- public SettingWindowViewModel(Updater updater, IPortable portable)
+ public SettingWindowViewModel(Settings settings)
{
- _storage = new FlowLauncherJsonStorage();
-
- Updater = updater;
- Portable = portable;
- Settings = _storage.Load();
+ _settings = settings;
}
- public async void UpdateApp()
- {
- await Updater.UpdateAppAsync(App.API, false);
- }
-
-
-
///
/// Save Flow settings. Plugins settings are not included.
///
public void Save()
{
- _storage.Save();
+ _settings.Save();
}
public double SettingWindowWidth
{
- get => Settings.SettingWindowWidth;
- set => Settings.SettingWindowWidth = value;
+ get => _settings.SettingWindowWidth;
+ set => _settings.SettingWindowWidth = value;
}
public double SettingWindowHeight
{
- get => Settings.SettingWindowHeight;
- set => Settings.SettingWindowHeight = value;
+ get => _settings.SettingWindowHeight;
+ set => _settings.SettingWindowHeight = value;
}
public double? SettingWindowTop
{
- get => Settings.SettingWindowTop;
- set => Settings.SettingWindowTop = value;
+ get => _settings.SettingWindowTop;
+ set => _settings.SettingWindowTop = value;
}
public double? SettingWindowLeft
{
- get => Settings.SettingWindowLeft;
- set => Settings.SettingWindowLeft = value;
+ get => _settings.SettingWindowLeft;
+ set => _settings.SettingWindowLeft = value;
}
}
diff --git a/Flow.Launcher/WelcomeWindow.xaml b/Flow.Launcher/WelcomeWindow.xaml
index 208fc807f..73f423d0b 100644
--- a/Flow.Launcher/WelcomeWindow.xaml
+++ b/Flow.Launcher/WelcomeWindow.xaml
@@ -36,8 +36,6 @@
-
-
-
-
+
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
index a7c230956..519141f6c 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
@@ -4,7 +4,7 @@
"Name": "Browser Bookmarks",
"Description": "Search your browser bookmarks",
"Author": "qianlifeng, Ioannis G.",
- "Version": "3.3.3",
+ "Version": "3.3.4",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
index 69c03b877..1b985acf9 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
@@ -62,7 +62,7 @@
-
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
index bb15cedf2..99e185928 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
@@ -4,7 +4,7 @@
"Name": "Calculator",
"Description": "Provide mathematical calculations.(Try 5*3-2 in Flow Launcher)",
"Author": "cxfksword",
- "Version": "3.1.4",
+ "Version": "3.1.5",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Calculator.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
index feccc74c8..3f3b7cb58 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
@@ -242,6 +242,7 @@ namespace Flow.Launcher.Plugin.Explorer
var name = "Plugin: Folder";
var message = $"File not found: {e.Message}";
Context.API.ShowMsgError(name, message);
+ return false;
}
return true;
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
index 29925aeef..549217027 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
@@ -45,6 +45,7 @@
+
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/he.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/he.xaml
index 0e1753d67..f87dd7d63 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/he.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/he.xaml
@@ -29,8 +29,8 @@
Everything Setting
Preview Panel
Size
- Date Created
- Date Modified
+ תאריך יצירה
+ תאריך שינוי
Display File Info
Date and time format
Sort Option:
@@ -126,20 +126,20 @@
Failed to load Everything SDK
אזהרה: שירות Everything אינו פועל
שגיאה במהלך שאילתה לEverything
- Sort By
+ מיין לפי
Name
- Path
+ נתיב
Size
Extension
Type Name
- Date Created
- Date Modified
- Attributes
+ תאריך יצירה
+ תאריך שינוי
+ מאפיינים
File List FileName
Run Count
- Date Recently Changed
- Date Accessed
- Date Run
+ תאריך שינוי אחרון
+ תאריך גישה
+ תאריך הרצה
↑
↓
Warning: This is not a Fast Sort option, searches may be slow
@@ -149,11 +149,11 @@
Click to launch or install Everything
Everything Installation
- Installing Everything service. Please wait...
- Successfully installed Everything service
- Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com
+ מתקין את שירות Everything. אנא המתן...
+ שירות Everything הותקן בהצלחה
+ התקנה אוטומטית של שירות Everything נכשלה. אנא הורד אותו ידנית מ- https://www.voidtools.com
Click here to start it
- Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you
+ לא מצליח למצוא התקנה של Everything, האם תרצה לבחור מיקום באופן ידני?{0}{0}לחץ על לא וEverything יותקן עבורך אוטומטית
Do you want to enable content search for Everything?
It can be very slow without index (which is only supported in Everything v1.5+)
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
index e7b43c555..4bddfd9b2 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
@@ -22,7 +22,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string WindowsIndexErrorImagePath = "Images\\index_error2.png";
internal const string GeneralSearchErrorImagePath = "Images\\robot_error.png";
-
internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory";
internal const string ToolTipOpenContainingFolder = "Ctrl + Enter to open the containing folder";
@@ -31,6 +30,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string DefaultContentSearchActionKeyword = "doc:";
+ internal const char UnixDirectorySeparator = '/';
+
internal const char DirectorySeparator = '\\';
internal const string WindowsIndexingOptions = "srchadmin.dll";
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
index 8fd167476..12df6c145 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
@@ -187,6 +187,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
var needToExpand = EnvironmentVariables.HasEnvironmentVar(querySearch);
var path = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch;
+ // if user uses the unix directory separator, we need to convert it to windows directory separator
+ path = path.Replace(Constants.UnixDirectorySeparator, Constants.DirectorySeparator);
+
// Check that actual location exists, otherwise directory search will throw directory not found exception
if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path).LocationExists())
return results.ToList();
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs
index 34a5b2760..73c35b1c8 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs
@@ -31,13 +31,13 @@ namespace Flow.Launcher.Plugin.Explorer.Views
}
private string actionKeyword;
- private readonly IPublicAPI api;
+ private readonly IPublicAPI _api;
private bool _keywordEnabled;
public ActionKeywordSetting(ActionKeywordModel selectedActionKeyword, IPublicAPI api)
{
CurrentActionKeyword = selectedActionKeyword;
- this.api = api;
+ _api = api;
ActionKeyword = selectedActionKeyword.Keyword;
KeywordEnabled = selectedActionKeyword.Enabled;
@@ -62,14 +62,14 @@ namespace Flow.Launcher.Plugin.Explorer.Views
switch (CurrentActionKeyword.KeywordProperty, KeywordEnabled)
{
case (Settings.ActionKeyword.FileContentSearchActionKeyword, true):
- api.ShowMsgBox(api.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
+ _api.ShowMsgBox(_api.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
return;
case (Settings.ActionKeyword.QuickAccessActionKeyword, true):
- api.ShowMsgBox(api.GetTranslation("plugin_explorer_quickaccess_globalActionKeywordInvalid"));
+ _api.ShowMsgBox(_api.GetTranslation("plugin_explorer_quickaccess_globalActionKeywordInvalid"));
return;
}
- if (!KeywordEnabled || !api.ActionKeywordAssigned(ActionKeyword))
+ if (!KeywordEnabled || !_api.ActionKeywordAssigned(ActionKeyword))
{
DialogResult = true;
Close();
@@ -77,7 +77,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
}
// The keyword is not valid, so show message
- api.ShowMsgBox(api.GetTranslation("newActionKeywordsHasBeenAssigned"));
+ _api.ShowMsgBox(_api.GetTranslation("newActionKeywordsHasBeenAssigned"));
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json
index d5156fbfd..4eb6bb83b 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json
@@ -10,7 +10,7 @@
"Name": "Explorer",
"Description": "Find and manage files and folders via Windows Search or Everything",
"Author": "Jeremy Wu",
- "Version": "3.2.3",
+ "Version": "3.2.4",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json
index 3584d04a4..2b4870792 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json
@@ -4,7 +4,7 @@
"Name": "Plugin Indicator",
"Description": "Provides plugin action keyword suggestions",
"Author": "qianlifeng",
- "Version": "3.0.6",
+ "Version": "3.0.7",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginIndicator.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs
index 17e9fe2bc..482e821dc 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs
@@ -59,7 +59,6 @@ namespace Flow.Launcher.Plugin.PluginsManager
var link = pluginManifestInfo.UrlSourceCode.StartsWith("https://github.com")
? Regex.Replace(pluginManifestInfo.UrlSourceCode, @"\/tree\/\w+$", "") + "/issues"
: pluginManifestInfo.UrlSourceCode;
-
Context.API.OpenUrl(link);
return true;
}
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
index de6a3a2fb..573ca9051 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
@@ -15,6 +15,8 @@
Installing Plugin
Download and install {0}
Plugin Uninstall
+ Keep plugin settings
+ Do you want to keep the settings of the plugin for the next usage?
Plugin {0} successfully installed. Restarting Flow, please wait...
Unable to find the plugin.json metadata file from the extracted zip file.
Error: A plugin which has the same or greater version with {0} already exists.
@@ -37,13 +39,13 @@
Plugin {0} successfully updated. Restarting Flow, please wait...
Installing from an unknown source
You are installing this plugin from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning via settings)
-
+
Plugin {0} successfully installed. Please restart Flow.
Plugin {0} successfully uninstalled. Please restart Flow.
Plugin {0} successfully updated. Please restart Flow.
{0} plugins successfully updated. Please restart Flow.
Plugin {0} has already been modified. Please restart Flow before making any further changes.
-
+
Plugins Manager
Management of installing, uninstalling or updating Flow Launcher plugins
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
index bec84f484..156135f81 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs
@@ -51,7 +51,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return query.FirstSearch.ToLower() switch
{
//search could be url, no need ToLower() when passed in
- Settings.InstallCommand => await pluginManager.RequestInstallOrUpdate(query.SecondToEndSearch, token, query.IsReQuery),
+ Settings.InstallCommand => await pluginManager.RequestInstallOrUpdateAsync(query.SecondToEndSearch, token, query.IsReQuery),
Settings.UninstallCommand => pluginManager.RequestUninstall(query.SecondToEndSearch),
Settings.UpdateCommand => await pluginManager.RequestUpdateAsync(query.SecondToEndSearch, token, query.IsReQuery),
_ => pluginManager.GetDefaultHotKeys().Where(hotkey =>
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
index 305d248d3..4ceadec56 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
@@ -17,7 +17,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
internal class PluginsManager
{
- const string zip = "zip";
+ private const string zip = "zip";
private PluginInitContext Context { get; set; }
@@ -144,34 +144,43 @@ namespace Flow.Launcher.Plugin.PluginsManager
try
{
+ using var cts = new CancellationTokenSource();
+
if (!plugin.IsFromLocalInstallPath)
{
- if (File.Exists(filePath))
- File.Delete(filePath);
-
- await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
+ await DownloadFileAsync(
+ $"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {plugin.Name}",
+ plugin.UrlDownload, filePath, cts);
}
else
{
filePath = plugin.LocalInstallPath;
}
- Install(plugin, filePath);
+ // check if user cancelled download before installing plugin
+ if (cts.IsCancellationRequested)
+ return;
+ else
+ Install(plugin, filePath);
}
catch (HttpRequestException e)
{
+ // show error message
Context.API.ShowMsgError(
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), plugin.Name),
Context.API.GetTranslation("plugin_pluginsmanager_download_error"));
Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
+
return;
}
catch (Exception e)
{
+ // show error message
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
plugin.Name));
Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
+
return;
}
@@ -190,6 +199,41 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
}
+ private async Task DownloadFileAsync(string prgBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
+ {
+ if (deleteFile && File.Exists(filePath))
+ File.Delete(filePath);
+
+ if (showProgress)
+ {
+ var exceptionHappened = false;
+ await Context.API.ShowProgressBoxAsync(prgBoxTitle,
+ async (reportProgress) =>
+ {
+ if (reportProgress == null)
+ {
+ // when reportProgress is null, it means there is expcetion with the progress box
+ // so we record it with exceptionHappened and return so that progress box will close instantly
+ exceptionHappened = true;
+ return;
+ }
+ else
+ {
+ await Context.API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
+ }
+ }, cts.Cancel);
+
+ // if exception happened while downloading and user does not cancel downloading,
+ // we need to redownload the plugin
+ if (exceptionHappened && (!cts.IsCancellationRequested))
+ await Context.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
+ }
+ else
+ {
+ await Context.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
+ }
+ }
+
internal async ValueTask> RequestUpdateAsync(string search, CancellationToken token,
bool usePrimaryUrlOnly = false)
{
@@ -277,43 +321,48 @@ namespace Flow.Launcher.Plugin.PluginsManager
_ = Task.Run(async delegate
{
+ using var cts = new CancellationTokenSource();
+
if (!x.PluginNewUserPlugin.IsFromLocalInstallPath)
{
- if (File.Exists(downloadToFilePath))
- {
- File.Delete(downloadToFilePath);
- }
-
- await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
- .ConfigureAwait(false);
+ await DownloadFileAsync(
+ $"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {x.PluginNewUserPlugin.Name}",
+ x.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
}
else
{
downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath;
}
-
- PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin,
- downloadToFilePath);
-
- if (Settings.AutoRestartAfterChanging)
+ // check if user cancelled download before installing plugin
+ if (cts.IsCancellationRequested)
{
- Context.API.ShowMsg(
- Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
- string.Format(
- Context.API.GetTranslation(
- "plugin_pluginsmanager_update_success_restart"),
- x.Name));
- Context.API.RestartApp();
+ return;
}
else
{
- Context.API.ShowMsg(
- Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
- string.Format(
- Context.API.GetTranslation(
- "plugin_pluginsmanager_update_success_no_restart"),
- x.Name));
+ PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin,
+ downloadToFilePath);
+
+ if (Settings.AutoRestartAfterChanging)
+ {
+ Context.API.ShowMsg(
+ Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
+ string.Format(
+ Context.API.GetTranslation(
+ "plugin_pluginsmanager_update_success_restart"),
+ x.Name));
+ Context.API.RestartApp();
+ }
+ else
+ {
+ Context.API.ShowMsg(
+ Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
+ string.Format(
+ Context.API.GetTranslation(
+ "plugin_pluginsmanager_update_success_no_restart"),
+ x.Name));
+ }
}
}).ContinueWith(t =>
{
@@ -324,7 +373,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
x.Name));
- }, TaskContinuationOptions.OnlyOnFaulted);
+ }, token, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
return true;
},
@@ -337,7 +386,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
});
// Update all result
- if (resultsForUpdate.Count() > 1)
+ if (resultsForUpdate.Count > 1)
{
var updateAllResult = new Result
{
@@ -351,13 +400,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_prompt"),
- resultsForUpdate.Count(), Environment.NewLine);
+ resultsForUpdate.Count, Environment.NewLine);
}
else
{
message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_prompt_no_restart"),
- resultsForUpdate.Count());
+ resultsForUpdate.Count);
}
if (Context.API.ShowMsgBox(message,
@@ -374,16 +423,18 @@ namespace Flow.Launcher.Plugin.PluginsManager
try
{
- if (File.Exists(downloadToFilePath))
- {
- File.Delete(downloadToFilePath);
- }
+ using var cts = new CancellationTokenSource();
- await Http.DownloadAsync(plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
- .ConfigureAwait(false);
+ await DownloadFileAsync(
+ $"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {plugin.PluginNewUserPlugin.Name}",
+ plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
- PluginManager.UpdatePlugin(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
- downloadToFilePath);
+ // check if user cancelled download before installing plugin
+ if (cts.IsCancellationRequested)
+ return;
+ else
+ PluginManager.UpdatePlugin(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
+ downloadToFilePath);
}
catch (Exception ex)
{
@@ -401,7 +452,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_success_restart"),
- resultsForUpdate.Count()));
+ resultsForUpdate.Count));
Context.API.RestartApp();
}
else
@@ -409,7 +460,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_success_no_restart"),
- resultsForUpdate.Count()));
+ resultsForUpdate.Count));
}
return true;
@@ -483,7 +534,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return false;
}
- Application.Current.MainWindow.Hide();
+ Context.API.HideMainWindow();
_ = InstallOrUpdateAsync(plugin);
return ShouldHideWindow;
@@ -521,7 +572,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return false;
}
- Application.Current.MainWindow.Hide();
+ Context.API.HideMainWindow();
_ = InstallOrUpdateAsync(plugin);
return ShouldHideWindow;
@@ -542,10 +593,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
return url.StartsWith(acceptedSource) &&
- Context.API.GetAllPlugins().Any(x => x.Metadata.Website.StartsWith(constructedUrlPart));
+ Context.API.GetAllPlugins().Any(x =>
+ !string.IsNullOrEmpty(x.Metadata.Website) &&
+ x.Metadata.Website.StartsWith(constructedUrlPart)
+ );
}
- internal async ValueTask> RequestInstallOrUpdate(string search, CancellationToken token,
+ internal async ValueTask> RequestInstallOrUpdateAsync(string search, CancellationToken token,
bool usePrimaryUrlOnly = false)
{
await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly);
@@ -575,7 +629,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return ShouldHideWindow;
}
- Application.Current.MainWindow.Hide();
+ Context.API.HideMainWindow();
_ = InstallOrUpdateAsync(x); // No need to wait
return ShouldHideWindow;
},
@@ -652,7 +706,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
- Application.Current.MainWindow.Hide();
+ Context.API.HideMainWindow();
Uninstall(x.Metadata);
if (Settings.AutoRestartAfterChanging)
{
@@ -682,7 +736,11 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
try
{
- PluginManager.UninstallPlugin(plugin, removeSettings: true);
+ var removePluginSettings = Context.API.ShowMsgBox(
+ Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_subtitle"),
+ Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_title"),
+ button: MessageBoxButton.YesNo) == MessageBoxResult.No;
+ PluginManager.UninstallPlugin(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings);
}
catch (ArgumentException e)
{
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
index 9800fa020..743f5b25b 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
@@ -1,10 +1,9 @@
using Flow.Launcher.Core.ExternalPlugins;
-using Flow.Launcher.Infrastructure.UserSettings;
using ICSharpCode.SharpZipLib.Zip;
-using Newtonsoft.Json;
using System.IO;
using System.IO.Compression;
using System.Linq;
+using System.Text.Json;
namespace Flow.Launcher.Plugin.PluginsManager
{
@@ -72,12 +71,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (pluginJsonEntry != null)
{
- using (StreamReader reader = new StreamReader(pluginJsonEntry.Open()))
- {
- string pluginJsonContent = reader.ReadToEnd();
- plugin = JsonConvert.DeserializeObject(pluginJsonContent);
- plugin.IcoPath = "Images\\zipfolder.png";
- }
+ using Stream stream = pluginJsonEntry.Open();
+ plugin = JsonSerializer.Deserialize(stream);
+ plugin.IcoPath = "Images\\zipfolder.png";
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json
index 6562b3cdf..df5a2c784 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json
@@ -6,7 +6,7 @@
"Name": "Plugins Manager",
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
"Author": "Jeremy Wu",
- "Version": "3.2.3",
+ "Version": "3.2.4",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
index 876bac1e7..4e216b7b2 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
@@ -50,6 +50,13 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt
new file mode 100644
index 000000000..7fa794755
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt
@@ -0,0 +1,2 @@
+QueryFullProcessImageName
+OpenProcess
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
index 5cc94dfdf..d8873bc20 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
@@ -1,11 +1,13 @@
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
+using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.System.Threading;
namespace Flow.Launcher.Plugin.ProcessKiller
{
@@ -126,43 +128,33 @@ namespace Flow.Launcher.Plugin.ProcessKiller
}
}
- public string TryGetProcessFilename(Process p)
+ public unsafe string TryGetProcessFilename(Process p)
{
try
{
- int capacity = 2000;
- StringBuilder builder = new StringBuilder(capacity);
- IntPtr ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, p.Id);
- if (!QueryFullProcessImageName(ptr, 0, builder, ref capacity))
+ var handle = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, (uint)p.Id);
+ if (handle.Value == IntPtr.Zero)
{
- return String.Empty;
+ return string.Empty;
}
- return builder.ToString();
+ using var safeHandle = new SafeProcessHandle(handle.Value, true);
+ uint capacity = 2000;
+ Span buffer = new char[capacity];
+ fixed (char* pBuffer = buffer)
+ {
+ if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity))
+ {
+ return string.Empty;
+ }
+
+ return buffer[..(int)capacity].ToString();
+ }
}
catch
{
- return "";
+ return string.Empty;
}
}
-
- [Flags]
- private enum ProcessAccessFlags : uint
- {
- QueryLimitedInformation = 0x00001000
- }
-
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern bool QueryFullProcessImageName(
- [In] IntPtr hProcess,
- [In] int dwFlags,
- [Out] StringBuilder lpExeName,
- ref int lpdwSize);
-
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern IntPtr OpenProcess(
- ProcessAccessFlags processAccess,
- bool bInheritHandle,
- int processId);
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json
index 882ccd029..956c4b4e1 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json
@@ -4,7 +4,7 @@
"Name":"Process Killer",
"Description":"Kill running processes from Flow",
"Author":"Flow-Launcher",
- "Version":"3.0.7",
+ "Version":"3.0.8",
"Language":"csharp",
"Website":"https://github.com/Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller",
"IcoPath":"Images\\app.png",
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
index c0ad63cfe..99c1a12e9 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
@@ -52,6 +52,10 @@
PreserveNewest
+
+
+
+
@@ -61,6 +65,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml
index 7ed711e17..790c9d2c6 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml
@@ -36,6 +36,8 @@
Hides programs with common uninstaller names, such as unins000.exe
Search in Program Description
Flow will search program's description
+ Hide duplicated apps
+ Hide duplicated Win32 programs that are already in the UWP list
Suffixes
Max Depth
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
index e311a0b94..b3763aaa6 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
@@ -41,9 +41,38 @@ namespace Flow.Launcher.Plugin.Program
"uninst000.exe",
"uninstall.exe"
};
- // For cases when the uninstaller is named like "Uninstall Program Name.exe"
- private const string CommonUninstallerPrefix = "uninstall";
- private const string CommonUninstallerSuffix = ".exe";
+ private static readonly string[] commonUninstallerPrefixs =
+ {
+ "uninstall",//en
+ "卸载",//zh-cn
+ "卸載",//zh-tw
+ "видалити",//uk-UA
+ "удалить",//ru
+ "désinstaller",//fr
+ "アンインストール",//ja
+ "deïnstalleren",//nl
+ "odinstaluj",//pl
+ "afinstallere",//da
+ "deinstallieren",//de
+ "삭제",//ko
+ "деинсталирај",//sr
+ "desinstalar",//pt-pt
+ "desinstalar",//pt-br
+ "desinstalar",//es
+ "desinstalar",//es-419
+ "disinstallare",//it
+ "avinstallere",//nb-NO
+ "odinštalovať",//sk
+ "kaldır",//tr
+ "odinstalovat",//cs
+ "إلغاء التثبيت",//ar
+ "gỡ bỏ",//vi-vn
+ "הסרה"//he
+ };
+ private const string ExeUninstallerSuffix = ".exe";
+ private const string InkUninstallerSuffix = ".lnk";
+
+ private static readonly string WindowsAppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps");
static Main()
{
@@ -63,11 +92,20 @@ namespace Flow.Launcher.Plugin.Program
{
try
{
+ // Collect all UWP Windows app directories
+ var uwpsDirectories = _settings.HideDuplicatedWindowsApp ? _uwps
+ .Where(uwp => !string.IsNullOrEmpty(uwp.Location)) // Exclude invalid paths
+ .Where(uwp => uwp.Location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase)) // Keep system apps
+ .Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray() : null;
+
return _win32s.Cast()
.Concat(_uwps)
.AsParallel()
.WithCancellation(token)
.Where(HideUninstallersFilter)
+ .Where(p => HideDuplicatedWindowsAppFilter(p, uwpsDirectories))
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, Context.API))
.Where(r => r?.Score > 0)
@@ -96,10 +134,50 @@ namespace Flow.Launcher.Plugin.Program
{
if (!_settings.HideUninstallers) return true;
if (program is not Win32 win32) return true;
+
+ // First check the executable path
var fileName = Path.GetFileName(win32.ExecutablePath);
- return !commonUninstallerNames.Contains(fileName, StringComparer.OrdinalIgnoreCase) &&
- !(fileName.StartsWith(CommonUninstallerPrefix, StringComparison.OrdinalIgnoreCase) &&
- fileName.EndsWith(CommonUninstallerSuffix, StringComparison.OrdinalIgnoreCase));
+ // For cases when the uninstaller is named like "uninst.exe"
+ if (commonUninstallerNames.Contains(fileName, StringComparer.OrdinalIgnoreCase)) return false;
+ // For cases when the uninstaller is named like "Uninstall Program Name.exe"
+ foreach (var prefix in commonUninstallerPrefixs)
+ {
+ if (fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
+ fileName.EndsWith(ExeUninstallerSuffix, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+
+ // Second check the lnk path
+ if (!string.IsNullOrEmpty(win32.LnkResolvedPath))
+ {
+ var inkFileName = Path.GetFileName(win32.FullPath);
+ // For cases when the uninstaller is named like "Uninstall Program Name.ink"
+ foreach (var prefix in commonUninstallerPrefixs)
+ {
+ if (inkFileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
+ inkFileName.EndsWith(InkUninstallerSuffix, StringComparison.OrdinalIgnoreCase))
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool HideDuplicatedWindowsAppFilter(IProgram program, string[] uwpsDirectories)
+ {
+ if (uwpsDirectories == null || uwpsDirectories.Length == 0) return true;
+ if (program is UWPApp) return true;
+
+ var location = program.Location.TrimEnd('\\'); // Ensure trailing slash
+ if (string.IsNullOrEmpty(location))
+ return true; // Keep if location is invalid
+
+ if (!location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase))
+ return true; // Keep if not a Windows app
+
+ // Check if the any Win32 executable directory contains UWP Windows app location matches
+ return !uwpsDirectories.Any(uwpDirectory =>
+ location.StartsWith(uwpDirectory, StringComparison.OrdinalIgnoreCase));
}
public async Task InitAsync(PluginInitContext context)
@@ -214,6 +292,7 @@ namespace Flow.Launcher.Plugin.Program
Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"),
Context.API.GetTranslation(
"flowlauncher_plugin_program_disable_dlgtitle_success_message"));
+ Context.API.ReQuery();
return false;
},
IcoPath = "Images/disable.png",
diff --git a/Plugins/Flow.Launcher.Plugin.Program/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.Program/NativeMethods.txt
new file mode 100644
index 000000000..ecd547dff
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Program/NativeMethods.txt
@@ -0,0 +1,10 @@
+SHGetLocalizedName
+LoadString
+LoadLibraryEx
+FreeLibrary
+ExpandEnvironmentStrings
+S_OK
+SLGP_FLAGS
+WIN32_FIND_DATAW
+SLR_FLAGS
+IShellLinkW
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
index 78c66d604..a77b2ace8 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
@@ -1,97 +1,17 @@
using System;
-using System.Text;
using System.Runtime.InteropServices;
-using Accessibility;
using System.Runtime.InteropServices.ComTypes;
using Flow.Launcher.Plugin.Program.Logger;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.Shell;
+using Windows.Win32.Storage.FileSystem;
namespace Flow.Launcher.Plugin.Program.Programs
{
class ShellLinkHelper
{
- [Flags()]
- public enum SLGP_FLAGS
- {
- SLGP_SHORTPATH = 0x1,
- SLGP_UNCPRIORITY = 0x2,
- SLGP_RAWPATH = 0x4
- }
-
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
- public struct WIN32_FIND_DATAW
- {
- public uint dwFileAttributes;
- public long ftCreationTime;
- public long ftLastAccessTime;
- public long ftLastWriteTime;
- public uint nFileSizeHigh;
- public uint nFileSizeLow;
- public uint dwReserved0;
- public uint dwReserved1;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
- public string cFileName;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
- public string cAlternateFileName;
- }
-
- [Flags()]
- public enum SLR_FLAGS
- {
- SLR_NO_UI = 0x1,
- SLR_ANY_MATCH = 0x2,
- SLR_UPDATE = 0x4,
- SLR_NOUPDATE = 0x8,
- SLR_NOSEARCH = 0x10,
- SLR_NOTRACK = 0x20,
- SLR_NOLINKINFO = 0x40,
- SLR_INVOKE_MSI = 0x80
- }
-
-
+
// Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW
- /// The IShellLink interface allows Shell links to be created, modified, and resolved
- [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
- interface IShellLinkW
- {
- /// Retrieves the path and file name of a Shell link object
- void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
- /// Retrieves the list of item identifiers for a Shell link object
- void GetIDList(out IntPtr ppidl);
- /// Sets the pointer to an item identifier list (PIDL) for a Shell link object.
- void SetIDList(IntPtr pidl);
- /// Retrieves the description string for a Shell link object
- void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
- /// Sets the description for a Shell link object. The description can be any application-defined string
- void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
- /// Retrieves the name of the working directory for a Shell link object
- void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
- /// Sets the name of the working directory for a Shell link object
- void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
- /// Retrieves the command-line arguments associated with a Shell link object
- void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
- /// Sets the command-line arguments for a Shell link object
- void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
- /// Retrieves the hot key for a Shell link object
- void GetHotkey(out short pwHotkey);
- /// Sets a hot key for a Shell link object
- void SetHotkey(short wHotkey);
- /// Retrieves the show command for a Shell link object
- void GetShowCmd(out int piShowCmd);
- /// Sets the show command for a Shell link object. The show command sets the initial show state of the window.
- void SetShowCmd(int iShowCmd);
- /// Retrieves the location (path and index) of the icon for a Shell link object
- void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
- int cchIconPath, out int piIcon);
- /// Sets the location (path and index) of the icon for a Shell link object
- void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
- /// Sets the relative path to the Shell link object
- void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
- /// Attempts to find the target of a Shell link, even if it has been moved or renamed
- void Resolve(ref Accessibility._RemotableHandle hwnd, SLR_FLAGS fFlags);
- /// Sets the path and file name of a Shell link object
- void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
- }
-
[ComImport(), Guid("00021401-0000-0000-C000-000000000046")]
public class ShellLink
{
@@ -102,29 +22,43 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string arguments = string.Empty;
// Retrieve the target path using Shell Link
- public string retrieveTargetPath(string path)
+ public unsafe string retrieveTargetPath(string path)
{
var link = new ShellLink();
const int STGM_READ = 0;
((IPersistFile)link).Load(path, STGM_READ);
- var hwnd = new _RemotableHandle();
- ((IShellLinkW)link).Resolve(ref hwnd, 0);
+ var hwnd = new HWND(IntPtr.Zero);
+ ((IShellLinkW)link).Resolve(hwnd, 0);
const int MAX_PATH = 260;
- StringBuilder buffer = new StringBuilder(MAX_PATH);
+ Span buffer = stackalloc char[MAX_PATH];
var data = new WIN32_FIND_DATAW();
- ((IShellLinkW)link).GetPath(buffer, buffer.Capacity, ref data, SLGP_FLAGS.SLGP_SHORTPATH);
- var target = buffer.ToString();
+ var target = string.Empty;
+ try
+ {
+ fixed (char* bufferPtr = buffer)
+ {
+ ((IShellLinkW)link).GetPath((PWSTR)bufferPtr, MAX_PATH, &data, (uint)SLGP_FLAGS.SLGP_SHORTPATH);
+ target = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString();
+ }
+ }
+ catch (COMException e)
+ {
+ ProgramLogger.LogException($"|IShellLinkW|retrieveTargetPath|{path}" +
+ "|Error occurred while getting program arguments", e);
+ }
// To set the app description
- if (!String.IsNullOrEmpty(target))
+ if (!string.IsNullOrEmpty(target))
{
try
{
- buffer = new StringBuilder(MAX_PATH);
- ((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
- description = buffer.ToString();
+ fixed (char* bufferPtr = buffer)
+ {
+ ((IShellLinkW)link).GetDescription(bufferPtr, MAX_PATH);
+ description = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString();
+ }
}
catch (COMException e)
{
@@ -134,15 +68,17 @@ namespace Flow.Launcher.Plugin.Program.Programs
e);
}
- buffer.Clear();
- ((IShellLinkW)link).GetArguments(buffer, MAX_PATH);
- arguments = buffer.ToString();
+ fixed (char* bufferPtr = buffer)
+ {
+ ((IShellLinkW)link).GetArguments(bufferPtr, MAX_PATH);
+ arguments = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString();
+ }
}
-
+
// To release unmanaged memory
Marshal.ReleaseComObject(link);
return target;
- }
+ }
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs
index 4f344d89e..fac3ab181 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs
@@ -1,8 +1,9 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
-using System.Text;
-
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.System.LibraryLoader;
namespace Flow.Launcher.Plugin.Program.Programs
{
@@ -13,51 +14,41 @@ namespace Flow.Launcher.Plugin.Program.Programs
///
public static class ShellLocalization
{
- internal const uint DONTRESOLVEDLLREFERENCES = 0x00000001;
- internal const uint LOADLIBRARYASDATAFILE = 0x00000002;
-
- [DllImport("shell32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
- internal static extern int SHGetLocalizedName(string pszPath, StringBuilder pszResModule, ref int cch, out int pidsRes);
-
- [DllImport("user32.dll", EntryPoint = "LoadStringW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)]
- internal static extern int LoadString(IntPtr hModule, int resourceID, StringBuilder resourceValue, int len);
-
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "LoadLibraryExW")]
- internal static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags);
-
- [DllImport("kernel32.dll", ExactSpelling = true)]
- internal static extern int FreeLibrary(IntPtr hModule);
-
- [DllImport("kernel32.dll", EntryPoint = "ExpandEnvironmentStringsW", CharSet = CharSet.Unicode, ExactSpelling = true)]
- internal static extern uint ExpandEnvironmentStrings(string lpSrc, StringBuilder lpDst, int nSize);
-
///
/// Returns the localized name of a shell item.
///
/// Path to the shell item (e. g. shortcut 'File Explorer.lnk').
/// The localized name as string or .
- public static string GetLocalizedName(string path)
+ public static unsafe string GetLocalizedName(string path)
{
- StringBuilder resourcePath = new StringBuilder(1024);
- StringBuilder localizedName = new StringBuilder(1024);
- int len, id;
- len = resourcePath.Capacity;
+ const int capacity = 1024;
+ Span buffer = new char[capacity];
// If there is no resource to localize a file name the method returns a non zero value.
- if (SHGetLocalizedName(path, resourcePath, ref len, out id) == 0)
+ fixed (char* bufferPtr = buffer)
{
- _ = ExpandEnvironmentStrings(resourcePath.ToString(), resourcePath, resourcePath.Capacity);
- IntPtr hMod = LoadLibraryEx(resourcePath.ToString(), IntPtr.Zero, DONTRESOLVEDLLREFERENCES | LOADLIBRARYASDATAFILE);
- if (hMod != IntPtr.Zero)
+ var result = PInvoke.SHGetLocalizedName(path, bufferPtr, capacity, out var id);
+ if (result != HRESULT.S_OK)
{
- if (LoadString(hMod, id, localizedName, localizedName.Capacity) != 0)
- {
- string lString = localizedName.ToString();
- _ = FreeLibrary(hMod);
- return lString;
- }
+ return string.Empty;
+ }
- _ = FreeLibrary(hMod);
+ var resourcePathStr = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString();
+ _ = PInvoke.ExpandEnvironmentStrings(resourcePathStr, bufferPtr, capacity);
+ using var handle = PInvoke.LoadLibraryEx(resourcePathStr,
+ LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES | LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_DATAFILE);
+ if (handle.IsInvalid)
+ {
+ return string.Empty;
+ }
+
+ // not sure about the behavior of Pinvoke.LoadString, so we clear the buffer before using it (so it must be a null-terminated string)
+ buffer.Clear();
+
+ if (PInvoke.LoadString(handle, (uint)id, bufferPtr, capacity) != 0)
+ {
+ var lString = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString();
+ return lString;
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs
index fb24f64d7..b2aad63b3 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs
@@ -121,6 +121,7 @@ namespace Flow.Launcher.Plugin.Program
public bool EnableRegistrySource { get; set; } = true;
public bool EnablePathSource { get; set; } = false;
public bool EnableUWP { get; set; } = true;
+ public bool HideDuplicatedWindowsApp { get; set; } = false;
internal const char SuffixSeparator = ';';
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml
index e5ca6967e..973ac9f60 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml
@@ -8,7 +8,7 @@
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
-
+
@@ -18,40 +18,40 @@
+ ToolTip="{DynamicResource flowlauncher_plugin_program_index_uwp_tooltip}"
+ Visibility="{Binding ShowUWPCheckbox, Converter={StaticResource BooleanToVisibilityConverter}}" />
@@ -67,21 +67,20 @@
BorderBrush="{DynamicResource Color03B}"
BorderThickness="1" />
@@ -91,11 +90,15 @@
IsChecked="{Binding HideUninstallers}"
ToolTip="{DynamicResource flowlauncher_plugin_program_enable_hideuninstallers_tooltip}" />
+
@@ -142,7 +145,7 @@
Minimum="0" />
@@ -151,7 +154,7 @@
+ Margin="0 0 20 0">
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
index 36b5acc8a..91864cb68 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
@@ -57,6 +57,16 @@ namespace Flow.Launcher.Plugin.Program.Views
}
}
+ public bool HideDuplicatedWindowsApp
+ {
+ get => _settings.HideDuplicatedWindowsApp;
+ set
+ {
+ Main.ResetCache();
+ _settings.HideDuplicatedWindowsApp = value;
+ }
+ }
+
public bool EnableRegistrySource
{
get => _settings.EnableRegistrySource;
diff --git a/Plugins/Flow.Launcher.Plugin.Program/plugin.json b/Plugins/Flow.Launcher.Plugin.Program/plugin.json
index 6c718ce45..5a95e75f4 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Program/plugin.json
@@ -4,7 +4,7 @@
"Name": "Program",
"Description": "Search programs in Flow.Launcher",
"Author": "qianlifeng",
- "Version": "3.3.3",
+ "Version": "3.3.4",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml
index 52aaf3c27..645a0e14f 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml
@@ -7,6 +7,7 @@
Press any key to close this window...
Do not close Command Prompt after command execution
Always run as administrator
+ Use Windows Terminal
Run as different user
Shell
Allows to execute system commands from Flow Launcher
@@ -15,4 +16,4 @@
Run As Administrator
Copy the command
Only show number of most used commands:
-
\ No newline at end of file
+
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Main.cs b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
index 921c6bc21..53479b81f 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
@@ -5,7 +5,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using System.Windows;
using WindowsInput;
using WindowsInput.Native;
using Flow.Launcher.Infrastructure.Hotkey;
@@ -202,28 +201,31 @@ namespace Flow.Launcher.Plugin.Shell
{
case Shell.Cmd:
{
- info.FileName = "cmd.exe";
- info.Arguments = $"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}";
-
- //// Use info.Arguments instead of info.ArgumentList to enable users better control over the arguments they are writing.
- //// Previous code using ArgumentList, commands needed to be separated correctly:
- //// Incorrect:
- // info.ArgumentList.Add(_settings.LeaveShellOpen ? "/k" : "/c");
- // info.ArgumentList.Add(command); //<== info.ArgumentList.Add("mkdir \"c:\\test new\"");
-
- //// Correct version should be:
- //info.ArgumentList.Add(_settings.LeaveShellOpen ? "/k" : "/c");
- //info.ArgumentList.Add("mkdir");
- //info.ArgumentList.Add(@"c:\test new");
-
- //https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.argumentlist?view=net-6.0#remarks
+ if (_settings.UseWindowsTerminal)
+ {
+ info.FileName = "wt.exe";
+ info.ArgumentList.Add("cmd");
+ }
+ else
+ {
+ info.FileName = "cmd.exe";
+ }
+ info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}");
break;
}
case Shell.Powershell:
{
- info.FileName = "powershell.exe";
+ if (_settings.UseWindowsTerminal)
+ {
+ info.FileName = "wt.exe";
+ info.ArgumentList.Add("powershell");
+ }
+ else
+ {
+ info.FileName = "powershell.exe";
+ }
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
@@ -232,21 +234,28 @@ namespace Flow.Launcher.Plugin.Shell
else
{
info.ArgumentList.Add("-Command");
- info.ArgumentList.Add($"{command}; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'; [System.Console]::ReadKey(); exit" : "")}");
+ info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
}
break;
}
case Shell.Pwsh:
{
- info.FileName = "pwsh.exe";
+ if (_settings.UseWindowsTerminal)
+ {
+ info.FileName = "wt.exe";
+ info.ArgumentList.Add("pwsh");
+ }
+ else
+ {
+ info.FileName = "pwsh.exe";
+ }
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
}
info.ArgumentList.Add("-Command");
- info.ArgumentList.Add($"{command}; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'; [System.Console]::ReadKey(); exit" : "")}");
-
+ info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
break;
}
@@ -369,7 +378,7 @@ namespace Flow.Launcher.Plugin.Shell
private void OnWinRPressed()
{
// show the main window and set focus to the query box
- Task.Run(() =>
+ _ = Task.Run(() =>
{
context.API.ShowMainWindow();
context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeywords[0]}{Plugin.Query.TermSeparator}");
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs b/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs
index 6f47d5d17..9ce2293a2 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs
@@ -14,6 +14,8 @@ namespace Flow.Launcher.Plugin.Shell
public bool RunAsAdministrator { get; set; } = true;
+ public bool UseWindowsTerminal { get; set; } = false;
+
public bool ShowOnlyMostUsedCMDs { get; set; }
public int ShowOnlyMostUsedCMDsNumber { get; set; }
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/ShellSetting.xaml b/Plugins/Flow.Launcher.Plugin.Shell/ShellSetting.xaml
index 2f02ef723..8a3b7f115 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/ShellSetting.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Shell/ShellSetting.xaml
@@ -16,6 +16,7 @@
+
+
CMD
@@ -51,7 +58,7 @@
Pwsh
RunCommand
-
+
+ {
+ _settings.UseWindowsTerminal = true;
+ };
+
+ UseWindowsTerminal.Unchecked += (o, e) =>
+ {
+ _settings.UseWindowsTerminal = false;
+ };
+
ReplaceWinR.Checked += (o, e) =>
{
_settings.ReplaceWinR = true;
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/plugin.json b/Plugins/Flow.Launcher.Plugin.Shell/plugin.json
index 6c81e8a26..681e8f751 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Shell/plugin.json
@@ -4,7 +4,7 @@
"Name": "Shell",
"Description": "Provide executing commands from Flow Launcher",
"Author": "qianlifeng",
- "Version": "3.2.4",
+ "Version": "3.2.5",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
index b797b3cf4..266c24170 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj
@@ -39,6 +39,7 @@
+
@@ -57,4 +58,11 @@
PreserveNewest
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Images/theme_selector.png b/Plugins/Flow.Launcher.Plugin.Sys/Images/theme_selector.png
new file mode 100644
index 000000000..704e9474e
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Sys/Images/theme_selector.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml
index 91f32a844..2a266f8f6 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml
@@ -27,6 +27,7 @@
Flow Launcher Tips
Flow Launcher UserData Folder
Toggle Game Mode
+ Set the Flow Launcher Theme
Shutdown Computer
@@ -49,8 +50,9 @@
Visit Flow Launcher's documentation for more help and how to use tips
Open the location where Flow Launcher's settings are stored
Toggle Game Mode
+ Quickly change the Flow Launcher theme
-
+
Success
All Flow Launcher settings saved
Reloaded all applicable plugin data
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
index e1b78c1bd..28747cc7c 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
@@ -4,49 +4,27 @@ using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
-using System.Windows.Forms;
-using System.Windows.Interop;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
-using Flow.Launcher.Plugin.SharedCommands;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Security;
+using Windows.Win32.System.Shutdown;
using Application = System.Windows.Application;
using Control = System.Windows.Controls.Control;
-using FormsApplication = System.Windows.Forms.Application;
namespace Flow.Launcher.Plugin.Sys
{
public class Main : IPlugin, ISettingProvider, IPluginI18n
{
private PluginInitContext context;
+ private ThemeSelector themeSelector;
private Dictionary KeywordTitleMappings = new Dictionary();
- #region DllImport
-
- internal const int EWX_LOGOFF = 0x00000000;
- internal const int EWX_SHUTDOWN = 0x00000001;
- internal const int EWX_REBOOT = 0x00000002;
- internal const int EWX_FORCE = 0x00000004;
- internal const int EWX_POWEROFF = 0x00000008;
- internal const int EWX_FORCEIFHUNG = 0x00000010;
-
- [DllImport("user32")]
- private static extern bool ExitWindowsEx(uint uFlags, uint dwReason);
-
- [DllImport("user32")]
- private static extern void LockWorkStation();
-
- [DllImport("Shell32.dll", CharSet = CharSet.Unicode)]
- private static extern uint SHEmptyRecycleBin(IntPtr hWnd, uint dwFlags);
-
- // http://www.pinvoke.net/default.aspx/Enums/HRESULT.html
- private enum HRESULT : uint
- {
- S_FALSE = 0x0001,
- S_OK = 0x0000
- }
-
- #endregion
+ // SHTDN_REASON_MAJOR_OTHER indicates a generic shutdown reason that isn't categorized under hardware failure, software updates, or other predefined reasons.
+ // SHTDN_REASON_FLAG_PLANNED marks the shutdown as planned rather than an unexpected shutdown or failure
+ private const SHUTDOWN_REASON REASON = SHUTDOWN_REASON.SHTDN_REASON_MAJOR_OTHER | SHUTDOWN_REASON.SHTDN_REASON_FLAG_PLANNED;
public Control CreateSettingPanel()
{
@@ -56,6 +34,11 @@ namespace Flow.Launcher.Plugin.Sys
public List Query(Query query)
{
+ if(query.Search.StartsWith(ThemeSelector.Keyword))
+ {
+ return themeSelector.Query(query);
+ }
+
var commands = Commands();
var results = new List();
foreach (var c in commands)
@@ -104,6 +87,7 @@ namespace Flow.Launcher.Plugin.Sys
public void Init(PluginInitContext context)
{
this.context = context;
+ themeSelector = new ThemeSelector(context);
KeywordTitleMappings = new Dictionary{
{"Shutdown", "flowlauncher_plugin_sys_shutdown_computer_cmd"},
{"Restart", "flowlauncher_plugin_sys_restart_computer_cmd"},
@@ -124,10 +108,49 @@ namespace Flow.Launcher.Plugin.Sys
{"Open Log Location", "flowlauncher_plugin_sys_open_log_location_cmd"},
{"Flow Launcher Tips", "flowlauncher_plugin_sys_open_docs_tips_cmd"},
{"Flow Launcher UserData Folder", "flowlauncher_plugin_sys_open_userdata_location_cmd"},
- {"Toggle Game Mode", "flowlauncher_plugin_sys_toggle_game_mode_cmd"}
+ {"Toggle Game Mode", "flowlauncher_plugin_sys_toggle_game_mode_cmd"},
+ {"Set Flow Launcher Theme", "flowlauncher_plugin_sys_theme_selector_cmd"}
};
}
+ private static unsafe bool EnableShutdownPrivilege()
+ {
+ try
+ {
+ if (!PInvoke.OpenProcessToken(Process.GetCurrentProcess().SafeHandle, TOKEN_ACCESS_MASK.TOKEN_ADJUST_PRIVILEGES | TOKEN_ACCESS_MASK.TOKEN_QUERY, out var tokenHandle))
+ {
+ return false;
+ }
+
+ if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_SHUTDOWN_NAME, out var luid))
+ {
+ return false;
+ }
+
+ var privileges = new TOKEN_PRIVILEGES
+ {
+ PrivilegeCount = 1,
+ Privileges = new() { e0 = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = TOKEN_PRIVILEGES_ATTRIBUTES.SE_PRIVILEGE_ENABLED } }
+ };
+
+ if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, &privileges, 0, null, null))
+ {
+ return false;
+ }
+
+ if (Marshal.GetLastWin32Error() != (int)WIN32_ERROR.NO_ERROR)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
private List Commands()
{
var results = new List();
@@ -148,10 +171,12 @@ namespace Flow.Launcher.Plugin.Sys
context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_shutdown_computer"),
context.API.GetTranslation("flowlauncher_plugin_sys_shutdown_computer"),
MessageBoxButton.YesNo, MessageBoxImage.Warning);
+
if (result == MessageBoxResult.Yes)
- {
- Process.Start("shutdown", "/s /t 0");
- }
+ if (EnableShutdownPrivilege())
+ PInvoke.ExitWindowsEx(EXIT_WINDOWS_FLAGS.EWX_SHUTDOWN | EXIT_WINDOWS_FLAGS.EWX_POWEROFF, REASON);
+ else
+ Process.Start("shutdown", "/s /t 0");
return true;
}
@@ -168,10 +193,12 @@ namespace Flow.Launcher.Plugin.Sys
context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_restart_computer"),
context.API.GetTranslation("flowlauncher_plugin_sys_restart_computer"),
MessageBoxButton.YesNo, MessageBoxImage.Warning);
+
if (result == MessageBoxResult.Yes)
- {
- Process.Start("shutdown", "/r /t 0");
- }
+ if (EnableShutdownPrivilege())
+ PInvoke.ExitWindowsEx(EXIT_WINDOWS_FLAGS.EWX_REBOOT, REASON);
+ else
+ Process.Start("shutdown", "/r /t 0");
return true;
}
@@ -190,7 +217,10 @@ namespace Flow.Launcher.Plugin.Sys
MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
- Process.Start("shutdown", "/r /o /t 0");
+ if (EnableShutdownPrivilege())
+ PInvoke.ExitWindowsEx(EXIT_WINDOWS_FLAGS.EWX_REBOOT | EXIT_WINDOWS_FLAGS.EWX_BOOTOPTIONS, REASON);
+ else
+ Process.Start("shutdown", "/r /o /t 0");
return true;
}
@@ -209,7 +239,7 @@ namespace Flow.Launcher.Plugin.Sys
MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
- ExitWindowsEx(EWX_LOGOFF, 0);
+ PInvoke.ExitWindowsEx(EXIT_WINDOWS_FLAGS.EWX_LOGOFF, REASON);
return true;
}
@@ -222,7 +252,7 @@ namespace Flow.Launcher.Plugin.Sys
IcoPath = "Images\\lock.png",
Action = c =>
{
- LockWorkStation();
+ PInvoke.LockWorkStation();
return true;
}
},
@@ -232,7 +262,11 @@ namespace Flow.Launcher.Plugin.Sys
SubTitle = context.API.GetTranslation("flowlauncher_plugin_sys_sleep"),
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xec46"),
IcoPath = "Images\\sleep.png",
- Action = c => FormsApplication.SetSuspendState(PowerState.Suspend, false, false)
+ Action = c =>
+ {
+ PInvoke.SetSuspendState(false, false, false);
+ return true;
+ }
},
new Result
{
@@ -242,12 +276,7 @@ namespace Flow.Launcher.Plugin.Sys
IcoPath = "Images\\hibernate.png",
Action= c =>
{
- var info = ShellCommand.SetProcessStartInfo("shutdown", arguments:"/h");
- info.WindowStyle = ProcessWindowStyle.Hidden;
- info.UseShellExecute = true;
-
- ShellCommand.Execute(info);
-
+ PInvoke.SetSuspendState(true, false, false);
return true;
}
},
@@ -259,10 +288,7 @@ namespace Flow.Launcher.Plugin.Sys
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe773"),
Action = c =>
{
- {
- System.Diagnostics.Process.Start("control.exe", "srchadmin.dll");
- }
-
+ Process.Start("control.exe", "srchadmin.dll");
return true;
}
},
@@ -277,11 +303,13 @@ namespace Flow.Launcher.Plugin.Sys
// http://www.pinvoke.net/default.aspx/shell32/SHEmptyRecycleBin.html
// FYI, couldn't find documentation for this but if the recycle bin is already empty, it will return -2147418113 (0x8000FFFF (E_UNEXPECTED))
// 0 for nothing
- var result = SHEmptyRecycleBin(new WindowInteropHelper(Application.Current.MainWindow).Handle, 0);
- if (result != (uint) HRESULT.S_OK && result != (uint) 0x8000FFFF)
+ var result = PInvoke.SHEmptyRecycleBin(new(), string.Empty, 0);
+ if (result != HRESULT.S_OK && result != HRESULT.E_UNEXPECTED)
{
- context.API.ShowMsgBox($"Error emptying recycle bin, error code: {result}\n" +
- "please refer to https://msdn.microsoft.com/en-us/library/windows/desktop/aa378137",
+ context.API.ShowMsgBox("Failed to empty the recycle bin. This might happen if:\n" +
+ "- A file in the recycle bin is in use\n" +
+ "- You don't have permission to delete some items\n" +
+ "Please close any applications that might be using these files and try again.",
"Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
@@ -298,10 +326,7 @@ namespace Flow.Launcher.Plugin.Sys
CopyText = recycleBinFolder,
Action = c =>
{
- {
- System.Diagnostics.Process.Start("explorer", recycleBinFolder);
- }
-
+ Process.Start("explorer", recycleBinFolder);
return true;
}
},
@@ -359,7 +384,7 @@ namespace Flow.Launcher.Plugin.Sys
Action = c =>
{
// Hide the window first then show msg after done because sometimes the reload could take a while, so not to make user think it's frozen.
- Application.Current.MainWindow.Hide();
+ context.API.HideMainWindow();
_ = context.API.ReloadAllPluginData().ContinueWith(_ =>
context.API.ShowMsg(
@@ -378,7 +403,7 @@ namespace Flow.Launcher.Plugin.Sys
IcoPath = "Images\\checkupdate.png",
Action = c =>
{
- Application.Current.MainWindow.Hide();
+ context.API.HideMainWindow();
context.API.CheckForNewUpdate();
return true;
}
@@ -433,6 +458,18 @@ namespace Flow.Launcher.Plugin.Sys
context.API.ToggleGameMode();
return true;
}
+ },
+ new Result
+ {
+ Title = "Set Flow Launcher Theme",
+ SubTitle = context.API.GetTranslation("flowlauncher_plugin_sys_theme_selector"),
+ IcoPath = "Images\\app.png",
+ Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue7fc"),
+ Action = c =>
+ {
+ context.API.ChangeQuery($"{ThemeSelector.Keyword} ");
+ return false;
+ }
}
});
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.Sys/NativeMethods.txt
new file mode 100644
index 000000000..4567e46a3
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Sys/NativeMethods.txt
@@ -0,0 +1,12 @@
+ExitWindowsEx
+LockWorkStation
+SHEmptyRecycleBin
+S_OK
+E_UNEXPECTED
+SetSuspendState
+OpenProcessToken
+WIN32_ERROR
+LookupPrivilegeValue
+AdjustTokenPrivileges
+TOKEN_PRIVILEGES
+SE_SHUTDOWN_NAME
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/ThemeSelector.cs b/Plugins/Flow.Launcher.Plugin.Sys/ThemeSelector.cs
new file mode 100644
index 000000000..38619cbbc
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Sys/ThemeSelector.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using System.Linq;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Infrastructure.UserSettings;
+
+namespace Flow.Launcher.Plugin.Sys
+{
+ public class ThemeSelector
+ {
+ public const string Keyword = "fltheme";
+
+ private readonly Settings _settings;
+ private readonly Theme _theme;
+ private readonly PluginInitContext _context;
+
+ #region Theme Selection
+
+ // Theme select codes simplified from SettingsPaneThemeViewModel.cs
+
+ private Theme.ThemeData _selectedTheme;
+ private Theme.ThemeData SelectedTheme
+ {
+ get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.CurrentTheme);
+ set
+ {
+ _selectedTheme = value;
+ _theme.ChangeTheme(value.FileNameWithoutExtension);
+
+ if (_theme.BlurEnabled && _settings.UseDropShadowEffect)
+ {
+ _theme.RemoveDropShadowEffectFromCurrentTheme();
+ _settings.UseDropShadowEffect = false;
+ }
+ }
+ }
+
+ private List Themes => _theme.LoadAvailableThemes();
+
+ #endregion
+
+ public ThemeSelector(PluginInitContext context)
+ {
+ _context = context;
+ _theme = Ioc.Default.GetRequiredService();
+ _settings = Ioc.Default.GetRequiredService();
+ }
+
+ public List Query(Query query)
+ {
+ var search = query.SecondToEndSearch;
+ if (string.IsNullOrWhiteSpace(search))
+ {
+ return Themes.Select(CreateThemeResult)
+ .OrderBy(x => x.Title)
+ .ToList();
+ }
+
+ return Themes.Select(theme => (theme, matchResult: _context.API.FuzzySearch(search, theme.Name)))
+ .Where(x => x.matchResult.IsSearchPrecisionScoreMet())
+ .Select(x => CreateThemeResult(x.theme, x.matchResult.Score, x.matchResult.MatchData))
+ .OrderBy(x => x.Title)
+ .ToList();
+ }
+
+ private Result CreateThemeResult(Theme.ThemeData theme) => CreateThemeResult(theme, 0, null);
+
+ private Result CreateThemeResult(Theme.ThemeData theme, int score, IList highlightData)
+ {
+ string themeName = theme.Name;
+ string title;
+ if (theme == SelectedTheme)
+ {
+ title = $"{theme.Name} ★";
+ // Set current theme to the top
+ score = 2000;
+ }
+ else
+ {
+ title = theme.Name;
+ // Set them to 1000 so that they are higher than other non-theme records
+ score = 1000;
+ }
+
+ string description = string.Empty;
+ if (theme.IsDark == true)
+ {
+ description += _context.API.GetTranslation("TypeIsDarkToolTip");
+ }
+
+ if (theme.HasBlur == true)
+ {
+ if (!string.IsNullOrEmpty(description))
+ description += " ";
+ description += _context.API.GetTranslation("TypeHasBlurToolTip");
+ }
+
+ return new Result
+ {
+ Title = title,
+ TitleHighlightData = highlightData,
+ SubTitle = description,
+ IcoPath = "Images\\theme_selector.png",
+ Glyph = new GlyphInfo("/Resources/#Segoe Fluent Icons", "\ue790"),
+ Score = score,
+ Action = c =>
+ {
+ SelectedTheme = theme;
+ _context.API.ReQuery();
+ return false;
+ }
+ };
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json
index 75be43587..90ca264cc 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json
@@ -4,7 +4,7 @@
"Name": "System Commands",
"Description": "Provide System related commands. e.g. shutdown,lock, setting etc.",
"Author": "qianlifeng",
- "Version": "3.1.6",
+ "Version": "3.1.7",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj
index 3db0cd0cb..6d338733e 100644
--- a/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj
@@ -42,7 +42,6 @@
-
diff --git a/Plugins/Flow.Launcher.Plugin.Url/Main.cs b/Plugins/Flow.Launcher.Plugin.Url/Main.cs
index 80425a8ff..03516636d 100644
--- a/Plugins/Flow.Launcher.Plugin.Url/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Url/Main.cs
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
-using System.Windows.Controls;
namespace Flow.Launcher.Plugin.Url
{
- public class Main : ISettingProvider,IPlugin, IPluginI18n
+ public class Main : IPlugin, IPluginI18n
{
//based on https://gist.github.com/dperini/729294
private const string urlPattern = "^" +
@@ -43,7 +42,6 @@ namespace Flow.Launcher.Plugin.Url
Regex reg = new Regex(urlPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
private PluginInitContext context;
private Settings _settings;
-
public List Query(Query query)
{
@@ -82,12 +80,6 @@ namespace Flow.Launcher.Plugin.Url
return new List(0);
}
-
- public Control CreateSettingPanel()
- {
- return new SettingsControl(context.API,_settings);
- }
-
public bool IsURL(string raw)
{
raw = raw.ToLower();
diff --git a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml
deleted file mode 100644
index 8ff7b5ab5..000000000
--- a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml.cs
deleted file mode 100644
index f68d1bb2d..000000000
--- a/Plugins/Flow.Launcher.Plugin.Url/SettingsControl.xaml.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Windows.Controls;
-
-namespace Flow.Launcher.Plugin.Url
-{
- public partial class SettingsControl : UserControl
- {
- private Settings _settings;
- private IPublicAPI _flowlauncherAPI;
-
- public SettingsControl(IPublicAPI flowlauncherAPI,Settings settings)
- {
- InitializeComponent();
- _settings = settings;
- _flowlauncherAPI = flowlauncherAPI;
-
- }
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Url/plugin.json b/Plugins/Flow.Launcher.Plugin.Url/plugin.json
index cab056870..73d9bff30 100644
--- a/Plugins/Flow.Launcher.Plugin.Url/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Url/plugin.json
@@ -4,7 +4,7 @@
"Name": "URL",
"Description": "Open the typed URL from Flow Launcher",
"Author": "qianlifeng",
- "Version": "3.0.7",
+ "Version": "3.0.8",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Url.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/ru.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/ru.xaml
index ea163da7c..c59182290 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/ru.xaml
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/ru.xaml
@@ -1,4 +1,4 @@
-
+
Search Source Setting
diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json
index 0314ddfdb..6b6792ad3 100644
--- a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json
@@ -26,7 +26,7 @@
"Name": "Web Searches",
"Description": "Provide the web search ability",
"Author": "qianlifeng",
- "Version": "3.1.3",
+ "Version": "3.1.4",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",
diff --git a/Plugins/Flow.Launcher.Plugin.WindowsSettings/Properties/Resources.de-DE.resx b/Plugins/Flow.Launcher.Plugin.WindowsSettings/Properties/Resources.de-DE.resx
index dac5f82bc..dcc74d520 100644
--- a/Plugins/Flow.Launcher.Plugin.WindowsSettings/Properties/Resources.de-DE.resx
+++ b/Plugins/Flow.Launcher.Plugin.WindowsSettings/Properties/Resources.de-DE.resx
@@ -2130,7 +2130,7 @@
Ändern, wie der Mauszeiger ausschaut
- Change power-saving settings
+ Energiespareinstellungen ändern
Optimieren für Blindheit
@@ -2143,7 +2143,7 @@
Windows-Features ein- oder ausschalten
- Show which operating system your computer is running
+ Betriebssystem, welches auf deinem Computer läuft, anzeigen
Lokale Dienste ansehen
diff --git a/Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json b/Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json
index 5c717827e..413a555d3 100644
--- a/Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json
@@ -4,7 +4,7 @@
"Description": "Search settings inside Control Panel and Settings App",
"Name": "Windows Settings",
"Author": "TobiasSekan",
- "Version": "4.0.11",
+ "Version": "4.0.12",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.WindowsSettings.dll",
diff --git a/README.md b/README.md
index 02ffc7932..6611f55dc 100644
--- a/README.md
+++ b/README.md
@@ -334,11 +334,14 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
-
-
+
+
+
+
+
diff --git a/appveyor.yml b/appveyor.yml
index 421088133..af5aaefdc 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: '1.19.4.{build}'
+version: '1.19.5.{build}'
init:
- ps: |