Merge branch 'dev' into search_delay

This commit is contained in:
Jack Ye 2025-03-30 09:38:59 +08:00 committed by GitHub
commit 7f5480dce3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 982 additions and 447 deletions

View file

@ -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;

View file

@ -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<Result> LoadContextMenus(Result selectedResult)
{

View file

@ -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);
}
}
}
}

View file

@ -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
{
/// <summary>
@ -112,7 +111,7 @@ namespace Flow.Launcher.Core.Plugin
metadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(configPath));
metadata.PluginDirectory = pluginDirectory;
// for plugins which doesn't has ActionKeywords key
metadata.ActionKeywords = metadata.ActionKeywords ?? new List<string> { metadata.ActionKeyword };
metadata.ActionKeywords ??= new List<string> { metadata.ActionKeyword };
// for plugin still use old ActionKeyword
metadata.ActionKeyword = metadata.ActionKeywords?[0];
}
@ -137,4 +136,4 @@ namespace Flow.Launcher.Core.Plugin
return metadata;
}
}
}
}

View file

@ -35,7 +35,7 @@ namespace Flow.Launcher.Core.Plugin
private static PluginsSettings Settings;
private static List<PluginMetadata> _metadatas;
private static List<string> _modifiedPlugins = new List<string>();
private static List<string> _modifiedPlugins = new();
/// <summary>
/// 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<PluginMetadata> 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);
}
}
}
/// <summary>
@ -225,10 +249,9 @@ namespace Flow.Launcher.Core.Plugin
if (query is null)
return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
return GlobalPlugins;
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair>
{
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
/// </summary>
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
/// <summary>
/// Uninstall a plugin.
/// </summary>
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);
}

View file

@ -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
};
});
}
}

View file

@ -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);
}
/// <summary>
@ -596,7 +611,7 @@ namespace Flow.Launcher.Core.Resource
var (backdropType, _) = GetActualValue();
SetBlurForWindow(GetCurrentTheme(), backdropType);
}, DispatcherPriority.Normal);
}, DispatcherPriority.Render);
}
/// <summary>

View file

@ -1,12 +0,0 @@
using System;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Resource
{
[Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Theme>() instead.")]
public class ThemeManager
{
public static Theme Instance
=> Ioc.Default.GetRequiredService<Theme>();
}
}

View file

@ -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";

View file

@ -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);

View file

@ -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
/// </summary>
/// <remarks>
/// 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 <see href="https://github.com/Cysharp/MemoryPack"/>
/// </remarks>
public class BinaryStorage<T>
{
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<T> DeserializeAsync(Stream stream, T defaultData)
private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
{
try
{
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
return t;
}
catch (System.Exception e)
catch (System.Exception)
{
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
return defaultData;

View file

@ -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!;

View file

@ -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);
}
}
}
}

View file

@ -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";

View file

@ -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;

View file

@ -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
/// </summary>
public double CustomWindowTop { get; set; } = 0;
public bool KeepMaxResults { get; set; } = false;
/// <summary>
/// Fixed window size
/// </summary>
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<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();

View file

@ -51,6 +51,9 @@ namespace Flow.Launcher.Plugin
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
}
/// <summary>
/// Default <see cref="SpecialKeyState"/> object with all keys not pressed.
/// </summary>
public static readonly SpecialKeyState Default = new () {
CtrlPressed = false,
ShiftPressed = false,

View file

@ -4,17 +4,42 @@ using System.Threading;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Interface for plugins that want to manually update their results
/// </summary>
public interface IResultUpdated : IFeatures
{
/// <summary>
/// Event that is triggered when the results are updated
/// </summary>
event ResultUpdatedEventHandler ResultsUpdated;
}
/// <summary>
/// Delegate for the ResultsUpdated event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
/// <summary>
/// Event arguments for the ResultsUpdated event
/// </summary>
public class ResultUpdatedEventArgs : EventArgs
{
/// <summary>
/// List of results that should be displayed
/// </summary>
public List<Result> Results;
/// <summary>
/// Query that triggered the update
/// </summary>
public Query Query;
/// <summary>
/// Token that can be used to cancel the update
/// </summary>
public CancellationToken Token { get; init; }
}
}
}

View file

@ -2,8 +2,15 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// This interface is used to create settings panel for .Net plugins
/// </summary>
public interface ISettingProvider
{
/// <summary>
/// Create settings panel control for .Net plugins
/// </summary>
/// <returns></returns>
Control CreateSettingPanel();
}
}

