Compare commits

...

23 commits

Author SHA1 Message Date
Diego Henrique
cf17c6bb8f
Merge 2c25734875 into dda900041a 2026-03-01 11:33:59 +08:00
Jack Ye
dda900041a
Merge pull request #4299 from Flow-Launcher/IsMinimumAppVersionSatisfied
Some checks failed
Build / build (push) Has been cancelled
Add MinimumAppVersion support for plugin.json
2026-03-01 11:15:01 +08:00
Jack251970
cce80ac5aa Ensure temporary folder deletion 2026-02-27 19:14:03 +08:00
Jack251970
036a3761ea Remove log for plugin version mismatch in PluginManager
Logging when a plugin's minimum Flow Launcher version is not met
has been removed. The method now returns false without logging
any informational message.
2026-02-27 19:05:25 +08:00
Jack251970
48878d2d8c Make log message context-neutral 2026-02-27 19:04:30 +08:00
Jack Ye
dca90916fe
Fix translations
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 19:02:50 +08:00
Jack251970
ba1908d605 Update plugin min version warning message formatting
Improved the message shown when a plugin requires a newer Flow Launcher version by adding line breaks for clarity and updating the title wording. Adjusted code to pass Environment.NewLine and modified resource strings in en.xaml to support the new format.
2026-02-27 18:54:13 +08:00
Jack251970
590bf20204 Add null check for plugin metadata deserialization
Throw a JsonException if deserializing plugin metadata from JSON returns null. This prevents null metadata from being used and improves error handling for invalid or corrupted plugin.json files.
2026-02-27 18:53:30 +08:00
Jack251970
b02a5a265d Refactor plugin min version check and improve logging
Refactored the plugin minimum app version check to use Version.TryParse instead of try-catch with Version.Parse, preventing exceptions on invalid input. Improved error handling by logging parse failures as errors and version mismatches as info, making the logic clearer and more robust.
2026-02-27 18:48:59 +08:00
Jack251970
b1b18ee215 Improve error handling for invalid plugin.json files
Previously, installing a plugin with an invalid or corrupted plugin.json would cause an unhandled exception. Now, deserialization errors are caught, a user-friendly error message is shown, and the exception is logged. Added a new localized error message for this scenario in en.xaml.
2026-02-27 18:42:07 +08:00
Jack Ye
8e4f7258ff
Improve translations
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 18:36:54 +08:00
Jack251970
2e0db3b90f Add MinimumAppVersion support for plugins
Introduced MinimumAppVersion property to PluginMetadata, enabling plugins to specify required Flow Launcher version. Plugin installation now checks this requirement and prompts users if unsatisfied. Minimum app version logic moved to PluginManager and applied to manifest updates. Added localized strings for user prompts. Refactored SameOrLesserPluginVersionExists to accept PluginMetadata.
2026-02-27 18:30:29 +08:00
Jack Ye
2c25734875
Merge branch 'dev' into feature/topmost_automatically 2026-02-26 21:02:15 +08:00
Jack Ye
63b492aa92
Merge branch 'dev' into feature/topmost_automatically 2026-02-05 20:32:52 +08:00
01Dri
e5414d97da name changes 2025-12-15 11:30:46 -03:00
Diego Henrique
c808488ee0
Update Flow.Launcher.Infrastructure/UserSettings/Settings.cs
Co-authored-by: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com>
2025-12-15 11:22:57 -03:00
01Dri
0c9bd7fcab name changes 2025-12-15 09:56:12 -03:00
01Dri
693ca2b436 code clean 2025-12-14 19:03:26 -03:00
01Dri
98c2adb12d remove test code 2025-12-14 19:02:26 -03:00
01Dri
492321072c doc 2025-12-14 18:54:14 -03:00
01Dri
8138e71d51 top most option dynamic resource 2025-12-13 22:09:06 -03:00
01Dri
c0b4c67399 Topmost option in interface 2025-12-13 22:08:54 -03:00
01Dri
72ff6034e7 TopMost option 2025-12-13 22:08:42 -03:00
7 changed files with 168 additions and 99 deletions

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Core.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins
{
@ -41,11 +41,10 @@ namespace Flow.Launcher.Core.ExternalPlugins
return false;
var updatedPluginResults = new List<UserPlugin>();
var appVersion = SemanticVersioning.Version.Parse(Constant.Version);
for (int i = 0; i < results.Count; i++)
{
if (IsMinimumAppVersionSatisfied(results[i], appVersion))
if (PluginManager.IsMinimumAppVersionSatisfied(results[i].Name, results[i].MinimumAppVersion))
updatedPluginResults.Add(results[i]);
}
@ -72,28 +71,5 @@ namespace Flow.Launcher.Core.ExternalPlugins
return false;
}
private static bool IsMinimumAppVersionSatisfied(UserPlugin plugin, SemanticVersioning.Version appVersion)
{
if (string.IsNullOrEmpty(plugin.MinimumAppVersion))
return true;
try
{
if (appVersion >= SemanticVersioning.Version.Parse(plugin.MinimumAppVersion))
return true;
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. "
+ "Plugin excluded from manifest", e);
return false;
}
PublicApi.Instance.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, "
+ $"but current version is {Constant.Version}. Plugin excluded from manifest.");
return false;
}
}
}

