diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 451df6147..7e9cc9a48 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -4,6 +4,7 @@ using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Windows; using System.Windows.Forms; @@ -116,7 +117,10 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments foreach (var metadata in PluginMetadataList) { if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase)) + { + metadata.AssemblyName = string.Empty; pluginPairs.Add(CreatePluginPair(filePath, metadata)); + } } return pluginPairs; diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 97c3c8981..88d595301 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -1,28 +1,16 @@ using Flow.Launcher.Core.Resource; -using Flow.Launcher.Infrastructure; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Microsoft.IO; using System.Windows; -using System.Windows.Controls; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; -using CheckBox = System.Windows.Controls.CheckBox; -using Control = System.Windows.Controls.Control; -using Orientation = System.Windows.Controls.Orientation; -using TextBox = System.Windows.Controls.TextBox; -using UserControl = System.Windows.Controls.UserControl; -using System.Windows.Documents; namespace Flow.Launcher.Core.Plugin { @@ -42,7 +30,7 @@ namespace Flow.Launcher.Core.Plugin private int RequestId { get; set; } private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); + private string SettingPath => Path.Combine(Context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "Settings.json"); public override List LoadContextMenus(Result selectedResult) { diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index 7248c6259..779dcf887 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -1,32 +1,15 @@ using Flow.Launcher.Core.Resource; -using Flow.Launcher.Infrastructure; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Microsoft.IO; -using System.Windows; -using System.Windows.Controls; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -using CheckBox = System.Windows.Controls.CheckBox; using Control = System.Windows.Controls.Control; -using Orientation = System.Windows.Controls.Orientation; -using TextBox = System.Windows.Controls.TextBox; -using UserControl = System.Windows.Controls.UserControl; -using System.Windows.Documents; -using static System.Windows.Forms.LinkLabel; -using Droplex; -using System.Windows.Forms; -using Microsoft.VisualStudio.Threading; namespace Flow.Launcher.Core.Plugin { @@ -44,8 +27,7 @@ namespace Flow.Launcher.Core.Plugin private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory, - Context.CurrentPluginMetadata.Name); + private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath; private string SettingPath => Path.Combine(SettingDirectory, "Settings.json"); @@ -166,13 +148,5 @@ namespace Flow.Launcher.Core.Plugin { return Settings.CreateSettingPanel(); } - - public void DeletePluginSettingsDirectory() - { - if (Directory.Exists(SettingDirectory)) - { - Directory.Delete(SettingDirectory, true); - } - } } } diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index dd6517a7f..163f97046 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.IO; @@ -9,7 +9,6 @@ using System.Text.Json; namespace Flow.Launcher.Core.Plugin { - internal abstract class PluginConfig { /// @@ -112,7 +111,7 @@ namespace Flow.Launcher.Core.Plugin metadata = JsonSerializer.Deserialize(File.ReadAllText(configPath)); metadata.PluginDirectory = pluginDirectory; // for plugins which doesn't has ActionKeywords key - metadata.ActionKeywords = metadata.ActionKeywords ?? new List { metadata.ActionKeyword }; + metadata.ActionKeywords ??= new List { metadata.ActionKeyword }; // for plugin still use old ActionKeyword metadata.ActionKeyword = metadata.ActionKeywords?[0]; } @@ -137,4 +136,4 @@ namespace Flow.Launcher.Core.Plugin return metadata; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index aab3caa40..eef4a0b71 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -35,7 +35,7 @@ namespace Flow.Launcher.Core.Plugin private static PluginsSettings Settings; private static List _metadatas; - private static List _modifiedPlugins = new List(); + private static List _modifiedPlugins = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -72,15 +72,20 @@ namespace Flow.Launcher.Core.Plugin { foreach (var pluginPair in AllPlugins) { - switch (pluginPair.Plugin) - { - case IDisposable disposable: - disposable.Dispose(); - break; - case IAsyncDisposable asyncDisposable: - await asyncDisposable.DisposeAsync(); - break; - } + await DisposePluginAsync(pluginPair); + } + } + + private static async Task DisposePluginAsync(PluginPair pluginPair) + { + switch (pluginPair.Plugin) + { + case IDisposable disposable: + disposable.Dispose(); + break; + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync(); + break; } } @@ -155,6 +160,25 @@ namespace Flow.Launcher.Core.Plugin Settings = settings; Settings.UpdatePluginSettings(_metadatas); AllPlugins = PluginsLoader.Plugins(_metadatas, Settings); + // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins + UpdatePluginDirectory(_metadatas); + } + + private static void UpdatePluginDirectory(List metadatas) + { + foreach (var metadata in metadatas) + { + if (AllowedLanguage.IsDotNet(metadata.Language)) + { + metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName); + metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName); + } + else + { + metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name); + metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name); + } + } } /// @@ -225,10 +249,9 @@ namespace Flow.Launcher.Core.Plugin if (query is null) return Array.Empty(); - if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) + if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) return GlobalPlugins; - var plugin = NonGlobalPlugins[query.ActionKeyword]; return new List { plugin @@ -442,10 +465,10 @@ namespace Flow.Launcher.Core.Plugin /// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url, /// unless it's a local path installation /// - public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) + public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) { InstallPlugin(newVersion, zipFilePath, checkModified:false); - UninstallPlugin(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); + await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); _modifiedPlugins.Add(existingVersion.ID); } @@ -460,9 +483,9 @@ namespace Flow.Launcher.Core.Plugin /// /// Uninstall a plugin. /// - public static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false) + public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false) { - UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, true); + await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true); } #endregion @@ -543,63 +566,62 @@ namespace Flow.Launcher.Core.Plugin } } - internal static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) + internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) { if (checkModified && PluginModified(plugin.ID)) { throw new ArgumentException($"Plugin {plugin.Name} has been modified"); } + if (removePluginSettings || removePluginFromSettings) + { + // If we want to remove plugin from AllPlugins, + // we need to dispose them so that they can release file handles + // which can help FL to delete the plugin settings & cache folders successfully + var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID); + foreach (var pluginPair in pluginPairs) + { + await DisposePluginAsync(pluginPair); + } + } + if (removePluginSettings) { - if (AllowedLanguage.IsDotNet(plugin.Language)) // for the plugin in .NET, we can use assembly loader + // For dotnet plugins, we need to remove their PluginJsonStorage instance + if (AllowedLanguage.IsDotNet(plugin.Language)) { - 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)); - } - } + method?.Invoke(API, new object[] { plugin.AssemblyName }); } - else // the plugin with json prc interface + + try { - 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)); - } - } + var pluginSettingsDirectory = plugin.PluginSettingsDirectoryPath; + if (Directory.Exists(pluginSettingsDirectory)) + Directory.Delete(pluginSettingsDirectory, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin settings folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); } } if (removePluginFromSettings) { + try + { + var pluginCacheDirectory = plugin.PluginCacheDirectoryPath; + if (Directory.Exists(pluginCacheDirectory)) + Directory.Delete(pluginCacheDirectory, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin cache folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"), + string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); + } 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 4827cf69d..a64457ffc 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -74,9 +74,11 @@ namespace Flow.Launcher.Core.Plugin typeof(IAsyncPlugin)); plugin = Activator.CreateInstance(type) as IAsyncPlugin; + + metadata.AssemblyName = assembly.GetName().Name; } #if DEBUG - catch (Exception e) + catch (Exception) { throw; } @@ -112,7 +114,7 @@ namespace Flow.Launcher.Core.Plugin if (erroredPlugins.Count > 0) { - var errorPluginString = String.Join(Environment.NewLine, erroredPlugins); + var errorPluginString = string.Join(Environment.NewLine, erroredPlugins); var errorMessage = "The following " + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") @@ -134,9 +136,13 @@ namespace Flow.Launcher.Core.Plugin { return source .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) - .Select(metadata => new PluginPair + .Select(metadata => { - Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata + return new PluginPair + { + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), + Metadata = metadata + }; }); } @@ -144,9 +150,13 @@ namespace Flow.Launcher.Core.Plugin { return source .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) - .Select(metadata => new PluginPair + .Select(metadata => { - Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata + return new PluginPair + { + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), + Metadata = metadata + }; }); } } diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 489002617..e5980b62f 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -24,7 +24,7 @@ namespace Flow.Launcher.Core.Resource { #region Properties & Fields - public bool BlurEnabled { get; set; } + public bool BlurEnabled { get; private set; } private const string ThemeMetadataNamePrefix = "Name:"; private const string ThemeMetadataIsDarkPrefix = "IsDark:"; @@ -42,6 +42,8 @@ namespace Flow.Launcher.Core.Resource private static string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); private static string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); + private Thickness _themeResizeBorderThickness; + #endregion #region Constructor @@ -463,7 +465,7 @@ namespace Flow.Launcher.Core.Resource var effectSetter = new Setter { - Property = Border.EffectProperty, + Property = UIElement.EffectProperty, Value = new DropShadowEffect { Opacity = 0.3, @@ -473,12 +475,12 @@ namespace Flow.Launcher.Core.Resource } }; - if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) is not Setter marginSetter) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is not Setter marginSetter) { var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); marginSetter = new Setter() { - Property = Border.MarginProperty, + Property = FrameworkElement.MarginProperty, Value = margin, }; windowBorderStyle.Setters.Add(marginSetter); @@ -508,12 +510,12 @@ namespace Flow.Launcher.Core.Resource var dict = GetCurrentResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; - if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) is Setter effectSetter) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == UIElement.EffectProperty) is Setter effectSetter) { windowBorderStyle.Setters.Remove(effectSetter); } - if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) is Setter marginSetter) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is Setter marginSetter) { var currentMargin = (Thickness)marginSetter.Value; var newMargin = new Thickness( @@ -529,28 +531,41 @@ namespace Flow.Launcher.Core.Resource UpdateResourceDictionary(dict); } + public void SetResizeBorderThickness(WindowChrome windowChrome, bool fixedWindowSize) + { + if (fixedWindowSize) + { + windowChrome.ResizeBorderThickness = new Thickness(0); + } + else + { + windowChrome.ResizeBorderThickness = _themeResizeBorderThickness; + } + } + // 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) + private void SetResizeBoarderThickness(Thickness? effectMargin) { var window = Application.Current.MainWindow; if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome) { - Thickness thickness; + // Save the theme resize border thickness so that we can restore it if we change ResizeWindow setting if (effectMargin == null) { - thickness = SystemParameters.WindowResizeBorderThickness; + _themeResizeBorderThickness = SystemParameters.WindowResizeBorderThickness; } else { - thickness = new Thickness( + _themeResizeBorderThickness = 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; + // Apply the resize border thickness to the window chrome + SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults); } } @@ -582,7 +597,7 @@ namespace Flow.Launcher.Core.Resource { AutoDropShadow(useDropShadowEffect); } - }, DispatcherPriority.Normal); + }, DispatcherPriority.Render); } /// @@ -596,7 +611,7 @@ namespace Flow.Launcher.Core.Resource var (backdropType, _) = GetActualValue(); SetBlurForWindow(GetCurrentTheme(), backdropType); - }, DispatcherPriority.Normal); + }, DispatcherPriority.Render); } /// diff --git a/Flow.Launcher.Core/Resource/ThemeManager.cs b/Flow.Launcher.Core/Resource/ThemeManager.cs deleted file mode 100644 index 3cbe8319a..000000000 --- a/Flow.Launcher.Core/Resource/ThemeManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.DependencyInjection; - -namespace Flow.Launcher.Core.Resource -{ - [Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService() instead.")] - public class ThemeManager - { - public static Theme Instance - => Ioc.Default.GetRequiredService(); - } -} diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index b4b2485c9..13da9f79f 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -48,6 +48,7 @@ namespace Flow.Launcher.Infrastructure public const string Themes = "Themes"; public const string Settings = "Settings"; public const string Logs = "Logs"; + public const string Cache = "Cache"; public const string Website = "https://flowlauncher.com"; public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher"; diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 7f847e287..9f5d6725e 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -12,13 +12,13 @@ namespace Flow.Launcher.Infrastructure.Logger { public static class Log { - public const string DirectoryName = "Logs"; + public const string DirectoryName = Constant.Logs; public static string CurrentLogDirectory { get; } static Log() { - CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version); + CurrentLogDirectory = DataLocation.VersionLogDirectory; if (!Directory.Exists(CurrentLogDirectory)) { Directory.CreateDirectory(CurrentLogDirectory); diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index 2a439b8cc..5b73faae6 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -1,9 +1,4 @@ -using System; -using System.IO; -using System.Reflection; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters; -using System.Runtime.Serialization.Formatters.Binary; +using System.IO; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -16,18 +11,16 @@ namespace Flow.Launcher.Infrastructure.Storage /// Normally, it has better performance, but not readable /// /// - /// It utilize MemoryPack, which means the object must be MemoryPackSerializable - /// https://github.com/Cysharp/MemoryPack + /// It utilize MemoryPack, which means the object must be MemoryPackSerializable /// public class BinaryStorage { - const string DirectoryName = "Cache"; + public const string FileSuffix = ".cache"; - const string FileSuffix = ".cache"; - - public BinaryStorage(string filename) + // Let the derived class to set the file path + public BinaryStorage(string filename, string directoryPath = null) { - var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); + directoryPath ??= DataLocation.CacheDirectory; Helper.ValidateDirectory(directoryPath); FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); @@ -58,14 +51,14 @@ namespace Flow.Launcher.Infrastructure.Storage } } - private async ValueTask DeserializeAsync(Stream stream, T defaultData) + private static async ValueTask DeserializeAsync(Stream stream, T defaultData) { try { var t = await MemoryPackSerializer.DeserializeAsync(stream); return t; } - catch (System.Exception e) + catch (System.Exception) { // Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); return defaultData; diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 507838d94..40106acd8 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -16,7 +16,7 @@ namespace Flow.Launcher.Infrastructure.Storage protected T? Data; // need a new directory name - public const string DirectoryName = "Settings"; + public const string DirectoryName = Constant.Settings; public const string FileSuffix = ".json"; protected string FilePath { get; init; } = null!; diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index bc3900da8..b377c81aa 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -13,7 +13,7 @@ namespace Flow.Launcher.Infrastructure.Storage // C# related, add python related below var dataType = typeof(T); AssemblyName = dataType.Assembly.GetName().Name; - DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName); + DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName); Helper.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); @@ -23,13 +23,5 @@ namespace Flow.Launcher.Infrastructure.Storage { Data = data; } - - public void DeleteDirectory() - { - if (Directory.Exists(DirectoryPath)) - { - Directory.Delete(DirectoryPath, true); - } - } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index e294f52b8..5b948e450 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -25,8 +25,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings return false; } + public static string VersionLogDirectory => Path.Combine(LogDirectory, Constant.Version); + public static string LogDirectory => Path.Combine(DataDirectory(), Constant.Logs); + + public static readonly string CacheDirectory = Path.Combine(DataDirectory(), Constant.Cache); + public static readonly string SettingsDirectory = Path.Combine(DataDirectory(), Constant.Settings); public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins); - public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins); + public static readonly string ThemesDirectory = Path.Combine(DataDirectory(), Constant.Themes); + + public static readonly string PluginSettingsDirectory = Path.Combine(SettingsDirectory, Constant.Plugins); + public static readonly string PluginCacheDirectory = Path.Combine(DataDirectory(), Constant.Cache, Constant.Plugins); public const string PythonEnvironmentName = "Python"; public const string NodeEnvironmentName = "Node.js"; diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 7e9e22063..d1473f693 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -32,10 +32,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings { foreach (var metadata in metadatas) { - if (Plugins.ContainsKey(metadata.ID)) + if (Plugins.TryGetValue(metadata.ID, out var settings)) { - var settings = Plugins[metadata.ID]; - if (string.IsNullOrEmpty(settings.Version)) settings.Version = metadata.Version; diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index de7ff527f..0b6fa4f59 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -68,11 +68,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings get => _theme; set { - if (value == _theme) - return; - _theme = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(MaxResultsToShow)); + if (value != _theme) + { + _theme = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(MaxResultsToShow)); + } } } public bool UseDropShadowEffect { get; set; } = true; @@ -113,6 +114,33 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double? SettingWindowLeft { get; set; } = null; public WindowState SettingWindowState { get; set; } = WindowState.Normal; + bool _showPlaceholder { get; set; } = false; + public bool ShowPlaceholder + { + get => _showPlaceholder; + set + { + if (_showPlaceholder != value) + { + _showPlaceholder = value; + OnPropertyChanged(); + } + } + } + string _placeholderText { get; set; } = string.Empty; + public string PlaceholderText + { + get => _placeholderText; + set + { + if (_placeholderText != value) + { + _placeholderText = value; + OnPropertyChanged(); + } + } + } + public int CustomExplorerIndex { get; set; } = 0; [JsonIgnore] @@ -241,8 +269,25 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// public double CustomWindowTop { get; set; } = 0; - public bool KeepMaxResults { get; set; } = false; + /// + /// Fixed window size + /// + private bool _keepMaxResults { get; set; } = false; + public bool KeepMaxResults + { + get => _keepMaxResults; + set + { + if (_keepMaxResults != value) + { + _keepMaxResults = value; + OnPropertyChanged(); + } + } + } + public int MaxResultsToShow { get; set; } = 5; + public int ActivateTimes { get; set; } public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection(); diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs index e31c8e31d..9e05bbd06 100644 --- a/Flow.Launcher.Plugin/ActionContext.cs +++ b/Flow.Launcher.Plugin/ActionContext.cs @@ -51,6 +51,9 @@ namespace Flow.Launcher.Plugin (WinPressed ? ModifierKeys.Windows : ModifierKeys.None); } + /// + /// Default object with all keys not pressed. + /// public static readonly SpecialKeyState Default = new () { CtrlPressed = false, ShiftPressed = false, diff --git a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs index fd21460ac..aa4e4a56d 100644 --- a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs +++ b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs @@ -4,17 +4,42 @@ using System.Threading; namespace Flow.Launcher.Plugin { + /// + /// Interface for plugins that want to manually update their results + /// public interface IResultUpdated : IFeatures { + /// + /// Event that is triggered when the results are updated + /// event ResultUpdatedEventHandler ResultsUpdated; } + /// + /// Delegate for the ResultsUpdated event + /// + /// + /// public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e); + /// + /// Event arguments for the ResultsUpdated event + /// public class ResultUpdatedEventArgs : EventArgs { + /// + /// List of results that should be displayed + /// public List Results; + + /// + /// Query that triggered the update + /// public Query Query; + + /// + /// Token that can be used to cancel the update + /// public CancellationToken Token { get; init; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs index d5ffba20b..f034243c3 100644 --- a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs +++ b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs @@ -2,8 +2,15 @@ namespace Flow.Launcher.Plugin { + /// + /// This interface is used to create settings panel for .Net plugins + /// public interface ISettingProvider { + /// + /// Create settings panel control for .Net plugins + /// + /// Control CreateSettingPanel(); } } diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index f040752bd..a42e3930c 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -5,10 +5,18 @@ /// public class PluginInitContext { + /// + /// Default constructor. + /// public PluginInitContext() { } + /// + /// Constructor. + /// + /// + /// public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) { CurrentPluginMetadata = currentPluginMetadata; diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index 8b3ca6331..bafce164b 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -4,24 +4,77 @@ using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { + /// + /// Plugin metadata + /// public class PluginMetadata : BaseModel { - private string _pluginDirectory; + /// + /// Plugin ID. + /// public string ID { get; set; } - public string Name { get; set; } - public string Author { get; set; } - public string Version { get; set; } - public string Language { get; set; } - public string Description { get; set; } - public string Website { get; set; } - public bool Disabled { get; set; } - public string ExecuteFilePath { get; private set;} + /// + /// Plugin name. + /// + public string Name { get; set; } + + /// + /// Plugin author. + /// + public string Author { get; set; } + + /// + /// Plugin version. + /// + public string Version { get; set; } + + /// + /// Plugin language. + /// See + /// + public string Language { get; set; } + + /// + /// Plugin description. + /// + public string Description { get; set; } + + /// + /// Plugin website. + /// + public string Website { get; set; } + + /// + /// Whether plugin is disabled. + /// + public bool Disabled { get; set; } + + /// + /// Plugin execute file path. + /// + public string ExecuteFilePath { get; private set; } + + /// + /// Plugin execute file name. + /// public string ExecuteFileName { get; set; } + /// + /// Plugin assembly name. + /// Only available for .Net plugins. + /// + [JsonIgnore] + public string AssemblyName { get; internal set; } + + private string _pluginDirectory; + + /// + /// Plugin source directory. + /// public string PluginDirectory { - get { return _pluginDirectory; } + get => _pluginDirectory; internal set { _pluginDirectory = value; @@ -30,32 +83,77 @@ namespace Flow.Launcher.Plugin } } + /// + /// The first action keyword of plugin. + /// public string ActionKeyword { get; set; } + /// + /// All action keywords of plugin. + /// public List ActionKeywords { get; set; } - + + /// + /// Hide plugin keyword setting panel. + /// public bool HideActionKeywordPanel { get; set; } - + + /// + /// Plugin search delay in ms. + /// public int SearchDelay { get; set; } + /// + /// Plugin icon path. + /// public string IcoPath { get; set;} - - public override string ToString() - { - return Name; - } + /// + /// Plugin priority. + /// [JsonIgnore] public int Priority { get; set; } /// - /// Init time include both plugin load time and init time + /// Init time include both plugin load time and init time. /// [JsonIgnore] public long InitTime { get; set; } + + /// + /// Average query time. + /// [JsonIgnore] public long AvgQueryTime { get; set; } + + /// + /// Query count. + /// [JsonIgnore] public int QueryCount { get; set; } + + /// + /// The path to the plugin settings directory which is not validated. + /// It is used to store plugin settings files and data files. + /// When plugin is deleted, FL will ask users whether to keep its settings. + /// If users do not want to keep, this directory will be deleted. + /// + public string PluginSettingsDirectoryPath { get; internal set; } + + /// + /// The path to the plugin cache directory which is not validated. + /// It is used to store cache files. + /// When plugin is deleted, this directory will be deleted as well. + /// + public string PluginCacheDirectoryPath { get; internal set; } + + /// + /// Convert to string. + /// + /// + public override string ToString() + { + return Name; + } } } diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs index 7bf634691..f2c14d70c 100644 --- a/Flow.Launcher.Plugin/PluginPair.cs +++ b/Flow.Launcher.Plugin/PluginPair.cs @@ -1,21 +1,37 @@ namespace Flow.Launcher.Plugin { + /// + /// Plugin instance and plugin metadata + /// public class PluginPair { + /// + /// Plugin instance + /// public IAsyncPlugin Plugin { get; internal set; } + + /// + /// Plugin metadata + /// public PluginMetadata Metadata { get; internal set; } - - + /// + /// Convert to string + /// + /// public override string ToString() { return Metadata.Name; } + /// + /// Compare by plugin metadata ID + /// + /// + /// public override bool Equals(object obj) { - PluginPair r = obj as PluginPair; - if (r != null) + if (obj is PluginPair r) { return string.Equals(r.Metadata.ID, Metadata.ID); } @@ -25,6 +41,10 @@ } } + /// + /// Get hash code + /// + /// public override int GetHashCode() { var hashcode = Metadata.ID?.GetHashCode() ?? 0; diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 15b2dd171..913dc31ae 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -2,10 +2,11 @@ namespace Flow.Launcher.Plugin { + /// + /// Represents a query that is sent to a plugin. + /// public class Query { - public Query() { } - /// /// Raw query, this includes action keyword if it has /// We didn't recommend use this property directly. You should always use Search property. @@ -54,13 +55,13 @@ namespace Flow.Launcher.Plugin /// public string ActionKeyword { get; init; } - [JsonIgnore] /// /// Splits by spaces and returns the first item. /// /// /// returns an empty string when does not have enough items. /// + [JsonIgnore] public string FirstSearch => SplitSearch(0); [JsonIgnore] diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 9b16cc1cb..910485438 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -13,7 +12,6 @@ namespace Flow.Launcher.Plugin /// public class Result { - private string _pluginDirectory; private string _icoPath; diff --git a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs index a7744ffac..752c85933 100644 --- a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs +++ b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs @@ -6,6 +6,9 @@ using System.Linq; namespace Flow.Launcher.Plugin.SharedCommands { + /// + /// Contains methods to open a search in a new browser window or tab. + /// public static class SearchWeb { private static string GetDefaultBrowserPath() @@ -106,4 +109,4 @@ namespace Flow.Launcher.Plugin.SharedCommands } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs index a0440e30d..288222d4f 100644 --- a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs +++ b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs @@ -8,12 +8,26 @@ using Windows.Win32.Foundation; namespace Flow.Launcher.Plugin.SharedCommands { + /// + /// Contains methods for running shell commands + /// public static class ShellCommand { + /// + /// Delegate for EnumThreadWindows + /// + /// + /// + /// public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam); private static bool containsSecurityWindow; + /// + /// Runs a windows command using the provided ProcessStartInfo + /// + /// + /// public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo) { processStartInfo.Verb = "RunAsUser"; @@ -65,6 +79,15 @@ namespace Flow.Launcher.Plugin.SharedCommands return buffer[..length].ToString(); } + /// + /// Runs a windows command using the provided ProcessStartInfo + /// + /// + /// + /// + /// + /// + /// public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "", bool createNoWindow = false) { diff --git a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs index 5144eb61d..36677d4bb 100644 --- a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs +++ b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs @@ -2,14 +2,29 @@ namespace Flow.Launcher.Plugin.SharedModels { + /// + /// Represents the result of a match operation. + /// public class MatchResult { + /// + /// Initializes a new instance of the class. + /// + /// + /// public MatchResult(bool success, SearchPrecisionScore searchPrecision) { Success = success; SearchPrecision = searchPrecision; } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) { Success = success; @@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels RawScore = rawScore; } + /// + /// Whether the match operation was successful. + /// public bool Success { get; set; } /// @@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels /// private int _rawScore; + /// + /// The raw calculated search score without any search precision filtering applied. + /// public int RawScore { get { return _rawScore; } @@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels /// public List MatchData { get; set; } + /// + /// The search precision score used to filter the search results. + /// public SearchPrecisionScore SearchPrecision { get; set; } + /// + /// Determines if the search precision score is met. + /// + /// public bool IsSearchPrecisionScoreMet() { return IsSearchPrecisionScoreMet(_rawScore); @@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels } } + /// + /// Represents the search precision score used to filter search results. + /// public enum SearchPrecisionScore { + /// + /// The highest search precision score. + /// Regular = 50, + + /// + /// The medium search precision score. + /// Low = 20, + + /// + /// The lowest search precision score. + /// None = 0 } } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 833c63ddf..7b1d113fb 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -177,7 +177,6 @@ namespace Flow.Launcher HotKeyMapper.Initialize(); // main windows needs initialized before theme change because of blur settings - // TODO: Clean ThemeManager.Instance in future Ioc.Default.GetRequiredService().ChangeTheme(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs index 151ce97dd..77bebe2d3 100644 --- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs +++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs @@ -12,9 +12,9 @@ namespace Flow.Launcher.Helper; public static class WallpaperPathRetrieval { - private static readonly int MAX_CACHE_SIZE = 3; - - private static readonly Dictionary<(string, DateTime), ImageBrush> wallpaperCache = new(); + private const int MaxCacheSize = 3; + private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new(); + private static readonly object CacheLock = new(); public static Brush GetWallpaperBrush() { @@ -27,46 +27,71 @@ public static class WallpaperPathRetrieval try { var wallpaperPath = Win32Helper.GetWallpaperPath(); - if (wallpaperPath is not null && File.Exists(wallpaperPath)) + if (string.IsNullOrEmpty(wallpaperPath) || !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); + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Wallpaper path is invalid: {wallpaperPath}"); + var wallpaperColor = GetWallpaperColor(); + return new SolidColorBrush(wallpaperColor); + } + + // 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); + lock (CacheLock) + { + WallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper); if (cachedWallpaper != null) { return cachedWallpaper; } + } + + using var fileStream = File.OpenRead(wallpaperPath); + var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None); + var frame = decoder.Frames[0]; + var originalWidth = frame.PixelWidth; + var originalHeight = frame.PixelHeight; - // 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 + if (originalWidth == 0 || originalHeight == 0) + { + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}"); + return new SolidColorBrush(Colors.Transparent); + } - // Manage cache size - if (wallpaperCache.Count >= MAX_CACHE_SIZE) + // Calculate the scaling factor to fit the image within 800x600 while preserving aspect ratio + var widthRatio = 800.0 / originalWidth; + var heightRatio = 600.0 / originalHeight; + var scaleFactor = Math.Min(widthRatio, heightRatio); + var decodedPixelWidth = (int)(originalWidth * scaleFactor); + var decodedPixelHeight = (int)(originalHeight * scaleFactor); + + // Set DecodePixelWidth and DecodePixelHeight to resize the image while preserving aspect ratio + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(wallpaperPath); + bitmap.DecodePixelWidth = decodedPixelWidth; + bitmap.DecodePixelHeight = decodedPixelHeight; + 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 + lock (CacheLock) + { + if (WallpaperCache.Count >= MaxCacheSize) { // Remove the oldest wallpaper from the cache - var oldestCache = wallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault(); + var oldestCache = WallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault(); if (oldestCache != default) { - wallpaperCache.Remove(oldestCache); + WallpaperCache.Remove(oldestCache); } } - wallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush); + WallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush); return wallpaperBrush; } - - var wallpaperColor = GetWallpaperColor(); - return new SolidColorBrush(wallpaperColor); } catch (Exception ex) { @@ -77,7 +102,7 @@ public static class WallpaperPathRetrieval private static Color GetWallpaperColor() { - RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true); + RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false); var result = key?.GetValue("Background", null); if (result is string strResult) { @@ -86,8 +111,9 @@ public static class WallpaperPathRetrieval var parts = strResult.Trim().Split(new[] { ' ' }, 3).Select(byte.Parse).ToList(); return Color.FromRgb(parts[0], parts[1], parts[2]); } - catch + catch (Exception ex) { + App.API.LogException(nameof(WallpaperPathRetrieval), "Error parsing wallpaper color", ex); } } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 7eac8a779..00665d9e3 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -39,7 +39,7 @@ Game Mode Suspend the use of Hotkeys. Position Reset - Reset search window position + Type here to search Settings @@ -72,8 +72,6 @@ Empty last Query Preserve Last Action Keyword Select Last Action Keyword - Fixed Window Height - The window height is not adjustable by dragging. Maximum results shown You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. Ignore hotkeys in fullscreen mode @@ -108,11 +106,6 @@ Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled - Backdrop Type - None - Acrylic - Mica - Mica Alt Search Plugin @@ -143,6 +136,8 @@ Uninstall Fail to remove plugin settings Plugins: {0} - Fail to remove plugin settings files, please remove them manually + Fail to remove plugin cache + Plugins: {0} - Fail to remove plugin cache files, please remove them manually Plugin Store @@ -205,8 +200,19 @@ Custom Clock Date + Backdrop Type + None + Acrylic + Mica + Mica Alt This theme supports two(light/dark) modes. This theme supports Blur Transparent Background. + Show placeholder + Display placeholder when query is empty + Placeholder text + Change placeholder text. Input empty will use: {0} + Fixed Window Size + The window size is not adjustable by dragging. Hotkey diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index b13e13254..ff9db9a94 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -24,7 +24,6 @@ Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Loaded="OnLoaded" LocationChanged="OnLocationChanged" - Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="OnKeyDown" PreviewKeyUp="OnKeyUp" PreviewMouseMove="OnPreviewMouseMove" @@ -219,6 +218,14 @@ + + Opacity="{Binding ClockPanelOpacity}" + Style="{DynamicResource ClockPanel}" + Visibility="{Binding ClockPanelVisibility}"> diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 0952a7025..de9be7758 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -12,6 +12,7 @@ using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; +using System.Windows.Shell; using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; @@ -61,7 +62,6 @@ namespace Flow.Launcher // Window Animation private const double DefaultRightMargin = 66; //* this value from base.xaml - private bool _animating; private bool _isClockPanelAnimating = false; // Search Delay @@ -82,7 +82,7 @@ namespace Flow.Launcher DataContext = _viewModel; InitializeComponent(); - UpdatePosition(true); + UpdatePosition(); InitSoundEffects(); DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste); @@ -111,17 +111,26 @@ namespace Flow.Launcher // Check first launch if (_settings.FirstLaunch) { + // Set First Launch to false _settings.FirstLaunch = false; + + // Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None + if (Win32Helper.IsBackdropSupported()) _settings.BackdropType = BackdropTypes.Acrylic; + + // Save settings App.API.SaveAppAllSettings(); - /* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */ - if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) - _settings.BackdropType = BackdropTypes.Acrylic; - var WelcomeWindow = new WelcomeWindow(); - WelcomeWindow.Show(); + + // Show Welcome Window + var welcomeWindow = new WelcomeWindow(); + welcomeWindow.Show(); } + // Initialize place holder + SetupPlaceholderText(); + _viewModel.PlaceholderText = _settings.PlaceholderText; + // Hide window if need - UpdatePosition(true); + UpdatePosition(); if (_settings.HideOnStartup) { _viewModel.Hide(); @@ -148,17 +157,20 @@ namespace Flow.Launcher InitProgressbarAnimation(); // Force update position - UpdatePosition(true); + UpdatePosition(); // Refresh frame - await Ioc.Default.GetRequiredService().RefreshFrameAsync(); + await _theme.RefreshFrameAsync(); + + // Initialize resize mode after refreshing frame + SetupResizeMode(); // Reset preview _viewModel.ResetPreview(); // Since the default main window visibility is visible, so we need set focus during startup QueryTextBox.Focus(); - + // Set the initial state of the QueryTextBoxCursorMovedToEnd property // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. _viewModel.QueryTextCursorMovedToEnd = false; @@ -174,24 +186,37 @@ namespace Flow.Launcher { if (_viewModel.MainWindowVisibilityStatus) { + // Play sound effect before activing the window if (_settings.UseSound) { SoundPlay(); } - - UpdatePosition(false); - _viewModel.ResetPreview(); + + // Update position & Activate + UpdatePosition(); Activate(); - QueryTextBox.Focus(); - _settings.ActivateTimes++; + + // Reset preview + _viewModel.ResetPreview(); + + // Select last query if need if (!_viewModel.LastQuerySelected) { QueryTextBox.SelectAll(); _viewModel.LastQuerySelected = true; } + // Focus query box + QueryTextBox.Focus(); + + // Play window animation if (_settings.UseAnimation) + { WindowAnimation(); + } + + // Update activate times + _settings.ActivateTimes++; } }); break; @@ -204,7 +229,6 @@ namespace Flow.Launcher Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); _viewModel.QueryTextCursorMovedToEnd = false; } - break; case nameof(MainViewModel.GameModeStatus): _notifyIcon.Icon = _viewModel.GameModeStatus @@ -234,6 +258,14 @@ namespace Flow.Launcher case nameof(Settings.WindowTop): Top = _settings.WindowTop; break; + case nameof(Settings.ShowPlaceholder): + SetupPlaceholderText(); + break; + case nameof(Settings.PlaceholderText): + _viewModel.PlaceholderText = _settings.PlaceholderText; + break; + case nameof(Settings.KeepMaxResults): + SetupResizeMode(); case nameof(Settings.SearchQueryResultsWithDelay): SetupSearchTextBoxReactiveness(_settings.SearchQueryResultsWithDelay); break; @@ -241,7 +273,7 @@ namespace Flow.Launcher }; // QueryTextBox.Text change detection (modified to only work when character count is 1 or higher) - QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); + QueryTextBox.TextChanged += (s, e) => UpdateClockPanelVisibility(); // Detecting ContextMenu.Visibility changes DependencyPropertyDescriptor @@ -250,7 +282,7 @@ namespace Flow.Launcher // Detect History.Visibility changes DependencyPropertyDescriptor - .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 + .FromProperty(VisibilityProperty, typeof(StackPanel)) .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } @@ -265,7 +297,8 @@ namespace Flow.Launcher Notification.Uninstall(); // After plugins are all disposed, we can close the main window _canClose = true; - Close(); + // Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event + Application.Current.Shutdown(); } } @@ -285,8 +318,6 @@ namespace Flow.Launcher private void OnLocationChanged(object sender, EventArgs e) { - if (_animating) return; - if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { _settings.WindowLeft = Left; @@ -298,9 +329,11 @@ namespace Flow.Launcher { _settings.WindowLeft = Left; _settings.WindowTop = Top; - ClockPanel.Opacity = 0; - SearchIcon.Opacity = 0; - //This condition stops extra hide call when animator is on, + + _viewModel.ClockPanelOpacity = 0.0; + _viewModel.SearchIconOpacity = 0.0; + + // This condition stops extra hide call when animator is on, // which causes the toggling to occasional hide instead of show. if (_viewModel.MainWindowVisibilityStatus) { @@ -308,8 +341,9 @@ namespace Flow.Launcher // This also stops the mainwindow from flickering occasionally after Settings window is opened // and always after Settings window is closed. if (_settings.UseAnimation) - + { await Task.Delay(100); + } if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) { @@ -349,7 +383,6 @@ namespace Flow.Launcher _viewModel.LoadContextMenuCommand.Execute(null); e.Handled = true; } - break; case Key.Left: if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0) @@ -357,7 +390,6 @@ namespace Flow.Launcher _viewModel.EscCommand.Execute(null); e.Handled = true; } - break; case Key.Back: if (specialKeyState.CtrlPressed) @@ -376,7 +408,6 @@ namespace Flow.Launcher } } } - break; default: break; @@ -438,23 +469,25 @@ namespace Flow.Launcher { _initialWidth = (int)Width; _initialHeight = (int)Height; + handled = true; } else if (msg == Win32Helper.WM_EXITSIZEMOVE) { if (_initialHeight != (int)Height) { - var shadowMargin = 0; - var (_, useDropShadowEffect) = _theme.GetActualValue(); - if (useDropShadowEffect) - { - shadowMargin = 32; - } - if (!_settings.KeepMaxResults) { - var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; + // Get shadow margin + var shadowMargin = 0; + var (_, useDropShadowEffect) = _theme.GetActualValue(); + if (useDropShadowEffect) + { + shadowMargin = 32; + } + // Calculate max results to show + var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; if (itemCount < 2) { _settings.MaxResultsToShow = 2; @@ -466,11 +499,16 @@ namespace Flow.Launcher } SizeToContent = SizeToContent.Height; - _viewModel.MainWindowWidth = Width; } if (_initialWidth != (int)Width) { + if (!_settings.KeepMaxResults) + { + // Update width + _viewModel.MainWindowWidth = Width; + } + SizeToContent = SizeToContent.Height; } @@ -606,13 +644,8 @@ namespace Flow.Launcher #region Window Position - private void UpdatePosition(bool force) + private void UpdatePosition() { - if (_animating && !force) - { - return; - } - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 InitializePosition(); InitializePosition(); @@ -786,19 +819,11 @@ namespace Flow.Launcher private void WindowAnimation() { - if (_animating) - return; - _isArrowKeyPressed = true; - _animating = true; - UpdatePosition(false); - ClockPanel.Opacity = 0; - SearchIcon.Opacity = 0; - var clocksb = new Storyboard(); var iconsb = new Storyboard(); - CircleEase easing = new CircleEase { EasingMode = EasingMode.EaseInOut }; + var easing = new CircleEase { EasingMode = EasingMode.EaseInOut }; var animationLength = _settings.AnimationSpeed switch { @@ -826,7 +851,7 @@ namespace Flow.Launcher FillBehavior = FillBehavior.HoldEnd }; - double TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0); + var TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0); var IconOpacity = new DoubleAnimation { @@ -837,7 +862,7 @@ namespace Flow.Launcher FillBehavior = FillBehavior.HoldEnd }; - double rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right; + var rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right; var thicknessAnimation = new ThicknessAnimation { @@ -864,24 +889,22 @@ namespace Flow.Launcher clocksb.Children.Add(ClockOpacity); iconsb.Children.Add(IconMotion); iconsb.Children.Add(IconOpacity); - - clocksb.Completed += (_, _) => _animating = false; + _settings.WindowLeft = Left; _isArrowKeyPressed = false; - - if (QueryTextBox.Text.Length == 0) - { - clocksb.Begin(ClockPanel); - } - + + clocksb.Begin(ClockPanel); iconsb.Begin(SearchIcon); } private void UpdateClockPanelVisibility() { if (QueryTextBox == null || ContextMenu == null || History == null || ClockPanel == null) + { return; + } + // ✅ Initialize animation length & duration var animationLength = _settings.AnimationSpeed switch { AnimationSpeeds.Slow => 560, @@ -889,32 +912,37 @@ namespace Flow.Launcher AnimationSpeeds.Fast => 160, _ => _settings.CustomAnimationLength }; - var animationDuration = TimeSpan.FromMilliseconds(animationLength * 2 / 3); // ✅ Conditions for showing ClockPanel (No query input & ContextMenu, History are closed) - bool shouldShowClock = QueryTextBox.Text.Length == 0 && + var shouldShowClock = QueryTextBox.Text.Length == 0 && ContextMenu.Visibility != Visibility.Visible && History.Visibility != Visibility.Visible; // ✅ 1. When ContextMenu opens, immediately set Visibility.Hidden (force hide without animation) if (ContextMenu.Visibility == Visibility.Visible) { - ClockPanel.Visibility = Visibility.Hidden; - ClockPanel.Opacity = 0.0; // Set to 0 in case Opacity animation affects it + _viewModel.ClockPanelVisibility = Visibility.Hidden; + _viewModel.ClockPanelOpacity = 0.0; // Set to 0 in case Opacity animation affects it return; } // ✅ 2. When ContextMenu is closed, keep it Hidden if there's text in the query (remember previous state) - if (ContextMenu.Visibility != Visibility.Visible && QueryTextBox.Text.Length > 0) + else if (QueryTextBox.Text.Length > 0) + { + _viewModel.ClockPanelVisibility = Visibility.Hidden; + _viewModel.ClockPanelOpacity = 0.0; + return; + } + + // ✅ Prevent multiple animations + if (_isClockPanelAnimating) { - ClockPanel.Visibility = Visibility.Hidden; - ClockPanel.Opacity = 0.0; return; } // ✅ 3. When hiding ClockPanel (apply fade-out animation) - if ((!shouldShowClock) && ClockPanel.Visibility == Visibility.Visible && !_isClockPanelAnimating) + if ((!shouldShowClock) && _viewModel.ClockPanelVisibility == Visibility.Visible) { _isClockPanelAnimating = true; @@ -928,39 +956,40 @@ namespace Flow.Launcher fadeOut.Completed += (s, e) => { - ClockPanel.Visibility = Visibility.Hidden; // ✅ Completely hide after animation + _viewModel.ClockPanelVisibility = Visibility.Hidden; // ✅ Completely hide after animation _isClockPanelAnimating = false; }; ClockPanel.BeginAnimation(OpacityProperty, fadeOut); } + // ✅ 4. When showing ClockPanel (apply fade-in animation) - else if (shouldShowClock && ClockPanel.Visibility != Visibility.Visible && !_isClockPanelAnimating) + else if (shouldShowClock && _viewModel.ClockPanelVisibility != Visibility.Visible) { _isClockPanelAnimating = true; - Application.Current.Dispatcher.Invoke(() => + _viewModel.ClockPanelVisibility = Visibility.Visible; // ✅ Set Visibility to Visible first + + var fadeIn = new DoubleAnimation { - ClockPanel.Visibility = Visibility.Visible; // ✅ Set Visibility to Visible first + From = 0.0, + To = 1.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; - var fadeIn = new DoubleAnimation - { - From = 0.0, - To = 1.0, - Duration = animationDuration, - FillBehavior = FillBehavior.HoldEnd - }; + fadeIn.Completed += (s, e) => _isClockPanelAnimating = false; - fadeIn.Completed += (s, e) => _isClockPanelAnimating = false; - ClockPanel.BeginAnimation(OpacityProperty, fadeIn); - }, DispatcherPriority.Render); + ClockPanel.BeginAnimation(OpacityProperty, fadeIn); } } private static double GetOpacityFromStyle(Style style, double defaultOpacity = 1.0) { if (style == null) + { return defaultOpacity; + } foreach (Setter setter in style.Setters.Cast()) { @@ -976,7 +1005,9 @@ namespace Flow.Launcher private static Thickness GetThicknessFromStyle(Style style, Thickness defaultThickness) { if (style == null) + { return defaultThickness; + } foreach (Setter setter in style.Setters.Cast()) { @@ -1034,6 +1065,56 @@ namespace Flow.Launcher e.Handled = true; } + #endregion + + #region Placeholder + + private void SetupPlaceholderText() + { + if (_settings.ShowPlaceholder) + { + QueryTextBox.TextChanged += QueryTextBox_TextChanged; + QueryTextSuggestionBox.TextChanged += QueryTextSuggestionBox_TextChanged; + SetPlaceholderText(); + } + else + { + QueryTextBox.TextChanged -= QueryTextBox_TextChanged; + QueryTextSuggestionBox.TextChanged -= QueryTextSuggestionBox_TextChanged; + QueryTextPlaceholderBox.Visibility = Visibility.Collapsed; + } + } + + private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + SetPlaceholderText(); + } + + private void QueryTextSuggestionBox_TextChanged(object sender, TextChangedEventArgs e) + { + SetPlaceholderText(); + } + + private void SetPlaceholderText() + { + var queryText = QueryTextBox.Text; + var suggestionText = QueryTextSuggestionBox.Text; + QueryTextPlaceholderBox.Visibility = string.IsNullOrEmpty(queryText) && string.IsNullOrEmpty(suggestionText) ? Visibility.Visible : Visibility.Collapsed; + } + + #endregion + + #region Resize Mode + + private void SetupResizeMode() + { + ResizeMode = _settings.KeepMaxResults ? ResizeMode.NoResize : ResizeMode.CanResize; + if (WindowChrome.GetWindowChrome(this) is WindowChrome windowChrome) + { + _theme.SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults); + } + } + #endregion #region Search Delay @@ -1048,7 +1129,7 @@ namespace Flow.Launcher _reactiveSubscription = null; } - QueryTextBox.TextChanged -= QueryTextBox_TextChanged; + QueryTextBox.TextChanged -= QueryTextBox_TextChanged1; if (showResultsWithDelay) { @@ -1062,11 +1143,11 @@ namespace Flow.Launcher } else { - QueryTextBox.TextChanged += QueryTextBox_TextChanged; + QueryTextBox.TextChanged += QueryTextBox_TextChanged1; } } - private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e) + private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) { var textBox = (TextBox)sender; PerformSearchQuery(false, textBox); diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 456f1ad47..e19ad2fdc 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -192,7 +192,7 @@ namespace Flow.Launcher private readonly ConcurrentDictionary _pluginJsonStorages = new(); - public object RemovePluginSettings(string assemblyName) + public void RemovePluginSettings(string assemblyName) { foreach (var keyValuePair in _pluginJsonStorages) { @@ -202,11 +202,8 @@ namespace Flow.Launcher if (name == assemblyName) { _pluginJsonStorages.Remove(key, out var pluginJsonStorage); - return pluginJsonStorage; } } - - return null; } /// diff --git a/Flow.Launcher/Resources/Dark.xaml b/Flow.Launcher/Resources/Dark.xaml index 25cc8e878..f1ebba080 100644 --- a/Flow.Launcher/Resources/Dark.xaml +++ b/Flow.Launcher/Resources/Dark.xaml @@ -115,6 +115,8 @@ + + diff --git a/Flow.Launcher/Resources/Light.xaml b/Flow.Launcher/Resources/Light.xaml index b9f65ce96..12b35971d 100644 --- a/Flow.Launcher/Resources/Light.xaml +++ b/Flow.Launcher/Resources/Light.xaml @@ -107,6 +107,7 @@ + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml index 1728195bd..32fdb62fc 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml @@ -110,8 +110,8 @@ - - + + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml index 04c76d027..f62cec3bf 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml @@ -60,7 +60,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal"> - + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml index c898ac9a0..3df4b506e 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml @@ -58,10 +58,10 @@ - + - - + + diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs index f82f8e34d..20f905411 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs @@ -102,13 +102,13 @@ public partial class SettingsPaneAboutViewModel : BaseModel [RelayCommand] private void OpenSettingsFolder() { - App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Settings)); + App.API.OpenDirectory(DataLocation.SettingsDirectory); } [RelayCommand] private void OpenParentOfSettingsFolder(object parameter) { - string settingsFolderPath = Path.Combine(DataLocation.DataDirectory(), Constant.Settings); + string settingsFolderPath = Path.Combine(DataLocation.SettingsDirectory); string parentFolderPath = Path.GetDirectoryName(settingsFolderPath); App.API.OpenDirectory(parentFolderPath); } @@ -140,7 +140,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel private static DirectoryInfo GetLogDir(string version = "") { - return new DirectoryInfo(Path.Combine(DataLocation.DataDirectory(), Constant.Logs, version)); + return new DirectoryInfo(Path.Combine(DataLocation.LogDirectory, version)); } private static List GetLogFiles(string version = "") diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs index 647a9bcfc..30cafdaf0 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs @@ -259,6 +259,23 @@ public partial class SettingsPaneThemeViewModel : BaseModel set => Settings.SoundVolume = value; } + public bool ShowPlaceholder + { + get => Settings.ShowPlaceholder; + set => Settings.ShowPlaceholder = value; + } + + public string PlaceholderTextTip + { + get => string.Format(App.API.GetTranslation("PlaceholderTextTip"), App.API.GetTranslation("queryTextBoxPlaceholder")); + } + + public string PlaceholderText + { + get => Settings.PlaceholderText; + set => Settings.PlaceholderText = value; + } + public bool UseClock { get => Settings.UseClock; @@ -469,7 +486,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel [RelayCommand] private void OpenThemesFolder() { - App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); + App.API.OpenDirectory(DataLocation.ThemesDirectory); } [RelayCommand] diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml index 614237146..5e1ba24ea 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml @@ -30,6 +30,7 @@ VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.ScrollUnit="Pixel"> + + @@ -68,6 +70,7 @@ + - + + - - - - + - + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml index a81d9e0d1..6d629de63 100644 --- a/Flow.Launcher/SettingWindow.xaml +++ b/Flow.Launcher/SettingWindow.xaml @@ -18,6 +18,8 @@ Loaded="OnLoaded" MouseDown="window_MouseDown" ResizeMode="CanResize" + SnapsToDevicePixels="True" + UseLayoutRounding="True" StateChanged="Window_StateChanged" Top="{Binding SettingWindowTop, Mode=TwoWay}" WindowStartupLocation="Manual" @@ -54,6 +56,7 @@ Width="16" Height="16" Margin="10 4 4 4" + RenderOptions.BitmapScalingMode="HighQuality" Source="/Images/app.png" /> - diff --git a/Flow.Launcher/Themes/League.xaml b/Flow.Launcher/Themes/League.xaml index f1c8ba192..ffecf3fcb 100644 --- a/Flow.Launcher/Themes/League.xaml +++ b/Flow.Launcher/Themes/League.xaml @@ -24,7 +24,6 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - diff --git a/Flow.Launcher/Themes/Pink.xaml b/Flow.Launcher/Themes/Pink.xaml index d7de4e246..5bbfa26d6 100644 --- a/Flow.Launcher/Themes/Pink.xaml +++ b/Flow.Launcher/Themes/Pink.xaml @@ -22,7 +22,6 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ba45eadf0..a4df8fed4 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.Windows.Input; using System.Linq; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using System.Windows; +using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; @@ -213,7 +213,8 @@ namespace Flow.Launcher.ViewModel queue.Clear(); } - Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); + if (!_disposed) + Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); } void continueAction(Task t) @@ -582,7 +583,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void IncreaseWidth() { - Settings.WindowSize += 100; + MainWindowWidth += 100; Settings.WindowLeft -= 50; OnPropertyChanged(nameof(MainWindowWidth)); } @@ -590,14 +591,14 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void DecreaseWidth() { - if (MainWindowWidth - 100 < 400 || Settings.WindowSize == 400) + if (MainWindowWidth - 100 < 400 || MainWindowWidth == 400) { - Settings.WindowSize = 400; + MainWindowWidth = 400; } else { + MainWindowWidth -= 100; Settings.WindowLeft += 50; - Settings.WindowSize -= 100; } OnPropertyChanged(nameof(MainWindowWidth)); @@ -629,26 +630,36 @@ namespace Flow.Launcher.ViewModel /// Force query even when Query Text doesn't change public void ChangeQueryText(string queryText, bool isReQuery = false) { - Application.Current.Dispatcher.Invoke(() => + _ = ChangeQueryTextAsync(queryText, isReQuery); + } + + /// + /// Async version of + /// + private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false) + { + // Must check access so that we will not block the UI thread which cause window visibility issue + if (!Application.Current.Dispatcher.CheckAccess()) { - BackToQueryResults(); + await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery)); + return; + } - if (QueryText != queryText) - { - // re-query is done in QueryText's setter method - QueryText = queryText; - Query(false); - // set to false so the subsequent set true triggers - // PropertyChanged and MoveQueryTextToEnd is called - QueryTextCursorMovedToEnd = false; - } - else if (isReQuery) - { - Query(false, isReQuery: true); - } + if (QueryText != queryText) + { + // re-query is done in QueryText's setter method + QueryText = queryText; + await QueryAsync(isReQuery: false); + // set to false so the subsequent set true triggers + // PropertyChanged and MoveQueryTextToEnd is called + QueryTextCursorMovedToEnd = false; + } + else if (isReQuery) + { + await QueryAsync(isReQuery: true); + } - QueryTextCursorMovedToEnd = true; - }); + QueryTextCursorMovedToEnd = true; } public bool LastQuerySelected { get; set; } @@ -734,15 +745,28 @@ namespace Flow.Launcher.ViewModel public Visibility ProgressBarVisibility { get; set; } public Visibility MainWindowVisibility { get; set; } - public double MainWindowOpacity { get; set; } = 1; - + // This is to be used for determining the visibility status of the mainwindow instead of MainWindowVisibility // because it is more accurate and reliable representation than using Visibility as a condition check public bool MainWindowVisibilityStatus { get; set; } = true; public event VisibilityChangedEventHandler VisibilityChanged; + public Visibility ClockPanelVisibility { get; set; } public Visibility SearchIconVisibility { get; set; } + public double ClockPanelOpacity { get; set; } = 1; + public double SearchIconOpacity { get; set; } = 1; + + private string _placeholderText; + public string PlaceholderText + { + get => string.IsNullOrEmpty(_placeholderText) ? App.API.GetTranslation("queryTextBoxPlaceholder") : _placeholderText; + set + { + _placeholderText = value; + OnPropertyChanged(); + } + } public double MainWindowWidth { @@ -1010,10 +1034,15 @@ namespace Flow.Launcher.ViewModel #region Query public void Query(bool searchDelay, bool isReQuery = false) + { + _ = QueryAsync(isReQuery); + } + + private async Task QueryAsync(bool isReQuery = false) { if (QueryResultsSelected()) { - _ = QueryResultsAsync(searchDelay, isReQuery); + await QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) { @@ -1047,10 +1076,10 @@ namespace Flow.Launcher.ViewModel ( r => { - var match = StringMatcher.FuzzySearch(query, r.Title); + var match = App.API.FuzzySearch(query, r.Title); if (!match.IsSearchPrecisionScoreMet()) { - match = StringMatcher.FuzzySearch(query, r.SubTitle); + match = App.API.FuzzySearch(query, r.SubTitle); } if (!match.IsSearchPrecisionScoreMet()) return false; @@ -1092,7 +1121,7 @@ namespace Flow.Launcher.ViewModel Action = _ => { SelectedResults = Results; - ChangeQueryText(h.Query); + App.API.ChangeQuery(h.Query); return false; } }; @@ -1103,8 +1132,8 @@ namespace Flow.Launcher.ViewModel { var filtered = results.Where ( - r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || - StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() + r => App.API.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() ).ToList(); History.AddResults(filtered, id); } @@ -1457,36 +1486,49 @@ namespace Flow.Launcher.ViewModel #region Public Methods +#pragma warning disable VSTHRD100 // Avoid async void methods + public void Show() { + // Invoke on UI thread Application.Current.Dispatcher.Invoke(() => { - if (Application.Current.MainWindow is MainWindow mainWindow) + // When application is exitting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) { // 📌 Remove DWM Cloak (Make the window visible normally) Win32Helper.DWMSetCloakForWindow(mainWindow, false); - // 📌 Restore UI elements - mainWindow.ClockPanel.Visibility = Visibility.Visible; - //mainWindow.SearchIcon.Visibility = Visibility.Visible; - SearchIconVisibility = Visibility.Visible; - } + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; - // Update WPF properties - MainWindowVisibility = Visibility.Visible; - MainWindowOpacity = 1; - MainWindowVisibilityStatus = true; - VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); - - if (StartWithEnglishMode) - { - Win32Helper.SwitchToEnglishKeyboardLayout(true); + // Set clock and search icon visibility + ClockPanelVisibility = string.IsNullOrEmpty(QueryText) ? Visibility.Visible : Visibility.Collapsed; + if (PluginIconSource != null) + { + SearchIconOpacity = 0.0; + } + else + { + SearchIconVisibility = Visibility.Visible; + } } - }); + }, DispatcherPriority.Render); + + // Update WPF properties + MainWindowVisibility = Visibility.Visible; + MainWindowVisibilityStatus = true; + VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); + + // Switch keyboard layout + if (StartWithEnglishMode) + { + Win32Helper.SwitchToEnglishKeyboardLayout(true); + } } -#pragma warning disable VSTHRD100 // Avoid async void methods - public async void Hide() { lastHistoryIndex = 1; @@ -1501,62 +1543,62 @@ namespace Flow.Launcher.ViewModel SelectedResults = Results; } - // 📌 Immediately apply text reset + force UI update - if (Settings.LastQueryMode == LastQueryMode.Empty) - { - ChangeQueryText(string.Empty); - await Task.Delay(1); // Wait for one frame to ensure UI reflects changes - Application.Current.Dispatcher.Invoke(Application.Current.MainWindow.UpdateLayout); // Force UI update - } - switch (Settings.LastQueryMode) { + case LastQueryMode.Empty: + await ChangeQueryTextAsync(string.Empty); + break; case LastQueryMode.Preserved: case LastQueryMode.Selected: - LastQuerySelected = (Settings.LastQueryMode == LastQueryMode.Preserved); + LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved; break; - case LastQueryMode.ActionKeywordPreserved: case LastQueryMode.ActionKeywordSelected: var newQuery = _lastQuery.ActionKeyword; + if (!string.IsNullOrEmpty(newQuery)) newQuery += " "; - ChangeQueryText(newQuery); + await ChangeQueryTextAsync(newQuery); if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected) LastQuerySelected = false; break; } - if (Application.Current.MainWindow is MainWindow mainWindow) + // Invoke on UI thread + Application.Current.Dispatcher.Invoke(() => { - // 📌 Set Opacity of icon and clock to 0 and apply Visibility.Hidden - Application.Current.Dispatcher.Invoke(() => + // When application is exitting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) { - mainWindow.ClockPanel.Opacity = 0; - mainWindow.SearchIcon.Opacity = 0; - mainWindow.ClockPanel.Visibility = Visibility.Hidden; - //mainWindow.SearchIcon.Visibility = Visibility.Hidden; + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = Visibility.Hidden; SearchIconVisibility = Visibility.Hidden; // Force UI update mainWindow.ClockPanel.UpdateLayout(); mainWindow.SearchIcon.UpdateLayout(); - }, DispatcherPriority.Render); - // 📌 Apply DWM Cloak (Completely hide the window) - Win32Helper.DWMSetCloakForWindow(mainWindow, true); - } + // 📌 Apply DWM Cloak (Completely hide the window) + Win32Helper.DWMSetCloakForWindow(mainWindow, true); + } + }, DispatcherPriority.Render); + // Switch keyboard layout if (StartWithEnglishMode) { Win32Helper.RestorePreviousKeyboardLayout(); } + // Delay for a while to make sure clock will not flicker await Task.Delay(50); // Update WPF properties - //MainWindowOpacity = 0; MainWindowVisibilityStatus = false; MainWindowVisibility = Visibility.Collapsed; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false }); diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 4ceadec56..79d6aedd5 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -341,7 +341,7 @@ namespace Flow.Launcher.Plugin.PluginsManager } else { - PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin, + await PluginManager.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin, downloadToFilePath); if (Settings.AutoRestartAfterChanging) @@ -433,7 +433,7 @@ namespace Flow.Launcher.Plugin.PluginsManager if (cts.IsCancellationRequested) return; else - PluginManager.UpdatePlugin(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, + await PluginManager.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath); } catch (Exception ex) @@ -684,7 +684,7 @@ namespace Flow.Launcher.Plugin.PluginsManager Title = $"{x.Metadata.Name} by {x.Metadata.Author}", SubTitle = x.Metadata.Description, IcoPath = x.Metadata.IcoPath, - Action = e => + AsyncAction = async e => { string message; if (Settings.AutoRestartAfterChanging) @@ -707,7 +707,7 @@ namespace Flow.Launcher.Plugin.PluginsManager MessageBoxButton.YesNo) == MessageBoxResult.Yes) { Context.API.HideMainWindow(); - Uninstall(x.Metadata); + await UninstallAsync(x.Metadata); if (Settings.AutoRestartAfterChanging) { Context.API.RestartApp(); @@ -732,7 +732,7 @@ namespace Flow.Launcher.Plugin.PluginsManager return Search(results, search); } - private void Uninstall(PluginMetadata plugin) + private async Task UninstallAsync(PluginMetadata plugin) { try { @@ -740,7 +740,7 @@ namespace Flow.Launcher.Plugin.PluginsManager 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); + await PluginManager.UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings); } catch (ArgumentException e) { diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 9ab1502a2..4f5d1becd 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -154,8 +154,9 @@ namespace Flow.Launcher.Plugin.ProcessKiller Action = (c) => { processHelper.TryKill(_context, p); + // Re-query to refresh process list _context.API.ReQuery(); - return false; + return true; } }); } @@ -180,8 +181,9 @@ namespace Flow.Launcher.Plugin.ProcessKiller { processHelper.TryKill(_context, p.Process); } + // Re-query to refresh process list _context.API.ReQuery(); - return false; + return true; } }); } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index b3763aaa6..3be23214c 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.Program.Programs; using Flow.Launcher.Plugin.Program.Views; using Flow.Launcher.Plugin.Program.Views.Models; @@ -188,9 +191,61 @@ namespace Flow.Launcher.Plugin.Program await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () => { - _win32Storage = new BinaryStorage("Win32"); + Helper.ValidateDirectory(Context.CurrentPluginMetadata.PluginCacheDirectoryPath); + + static void MoveFile(string sourcePath, string destinationPath) + { + if (!File.Exists(sourcePath)) + { + return; + } + + if (File.Exists(destinationPath)) + { + try + { + File.Delete(sourcePath); + } + catch (Exception) + { + // Ignore, we will handle next time we start the plugin + } + return; + } + + var destinationDirectory = Path.GetDirectoryName(destinationPath); + if (!Directory.Exists(destinationDirectory) && (!string.IsNullOrEmpty(destinationDirectory))) + { + try + { + Directory.CreateDirectory(destinationDirectory); + } + catch (Exception) + { + // Ignore, we will handle next time we start the plugin + } + } + try + { + File.Move(sourcePath, destinationPath); + } + catch (Exception) + { + // Ignore, we will handle next time we start the plugin + } + } + + // Move old cache files to the new cache directory + var oldWin32CacheFile = Path.Combine(DataLocation.CacheDirectory, $"Win32.cache"); + var newWin32CacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"Win32.cache"); + MoveFile(oldWin32CacheFile, newWin32CacheFile); + var oldUWPCacheFile = Path.Combine(DataLocation.CacheDirectory, $"UWP.cache"); + var newUWPCacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"UWP.cache"); + MoveFile(oldUWPCacheFile, newUWPCacheFile); + + _win32Storage = new BinaryStorage("Win32", Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _win32s = await _win32Storage.TryLoadAsync(Array.Empty()); - _uwpStorage = new BinaryStorage("UWP"); + _uwpStorage = new BinaryStorage("UWP", Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _uwps = await _uwpStorage.TryLoadAsync(Array.Empty()); }); Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 77f3b56a6..94a9d0348 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Windows; @@ -191,8 +190,6 @@ namespace Flow.Launcher.Plugin.Sys private List Commands() { var results = new List(); - var logPath = Path.Combine(DataLocation.DataDirectory(), "Logs", Constant.Version); - var userDataPath = DataLocation.DataDirectory(); var recycleBinFolder = "shell:RecycleBinFolder"; results.AddRange(new[] { @@ -302,7 +299,7 @@ namespace Flow.Launcher.Plugin.Sys new Result { Title = "Hibernate", - Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe945"), + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe8be"), IcoPath = "Images\\hibernate.png", Action= c => { @@ -325,7 +322,7 @@ namespace Flow.Launcher.Plugin.Sys { Title = "Empty Recycle Bin", IcoPath = "Images\\recyclebin.png", - Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe74d"), + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xea99"), Action = c => { // http://www.pinvoke.net/default.aspx/shell32/SHEmptyRecycleBin.html @@ -361,6 +358,7 @@ namespace Flow.Launcher.Plugin.Sys { Title = "Exit", IcoPath = "Images\\app.png", + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe89f"), Action = c => { Application.Current.MainWindow.Close(); @@ -370,6 +368,7 @@ namespace Flow.Launcher.Plugin.Sys new Result { Title = "Save Settings", + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xea35"), IcoPath = "Images\\app.png", Action = c => { @@ -381,6 +380,7 @@ namespace Flow.Launcher.Plugin.Sys }, new Result { + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe72c"), Title = "Restart Flow Launcher", IcoPath = "Images\\app.png", Action = c => @@ -392,6 +392,7 @@ namespace Flow.Launcher.Plugin.Sys new Result { Title = "Settings", + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf210"), IcoPath = "Images\\app.png", Action = c => { @@ -403,6 +404,7 @@ namespace Flow.Launcher.Plugin.Sys { Title = "Reload Plugin Data", IcoPath = "Images\\app.png", + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe72c"), 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. @@ -421,6 +423,7 @@ namespace Flow.Launcher.Plugin.Sys new Result { Title = "Check For Update", + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xede4"), IcoPath = "Images\\checkupdate.png", Action = c => { @@ -431,19 +434,21 @@ namespace Flow.Launcher.Plugin.Sys }, new Result { + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"), Title = "Open Log Location", IcoPath = "Images\\app.png", - CopyText = logPath, - AutoCompleteText = logPath, + CopyText = DataLocation.VersionLogDirectory, + AutoCompleteText = DataLocation.VersionLogDirectory, Action = c => { - _context.API.OpenDirectory(logPath); + _context.API.OpenDirectory(DataLocation.VersionLogDirectory); return true; } }, new Result { Title = "Flow Launcher Tips", + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe897"), IcoPath = "Images\\app.png", CopyText = Constant.Documentation, AutoCompleteText = Constant.Documentation, @@ -456,12 +461,13 @@ namespace Flow.Launcher.Plugin.Sys new Result { Title = "Flow Launcher UserData Folder", + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"), IcoPath = "Images\\app.png", - CopyText = userDataPath, - AutoCompleteText = userDataPath, + CopyText = DataLocation.DataDirectory(), + AutoCompleteText = DataLocation.DataDirectory(), Action = c => { - _context.API.OpenDirectory(userDataPath); + _context.API.OpenDirectory(DataLocation.DataDirectory()); return true; } }, @@ -480,7 +486,7 @@ namespace Flow.Launcher.Plugin.Sys { Title = "Set Flow Launcher Theme", IcoPath = "Images\\app.png", - Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue7fc"), + Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue790"), Action = c => { _context.API.ChangeQuery($"{ThemeSelector.Keyword} "); diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs index ce53c7da5..7ad9715bb 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Plugin.WebSearch @@ -183,9 +182,8 @@ namespace Flow.Launcher.Plugin.WebSearch DefaultImagesDirectory = Path.Combine(pluginDirectory, Images); Helper.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory); - // Custom images directory is in the WebSearch's data location folder - var name = Path.GetFileNameWithoutExtension(_context.CurrentPluginMetadata.ExecuteFileName); - CustomImagesDirectory = Path.Combine(DataLocation.PluginSettingsDirectory, name, "CustomIcons"); + // Custom images directory is in the WebSearch's data location folder + CustomImagesDirectory = Path.Combine(_context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "CustomIcons"); }; }