View file

@ -5,10 +5,18 @@
/// </summary>
public class PluginInitContext
{
/// <summary>
/// Default constructor.
/// </summary>
public PluginInitContext()
{
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="currentPluginMetadata"></param>
/// <param name="api"></param>
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
{
CurrentPluginMetadata = currentPluginMetadata;

View file

@ -4,24 +4,77 @@ using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Plugin metadata
/// </summary>
public class PluginMetadata : BaseModel
{
private string _pluginDirectory;
/// <summary>
/// Plugin ID.
/// </summary>
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;}
/// <summary>
/// Plugin name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Plugin author.
/// </summary>
public string Author { get; set; }
/// <summary>
/// Plugin version.
/// </summary>
public string Version { get; set; }
/// <summary>
/// Plugin language.
/// See <see cref="AllowedLanguage"/>
/// </summary>
public string Language { get; set; }
/// <summary>
/// Plugin description.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Plugin website.
/// </summary>
public string Website { get; set; }
/// <summary>
/// Whether plugin is disabled.
/// </summary>
public bool Disabled { get; set; }
/// <summary>
/// Plugin execute file path.
/// </summary>
public string ExecuteFilePath { get; private set; }
/// <summary>
/// Plugin execute file name.
/// </summary>
public string ExecuteFileName { get; set; }
/// <summary>
/// Plugin assembly name.
/// Only available for .Net plugins.
/// </summary>
[JsonIgnore]
public string AssemblyName { get; internal set; }
private string _pluginDirectory;
/// <summary>
/// Plugin source directory.
/// </summary>
public string PluginDirectory
{
get { return _pluginDirectory; }
get => _pluginDirectory;
internal set
{
_pluginDirectory = value;
@ -30,32 +83,77 @@ namespace Flow.Launcher.Plugin
}
}
/// <summary>
/// The first action keyword of plugin.
/// </summary>
public string ActionKeyword { get; set; }
/// <summary>
/// All action keywords of plugin.
/// </summary>
public List<string> ActionKeywords { get; set; }
/// <summary>
/// Hide plugin keyword setting panel.
/// </summary>
public bool HideActionKeywordPanel { get; set; }
/// <summary>
/// Plugin search delay in ms.
/// </summary>
public int SearchDelay { get; set; }
/// <summary>
/// Plugin icon path.
/// </summary>
public string IcoPath { get; set;}
public override string ToString()
{
return Name;
}
/// <summary>
/// Plugin priority.
/// </summary>
[JsonIgnore]
public int Priority { get; set; }
/// <summary>
/// Init time include both plugin load time and init time
/// Init time include both plugin load time and init time.
/// </summary>
[JsonIgnore]
public long InitTime { get; set; }
/// <summary>
/// Average query time.
/// </summary>
[JsonIgnore]
public long AvgQueryTime { get; set; }
/// <summary>
/// Query count.
/// </summary>
[JsonIgnore]
public int QueryCount { get; set; }
/// <summary>
/// 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.
/// </summary>
public string PluginSettingsDirectoryPath { get; internal set; }
/// <summary>
/// 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.
/// </summary>
public string PluginCacheDirectoryPath { get; internal set; }
/// <summary>
/// Convert <see cref="PluginMetadata"/> to string.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Name;
}
}
}

View file

@ -1,21 +1,37 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Plugin instance and plugin metadata
/// </summary>
public class PluginPair
{
/// <summary>
/// Plugin instance
/// </summary>
public IAsyncPlugin Plugin { get; internal set; }
/// <summary>
/// Plugin metadata
/// </summary>
public PluginMetadata Metadata { get; internal set; }
/// <summary>
/// Convert to string
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Metadata.Name;
}
/// <summary>
/// Compare by plugin metadata ID
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
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 @@
}
}
/// <summary>
/// Get hash code
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;

View file