View file

@ -6,6 +6,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
@ -813,15 +814,13 @@ namespace Flow.Launcher.Core.Plugin
return string.Empty;
}
private static bool SameOrLesserPluginVersionExists(string metadataPath)
private static bool SameOrLesserPluginVersionExists(PluginMetadata metadata)
{
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
if (!Version.TryParse(newMetadata.Version, out var newVersion))
if (!Version.TryParse(metadata.Version, out var newVersion))
return true; // If version is not valid, we assume it is lesser than any existing version
// Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists
return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID
return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == metadata.ID
&& Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
}
@ -881,84 +880,116 @@ namespace Flow.Launcher.Core.Plugin
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
if(!plugin.IsFromLocalInstallPath)
File.Delete(zipFilePath);
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
var metadataJsonFilePath = string.Empty;
if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
try
{
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.fileNotFoundMessage(pluginFolderPath));
return false;
}
if (!plugin.IsFromLocalInstallPath)
File.Delete(zipFilePath);
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
{
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.pluginExistAlreadyMessage());
return false;
}
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
var metadataJsonFilePath = string.Empty;
if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);
var defaultPluginIDs = new List<string>
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
{
"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
};
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.fileNotFoundMessage(pluginFolderPath));
return false;
}
// Treat default plugin differently, it needs to be removable along with each flow release
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
? DataLocation.PluginsDirectory
: Constant.PreinstalledDirectory;
PluginMetadata newMetadata;
try
{
newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataJsonFilePath)) ??
throw new JsonException("Deserialized metadata is null");
}
catch (Exception ex)
{
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.pluginJsonInvalidOrCorrupted());
PublicApi.Instance.LogException(ClassName,
$"Failed to deserialize plugin metadata for plugin {plugin.Name} from file {metadataJsonFilePath}", ex);
return false;
}
var newPluginPath = Path.Combine(installDirectory, folderName);
if (SameOrLesserPluginVersionExists(newMetadata))
{
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.pluginExistAlreadyMessage());
return false;
}
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s));
if (!IsMinimumAppVersionSatisfied(newMetadata.Name, newMetadata.MinimumAppVersion))
{
// Ask users if they want to install the plugin that doesn't satisfy the minimum app version requirement
if (PublicApi.Instance.ShowMsgBox(
Localize.pluginMinimumAppVersionUnsatisfiedMessage(newMetadata.Name, Environment.NewLine),
Localize.pluginMinimumAppVersionUnsatisfiedTitle(newMetadata.Name, newMetadata.MinimumAppVersion),
MessageBoxButton.YesNo) == MessageBoxResult.No)
{
return false;
}
}
// Check if marker file exists and delete it
try
{
var markerFilePath = Path.Combine(newPluginPath, DataLocation.PluginDeleteFile);
if (File.Exists(markerFilePath))
File.Delete(markerFilePath);
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
var defaultPluginIDs = new List<string>
{
"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)
? DataLocation.PluginsDirectory
: Constant.PreinstalledDirectory;
var newPluginPath = Path.Combine(installDirectory, folderName);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s));
// Check if marker file exists and delete it
try
{
var markerFilePath = Path.Combine(newPluginPath, DataLocation.PluginDeleteFile);
if (File.Exists(markerFilePath))
File.Delete(markerFilePath);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin marker file in {newPluginPath}", e);
}
if (checkModified)
{
ModifiedPlugins.Add(plugin.ID);
}
return true;
}
catch (Exception e)
finally
{
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin marker file in {newPluginPath}", e);
try
{
if (Directory.Exists(tempFolderPluginPath))
Directory.Delete(tempFolderPluginPath, true);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
}
}
try
{
if (Directory.Exists(tempFolderPluginPath))
Directory.Delete(tempFolderPluginPath, true);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
}
if (checkModified)
{
ModifiedPlugins.Add(plugin.ID);
}
return true;
}
internal static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
@ -1050,6 +1081,27 @@ namespace Flow.Launcher.Core.Plugin
return true;
}
internal static bool IsMinimumAppVersionSatisfied(string pluginName, string minimumAppVersion)
{
// If the minimum app version is not specified in plugin.json, this plugin is compatible with all app versions
if (string.IsNullOrEmpty(minimumAppVersion))
return true;
var appVersion = Version.Parse(Constant.Version);
if (!Version.TryParse(minimumAppVersion, out var minimumVersion))
{
PublicApi.Instance.LogError(ClassName,
$"Failed to parse the minimum app version {minimumAppVersion} for plugin {pluginName}.");
return false; // If the minimum app version specified in plugin.json is invalid, we assume it is not satisfied
}
if (appVersion >= minimumVersion)
return true;
return false;
}
#endregion
#endregion

