Merge pull request #3272 from Jack251970/plugin_settings_cache_path

Improve Plugin Metadata & Path Management
This commit is contained in:
Jack Ye 2025-03-30 08:27:05 +08:00 committed by GitHub
commit be966b7a9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 457 additions and 194 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

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

@ -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,30 +83,72 @@ 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 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

@ -131,6 +131,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>

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

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

@ -486,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

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

@ -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[]
{
@ -440,11 +437,11 @@ namespace Flow.Launcher.Plugin.Sys
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;
}
},
@ -466,11 +463,11 @@ namespace Flow.Launcher.Plugin.Sys
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;
}
},

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