@ -2,10 +2,11 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Represents a query that is sent to a plugin.
/// </summary>
public class Query
{
public Query() { }
/// <summary>
/// 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
/// </summary>
public string ActionKeyword { get; init; }
[JsonIgnore]
/// <summary>
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
/// </summary>
/// <remarks>
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
/// </remarks>
[JsonIgnore]
public string FirstSearch => SplitSearch(0);
[JsonIgnore]

View file

@ -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
/// </summary>
public class Result
{
private string _pluginDirectory;
private string _icoPath;

View file

@ -6,6 +6,9 @@ using System.Linq;
namespace Flow.Launcher.Plugin.SharedCommands
{
/// <summary>
/// Contains methods to open a search in a new browser window or tab.
/// </summary>
public static class SearchWeb
{
private static string GetDefaultBrowserPath()
@ -106,4 +109,4 @@ namespace Flow.Launcher.Plugin.SharedCommands
}
}
}
}
}

View file

@ -8,12 +8,26 @@ using Windows.Win32.Foundation;
namespace Flow.Launcher.Plugin.SharedCommands
{
/// <summary>
/// Contains methods for running shell commands
/// </summary>
public static class ShellCommand
{
/// <summary>
/// Delegate for EnumThreadWindows
/// </summary>
/// <param name="hwnd"></param>
/// <param name="lParam"></param>
/// <returns></returns>
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
private static bool containsSecurityWindow;
/// <summary>
/// Runs a windows command using the provided ProcessStartInfo
/// </summary>
/// <param name="processStartInfo"></param>
/// <returns></returns>
public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo)
{
processStartInfo.Verb = "RunAsUser";
@ -65,6 +79,15 @@ namespace Flow.Launcher.Plugin.SharedCommands
return buffer[..length].ToString();
}
/// <summary>
/// Runs a windows command using the provided ProcessStartInfo
/// </summary>
/// <param name="fileName"></param>
/// <param name="workingDirectory"></param>
/// <param name="arguments"></param>
/// <param name="verb"></param>
/// <param name="createNoWindow"></param>
/// <returns></returns>
public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "",
string arguments = "", string verb = "", bool createNoWindow = false)
{

View file

@ -2,14 +2,29 @@
namespace Flow.Launcher.Plugin.SharedModels
{
/// <summary>
/// Represents the result of a match operation.
/// </summary>
public class MatchResult
{
/// <summary>
/// Initializes a new instance of the <see cref="MatchResult"/> class.
/// </summary>
/// <param name="success"></param>
/// <param name="searchPrecision"></param>
public MatchResult(bool success, SearchPrecisionScore searchPrecision)
{
Success = success;
SearchPrecision = searchPrecision;
}
/// <summary>
/// Initializes a new instance of the <see cref="MatchResult"/> class.
/// </summary>
/// <param name="success"></param>
/// <param name="searchPrecision"></param>
/// <param name="matchData"></param>
/// <param name="rawScore"></param>
public MatchResult(bool success, SearchPrecisionScore searchPrecision, List<int> matchData, int rawScore)
{
Success = success;
@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels
RawScore = rawScore;
}
/// <summary>
/// Whether the match operation was successful.
/// </summary>
public bool Success { get; set; }
/// <summary>
@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels
/// </summary>
private int _rawScore;
/// <summary>
/// The raw calculated search score without any search precision filtering applied.
/// </summary>
public int RawScore
{
get { return _rawScore; }
@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels
/// </summary>
public List<int> MatchData { get; set; }
/// <summary>
/// The search precision score used to filter the search results.
/// </summary>
public SearchPrecisionScore SearchPrecision { get; set; }
/// <summary>
/// Determines if the search precision score is met.
/// </summary>
/// <returns></returns>
public bool IsSearchPrecisionScoreMet()
{
return IsSearchPrecisionScoreMet(_rawScore);
@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels
}
}
/// <summary>
/// Represents the search precision score used to filter search results.
/// </summary>
public enum SearchPrecisionScore
{
/// <summary>
/// The highest search precision score.
/// </summary>
Regular = 50,
/// <summary>
/// The medium search precision score.
/// </summary>
Low = 20,
/// <summary>
/// The lowest search precision score.
/// </summary>
None = 0
}
}

View file

@ -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<Theme>().ChangeTheme();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

View file

@ -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);
}
}

View file