View file

@ -497,6 +497,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
}
private bool _autoTopmostLastOpenedResults = false;
public bool AutoTopmostLastOpenedResult
{
get => _autoTopmostLastOpenedResults;
set
{
if (_autoTopmostLastOpenedResults != value)
{
_autoTopmostLastOpenedResults = value;
OnPropertyChanged();
}
}
}
public bool SearchQueryResultsWithDelay { get; set; }
public int SearchDelayTime { get; set; } = 150;

View file

@ -137,6 +137,11 @@ namespace Flow.Launcher.Plugin
[JsonIgnore]
public int QueryCount { get; set; }
/// <summary>
/// The minimum Flow Launcher version required for this plugin. Default is "".
/// </summary>
public string MinimumAppVersion { get; set; } = string.Empty;
/// <summary>
/// The path to the plugin settings directory which is not validated.
/// It is used to store plugin settings files and data files.

View file

@ -179,6 +179,8 @@
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>
<system:String x:Key="autoTopmostLastOpenedResult">Auto Topmost Results</system:String>
<system:String x:Key="autoTopmostLastOpenedResultToolTip">Automatically mark selected results as topmost in their queries for faster access in future searches.</system:String>
<system:String x:Key="autoRestartAfterChanging">Restart after modifying plugin via Plugin Store</system:String>
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
@ -232,6 +234,9 @@
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
<system:String x:Key="errorCreatingSettingPanel">Error creating setting panel for plugin {0}:{1}{2}</system:String>
<system:String x:Key="pluginMinimumAppVersionUnsatisfiedTitle">{0} requires Flow version {1} to run</system:String>
<system:String x:Key="pluginMinimumAppVersionUnsatisfiedMessage">Flow does not meet the minimum version requirements for {0} to run. Do you want to continue installing it?{1}{1}We recommend updating Flow to the latest version to ensure that {0} works without issues.</system:String>
<system:String x:Key="pluginJsonInvalidOrCorrupted">Failed to install plugin because plugin.json is invalid or corrupted</system:String>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plugin Store</system:String>

View file

@ -106,6 +106,20 @@
OnContent="{DynamicResource enable}" />
</ui:SettingsCard>
<ui:SettingsCard
Margin="0 4 0 0"
Description="{DynamicResource autoTopmostLastOpenedResultToolTip}"
Header="{DynamicResource autoTopmostLastOpenedResult}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xf5ed;" />
</ui:SettingsCard.HeaderIcon>
<ui:ToggleSwitch
IsOn="{Binding Settings.AutoTopmostLastOpenedResult}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</ui:SettingsCard>
<ui:SettingsCard Margin="0 4 0 0" Header="{DynamicResource SearchWindowPosition}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe7f4;" />

View file

@ -545,12 +545,14 @@ namespace Flow.Launcher.ViewModel
// Record user selected result for result ranking
_userSelectedRecord.Add(result);
// Add item to history only if it is from results but not context menu or history
// Add item to history and topmost only if it is from results but not context menu or history
if (queryResultsSelected)
{
_history.Add(result);
lastHistoryIndex = 1;
}
if (Settings.AutoTopmostLastOpenedResult)
_topMostRecord.AddOrUpdate(result);
}
}
private static IReadOnlyList<Result> DeepCloneResults(IReadOnlyList<Result> results, bool isDialogJump, CancellationToken token = default)