Compare commits

...

15 commits

Author SHA1 Message Date
Copilot
bee2879ddf
Merge 0fad9c7157 into 984b3dabdc 2026-03-08 13:12:33 +11:00
Jack Ye
984b3dabdc
Fix Explorer plugin Quick Access items missing from general search results (#4294)
Some checks failed
Build / build (push) Has been cancelled
2026-03-04 21:25:25 +11: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
54dc790cb6
Merge pull request #4297 from Flow-Launcher/dependabot/github_actions/actions/upload-artifact-7
Some checks failed
Build / build (push) Has been cancelled
Bump actions/upload-artifact from 6 to 7
2026-02-27 12:08:19 +08:00
dependabot[bot]
ed6fe34192
Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 22:42:54 +00:00
6 changed files with 138 additions and 105 deletions

View file

@ -54,14 +54,14 @@ jobs:
shell: powershell
run: .\Scripts\post_build.ps1
- name: Upload Plugin Nupkg
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Plugin nupkg
path: |
Output\Release\Flow.Launcher.Plugin.*.nupkg
compression-level: 0
- name: Upload Setup
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Flow Installer
path: |
@ -75,7 +75,7 @@ jobs:
Output\Packages\Flow-Launcher-Setup-FD.exe
compression-level: 0
- name: Upload Portable Version
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Portable Version
path: |
@ -89,7 +89,7 @@ jobs:
Output\Packages\Flow-Launcher-Portable-FD.zip
compression-level: 0
- name: Upload Full Nupkg
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Full nupkg
path: |
@ -97,7 +97,7 @@ jobs:
compression-level: 0
- name: Upload Release Information
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: RELEASES
path: |

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

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

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

@ -299,9 +299,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search
private List<Result> GetQuickAccessResultsFilteredByActionKeyword(Query query, List<ActionKeyword> actions)
{
if (!Settings.QuickAccessKeywordEnabled)
return [];
var results = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
if (results.Count == 0)
return [];