@ -39,7 +39,7 @@
<system:String x:Key="GameMode">Game Mode</system:String>
<system:String x:Key="GameModeToolTip">Suspend the use of Hotkeys.</system:String>
<system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Settings</system:String>
@ -72,8 +72,6 @@
<system:String x:Key="LastQueryEmpty">Empty last Query</system:String>
<system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String>
<system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Height</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window height is not adjustable by dragging.</system:String>
<system:String x:Key="maxShowResults">Maximum results shown</system:String>
<system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
@ -108,11 +106,6 @@
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropTypesNone">None</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>
@ -143,6 +136,8 @@
<system:String x:Key="plugin_uninstall">Uninstall</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plugin Store</system:String>
@ -205,8 +200,19 @@
<system:String x:Key="AnimationSpeedCustom">Custom</system:String>
<system:String x:Key="Clock">Clock</system:String>
<system:String x:Key="Date">Date</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropTypesNone">None</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String>
<system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="PlaceholderText">Placeholder text</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<!-- Setting Hotkey -->
<system:String x:Key="hotkey">Hotkey</system:String>

View file

@ -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 @@
<Grid x:Name="QueryBoxArea">
<Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}">
<Grid>
<TextBox
x:Name="QueryTextPlaceholderBox"
Height="{Binding MainWindowHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
FontSize="{Binding QueryBoxFontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="False"
Style="{DynamicResource QuerySuggestionBoxStyle}"
Text="{Binding PlaceholderText, Mode=OneWay}"
Visibility="Collapsed" />
<TextBox
x:Name="QueryTextSuggestionBox"
Height="{Binding MainWindowHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
@ -290,7 +297,9 @@
<StackPanel
x:Name="ClockPanel"
IsHitTestVisible="False"
Style="{DynamicResource ClockPanel}">
Opacity="{Binding ClockPanelOpacity}"
Style="{DynamicResource ClockPanel}"
Visibility="{Binding ClockPanelVisibility}">
<TextBlock
x:Name="ClockBox"
Style="{DynamicResource ClockBox}"
@ -318,6 +327,7 @@
Name="SearchIcon"
Margin="0"
Data="{DynamicResource SearchIconImg}"
Opacity="{Binding SearchIconOpacity}"
Stretch="Fill"
Style="{DynamicResource SearchIconStyle}"
Visibility="{Binding SearchIconVisibility}" />

View file

@ -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<Theme>().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<Setter>())
{
@ -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<Setter>())
{
@ -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);

View file

@ -192,7 +192,7 @@ namespace Flow.Launcher
private readonly ConcurrentDictionary<Type, object> _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;
}
/// <summary>

View file

@ -115,6 +115,8 @@
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#FCE100" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#433519" />
<SolidColorBrush x:Key="InfoBarBD" Color="#19000000" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />
<SolidColorBrush x:Key="ButtonOutBorder" Color="Transparent" />
<SolidColorBrush x:Key="ButtonInsideBorder" Color="#3f3f3f" />

View file

@ -107,6 +107,7 @@
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#FFF4CE" />
<SolidColorBrush x:Key="InfoBarBD" Color="#0F000000" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />
<SolidColorBrush x:Key="ButtonOutBorder" Color="#e5e5e5" />
<SolidColorBrush x:Key="ButtonInsideBorder" Color="#d3d3d3" />

View file

@ -110,8 +110,8 @@
<Border.Background>
<LinearGradientBrush StartPoint="0 0" EndPoint="1 1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#1494df" />
<GradientStop Offset="1.0" Color="#1073bd" />
<GradientStop Offset="0.0" Color="#2A4D8C" />
<GradientStop Offset="1.0" Color="#1E3160" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>

View file

@ -60,7 +60,7 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Border Width="450" Style="{DynamicResource WindowBorderStyle}">
<Border Width="450" Style="{DynamicResource PreviewWindowBorderStyle}">
<Border Style="{DynamicResource WindowRadius}">
<Grid>
<Grid.RowDefinitions>

View file

@ -58,10 +58,10 @@
<Border Grid.Row="0" HorizontalAlignment="Stretch">
<Border.Background>
<LinearGradientBrush StartPoint="0 0" EndPoint="1 1">
<LinearGradientBrush StartPoint="0 1" EndPoint="0 0">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#7b83eb" />
<GradientStop Offset="1.0" Color="#555dc0" />
<GradientStop Offset="0.0" Color="#E5F3F7" />
<GradientStop Offset="1.0" Color="#FAFAFD" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>

View file

@ -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<FileInfo> GetLogFiles(string version = "")

View file

@ -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]

View file

