diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs
index 069154364..2b570d2c0 100644
--- a/Flow.Launcher.Core/Configuration/Portable.cs
+++ b/Flow.Launcher.Core/Configuration/Portable.cs
@@ -22,7 +22,7 @@ namespace Flow.Launcher.Core.Configuration
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
///
///
- private UpdateManager NewUpdateManager()
+ private static UpdateManager NewUpdateManager()
{
var applicationFolderName = Constant.ApplicationDirectory
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
@@ -81,20 +81,16 @@ namespace Flow.Launcher.Core.Configuration
public void RemoveShortcuts()
{
- using (var portabilityUpdater = NewUpdateManager())
- {
- portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
- portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
- portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
- }
+ using var portabilityUpdater = NewUpdateManager();
+ portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
+ portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
+ portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
}
public void RemoveUninstallerEntry()
{
- using (var portabilityUpdater = NewUpdateManager())
- {
- portabilityUpdater.RemoveUninstallerRegistryEntry();
- }
+ using var portabilityUpdater = NewUpdateManager();
+ portabilityUpdater.RemoveUninstallerRegistryEntry();
}
public void MoveUserDataFolder(string fromLocation, string toLocation)
@@ -110,12 +106,10 @@ namespace Flow.Launcher.Core.Configuration
public void CreateShortcuts()
{
- using (var portabilityUpdater = NewUpdateManager())
- {
- portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
- portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
- portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
- }
+ using var portabilityUpdater = NewUpdateManager();
+ portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
+ portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
+ portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
}
public void CreateUninstallerEntry()
@@ -129,18 +123,14 @@ namespace Flow.Launcher.Core.Configuration
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
}
- using (var portabilityUpdater = NewUpdateManager())
- {
- _ = portabilityUpdater.CreateUninstallerRegistryEntry();
- }
+ using var portabilityUpdater = NewUpdateManager();
+ _ = portabilityUpdater.CreateUninstallerRegistryEntry();
}
- internal void IndicateDeletion(string filePathTodelete)
+ private static void IndicateDeletion(string filePathTodelete)
{
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
- using (var _ = File.CreateText(deleteFilePath))
- {
- }
+ using var _ = File.CreateText(deleteFilePath);
}
///
diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs
index 68be746f2..e9713564e 100644
--- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs
@@ -1,5 +1,6 @@
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Plugin;
using System;
using System.Collections.Generic;
using System.Net;
diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs
index affd7c312..1f23c2f66 100644
--- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs
@@ -2,6 +2,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins
{
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
index 451df6147..bbb6cf638 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
@@ -1,14 +1,14 @@
-using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Infrastructure.UserSettings;
-using Flow.Launcher.Plugin;
-using Flow.Launcher.Plugin.SharedCommands;
-using System;
+using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
-using Flow.Launcher.Core.Resource;
using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@@ -42,8 +42,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal IEnumerable Setup()
{
+ // If no plugin is using the language, return empty list
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
+ {
return new List();
+ }
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
{
@@ -55,24 +58,55 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
var noRuntimeMessage = string.Format(
- InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
+ API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
Language,
EnvName,
Environment.NewLine
);
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{
- var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
- string selectedFile;
+ var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
- selectedFile = GetFileFromDialog(msg, FileDialogFilter);
+ var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
if (!string.IsNullOrEmpty(selectedFile))
+ {
PluginsSettingsFilePath = selectedFile;
-
+ }
// Nothing selected because user pressed cancel from the file dialog window
- if (string.IsNullOrEmpty(selectedFile))
- InstallEnvironment();
+ else
+ {
+ var forceDownloadMessage = string.Format(
+ API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
+ Language,
+ EnvName,
+ Environment.NewLine
+ );
+
+ // Let users select valid path or choose to download
+ while (string.IsNullOrEmpty(selectedFile))
+ {
+ if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+ {
+ // Continue select file
+ selectedFile = GetFileFromDialog(msg, FileDialogFilter);
+ }
+ else
+ {
+ // User selected no, break the loop
+ break;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(selectedFile))
+ {
+ PluginsSettingsFilePath = selectedFile;
+ }
+ else
+ {
+ InstallEnvironment();
+ }
+ }
}
else
{
@@ -85,7 +119,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
else
{
- API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
+ API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
Log.Error("PluginsLoader",
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");
@@ -98,13 +132,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
{
- if (expectedPath == currentPath)
- return;
+ if (expectedPath == currentPath) return;
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
InstallEnvironment();
-
}
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
@@ -116,13 +148,16 @@ 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;
}
- private string GetFileFromDialog(string title, string filter = "")
+ private static string GetFileFromDialog(string title, string filter = "")
{
var dlg = new OpenFileDialog
{
@@ -136,7 +171,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
var result = dlg.ShowDialog();
return result == DialogResult.OK ? dlg.FileName : string.Empty;
-
}
///
@@ -179,31 +213,33 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
else
{
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
+ {
settings.PluginSettings.PythonExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
+ }
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
+ {
settings.PluginSettings.NodeExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
+ }
}
}
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
{
- if (string.IsNullOrEmpty(filePath))
- return false;
+ if (string.IsNullOrEmpty(filePath)) return false;
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
// of date path is also a portable path.
- var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}";
+ var portableAppEnvLocation = Path.Combine("UserData", DataLocation.PluginEnvironments, pluginEnvironmentName);
return filePath.Contains(portableAppEnvLocation);
}
private static bool IsUsingRoamingPath(string filePath)
{
- if (string.IsNullOrEmpty(filePath))
- return false;
+ if (string.IsNullOrEmpty(filePath)) return false;
return filePath.StartsWith(DataLocation.RoamingDataPath);
}
@@ -213,8 +249,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
// get the substring after "Environments" because we can not determine it dynamically
- var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count());
- return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}";
+ var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
+ return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
}
}
}
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs
index 607c19062..fab5738de 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs
@@ -1,10 +1,10 @@
-using Droplex;
+using System.Collections.Generic;
+using System.IO;
+using Droplex;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
-using System.Collections.Generic;
-using System.IO;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@@ -22,7 +22,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string FileDialogFilter => "Python|pythonw.exe";
- internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; }
+ internal override string PluginsSettingsFilePath
+ {
+ get => PluginSettings.PythonExecutablePath;
+ set => PluginSettings.PythonExecutablePath = value;
+ }
internal PythonEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs
index 399f7cc03..8a4f527ba 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs
@@ -1,10 +1,10 @@
using System.Collections.Generic;
-using Droplex;
-using Flow.Launcher.Infrastructure.UserSettings;
-using Flow.Launcher.Plugin.SharedCommands;
-using Flow.Launcher.Plugin;
using System.IO;
+using Droplex;
using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@@ -19,7 +19,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
- internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; }
+ internal override string PluginsSettingsFilePath
+ {
+ get => PluginSettings.NodeExecutablePath;
+ set => PluginSettings.NodeExecutablePath = value;
+ }
internal TypeScriptEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs
index e8cb72e11..61fd28376 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs
@@ -1,10 +1,10 @@
using System.Collections.Generic;
-using Droplex;
-using Flow.Launcher.Infrastructure.UserSettings;
-using Flow.Launcher.Plugin.SharedCommands;
-using Flow.Launcher.Plugin;
using System.IO;
+using Droplex;
using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
@@ -19,7 +19,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
- internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; }
+ internal override string PluginsSettingsFilePath
+ {
+ get => PluginSettings.NodeExecutablePath;
+ set => PluginSettings.NodeExecutablePath = value;
+ }
internal TypeScriptV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
index ac8abcdcc..44d3ef0ff 100644
--- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs
@@ -1,8 +1,9 @@
-using Flow.Launcher.Infrastructure.Logger;
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins
{
@@ -17,11 +18,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
private static DateTime lastFetchedAt = DateTime.MinValue;
- private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
+ private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
public static List UserPlugins { get; private set; }
- public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
+ public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
{
try
{
@@ -43,7 +44,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
}
catch (Exception e)
{
- Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e);
+ Ioc.Default.GetRequiredService().LogException(nameof(PluginsManifest), "Http request failed", e);
}
finally
{
diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
deleted file mode 100644
index 79d6d7605..000000000
--- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-
-namespace Flow.Launcher.Core.ExternalPlugins
-{
- public record UserPlugin
- {
- public string ID { get; set; }
- public string Name { get; set; }
- public string Description { get; set; }
- public string Author { get; set; }
- public string Version { get; set; }
- public string Language { get; set; }
- public string Website { get; set; }
- public string UrlDownload { get; set; }
- public string UrlSourceCode { get; set; }
- public string LocalInstallPath { get; set; }
- public string IcoPath { get; set; }
- public DateTime? LatestReleaseDate { get; set; }
- public DateTime? DateAdded { get; set; }
-
- public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
- }
-}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
index 97c3c8981..88d595301 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
@@ -1,28 +1,16 @@
using Flow.Launcher.Core.Resource;
-using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
-using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Microsoft.IO;
using System.Windows;
-using System.Windows.Controls;
-using YamlDotNet.Serialization;
-using YamlDotNet.Serialization.NamingConventions;
-using CheckBox = System.Windows.Controls.CheckBox;
-using Control = System.Windows.Controls.Control;
-using Orientation = System.Windows.Controls.Orientation;
-using TextBox = System.Windows.Controls.TextBox;
-using UserControl = System.Windows.Controls.UserControl;
-using System.Windows.Documents;
namespace Flow.Launcher.Core.Plugin
{
@@ -42,7 +30,7 @@ namespace Flow.Launcher.Core.Plugin
private int RequestId { get; set; }
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
- private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
+ private string SettingPath => Path.Combine(Context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "Settings.json");
public override List LoadContextMenus(Result selectedResult)
{
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
index 7248c6259..779dcf887 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
@@ -1,32 +1,15 @@
using Flow.Launcher.Core.Resource;
-using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Linq;
-using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
-using Microsoft.IO;
-using System.Windows;
-using System.Windows.Controls;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
-using CheckBox = System.Windows.Controls.CheckBox;
using Control = System.Windows.Controls.Control;
-using Orientation = System.Windows.Controls.Orientation;
-using TextBox = System.Windows.Controls.TextBox;
-using UserControl = System.Windows.Controls.UserControl;
-using System.Windows.Documents;
-using static System.Windows.Forms.LinkLabel;
-using Droplex;
-using System.Windows.Forms;
-using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.Core.Plugin
{
@@ -44,8 +27,7 @@ namespace Flow.Launcher.Core.Plugin
private string SettingConfigurationPath =>
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
- private string SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory,
- Context.CurrentPluginMetadata.Name);
+ private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath;
private string SettingPath => Path.Combine(SettingDirectory, "Settings.json");
@@ -166,13 +148,5 @@ namespace Flow.Launcher.Core.Plugin
{
return Settings.CreateSettingPanel();
}
-
- public void DeletePluginSettingsDirectory()
- {
- if (Directory.Exists(SettingDirectory))
- {
- Directory.Delete(SettingDirectory, true);
- }
- }
}
}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
index 944b2fd10..e0a217251 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
@@ -23,6 +23,8 @@ namespace Flow.Launcher.Core.Plugin
protected ConcurrentDictionary Settings { get; set; } = null!;
public required IPublicAPI API { get; init; }
+ private static readonly string ClassName = nameof(JsonRPCPluginSettings);
+
private JsonStorage> _storage = null!;
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
@@ -122,12 +124,26 @@ namespace Flow.Launcher.Core.Plugin
public async Task SaveAsync()
{
- await _storage.SaveAsync();
+ try
+ {
+ await _storage.SaveAsync();
+ }
+ catch (System.Exception e)
+ {
+ API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
+ }
}
public void Save()
{
- _storage.Save();
+ try
+ {
+ _storage.Save();
+ }
+ catch (System.Exception e)
+ {
+ API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
+ }
}
public bool NeedCreateSettingPanel()
diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs
index dd6517a7f..163f97046 100644
--- a/Flow.Launcher.Core/Plugin/PluginConfig.cs
+++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
@@ -9,7 +9,6 @@ using System.Text.Json;
namespace Flow.Launcher.Core.Plugin
{
-
internal abstract class PluginConfig
{
///
@@ -112,7 +111,7 @@ namespace Flow.Launcher.Core.Plugin
metadata = JsonSerializer.Deserialize(File.ReadAllText(configPath));
metadata.PluginDirectory = pluginDirectory;
// for plugins which doesn't has ActionKeywords key
- metadata.ActionKeywords = metadata.ActionKeywords ?? new List { metadata.ActionKeyword };
+ metadata.ActionKeywords ??= new List { metadata.ActionKeyword };
// for plugin still use old ActionKeyword
metadata.ActionKeyword = metadata.ActionKeywords?[0];
}
@@ -137,4 +136,4 @@ namespace Flow.Launcher.Core.Plugin
return metadata;
}
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index aab3caa40..aa6c54a94 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -1,20 +1,19 @@
-using Flow.Launcher.Core.ExternalPlugins;
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
-using ISavable = Flow.Launcher.Plugin.ISavable;
using Flow.Launcher.Plugin.SharedCommands;
-using System.Text.Json;
-using Flow.Launcher.Core.Resource;
-using CommunityToolkit.Mvvm.DependencyInjection;
+using ISavable = Flow.Launcher.Plugin.ISavable;
namespace Flow.Launcher.Core.Plugin
{
@@ -35,7 +34,7 @@ namespace Flow.Launcher.Core.Plugin
private static PluginsSettings Settings;
private static List _metadatas;
- private static List _modifiedPlugins = new List();
+ private static List _modifiedPlugins = new();
///
/// Directories that will hold Flow Launcher plugin directory
@@ -72,15 +71,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 +159,25 @@ namespace Flow.Launcher.Core.Plugin
Settings = settings;
Settings.UpdatePluginSettings(_metadatas);
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
+ // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
+ UpdatePluginDirectory(_metadatas);
+ }
+
+ private static void UpdatePluginDirectory(List metadatas)
+ {
+ foreach (var metadata in metadatas)
+ {
+ if (AllowedLanguage.IsDotNet(metadata.Language))
+ {
+ metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName);
+ metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName);
+ }
+ else
+ {
+ metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name);
+ metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name);
+ }
+ }
}
///
@@ -225,10 +248,9 @@ namespace Flow.Launcher.Core.Plugin
if (query is null)
return Array.Empty();
- if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
+ if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
return GlobalPlugins;
- var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List
{
plugin
@@ -432,37 +454,26 @@ namespace Flow.Launcher.Core.Plugin
#region Public functions
- public static bool PluginModified(string uuid)
+ public static bool PluginModified(string id)
{
- return _modifiedPlugins.Contains(uuid);
+ return _modifiedPlugins.Contains(id);
}
-
- ///
- /// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
- /// unless it's a local path installation
- ///
- public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
+ public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{
InstallPlugin(newVersion, zipFilePath, checkModified:false);
- UninstallPlugin(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
+ await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
_modifiedPlugins.Add(existingVersion.ID);
}
- ///
- /// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
- ///
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
{
InstallPlugin(plugin, zipFilePath, checkModified: true);
}
- ///
- /// Uninstall a plugin.
- ///
- public static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
+ public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
{
- UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, true);
+ await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
}
#endregion
@@ -503,20 +514,20 @@ namespace Flow.Launcher.Core.Plugin
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
var defaultPluginIDs = new List
- {
- "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
- "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
- "572be03c74c642baae319fc283e561a8", // Explorer
- "6A122269676E40EB86EB543B945932B9", // PluginIndicator
- "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
- "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
- "791FC278BA414111B8D1886DFE447410", // Program
- "D409510CD0D2481F853690A07E6DC426", // Shell
- "CEA08895D2544B019B2E9C5009600DF4", // Sys
- "0308FD86DE0A4DEE8D62B9B535370992", // URL
- "565B73353DBF4806919830B9202EE3BF", // WebSearch
- "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
- };
+ {
+ "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
+ "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
+ "572be03c74c642baae319fc283e561a8", // Explorer
+ "6A122269676E40EB86EB543B945932B9", // PluginIndicator
+ "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
+ "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
+ "791FC278BA414111B8D1886DFE447410", // Program
+ "D409510CD0D2481F853690A07E6DC426", // Shell
+ "CEA08895D2544B019B2E9C5009600DF4", // Sys
+ "0308FD86DE0A4DEE8D62B9B535370992", // URL
+ "565B73353DBF4806919830B9202EE3BF", // WebSearch
+ "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
+ };
// Treat default plugin differently, it needs to be removable along with each flow release
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
@@ -543,64 +554,63 @@ 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)
{
- Settings.Plugins.Remove(plugin.ID);
+ 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.RemovePluginSettings(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
}
diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
index 4827cf69d..495a4c1ab 100644
--- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs
+++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs
@@ -50,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
- public static IEnumerable DotNetPlugins(List source)
+ private static IEnumerable DotNetPlugins(List source)
{
var erroredPlugins = new List();
@@ -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 ")
@@ -130,23 +132,31 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
- public static IEnumerable ExecutablePlugins(IEnumerable source)
+ private static IEnumerable ExecutablePlugins(IEnumerable source)
{
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
+ };
});
}
- public static IEnumerable ExecutableV2Plugins(IEnumerable source)
+ private static IEnumerable ExecutableV2Plugins(IEnumerable source)
{
return source
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
- .Select(metadata => new PluginPair
+ .Select(metadata =>
{
- Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata
+ return new PluginPair
+ {
+ Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
+ Metadata = metadata
+ };
});
}
}
diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs
index 5db2c2d14..729a1169b 100644
--- a/Flow.Launcher.Core/Updater.cs
+++ b/Flow.Launcher.Core/Updater.cs
@@ -4,10 +4,12 @@ using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows;
-using JetBrains.Annotations;
-using Squirrel;
+using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Infrastructure;
@@ -15,8 +17,8 @@ using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
-using System.Text.Json.Serialization;
-using System.Threading;
+using JetBrains.Annotations;
+using Squirrel;
namespace Flow.Launcher.Core
{
@@ -59,7 +61,7 @@ namespace Flow.Launcher.Core
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = Version.Parse(Constant.Version);
- Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>");
+ Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
if (newReleaseVersion <= currentVersion)
{
@@ -78,7 +80,7 @@ namespace Flow.Launcher.Core
if (DataLocation.PortableDataLocationInUse())
{
- var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
+ var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
@@ -130,7 +132,7 @@ namespace Flow.Launcher.Core
}
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
- private async Task GitHubUpdateManagerAsync(string repository)
+ private static async Task GitHubUpdateManagerAsync(string repository)
{
var uri = new Uri(repository);
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
@@ -152,12 +154,22 @@ namespace Flow.Launcher.Core
return manager;
}
- public string NewVersionTips(string version)
+ private static string NewVersionTips(string version)
{
- var translator = InternationalizationManager.Instance;
+ var translator = Ioc.Default.GetRequiredService();
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
return tips;
}
+
+ private static string Formatted(T t)
+ {
+ var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ });
+
+ return formatted;
+ }
}
}
diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs
index b4b2485c9..13da9f79f 100644
--- a/Flow.Launcher.Infrastructure/Constant.cs
+++ b/Flow.Launcher.Infrastructure/Constant.cs
@@ -48,6 +48,7 @@ namespace Flow.Launcher.Infrastructure
public const string Themes = "Themes";
public const string Settings = "Settings";
public const string Logs = "Logs";
+ public const string Cache = "Cache";
public const string Website = "https://flowlauncher.com";
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
diff --git a/Flow.Launcher.Infrastructure/Helper.cs b/Flow.Launcher.Infrastructure/Helper.cs
index 864d796c7..b02d84ca7 100644
--- a/Flow.Launcher.Infrastructure/Helper.cs
+++ b/Flow.Launcher.Infrastructure/Helper.cs
@@ -1,19 +1,11 @@
#nullable enable
using System;
-using System.IO;
-using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure
{
public static class Helper
{
- static Helper()
- {
- jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
- }
-
///
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
///
@@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure
throw new NullReferenceException();
}
}
-
- public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
- {
- if (!Directory.Exists(dataDirectory))
- {
- Directory.CreateDirectory(dataDirectory);
- }
-
- foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
- {
- var data = Path.GetFileName(bundledDataPath);
- var dataPath = Path.Combine(dataDirectory, data.NonNull());
- if (!File.Exists(dataPath))
- {
- File.Copy(bundledDataPath, dataPath);
- }
- else
- {
- var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
- var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
- if (time1 != time2)
- {
- File.Copy(bundledDataPath, dataPath, true);
- }
- }
- }
- }
-
- public static void ValidateDirectory(string path)
- {
- if (!Directory.Exists(path))
- {
- Directory.CreateDirectory(path);
- }
- }
-
- private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions
- {
- WriteIndented = true
- };
-
- public static string Formatted(this T t)
- {
- var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
- {
- WriteIndented = true
- });
-
- return formatted;
- }
}
}
diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs
index 6f7b1cd90..c8d3ffbc4 100644
--- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs
+++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs
@@ -5,12 +5,10 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
-using static Flow.Launcher.Infrastructure.Http.Http;
namespace Flow.Launcher.Infrastructure.Image
{
@@ -28,7 +26,6 @@ namespace Flow.Launcher.Infrastructure.Image
public const int SmallIconSize = 64;
public const int FullIconSize = 256;
-
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
public static async Task InitializeAsync()
@@ -61,7 +58,7 @@ namespace Flow.Launcher.Infrastructure.Image
});
}
- public static async Task Save()
+ public static async Task SaveAsync()
{
await storageLock.WaitAsync();
@@ -71,12 +68,22 @@ namespace Flow.Launcher.Infrastructure.Image
.Select(x => x.Key)
.ToList());
}
+ catch (System.Exception e)
+ {
+ Log.Exception($"|ImageLoader.SaveAsync|Failed to save image cache to file", e);
+ }
finally
{
storageLock.Release();
}
}
+ public static async Task WaitSaveAsync()
+ {
+ await storageLock.WaitAsync();
+ storageLock.Release();
+ }
+
private static async Task> LoadStorageToConcurrentDictionaryAsync()
{
await storageLock.WaitAsync();
@@ -173,7 +180,7 @@ namespace Flow.Launcher.Infrastructure.Image
private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
{
// Download image from url
- await using var resp = await GetStreamAsync(uriResult);
+ await using var resp = await Http.Http.GetStreamAsync(uriResult);
await using var buffer = new MemoryStream();
await resp.CopyToAsync(buffer);
buffer.Seek(0, SeekOrigin.Begin);
diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs
index 7f847e287..9f5d6725e 100644
--- a/Flow.Launcher.Infrastructure/Logger/Log.cs
+++ b/Flow.Launcher.Infrastructure/Logger/Log.cs
@@ -12,13 +12,13 @@ namespace Flow.Launcher.Infrastructure.Logger
{
public static class Log
{
- public const string DirectoryName = "Logs";
+ public const string DirectoryName = Constant.Logs;
public static string CurrentLogDirectory { get; }
static Log()
{
- CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version);
+ CurrentLogDirectory = DataLocation.VersionLogDirectory;
if (!Directory.Exists(CurrentLogDirectory))
{
Directory.CreateDirectory(CurrentLogDirectory);
diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs
index 2a439b8cc..a8d5f5d62 100644
--- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs
@@ -1,12 +1,8 @@
-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;
+using Flow.Launcher.Plugin.SharedCommands;
using MemoryPack;
namespace Flow.Launcher.Infrastructure.Storage
@@ -16,19 +12,17 @@ namespace Flow.Launcher.Infrastructure.Storage
/// Normally, it has better performance, but not readable
///
///
- /// It utilize MemoryPack, which means the object must be MemoryPackSerializable
- /// https://github.com/Cysharp/MemoryPack
+ /// It utilize MemoryPack, which means the object must be MemoryPackSerializable
///
public class BinaryStorage
{
- const string DirectoryName = "Cache";
+ public const string FileSuffix = ".cache";
- const string FileSuffix = ".cache";
-
- public BinaryStorage(string filename)
+ // Let the derived class to set the file path
+ public BinaryStorage(string filename, string directoryPath = null)
{
- var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
- Helper.ValidateDirectory(directoryPath);
+ directoryPath ??= DataLocation.CacheDirectory;
+ FilesFolders.ValidateDirectory(directoryPath);
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
}
@@ -58,14 +52,14 @@ namespace Flow.Launcher.Infrastructure.Storage
}
}
- private async ValueTask DeserializeAsync(Stream stream, T defaultData)
+ private static async ValueTask DeserializeAsync(Stream stream, T defaultData)
{
try
{
var t = await MemoryPackSerializer.DeserializeAsync(stream);
return t;
}
- catch (System.Exception e)
+ catch (System.Exception)
{
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
return defaultData;
diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs
index 865041fb3..8b4062b6b 100644
--- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs
@@ -1,17 +1,51 @@
using System.IO;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
public class FlowLauncherJsonStorage : JsonStorage where T : new()
{
+ private static readonly string ClassName = "FlowLauncherJsonStorage";
+
+ // We should not initialize API in static constructor because it will create another API instance
+ private static IPublicAPI api = null;
+ private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService();
+
public FlowLauncherJsonStorage()
{
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
- Helper.ValidateDirectory(directoryPath);
+ FilesFolders.ValidateDirectory(directoryPath);
var filename = typeof(T).Name;
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
}
+
+ public new void Save()
+ {
+ try
+ {
+ base.Save();
+ }
+ catch (System.Exception e)
+ {
+ API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
+ }
+ }
+
+ public new async Task SaveAsync()
+ {
+ try
+ {
+ await base.SaveAsync();
+ }
+ catch (System.Exception e)
+ {
+ API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
index 507838d94..a3488124b 100644
--- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
+using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
@@ -16,7 +17,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!;
@@ -37,7 +38,7 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = filePath;
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
- Helper.ValidateDirectory(DirectoryPath);
+ FilesFolders.ValidateDirectory(DirectoryPath);
}
public async Task LoadAsync()
diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
index bc3900da8..e8cbd70fb 100644
--- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
@@ -1,5 +1,9 @@
using System.IO;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
@@ -8,13 +12,19 @@ namespace Flow.Launcher.Infrastructure.Storage
// Use assembly name to check which plugin is using this storage
public readonly string AssemblyName;
+ private static readonly string ClassName = "PluginJsonStorage";
+
+ // We should not initialize API in static constructor because it will create another API instance
+ private static IPublicAPI api = null;
+ private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService();
+
public PluginJsonStorage()
{
// C# related, add python related below
var dataType = typeof(T);
AssemblyName = dataType.Assembly.GetName().Name;
- DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName);
- Helper.ValidateDirectory(DirectoryPath);
+ DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName);
+ FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
}
@@ -24,11 +34,27 @@ namespace Flow.Launcher.Infrastructure.Storage
Data = data;
}
- public void DeleteDirectory()
+ public new void Save()
{
- if (Directory.Exists(DirectoryPath))
+ try
{
- Directory.Delete(DirectoryPath, true);
+ base.Save();
+ }
+ catch (System.Exception e)
+ {
+ API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
+ }
+ }
+
+ public new async Task SaveAsync()
+ {
+ try
+ {
+ await base.SaveAsync();
+ }
+ catch (System.Exception e)
+ {
+ API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
}
}
}
diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs
index e294f52b8..5b948e450 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs
@@ -25,8 +25,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
return false;
}
+ public static string VersionLogDirectory => Path.Combine(LogDirectory, Constant.Version);
+ public static string LogDirectory => Path.Combine(DataDirectory(), Constant.Logs);
+
+ public static readonly string CacheDirectory = Path.Combine(DataDirectory(), Constant.Cache);
+ public static readonly string SettingsDirectory = Path.Combine(DataDirectory(), Constant.Settings);
public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins);
- public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins);
+ public static readonly string ThemesDirectory = Path.Combine(DataDirectory(), Constant.Themes);
+
+ public static readonly string PluginSettingsDirectory = Path.Combine(SettingsDirectory, Constant.Plugins);
+ public static readonly string PluginCacheDirectory = Path.Combine(DataDirectory(), Constant.Cache, Constant.Plugins);
public const string PythonEnvironmentName = "Python";
public const string NodeEnvironmentName = "Node.js";
diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
index 98f4dccda..da92a3583 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
@@ -6,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public class PluginsSettings : BaseModel
{
private string pythonExecutablePath = string.Empty;
- public string PythonExecutablePath {
- get { return pythonExecutablePath; }
+ public string PythonExecutablePath
+ {
+ get => pythonExecutablePath;
set
{
pythonExecutablePath = value;
@@ -18,7 +20,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
private string nodeExecutablePath = string.Empty;
public string NodeExecutablePath
{
- get { return nodeExecutablePath; }
+ get => nodeExecutablePath;
set
{
nodeExecutablePath = value;
@@ -26,19 +28,32 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
}
- public Dictionary Plugins { get; set; } = new Dictionary();
+ ///
+ /// Only used for serialization
+ ///
+ public Dictionary Plugins { get; set; } = new();
+ ///
+ /// Update plugin settings with metadata.
+ /// FL will get default values from metadata first and then load settings to metadata
+ ///
+ /// Parsed plugin metadatas
public void UpdatePluginSettings(List metadatas)
{
foreach (var metadata in metadatas)
{
- if (Plugins.ContainsKey(metadata.ID))
+ if (Plugins.TryGetValue(metadata.ID, out var settings))
{
- var settings = Plugins[metadata.ID];
-
+ // If settings exist, update settings & metadata value
+ // update settings values with metadata
if (string.IsNullOrEmpty(settings.Version))
+ {
settings.Version = metadata.Version;
+ }
+ settings.DefaultActionKeywords = metadata.ActionKeywords; // metadata provides default values
+ settings.DefaultSearchDelayTime = metadata.SearchDelayTime; // metadata provides default values
+ // update metadata values with settings
if (settings.ActionKeywords?.Count > 0)
{
metadata.ActionKeywords = settings.ActionKeywords;
@@ -51,30 +66,65 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
metadata.Disabled = settings.Disabled;
metadata.Priority = settings.Priority;
+ metadata.SearchDelayTime = settings.SearchDelayTime;
}
else
{
+ // If settings does not exist, create a new one
Plugins[metadata.ID] = new Plugin
{
ID = metadata.ID,
Name = metadata.Name,
Version = metadata.Version,
- ActionKeywords = metadata.ActionKeywords,
+ DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values
+ ActionKeywords = metadata.ActionKeywords, // use default value
Disabled = metadata.Disabled,
- Priority = metadata.Priority
+ Priority = metadata.Priority,
+ DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values
+ SearchDelayTime = metadata.SearchDelayTime, // use default value
};
}
}
}
+
+ public Plugin GetPluginSettings(string id)
+ {
+ if (Plugins.TryGetValue(id, out var plugin))
+ {
+ return plugin;
+ }
+ return null;
+ }
+
+ public Plugin RemovePluginSettings(string id)
+ {
+ Plugins.Remove(id, out var plugin);
+ return plugin;
+ }
}
+
public class Plugin
{
public string ID { get; set; }
+
public string Name { get; set; }
+
public string Version { get; set; }
- public List ActionKeywords { get; set; } // a reference of the action keywords from plugin manager
+
+ [JsonIgnore]
+ public List DefaultActionKeywords { get; set; }
+
+ // a reference of the action keywords from plugin manager
+ public List ActionKeywords { get; set; }
+
public int Priority { get; set; }
+ [JsonIgnore]
+ public SearchDelayTime? DefaultSearchDelayTime { get; set; }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public SearchDelayTime? SearchDelayTime { get; set; }
+
///
/// Used only to save the state of the plugin in settings
///
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 8352cdfa9..86ac320f7 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -312,7 +312,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
bool _hideNotifyIcon { get; set; }
public bool HideNotifyIcon
{
- get { return _hideNotifyIcon; }
+ get => _hideNotifyIcon;
set
{
_hideNotifyIcon = value;
@@ -322,6 +322,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactivated { get; set; } = true;
+ public bool SearchQueryResultsWithDelay { get; set; }
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public SearchDelayTime SearchDelayTime { get; set; } = SearchDelayTime.Normal;
+
[JsonConverter(typeof(JsonStringEnumConverter))]
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
@@ -343,7 +348,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
[JsonIgnore]
public bool WMPInstalled { get; set; } = true;
-
+
// This needs to be loaded last by staying at the bottom
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs
index 7a3a0c36e..f9c548de8 100644
--- a/Flow.Launcher.Infrastructure/Win32Helper.cs
+++ b/Flow.Launcher.Infrastructure/Win32Helper.cs
@@ -124,6 +124,16 @@ namespace Flow.Launcher.Infrastructure
return PInvoke.SetForegroundWindow(new(handle));
}
+ public static bool IsForegroundWindow(Window window)
+ {
+ return IsForegroundWindow(GetWindowHandle(window));
+ }
+
+ internal static bool IsForegroundWindow(HWND handle)
+ {
+ return handle.Equals(PInvoke.GetForegroundWindow());
+ }
+
#endregion
#region Task Switching
@@ -354,10 +364,20 @@ namespace Flow.Launcher.Infrastructure
// No installed English layout found
if (enHKL == HKL.Null) return;
- // Get the current foreground window
- var hwnd = PInvoke.GetForegroundWindow();
+ // When application is exiting, the Application.Current will be null
+ if (Application.Current == null) return;
+
+ // Get the FL main window
+ var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
if (hwnd == HWND.Null) return;
+ // Check if the FL main window is the current foreground window
+ if (!IsForegroundWindow(hwnd))
+ {
+ var result = PInvoke.SetForegroundWindow(hwnd);
+ if (!result) throw new Win32Exception(Marshal.GetLastWin32Error());
+ }
+
// Get the current foreground window thread ID
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
@@ -367,12 +387,10 @@ namespace Flow.Launcher.Infrastructure
// the IME mode instead of switching to another layout.
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
- foreach (var langTag in ImeLanguageTags)
+ foreach (var imeLangTag in ImeLanguageTags)
{
- if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
+ var langTag = GetLanguageTag(currentLangId);
+ if (langTag.StartsWith(imeLangTag, StringComparison.OrdinalIgnoreCase)) return;
}
// Backup current keyboard layout
@@ -488,5 +506,16 @@ namespace Flow.Launcher.Infrastructure
}
#endregion
+
+ #region Notification
+
+ public static bool IsNotificationSupported()
+ {
+ // Notifications only supported on Windows 10 19041+
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
+ Environment.OSVersion.Version.Build >= 19041;
+ }
+
+ #endregion
}
}
diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs
index e31c8e31d..9e05bbd06 100644
--- a/Flow.Launcher.Plugin/ActionContext.cs
+++ b/Flow.Launcher.Plugin/ActionContext.cs
@@ -51,6 +51,9 @@ namespace Flow.Launcher.Plugin
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
}
+ ///
+ /// Default object with all keys not pressed.
+ ///
public static readonly SpecialKeyState Default = new () {
CtrlPressed = false,
ShiftPressed = false,
diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index f178ebb90..eeb3f5de3 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -344,5 +344,62 @@ namespace Flow.Launcher.Plugin
/// Stop the loading bar in main window
///
public void StopLoadingBar();
+
+ ///
+ /// Update the plugin manifest
+ ///
+ ///
+ /// FL has multiple urls to download the plugin manifest. Set this to true to only use the primary url.
+ ///
+ ///
+ /// True if the manifest is updated successfully, false otherwise
+ public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default);
+
+ ///
+ /// Get the plugin manifest
+ ///
+ ///
+ public IReadOnlyList GetPluginManifest();
+
+ ///
+ /// Check if the plugin has been modified.
+ /// If this plugin is updated, installed or uninstalled and users do not restart the app,
+ /// it will be marked as modified
+ ///
+ /// Plugin id
+ ///
+ public bool PluginModified(string id);
+
+ ///
+ /// 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
+ ///
+ /// The metadata of the old plugin to update
+ /// The new plugin to update
+ ///
+ /// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
+ ///
+ ///
+ public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
+
+ ///
+ /// Install a plugin. By default will remove the zip file if installation is from url,
+ /// unless it's a local path installation
+ ///
+ /// The plugin to install
+ ///
+ /// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
+ ///
+ public void InstallPlugin(UserPlugin plugin, string zipFilePath);
+
+ ///
+ /// Uninstall a plugin
+ ///
+ /// The metadata of the plugin to uninstall
+ ///
+ /// Plugin has their own settings. If this is set to true, the plugin settings will be removed.
+ ///
+ ///
+ public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
}
}
diff --git a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
index fd21460ac..aa4e4a56d 100644
--- a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
@@ -4,17 +4,42 @@ using System.Threading;
namespace Flow.Launcher.Plugin
{
+ ///
+ /// Interface for plugins that want to manually update their results
+ ///
public interface IResultUpdated : IFeatures
{
+ ///
+ /// Event that is triggered when the results are updated
+ ///
event ResultUpdatedEventHandler ResultsUpdated;
}
+ ///
+ /// Delegate for the ResultsUpdated event
+ ///
+ ///
+ ///
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
+ ///
+ /// Event arguments for the ResultsUpdated event
+ ///
public class ResultUpdatedEventArgs : EventArgs
{
+ ///
+ /// List of results that should be displayed
+ ///
public List Results;
+
+ ///
+ /// Query that triggered the update
+ ///
public Query Query;
+
+ ///
+ /// Token that can be used to cancel the update
+ ///
public CancellationToken Token { get; init; }
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs
index d5ffba20b..f034243c3 100644
--- a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs
+++ b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs
@@ -2,8 +2,15 @@
namespace Flow.Launcher.Plugin
{
+ ///
+ /// This interface is used to create settings panel for .Net plugins
+ ///
public interface ISettingProvider
{
+ ///
+ /// Create settings panel control for .Net plugins
+ ///
+ ///
Control CreateSettingPanel();
}
}
diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs
index f040752bd..a42e3930c 100644
--- a/Flow.Launcher.Plugin/PluginInitContext.cs
+++ b/Flow.Launcher.Plugin/PluginInitContext.cs
@@ -5,10 +5,18 @@
///
public class PluginInitContext
{
+ ///
+ /// Default constructor.
+ ///
public PluginInitContext()
{
}
+ ///
+ /// Constructor.
+ ///
+ ///
+ ///
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
{
CurrentPluginMetadata = currentPluginMetadata;
diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs
index 91256298b..1496765ce 100644
--- a/Flow.Launcher.Plugin/PluginMetadata.cs
+++ b/Flow.Launcher.Plugin/PluginMetadata.cs
@@ -4,24 +4,77 @@ using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin
{
+ ///
+ /// Plugin metadata
+ ///
public class PluginMetadata : BaseModel
{
- private string _pluginDirectory;
+ ///
+ /// Plugin ID.
+ ///
public string ID { get; set; }
- public string Name { get; set; }
- public string Author { get; set; }
- public string Version { get; set; }
- public string Language { get; set; }
- public string Description { get; set; }
- public string Website { get; set; }
- public bool Disabled { get; set; }
- public string ExecuteFilePath { get; private set;}
+ ///
+ /// Plugin name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Plugin author.
+ ///
+ public string Author { get; set; }
+
+ ///
+ /// Plugin version.
+ ///
+ public string Version { get; set; }
+
+ ///
+ /// Plugin language.
+ /// See
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Plugin description.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Plugin website.
+ ///
+ public string Website { get; set; }
+
+ ///
+ /// Whether plugin is disabled.
+ ///
+ public bool Disabled { get; set; }
+
+ ///
+ /// Plugin execute file path.
+ ///
+ public string ExecuteFilePath { get; private set; }
+
+ ///
+ /// Plugin execute file name.
+ ///
public string ExecuteFileName { get; set; }
+ ///
+ /// Plugin assembly name.
+ /// Only available for .Net plugins.
+ ///
+ [JsonIgnore]
+ public string AssemblyName { get; internal set; }
+
+ private string _pluginDirectory;
+
+ ///
+ /// Plugin source directory.
+ ///
public string PluginDirectory
{
- get { return _pluginDirectory; }
+ get => _pluginDirectory;
internal set
{
_pluginDirectory = value;
@@ -30,30 +83,78 @@ namespace Flow.Launcher.Plugin
}
}
+ ///
+ /// The first action keyword of plugin.
+ ///
public string ActionKeyword { get; set; }
+ ///
+ /// All action keywords of plugin.
+ ///
public List ActionKeywords { get; set; }
+ ///
+ /// Hide plugin keyword setting panel.
+ ///
public bool HideActionKeywordPanel { get; set; }
- public string IcoPath { get; set;}
-
- public override string ToString()
- {
- return Name;
- }
+ ///
+ /// Plugin search delay time. Null means use default search delay time.
+ ///
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public SearchDelayTime? SearchDelayTime { get; set; } = null;
+ ///
+ /// Plugin icon path.
+ ///
+ public string IcoPath { get; set;}
+
+ ///
+ /// Plugin priority.
+ ///
[JsonIgnore]
public int Priority { get; set; }
///
- /// Init time include both plugin load time and init time
+ /// Init time include both plugin load time and init time.
///
[JsonIgnore]
public long InitTime { get; set; }
+
+ ///
+ /// Average query time.
+ ///
[JsonIgnore]
public long AvgQueryTime { get; set; }
+
+ ///
+ /// Query count.
+ ///
[JsonIgnore]
public int QueryCount { get; set; }
+
+ ///
+ /// The path to the plugin settings directory which is not validated.
+ /// It is used to store plugin settings files and data files.
+ /// When plugin is deleted, FL will ask users whether to keep its settings.
+ /// If users do not want to keep, this directory will be deleted.
+ ///
+ public string PluginSettingsDirectoryPath { get; internal set; }
+
+ ///
+ /// The path to the plugin cache directory which is not validated.
+ /// It is used to store cache files.
+ /// When plugin is deleted, this directory will be deleted as well.
+ ///
+ public string PluginCacheDirectoryPath { get; internal set; }
+
+ ///
+ /// Convert to string.
+ ///
+ ///
+ public override string ToString()
+ {
+ return Name;
+ }
}
}
diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs
index 7bf634691..f2c14d70c 100644
--- a/Flow.Launcher.Plugin/PluginPair.cs
+++ b/Flow.Launcher.Plugin/PluginPair.cs
@@ -1,21 +1,37 @@
namespace Flow.Launcher.Plugin
{
+ ///
+ /// Plugin instance and plugin metadata
+ ///
public class PluginPair
{
+ ///
+ /// Plugin instance
+ ///
public IAsyncPlugin Plugin { get; internal set; }
+
+ ///
+ /// Plugin metadata
+ ///
public PluginMetadata Metadata { get; internal set; }
-
-
+ ///
+ /// Convert to string
+ ///
+ ///
public override string ToString()
{
return Metadata.Name;
}
+ ///
+ /// Compare by plugin metadata ID
+ ///
+ ///
+ ///
public override bool Equals(object obj)
{
- PluginPair r = obj as PluginPair;
- if (r != null)
+ if (obj is PluginPair r)
{
return string.Equals(r.Metadata.ID, Metadata.ID);
}
@@ -25,6 +41,10 @@
}
}
+ ///
+ /// Get hash code
+ ///
+ ///
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs
index 15b2dd171..913dc31ae 100644
--- a/Flow.Launcher.Plugin/Query.cs
+++ b/Flow.Launcher.Plugin/Query.cs
@@ -2,10 +2,11 @@
namespace Flow.Launcher.Plugin
{
+ ///
+ /// Represents a query that is sent to a plugin.
+ ///
public class Query
{
- public Query() { }
-
///
/// Raw query, this includes action keyword if it has
/// We didn't recommend use this property directly. You should always use Search property.
@@ -54,13 +55,13 @@ namespace Flow.Launcher.Plugin
///
public string ActionKeyword { get; init; }
- [JsonIgnore]
///
/// Splits by spaces and returns the first item.
///
///
/// returns an empty string when does not have enough items.
///
+ [JsonIgnore]
public string FirstSearch => SplitSearch(0);
[JsonIgnore]
diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs
index 9b16cc1cb..910485438 100644
--- a/Flow.Launcher.Plugin/Result.cs
+++ b/Flow.Launcher.Plugin/Result.cs
@@ -1,5 +1,4 @@
using System;
-using System.Runtime;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -13,7 +12,6 @@ namespace Flow.Launcher.Plugin
///
public class Result
{
-
private string _pluginDirectory;
private string _icoPath;
diff --git a/Flow.Launcher.Plugin/SearchDelayTime.cs b/Flow.Launcher.Plugin/SearchDelayTime.cs
new file mode 100644
index 000000000..ae1daabe0
--- /dev/null
+++ b/Flow.Launcher.Plugin/SearchDelayTime.cs
@@ -0,0 +1,32 @@
+namespace Flow.Launcher.Plugin;
+
+///
+/// Enum for search delay time
+///
+public enum SearchDelayTime
+{
+ ///
+ /// Very long search delay time. 250ms.
+ ///
+ VeryLong,
+
+ ///
+ /// Long search delay time. 200ms.
+ ///
+ Long,
+
+ ///
+ /// Normal search delay time. 150ms. Default value.
+ ///
+ Normal,
+
+ ///
+ /// Short search delay time. 100ms.
+ ///
+ Short,
+
+ ///
+ /// Very short search delay time. 50ms.
+ ///
+ VeryShort
+}
diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
index 5f003e351..1de5841a5 100644
--- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
@@ -318,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands
{
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
}
+
+ ///
+ /// Validates a directory, creating it if it doesn't exist
+ ///
+ ///
+ public static void ValidateDirectory(string path)
+ {
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+ }
+
+ ///
+ /// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it.
+ /// If files are missing or outdated, they are copied from the bundled directory to the data directory.
+ ///
+ ///
+ ///
+ public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
+ {
+ if (!Directory.Exists(dataDirectory))
+ {
+ Directory.CreateDirectory(dataDirectory);
+ }
+
+ foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
+ {
+ var data = Path.GetFileName(bundledDataPath);
+ if (data == null) continue;
+ var dataPath = Path.Combine(dataDirectory, data);
+ if (!File.Exists(dataPath))
+ {
+ File.Copy(bundledDataPath, dataPath);
+ }
+ else
+ {
+ var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
+ var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
+ if (time1 != time2)
+ {
+ File.Copy(bundledDataPath, dataPath, true);
+ }
+ }
+ }
+ }
}
}
diff --git a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs
index a7744ffac..752c85933 100644
--- a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs
@@ -6,6 +6,9 @@ using System.Linq;
namespace Flow.Launcher.Plugin.SharedCommands
{
+ ///
+ /// Contains methods to open a search in a new browser window or tab.
+ ///
public static class SearchWeb
{
private static string GetDefaultBrowserPath()
@@ -106,4 +109,4 @@ namespace Flow.Launcher.Plugin.SharedCommands
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs
index a0440e30d..288222d4f 100644
--- a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs
@@ -8,12 +8,26 @@ using Windows.Win32.Foundation;
namespace Flow.Launcher.Plugin.SharedCommands
{
+ ///
+ /// Contains methods for running shell commands
+ ///
public static class ShellCommand
{
+ ///
+ /// Delegate for EnumThreadWindows
+ ///
+ ///
+ ///
+ ///
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
private static bool containsSecurityWindow;
+ ///
+ /// Runs a windows command using the provided ProcessStartInfo
+ ///
+ ///
+ ///
public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo)
{
processStartInfo.Verb = "RunAsUser";
@@ -65,6 +79,15 @@ namespace Flow.Launcher.Plugin.SharedCommands
return buffer[..length].ToString();
}
+ ///
+ /// Runs a windows command using the provided ProcessStartInfo
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "",
string arguments = "", string verb = "", bool createNoWindow = false)
{
diff --git a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs
index 5144eb61d..36677d4bb 100644
--- a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs
+++ b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs
@@ -2,14 +2,29 @@
namespace Flow.Launcher.Plugin.SharedModels
{
+ ///
+ /// Represents the result of a match operation.
+ ///
public class MatchResult
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
public MatchResult(bool success, SearchPrecisionScore searchPrecision)
{
Success = success;
SearchPrecision = searchPrecision;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ ///
+ ///
public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore)
{
Success = success;
@@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels
RawScore = rawScore;
}
+ ///
+ /// Whether the match operation was successful.
+ ///
public bool Success { get; set; }
///
@@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels
///
private int _rawScore;
+ ///
+ /// The raw calculated search score without any search precision filtering applied.
+ ///
public int RawScore
{
get { return _rawScore; }
@@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels
///
public List MatchData { get; set; }
+ ///
+ /// The search precision score used to filter the search results.
+ ///
public SearchPrecisionScore SearchPrecision { get; set; }
+ ///
+ /// Determines if the search precision score is met.
+ ///
+ ///
public bool IsSearchPrecisionScoreMet()
{
return IsSearchPrecisionScoreMet(_rawScore);
@@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels
}
}
+ ///
+ /// Represents the search precision score used to filter search results.
+ ///
public enum SearchPrecisionScore
{
+ ///
+ /// The highest search precision score.
+ ///
Regular = 50,
+
+ ///
+ /// The medium search precision score.
+ ///
Low = 20,
+
+ ///
+ /// The lowest search precision score.
+ ///
None = 0
}
}
diff --git a/Flow.Launcher.Plugin/UserPlugin.cs b/Flow.Launcher.Plugin/UserPlugin.cs
new file mode 100644
index 000000000..74a16b83d
--- /dev/null
+++ b/Flow.Launcher.Plugin/UserPlugin.cs
@@ -0,0 +1,80 @@
+using System;
+
+namespace Flow.Launcher.Plugin
+{
+ ///
+ /// User Plugin Model for Flow Launcher
+ ///
+ public record UserPlugin
+ {
+ ///
+ /// Unique identifier of the plugin
+ ///
+ public string ID { get; set; }
+
+ ///
+ /// Name of the plugin
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Description of the plugin
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Author of the plugin
+ ///
+ public string Author { get; set; }
+
+ ///
+ /// Version of the plugin
+ ///
+ public string Version { get; set; }
+
+ ///
+ /// Allow language of the plugin
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Website of the plugin
+ ///
+ public string Website { get; set; }
+
+ ///
+ /// URL to download the plugin
+ ///
+ public string UrlDownload { get; set; }
+
+ ///
+ /// URL to the source code of the plugin
+ ///
+ public string UrlSourceCode { get; set; }
+
+ ///
+ /// Local path where the plugin is installed
+ ///
+ public string LocalInstallPath { get; set; }
+
+ ///
+ /// Icon path of the plugin
+ ///
+ public string IcoPath { get; set; }
+
+ ///
+ /// The date when the plugin was last updated
+ ///
+ public DateTime? LatestReleaseDate { get; set; }
+
+ ///
+ /// The date when the plugin was added to the local system
+ ///
+ public DateTime? DateAdded { get; set; }
+
+ ///
+ /// Indicates whether the plugin is installed from a local path
+ ///
+ public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
+ }
+}
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index 7b2f44a9f..a49a0d42e 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -142,6 +142,11 @@ namespace Flow.Launcher
{
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
{
+ // Because new message box api uses MessageBoxEx window,
+ // if it is created and closed before main window is created, it will cause the application to exit.
+ // So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
+ Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
Log.SetLogLevel(_settings.LogLevel);
Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate();
@@ -178,8 +183,6 @@ namespace Flow.Launcher
Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher;
- HotKeyMapper.Initialize();
-
// main windows needs initialized before theme change because of blur settings
Ioc.Default.GetRequiredService().ChangeTheme();
@@ -303,6 +306,14 @@ namespace Flow.Launcher
return;
}
+ // If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close()
+ // Accessing _mainWindow?.Dispatcher will cause the application stuck
+ // So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher
+ if (!_mainWindow.CanClose)
+ {
+ return;
+ }
+
_disposed = true;
}
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index d93e13977..a4b212102 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -9,6 +9,11 @@
Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable
Please select the {0} executable
+
+ Your selected {0} executable is invalid.
+ {2}{2}
+ Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
+
Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).
Fail to Init Plugins
Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help
@@ -102,6 +107,15 @@
Always Preview
Always open preview panel when Flow activates. Press {0} to toggle preview.
Shadow effect is not allowed while current theme has blur effect enabled
+ Search Delay
+ Delay for a while to search when typing. This reduces interface jumpiness and result load.
+ Default Search Delay Time
+ Plugin default delay time after which search results appear when typing is stopped.
+ Very long
+ Long
+ Normal
+ Short
+ Very short
Search Plugin
@@ -118,6 +132,8 @@
Current action keyword
New action keyword
Change Action Keywords
+ Plugin seach delay time
+ Change Plugin Seach Delay Time
Current Priority
New Priority
Priority
@@ -131,6 +147,9 @@
Uninstall
Fail to remove plugin settings
Plugins: {0} - Fail to remove plugin settings files, please remove them manually
+ Fail to remove plugin cache
+ Plugins: {0} - Fail to remove plugin cache files, please remove them manually
+ Default
Plugin Store
@@ -194,6 +213,7 @@
Clock
Date
Backdrop Type
+ Backdrop supported starting from Windows 11 build 22000 and above
None
Acrylic
Mica
@@ -305,6 +325,9 @@
Log Folder
Clear Logs
Are you sure you want to delete all logs?
+ Clear Caches
+ Are you sure you want to delete all caches?
+ Failed to clear part of folders and files. Please see log file for more information
Wizard
Release Channel
Stable
@@ -354,6 +377,12 @@
Completed successfully
Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.
+
+ Search Delay Time Setting
+ Select the search delay time you like to use for the plugin. Select "{0}" if you don't want to specify any, and the plugin will use default search delay time.
+ Current search delay time
+ New search delay time
+
Custom Query Hotkey
Press a custom hotkey to open Flow Launcher and input the specified query automatically.
diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml
index 82ac63b7d..31bc2ba50 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -251,7 +251,8 @@
PreviewDragOver="QueryTextBox_OnPreviewDragOver"
PreviewKeyUp="QueryTextBox_KeyUp"
Style="{DynamicResource QueryBoxStyle}"
- Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
+ Text="{Binding QueryText, Mode=OneWay}"
+ TextChanged="QueryTextBox_TextChanged1"
Visibility="Visible"
WindowChrome.IsHitTestVisibleInChrome="True">
diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index 8f22d64b8..30afe67a1 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -16,12 +16,16 @@ using System.Windows.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
+using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
using ModernWpf.Controls;
+using DataObject = System.Windows.DataObject;
+using Key = System.Windows.Input.Key;
using MouseButtons = System.Windows.Forms.MouseButtons;
using NotifyIcon = System.Windows.Forms.NotifyIcon;
using Screen = System.Windows.Forms.Screen;
@@ -30,6 +34,13 @@ namespace Flow.Launcher
{
public partial class MainWindow : IDisposable
{
+ #region Public Property
+
+ // Window Event: Close Event
+ public bool CanClose { get; set; } = false;
+
+ #endregion
+
#region Private Fields
// Dependency Injection
@@ -43,8 +54,6 @@ namespace Flow.Launcher
private readonly ContextMenu _contextMenu = new();
private readonly MainViewModel _viewModel;
- // Window Event: Close Event
- private bool _canClose = false;
// Window Event: Key Event
private bool _isArrowKeyPressed = false;
@@ -165,6 +174,11 @@ namespace Flow.Launcher
// 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;
+
+ // Initialize hotkey mapper after window is loaded
+ HotKeyMapper.Initialize();
+
+ // View model property changed event
_viewModel.PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
@@ -227,6 +241,7 @@ namespace Flow.Launcher
}
};
+ // Settings property changed event
_settings.PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
@@ -274,15 +289,16 @@ namespace Flow.Launcher
private async void OnClosing(object sender, CancelEventArgs e)
{
- if (!_canClose)
+ if (!CanClose)
{
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
+ await ImageLoader.WaitSaveAsync();
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
// After plugins are all disposed, we can close the main window
- _canClose = true;
+ CanClose = true;
// Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
Application.Current.Shutdown();
}
@@ -1050,7 +1066,7 @@ namespace Flow.Launcher
{
e.Handled = true;
}
-
+
#endregion
#region Placeholder
@@ -1101,6 +1117,17 @@ namespace Flow.Launcher
}
}
+ #endregion
+
+ #region Search Delay
+
+ private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e)
+ {
+ var textBox = (TextBox)sender;
+ _viewModel.QueryText = textBox.Text;
+ _viewModel.Query(_settings.SearchQueryResultsWithDelay);
+ }
+
#endregion
#region IDisposable
diff --git a/Flow.Launcher/Notification.cs b/Flow.Launcher/Notification.cs
index cb1cbb729..30b3a0673 100644
--- a/Flow.Launcher/Notification.cs
+++ b/Flow.Launcher/Notification.cs
@@ -9,8 +9,8 @@ namespace Flow.Launcher
{
internal static class Notification
{
- internal static bool legacy = Environment.OSVersion.Version.Build < 19041;
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")]
+ internal static bool legacy = !Win32Helper.IsNotificationSupported();
+
internal static void Uninstall()
{
if (!legacy)
@@ -25,7 +25,6 @@ namespace Flow.Launcher
});
}
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")]
private static void ShowInternal(string title, string subTitle, string iconPath = null)
{
// Handle notification for win7/8/early win10
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index 456f1ad47..c40e40ebb 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -28,6 +28,7 @@ using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
using JetBrains.Annotations;
using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Core.ExternalPlugins;
namespace Flow.Launcher
{
@@ -37,6 +38,8 @@ namespace Flow.Launcher
private readonly Internationalization _translater;
private readonly MainViewModel _mainVM;
+ private readonly object _saveSettingsLock = new();
+
#region Constructor
public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM)
@@ -57,21 +60,28 @@ namespace Flow.Launcher
_mainVM.ChangeQueryText(query, requery);
}
- public void RestartApp()
+#pragma warning disable VSTHRD100 // Avoid async void methods
+
+ public async void RestartApp()
{
_mainVM.Hide();
- // we must manually save
+ // We must manually save
// UpdateManager.RestartApp() will call Environment.Exit(0)
// which will cause ungraceful exit
SaveAppAllSettings();
+ // Wait for all image caches to be saved before restarting
+ await ImageLoader.WaitSaveAsync();
+
// Restart requires Squirrel's Update.exe to be present in the parent folder,
// it is only published from the project's release pipeline. When debugging without it,
// the project may not restart or just terminates. This is expected.
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
+#pragma warning restore VSTHRD100 // Avoid async void methods
+
public void ShowMainWindow() => _mainVM.Show();
public void HideMainWindow() => _mainVM.Hide();
@@ -85,10 +95,13 @@ namespace Flow.Launcher
public void SaveAppAllSettings()
{
- PluginManager.Save();
- _mainVM.Save();
- _settings.Save();
- _ = ImageLoader.Save();
+ lock (_saveSettingsLock)
+ {
+ _settings.Save();
+ PluginManager.Save();
+ _mainVM.Save();
+ }
+ _ = ImageLoader.SaveAsync();
}
public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();
@@ -192,7 +205,7 @@ namespace Flow.Launcher
private readonly ConcurrentDictionary _pluginJsonStorages = new();
- public object RemovePluginSettings(string assemblyName)
+ public void RemovePluginSettings(string assemblyName)
{
foreach (var keyValuePair in _pluginJsonStorages)
{
@@ -202,11 +215,8 @@ namespace Flow.Launcher
if (name == assemblyName)
{
_pluginJsonStorages.Remove(key, out var pluginJsonStorage);
- return pluginJsonStorage;
}
}
-
- return null;
}
///
@@ -345,6 +355,22 @@ namespace Flow.Launcher
public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress);
+ public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) =>
+ PluginsManifest.UpdateManifestAsync(usePrimaryUrlOnly, token);
+
+ public IReadOnlyList GetPluginManifest() => PluginsManifest.UserPlugins;
+
+ public bool PluginModified(string id) => PluginManager.PluginModified(id);
+
+ public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) =>
+ PluginManager.UpdatePluginAsync(pluginMetadata, plugin, zipFilePath);
+
+ public void InstallPlugin(UserPlugin plugin, string zipFilePath) =>
+ PluginManager.InstallPlugin(plugin, zipFilePath);
+
+ public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) =>
+ PluginManager.UninstallPluginAsync(pluginMetadata, removePluginSettings);
+
#endregion
#region Private Methods
diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml
index c29a5f602..33c1299a9 100644
--- a/Flow.Launcher/Resources/Controls/Card.xaml
+++ b/Flow.Launcher/Resources/Controls/Card.xaml
@@ -20,21 +20,21 @@
-
-
+
+
-
+
-
+
-
+
-
-
+
+
@@ -73,7 +73,7 @@
@@ -91,7 +91,7 @@
@@ -107,8 +107,8 @@
-
-
+
+
@@ -120,11 +120,11 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SearchDelayTimeWindow.xaml.cs b/Flow.Launcher/SearchDelayTimeWindow.xaml.cs
new file mode 100644
index 000000000..4a3c9f5a7
--- /dev/null
+++ b/Flow.Launcher/SearchDelayTimeWindow.xaml.cs
@@ -0,0 +1,57 @@
+using System.Linq;
+using System.Windows;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.SettingPages.ViewModels;
+using Flow.Launcher.ViewModel;
+using static Flow.Launcher.SettingPages.ViewModels.SettingsPaneGeneralViewModel;
+
+namespace Flow.Launcher;
+
+public partial class SearchDelayTimeWindow : Window
+{
+ private readonly PluginViewModel _pluginViewModel;
+
+ public SearchDelayTimeWindow(PluginViewModel pluginViewModel)
+ {
+ InitializeComponent();
+ _pluginViewModel = pluginViewModel;
+ }
+
+ private void SearchDelayTimeWindow_OnLoaded(object sender, RoutedEventArgs e)
+ {
+ tbSearchDelayTimeTips.Text = string.Format(App.API.GetTranslation("searchDelayTime_tips"),
+ App.API.GetTranslation("default"));
+ tbOldSearchDelayTime.Text = _pluginViewModel.SearchDelayTimeText;
+ var searchDelayTimes = DropdownDataGeneric.GetValues("SearchDelayTime");
+ SearchDelayTimeData selected = null;
+ // Because default value is SearchDelayTime.VeryShort, we need to get selected value before adding default value
+ if (_pluginViewModel.PluginSearchDelayTime != null)
+ {
+ selected = searchDelayTimes.FirstOrDefault(x => x.Value == _pluginViewModel.PluginSearchDelayTime);
+ }
+ // Add default value to the beginning of the list
+ // When _pluginViewModel.PluginSearchDelayTime equals null, we will select this
+ searchDelayTimes.Insert(0, new SearchDelayTimeData { Display = App.API.GetTranslation("default"), LocalizationKey = "default" });
+ selected ??= searchDelayTimes.FirstOrDefault();
+ cbDelay.ItemsSource = searchDelayTimes;
+ cbDelay.SelectedItem = selected;
+ cbDelay.Focus();
+ }
+
+ private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void btnDone_OnClick(object sender, RoutedEventArgs _)
+ {
+ // Update search delay time
+ var selected = cbDelay.SelectedItem as SearchDelayTimeData;
+ SearchDelayTime? changedValue = selected?.LocalizationKey != "default" ? selected.Value : null;
+ _pluginViewModel.PluginSearchDelayTime = changedValue;
+
+ // Update search delay time text and close window
+ _pluginViewModel.OnSearchDelayTimeChanged();
+ Close();
+ }
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs
index 15a814436..c8c119e94 100644
--- a/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.SettingPages.ViewModels;
@@ -9,7 +8,7 @@ public class DropdownDataGeneric : BaseModel where TValue : Enum
{
public string Display { get; set; }
public TValue Value { get; private init; }
- private string LocalizationKey { get; init; }
+ public string LocalizationKey { get; set; }
public static List GetValues
(string keyPrefix) where TR : DropdownDataGeneric, new()
{
@@ -19,7 +18,7 @@ public class DropdownDataGeneric : BaseModel where TValue : Enum
foreach (var value in enumValues)
{
var key = keyPrefix + value;
- var display = InternationalizationManager.Instance.GetTranslation(key);
+ var display = App.API.GetTranslation(key);
data.Add(new TR { Display = display, Value = value, LocalizationKey = key });
}
@@ -30,7 +29,7 @@ public class DropdownDataGeneric : BaseModel where TValue : Enum
{
foreach (var item in options)
{
- item.Display = InternationalizationManager.Instance.GetTranslation(item.LocalizationKey);
+ item.Display = App.API.GetTranslation(item.LocalizationKey);
}
}
}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs
index 11912e219..e33f66519 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core;
-using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
@@ -16,6 +15,8 @@ namespace Flow.Launcher.SettingPages.ViewModels;
public partial class SettingsPaneAboutViewModel : BaseModel
{
+ private static readonly string ClassName = nameof(SettingsPaneAboutViewModel);
+
private readonly Settings _settings;
private readonly Updater _updater;
@@ -24,7 +25,16 @@ public partial class SettingsPaneAboutViewModel : BaseModel
get
{
var size = GetLogFiles().Sum(file => file.Length);
- return $"{InternationalizationManager.Instance.GetTranslation("clearlogfolder")} ({BytesToReadableString(size)})";
+ return $"{App.API.GetTranslation("clearlogfolder")} ({BytesToReadableString(size)})";
+ }
+ }
+
+ public string CacheFolderSize
+ {
+ get
+ {
+ var size = GetCacheFiles().Sum(file => file.Length);
+ return $"{App.API.GetTranslation("clearcachefolder")} ({BytesToReadableString(size)})";
}
}
@@ -42,7 +52,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
};
public string ActivatedTimes => string.Format(
- InternationalizationManager.Instance.GetTranslation("about_activate_times"),
+ App.API.GetTranslation("about_activate_times"),
_settings.ActivateTimes
);
@@ -98,32 +108,52 @@ public partial class SettingsPaneAboutViewModel : BaseModel
private void AskClearLogFolderConfirmation()
{
var confirmResult = App.API.ShowMsgBox(
- InternationalizationManager.Instance.GetTranslation("clearlogfolderMessage"),
- InternationalizationManager.Instance.GetTranslation("clearlogfolder"),
+ App.API.GetTranslation("clearlogfolderMessage"),
+ App.API.GetTranslation("clearlogfolder"),
MessageBoxButton.YesNo
);
if (confirmResult == MessageBoxResult.Yes)
{
- ClearLogFolder();
+ if (!ClearLogFolder())
+ {
+ App.API.ShowMsgBox(App.API.GetTranslation("clearfolderfailMessage"));
+ }
+ }
+ }
+
+ [RelayCommand]
+ private void AskClearCacheFolderConfirmation()
+ {
+ var confirmResult = App.API.ShowMsgBox(
+ App.API.GetTranslation("clearcachefolderMessage"),
+ App.API.GetTranslation("clearcachefolder"),
+ MessageBoxButton.YesNo
+ );
+
+ if (confirmResult == MessageBoxResult.Yes)
+ {
+ if (!ClearCacheFolder())
+ {
+ App.API.ShowMsgBox(App.API.GetTranslation("clearfolderfailMessage"));
+ }
}
}
[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);
}
-
[RelayCommand]
private void OpenLogsFolder()
{
@@ -131,26 +161,52 @@ public partial class SettingsPaneAboutViewModel : BaseModel
}
[RelayCommand]
- private Task UpdateApp() => _updater.UpdateAppAsync(false);
+ private Task UpdateAppAsync() => _updater.UpdateAppAsync(false);
- private void ClearLogFolder()
+ private bool ClearLogFolder()
{
+ var success = true;
var logDirectory = GetLogDir();
var logFiles = GetLogFiles();
- logFiles.ForEach(f => f.Delete());
+ logFiles.ForEach(f =>
+ {
+ try
+ {
+ f.Delete();
+ }
+ catch (Exception e)
+ {
+ App.API.LogException(ClassName, $"Failed to delete log file: {f.Name}", e);
+ success = false;
+ }
+ });
logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
+ // Do not clean log files of current version
.Where(dir => !Constant.Version.Equals(dir.Name))
.ToList()
- .ForEach(dir => dir.Delete());
+ .ForEach(dir =>
+ {
+ try
+ {
+ dir.Delete(true);
+ }
+ catch (Exception e)
+ {
+ App.API.LogException(ClassName, $"Failed to delete log directory: {dir.Name}", e);
+ success = false;
+ }
+ });
OnPropertyChanged(nameof(LogFolderSize));
+
+ return success;
}
private static DirectoryInfo GetLogDir(string version = "")
{
- return new DirectoryInfo(Path.Combine(DataLocation.DataDirectory(), Constant.Logs, version));
+ return new DirectoryInfo(Path.Combine(DataLocation.LogDirectory, version));
}
private static List GetLogFiles(string version = "")
@@ -158,6 +214,55 @@ public partial class SettingsPaneAboutViewModel : BaseModel
return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList();
}
+ private bool ClearCacheFolder()
+ {
+ var success = true;
+ var cacheDirectory = GetCacheDir();
+ var cacheFiles = GetCacheFiles();
+
+ cacheFiles.ForEach(f =>
+ {
+ try
+ {
+ f.Delete();
+ }
+ catch (Exception e)
+ {
+ App.API.LogException(ClassName, $"Failed to delete cache file: {f.Name}", e);
+ success = false;
+ }
+ });
+
+ cacheDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
+ .ToList()
+ .ForEach(dir =>
+ {
+ try
+ {
+ dir.Delete(true);
+ }
+ catch (Exception e)
+ {
+ App.API.LogException(ClassName, $"Failed to delete cache directory: {dir.Name}", e);
+ success = false;
+ }
+ });
+
+ OnPropertyChanged(nameof(CacheFolderSize));
+
+ return success;
+ }
+
+ private static DirectoryInfo GetCacheDir()
+ {
+ return new DirectoryInfo(DataLocation.CacheDirectory);
+ }
+
+ private static List GetCacheFiles()
+ {
+ return GetCacheDir().EnumerateFiles("*", SearchOption.AllDirectories).ToList();
+ }
+
private static string BytesToReadableString(long bytes)
{
const int scale = 1024;
@@ -166,8 +271,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
foreach (string order in orders)
{
- if (bytes > max)
- return $"{decimal.Divide(bytes, max):##.##} {order}";
+ if (bytes > max) return $"{decimal.Divide(bytes, max):##.##} {order}";
max /= scale;
}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
index de4f158ad..cec8c318c 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core;
@@ -30,6 +31,7 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
public class SearchWindowAlignData : DropdownDataGeneric { }
public class SearchPrecisionData : DropdownDataGeneric { }
public class LastQueryModeData : DropdownDataGeneric { }
+ public class SearchDelayTimeData : DropdownDataGeneric { }
public bool StartFlowLauncherOnSystemStartup
{
@@ -142,12 +144,33 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
public List LastQueryModes { get; } =
DropdownDataGeneric.GetValues("LastQuery");
+ public List SearchDelayTimes { get; } =
+ DropdownDataGeneric.GetValues("SearchDelayTime");
+
+ public SearchDelayTimeData SearchDelayTime
+ {
+ get => SearchDelayTimes.FirstOrDefault(x => x.Value == Settings.SearchDelayTime) ??
+ SearchDelayTimes.FirstOrDefault(x => x.Value == Plugin.SearchDelayTime.Normal) ??
+ SearchDelayTimes.FirstOrDefault();
+ set
+ {
+ if (value == null)
+ return;
+
+ if (Settings.SearchDelayTime != value.Value)
+ {
+ Settings.SearchDelayTime = value.Value;
+ }
+ }
+ }
+
private void UpdateEnumDropdownLocalizations()
{
DropdownDataGeneric.UpdateLabels(SearchWindowScreens);
DropdownDataGeneric.UpdateLabels(SearchWindowAligns);
DropdownDataGeneric.UpdateLabels(SearchPrecisionScores);
DropdownDataGeneric.UpdateLabels(LastQueryModes);
+ DropdownDataGeneric.UpdateLabels(SearchDelayTimes);
}
public string Language
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
index 15579a61d..84d8a2ff9 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
@@ -2,7 +2,6 @@
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
-using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
@@ -14,7 +13,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
public string FilterText { get; set; } = string.Empty;
public IList ExternalPlugins =>
- PluginsManifest.UserPlugins?.Select(p => new PluginStoreItemViewModel(p))
+ App.API.GetPluginManifest()?.Select(p => new PluginStoreItemViewModel(p))
.OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease)
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated)
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.None)
@@ -24,7 +23,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
[RelayCommand]
private async Task RefreshExternalPluginsAsync()
{
- if (await PluginsManifest.UpdateManifestAsync())
+ if (await App.API.UpdatePluginManifestAsync())
{
OnPropertyChanged(nameof(ExternalPlugins));
}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
index dd9e5786d..3c1aba400 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
@@ -30,8 +30,9 @@ public class SettingsPanePluginsViewModel : BaseModel
.Select(plugin => new PluginViewModel
{
PluginPair = plugin,
- PluginSettingsObject = _settings.PluginSettings.Plugins[plugin.Metadata.ID]
+ PluginSettingsObject = _settings.PluginSettings.GetPluginSettings(plugin.Metadata.ID)
})
+ .Where(plugin => plugin.PluginSettingsObject != null)
.ToList();
public List FilteredPluginViewModels => PluginViewModels
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
index e35c978ed..6e2488fe1 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
@@ -21,10 +21,11 @@ namespace Flow.Launcher.SettingPages.ViewModels;
public partial class SettingsPaneThemeViewModel : BaseModel
{
private const string DefaultFont = "Segoe UI";
+ public string BackdropSubText => !Win32Helper.IsBackdropSupported() ? App.API.GetTranslation("BackdropTypeDisabledToolTip") : "";
public Settings Settings { get; }
private readonly Theme _theme = Ioc.Default.GetRequiredService();
- public static string LinkHowToCreateTheme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme";
+ public static string LinkHowToCreateTheme => @"https://www.flowlauncher.com/theme-builder/";
public static string LinkThemeGallery => "https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438";
private List _themes;
@@ -486,7 +487,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
[RelayCommand]
private void OpenThemesFolder()
{
- App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes));
+ App.API.OpenDirectory(DataLocation.ThemesDirectory);
}
[RelayCommand]
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
index 970137af0..9f1f4576d 100644
--- a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
@@ -96,6 +96,10 @@
Margin="0 12 0 0"
Icon="">
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Icon=""
+ Sub="{Binding BackdropSubText}">
-
@@ -500,62 +500,29 @@
Uri="{Binding LinkThemeGallery}" />
-
-
-
-
+
+
+
+
+
+
+ Type="InsideFit">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -602,22 +569,45 @@
-
-
+
+
+
+
+
+ Title="{DynamicResource PlaceholderText}"
+ Sub="{Binding PlaceholderTextTip}"
+ Type="InsideFit">
+
+
+
+
+
+
+
-
+
+ Type="InsideFit">
-
+
-
-
+
+
-
+
+ Type="InsideFit">
-
+
+
+
+
+
+
+
+
+
+
@@ -488,7 +512,6 @@
x:Key="ItemHotkeyStyle"
BasedOn="{StaticResource BaseItemHotkeyStyle}"
TargetType="{x:Type TextBlock}">
-
@@ -496,7 +519,6 @@
x:Key="ItemHotkeySelectedStyle"
BasedOn="{StaticResource BaseItemHotkeySelectedStyle}"
TargetType="{x:Type TextBlock}">
-
diff --git a/Flow.Launcher/Themes/BlurBlack Darker.xaml b/Flow.Launcher/Themes/BlurBlack Darker.xaml
index 2bef19373..b68641984 100644
--- a/Flow.Launcher/Themes/BlurBlack Darker.xaml
+++ b/Flow.Launcher/Themes/BlurBlack Darker.xaml
@@ -15,6 +15,7 @@
Dark
#C7000000
#C7000000
+ 0 0 0 8
-
-
diff --git a/Flow.Launcher/Themes/BlurBlack.xaml b/Flow.Launcher/Themes/BlurBlack.xaml
index c45827074..5ce3932e1 100644
--- a/Flow.Launcher/Themes/BlurBlack.xaml
+++ b/Flow.Launcher/Themes/BlurBlack.xaml
@@ -14,6 +14,7 @@
Dark
#B0000000
#B6000000
+ 0 0 0 8
-
-
diff --git a/Flow.Launcher/Themes/BlurWhite.xaml b/Flow.Launcher/Themes/BlurWhite.xaml
index 8bf1f06e2..25cbfe9c9 100644
--- a/Flow.Launcher/Themes/BlurWhite.xaml
+++ b/Flow.Launcher/Themes/BlurWhite.xaml
@@ -14,6 +14,8 @@
Light
#BFFAFAFA
#BFFAFAFA
+ 0 0 0 8
+
-
-
diff --git a/Flow.Launcher/Themes/Circle System.xaml b/Flow.Launcher/Themes/Circle System.xaml
index 600b9e9dc..24c7bd65b 100644
--- a/Flow.Launcher/Themes/Circle System.xaml
+++ b/Flow.Launcher/Themes/Circle System.xaml
@@ -26,7 +26,7 @@
x:Key="QueryBoxStyle"
BasedOn="{StaticResource BaseQueryBoxStyle}"
TargetType="{x:Type TextBox}">
-
+
@@ -36,7 +36,7 @@
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
-
+
diff --git a/Flow.Launcher/Themes/Cyan Dark.xaml b/Flow.Launcher/Themes/Cyan Dark.xaml
index 106b1b6d9..59ebad0f6 100644
--- a/Flow.Launcher/Themes/Cyan Dark.xaml
+++ b/Flow.Launcher/Themes/Cyan Dark.xaml
@@ -33,7 +33,7 @@
x:Key="QueryBoxStyle"
BasedOn="{StaticResource BaseQueryBoxStyle}"
TargetType="{x:Type TextBox}">
-
+
@@ -44,7 +44,7 @@
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
-
+
@@ -56,7 +56,7 @@
BasedOn="{StaticResource BaseWindowBorderStyle}"
TargetType="{x:Type Border}">
-
+
diff --git a/Flow.Launcher/Themes/Darker Glass.xaml b/Flow.Launcher/Themes/Darker Glass.xaml
index 2faddd38b..9ffaaf566 100644
--- a/Flow.Launcher/Themes/Darker Glass.xaml
+++ b/Flow.Launcher/Themes/Darker Glass.xaml
@@ -1,58 +1,57 @@
+
+
- 0 0 0 8
-
-
+ TargetType="{x:Type Window}" />
+
+
- #545454
+ #2e436e
+ TargetType="{x:Type ScrollBar}" />
+
-
+
+
+ 8
+ 10 0 10 0
+ 0 0 0 10
+
-
-
\ No newline at end of file
+
diff --git a/Flow.Launcher/Themes/Discord Dark.xaml b/Flow.Launcher/Themes/Discord Dark.xaml
index fb88da313..c1336309b 100644
--- a/Flow.Launcher/Themes/Discord Dark.xaml
+++ b/Flow.Launcher/Themes/Discord Dark.xaml
@@ -21,7 +21,7 @@
-
+
-
+
- #49443c
+ #36363d
-
-
-
@@ -123,7 +125,7 @@
-
+
-
+
-
+
@@ -56,7 +56,7 @@
TargetType="{x:Type Rectangle}">
-
+
-
-
F1 M12000,12000z M0,0z M10354,10962C10326,10951 10279,10927 10249,10907 10216,10886 9476,10153 8370,9046 7366,8042 6541,7220 6536,7220 6532,7220 6498,7242 6461,7268 6213,7447 5883,7619 5592,7721 5194,7860 4802,7919 4360,7906 3612,7886 2953,7647 2340,7174 2131,7013 1832,6699 1664,6465 1394,6088 1188,5618 1097,5170 1044,4909 1030,4764 1030,4470 1030,4130 1056,3914 1135,3609 1263,3110 1511,2633 1850,2235 1936,2134 2162,1911 2260,1829 2781,1395 3422,1120 4090,1045 4271,1025 4667,1025 4848,1045 5505,1120 6100,1368 6630,1789 6774,1903 7081,2215 7186,2355 7362,2588 7467,2759 7579,2990 7802,3455 7911,3937 7911,4460 7911,4854 7861,5165 7737,5542 7684,5702 7675,5724 7602,5885 7517,6071 7390,6292 7270,6460 7242,6499 7220,6533 7220,6538 7220,6542 8046,7371 9055,8380 10441,9766 10898,10229 10924,10274 10945,10308 10966,10364 10976,10408 10990,10472 10991,10493 10980,10554 10952,10717 10840,10865 10690,10937 10621,10971 10607,10974 10510,10977 10425,10980 10395,10977 10354,10962z M4685,7050C5214,7001 5694,6809 6100,6484 6209,6396 6396,6209 6484,6100 7151,5267 7246,4110 6721,3190 6369,2571 5798,2137 5100,1956 4706,1855 4222,1855 3830,1957 3448,2056 3140,2210 2838,2453 2337,2855 2010,3427 1908,4080 1877,4274 1877,4656 1908,4850 1948,5105 2028,5370 2133,5590 2459,6272 3077,6782 3810,6973 3967,7014 4085,7034 4290,7053 4371,7061 4583,7059 4685,7050z
@@ -140,7 +144,7 @@
x:Key="PreviewBorderStyle"
BasedOn="{StaticResource BasePreviewBorderStyle}"
TargetType="{x:Type Border}">
-
+
-
+
@@ -149,7 +149,7 @@
x:Key="ClockPanel"
BasedOn="{StaticResource ClockPanel}"
TargetType="{x:Type StackPanel}">
-
+
-
-
-
-
-
+
-
diff --git a/Flow.Launcher/Themes/Win11Light.xaml b/Flow.Launcher/Themes/Win11Light.xaml
index b6f9cc249..a08b8eef9 100644
--- a/Flow.Launcher/Themes/Win11Light.xaml
+++ b/Flow.Launcher/Themes/Win11Light.xaml
@@ -156,13 +156,26 @@
+
+
+