@ -30,6 +30,7 @@
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.ScrollUnit="Pixel">
<StackPanel>
<!-- Page title -->
<TextBlock
Margin="5 23 0 5"
@ -38,6 +39,7 @@
Text="{DynamicResource appearance}"
TextAlignment="left"
Visibility="Collapsed" />
<!-- Theme Preview and Editor -->
<Grid>
<Grid.Style>
@ -68,6 +70,7 @@
<ColumnDefinition Width="8*" />
<ColumnDefinition MaxWidth="250" Style="{StaticResource SecondColumnStyle}" />
</Grid.ColumnDefinitions>
<!-- Theme Size Editor -->
<Border
Grid.Column="1"
@ -260,7 +263,8 @@
</StackPanel>
</ScrollViewer>
</Border>
<!-- Theme Preview -->
<!-- Theme preview -->
<Border
Grid.Column="0"
Background="{Binding PreviewBackground}"
@ -477,7 +481,6 @@
<!-- ✅ 추가 -->
</cc:Card>
<!-- Drop shadow effect -->
<cc:Card
Title="{DynamicResource queryWindowShadowEffect}"
@ -496,18 +499,20 @@
Text="{DynamicResource browserMoreThemes}"
Uri="{Binding LinkThemeGallery}" />
<!-- Fixed Height -->
<!-- Fixed size -->
<cc:CardGroup Margin="0 20 0 0">
<cc:Card
Title="{DynamicResource KeepMaxResults}"
Icon="&#xe8fd;"
Icon="&#xE744;"
Sub="{DynamicResource KeepMaxResultsToolTip}">
<ui:ToggleSwitch IsOn="{Binding KeepMaxResults}" />
<ui:ToggleSwitch
IsOn="{Binding KeepMaxResults}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource maxShowResults}"
Icon="&#xe8fd;"
Sub="{DynamicResource maxShowResultsToolTip}"
Visibility="{Binding KeepMaxResults, Converter={StaticResource BoolToVisibilityConverter}}">
<ComboBox
@ -529,6 +534,29 @@
OnContent="{DynamicResource enable}" />
</cc:Card>
<!-- Placeholder text -->
<cc:CardGroup Margin="0 14 0 0">
<cc:Card
Title="{DynamicResource ShowPlaceholder}"
Icon="&#xE82F;"
Sub="{DynamicResource ShowPlaceholderTip}">
<ui:ToggleSwitch
IsOn="{Binding ShowPlaceholder}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource PlaceholderText}"
Icon="&#xED59;"
Sub="{Binding PlaceholderTextTip}">
<TextBox
MinWidth="150"
Text="{Binding PlaceholderText}"
TextWrapping="NoWrap" />
</cc:Card>
</cc:CardGroup>
<!-- Time and date -->
<cc:CardGroup Margin="0 14 0 0">
<cc:Card Title="{DynamicResource Clock}" Icon="&#xec92;">

View file

@ -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" />
<TextBlock
Grid.Row="0"

View file

@ -28,7 +28,6 @@
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Foreground" Value="#72767d" />
<Setter Property="Background" Value="#36393f" />
<Setter Property="Height" Value="42" />
<Setter Property="FontSize" Value="24" />
<Setter Property="Padding" Value="0,0,66,0" />

View file

@ -24,7 +24,6 @@
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="#161614" />
<Setter Property="Foreground" Value="#a09b8c" />
<Setter Property="Padding" Value="0,0,66,0" />
<Setter Property="Height" Value="42" />

View file

@ -22,7 +22,6 @@
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="#1f1d1f" />
<Setter Property="Foreground" Value="#71114b" />
</Style>

View file

@ -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
/// <param name="isReQuery">Force query even when Query Text doesn't change</param>
public void ChangeQueryText(string queryText, bool isReQuery = false)
{
Application.Current.Dispatcher.Invoke(() =>
_ = ChangeQueryTextAsync(queryText, isReQuery);
}
/// <summary>
/// Async version of <see cref="ChangeQueryText"/>
/// </summary>
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 });

View file

@ -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)
{

View file

@ -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;
}
});
}

View file

@ -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[]>("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[]>("Win32", Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
_win32s = await _win32Storage.TryLoadAsync(Array.Empty<Win32>());
_uwpStorage = new BinaryStorage<UWPApp[]>("UWP");
_uwpStorage = new BinaryStorage<UWPApp[]>("UWP", Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
_uwps = await _uwpStorage.TryLoadAsync(Array.Empty<UWPApp>());
});
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");

View file

@ -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<Result> Commands()
{
var results = new List<Result>();
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} ");

View file

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