Merge branch 'dev' into rename-file

This commit is contained in:
Jack Ye 2025-07-07 20:39:20 +08:00 committed by GitHub
commit 35474f2cbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 689 additions and 118 deletions

View file

@ -99,3 +99,7 @@ pluginsmanager
alreadyexists
Softpedia
img
Reloadable
metadatas
WMP
VSTHRD

View file

@ -133,3 +133,4 @@
\bPortuguês (Brasil)\b
\bčeština\b
\bPortuguês\b
\bIoc\b

View file

@ -0,0 +1,353 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin;
/// <summary>
/// Class for installing, updating, and uninstalling plugins.
/// </summary>
public static class PluginInstaller
{
private static readonly string ClassName = nameof(PluginInstaller);
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
// 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<IPublicAPI>();
/// <summary>
/// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
/// </summary>
/// <param name="newPlugin">The plugin to install.</param>
/// <returns>A Task representing the asynchronous install operation.</returns>
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
{
if (API.PluginModified(newPlugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), newPlugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
return;
}
if (API.ShowMsgBox(
string.Format(
API.GetTranslation("InstallPromptSubtitle"),
newPlugin.Name, newPlugin.Author, Environment.NewLine),
API.GetTranslation("InstallPromptTitle"),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
try
{
// at minimum should provide a name, but handle plugin that is not downloaded from plugins manifest and is a url download
var downloadFilename = string.IsNullOrEmpty(newPlugin.Version)
? $"{newPlugin.Name}-{Guid.NewGuid()}.zip"
: $"{newPlugin.Name}-{newPlugin.Version}.zip";
var filePath = Path.Combine(Path.GetTempPath(), downloadFilename);
using var cts = new CancellationTokenSource();
if (!newPlugin.IsFromLocalInstallPath)
{
await DownloadFileAsync(
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
newPlugin.UrlDownload, filePath, cts);
}
else
{
filePath = newPlugin.LocalInstallPath;
}
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
}
if (!API.InstallPlugin(newPlugin, filePath))
{
return;
}
if (!newPlugin.IsFromLocalInstallPath)
{
File.Delete(filePath);
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to install plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin"));
return; // do not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
API.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("installbtn"),
string.Format(
API.GetTranslation(
"InstallSuccessNoRestart"),
newPlugin.Name));
}
}
/// <summary>
/// Installs a plugin from a local zip file and restarts the application if required by settings. Validates the zip and prompts user for confirmation.
/// </summary>
/// <param name="filePath">The path to the plugin zip file.</param>
/// <returns>A Task representing the asynchronous install operation.</returns>
public static async Task InstallPluginAndCheckRestartAsync(string filePath)
{
UserPlugin plugin;
try
{
using ZipArchive archive = ZipFile.OpenRead(filePath);
var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ??
throw new FileNotFoundException("The zip file does not contain a plugin.json file.");
using Stream stream = pluginJsonEntry.Open();
plugin = JsonSerializer.Deserialize<UserPlugin>(stream);
plugin.IcoPath = "Images\\zipfolder.png";
plugin.LocalInstallPath = filePath;
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to validate zip file", e);
API.ShowMsgError(API.GetTranslation("ZipFileNotHavePluginJson"));
return;
}
if (API.PluginModified(plugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
return;
}
if (Settings.ShowUnknownSourceWarning)
{
if (!InstallSourceKnown(plugin.Website)
&& API.ShowMsgBox(string.Format(
API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine),
API.GetTranslation("InstallFromUnknownSourceTitle"),
MessageBoxButton.YesNo) == MessageBoxResult.No)
return;
}
await InstallPluginAndCheckRestartAsync(plugin);
}
/// <summary>
/// Uninstalls a plugin and restarts the application if required by settings. Prompts user for confirmation and whether to keep plugin settings.
/// </summary>
/// <param name="oldPlugin">The plugin metadata to uninstall.</param>
/// <returns>A Task representing the asynchronous uninstall operation.</returns>
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
{
if (API.PluginModified(oldPlugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), oldPlugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
return;
}
if (API.ShowMsgBox(
string.Format(
API.GetTranslation("UninstallPromptSubtitle"),
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
API.GetTranslation("UninstallPromptTitle"),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
var removePluginSettings = API.ShowMsgBox(
API.GetTranslation("KeepPluginSettingsSubtitle"),
API.GetTranslation("KeepPluginSettingsTitle"),
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
try
{
if (!await API.UninstallPluginAsync(oldPlugin, removePluginSettings))
{
return;
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to uninstall plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin"));
return; // don not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
API.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("uninstallbtn"),
string.Format(
API.GetTranslation(
"UninstallSuccessNoRestart"),
oldPlugin.Name));
}
}
/// <summary>
/// Updates a plugin to a new version and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
/// </summary>
/// <param name="newPlugin">The new plugin version to install.</param>
/// <param name="oldPlugin">The existing plugin metadata to update.</param>
/// <returns>A Task representing the asynchronous update operation.</returns>
public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin)
{
if (API.ShowMsgBox(
string.Format(
API.GetTranslation("UpdatePromptSubtitle"),
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
API.GetTranslation("UpdatePromptTitle"),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
try
{
var filePath = Path.Combine(Path.GetTempPath(), $"{newPlugin.Name}-{newPlugin.Version}.zip");
using var cts = new CancellationTokenSource();
if (!newPlugin.IsFromLocalInstallPath)
{
await DownloadFileAsync(
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
newPlugin.UrlDownload, filePath, cts);
}
else
{
filePath = newPlugin.LocalInstallPath;
}
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
if (!await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
{
return;
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to update plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
return; // do not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
API.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("updatebtn"),
string.Format(
API.GetTranslation(
"UpdateSuccessNoRestart"),
newPlugin.Name));
}
}
/// <summary>
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
/// </summary>
/// <param name="progressBoxTitle">The title for the progress box.</param>
/// <param name="downloadUrl">The URL to download from.</param>
/// <param name="filePath">The local file path to save to.</param>
/// <param name="cts">Cancellation token source for cancelling the download.</param>
/// <param name="deleteFile">Whether to delete the file if it already exists.</param>
/// <param name="showProgress">Whether to show a progress box during download.</param>
/// <returns>A Task representing the asynchronous download operation.</returns>
private static async Task DownloadFileAsync(string progressBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
{
if (deleteFile && File.Exists(filePath))
File.Delete(filePath);
if (showProgress)
{
var exceptionHappened = false;
await API.ShowProgressBoxAsync(progressBoxTitle,
async (reportProgress) =>
{
if (reportProgress == null)
{
// when reportProgress is null, it means there is exception with the progress box
// so we record it with exceptionHappened and return so that progress box will close instantly
exceptionHappened = true;
return;
}
else
{
await API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
}
}, cts.Cancel);
// if exception happened while downloading and user does not cancel downloading,
// we need to redownload the plugin
if (exceptionHappened && (!cts.IsCancellationRequested))
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
else
{
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
}
/// <summary>
/// Determines if the plugin install source is a known/approved source (e.g., GitHub and matches an existing plugin author).
/// </summary>
/// <param name="url">The URL to check.</param>
/// <returns>True if the source is known, otherwise false.</returns>
private static bool InstallSourceKnown(string url)
{
if (string.IsNullOrEmpty(url))
return false;
var pieces = url.Split('/');
if (pieces.Length < 4)
return false;
var author = pieces[3];
var acceptedHost = "github.com";
var acceptedSource = "https://github.com";
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
return false;
return API.GetAllPlugins().Any(x =>
!string.IsNullOrEmpty(x.Metadata.Website) &&
x.Metadata.Website.StartsWith(constructedUrlPart)
);
}
}

View file

@ -20,7 +20,7 @@ using ISavable = Flow.Launcher.Plugin.ISavable;
namespace Flow.Launcher.Core.Plugin
{
/// <summary>
/// The entry for managing Flow Launcher plugins
/// Class for co-ordinating and managing all plugin lifecycle.
/// </summary>
public static class PluginManager
{
@ -693,8 +693,13 @@ namespace Flow.Launcher.Core.Plugin
private static bool SameOrLesserPluginVersionExists(string metadataPath)
{
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
if (!Version.TryParse(newMetadata.Version, out var newVersion))
return true; // If version is not valid, we assume it is lesser than any existing version
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
&& Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
}
#region Public functions
@ -704,33 +709,46 @@ namespace Flow.Launcher.Core.Plugin
return ModifiedPlugins.Contains(id);
}
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
public static async Task<bool> UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{
InstallPlugin(newVersion, zipFilePath, checkModified: false);
await UninstallPluginAsync(existingVersion, removePluginFromSettings: false, removePluginSettings: false, checkModified: false);
if (PluginModified(existingVersion.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), existingVersion.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
return false;
}
var installSuccess = InstallPlugin(newVersion, zipFilePath, checkModified: false);
if (!installSuccess) return false;
var uninstallSuccess = await UninstallPluginAsync(existingVersion, removePluginFromSettings: false, removePluginSettings: false, checkModified: false);
if (!uninstallSuccess) return false;
ModifiedPlugins.Add(existingVersion.ID);
return true;
}
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
public static bool InstallPlugin(UserPlugin plugin, string zipFilePath)
{
InstallPlugin(plugin, zipFilePath, checkModified: true);
return InstallPlugin(plugin, zipFilePath, checkModified: true);
}
public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginSettings = false)
public static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginSettings = false)
{
await UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings, checkModified: true);
return await UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings, checkModified: true);
}
#endregion
#region Internal functions
internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
// Distinguish exception from installing same or less version
throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin));
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
return false;
}
// Unzip plugin files to temp folder
@ -748,12 +766,16 @@ namespace Flow.Launcher.Core.Plugin
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
{
throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist");
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
string.Format(API.GetTranslation("fileNotFoundMessage"), pluginFolderPath));
return false;
}
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
{
throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}");
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
API.GetTranslation("pluginExistAlreadyMessage"));
return false;
}
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
@ -797,13 +819,17 @@ namespace Flow.Launcher.Core.Plugin
{
ModifiedPlugins.Add(plugin.ID);
}
return true;
}
internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
internal static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
return false;
}
if (removePluginSettings || removePluginFromSettings)
@ -872,6 +898,8 @@ namespace Flow.Launcher.Core.Plugin
{
ModifiedPlugins.Add(plugin.ID);
}
return true;
}
#endregion

View file

@ -398,6 +398,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public int MaxHistoryResultsToShowForHomePage { get; set; } = 5;
public bool AutoRestartAfterChanging { get; set; } = false;
public bool ShowUnknownSourceWarning { get; set; } = true;
public int CustomExplorerIndex { get; set; } = 0;
[JsonIgnore]

View file

@ -23,8 +23,8 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <param name="query">query text</param>
/// <param name="requery">
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
/// Set this to <see langword="true"/> to force Flow Launcher requerying
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
/// Set this to <see langword="true"/> to force Flow Launcher re-querying
/// </param>
void ChangeQuery(string query, bool requery = false);
@ -49,7 +49,7 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <param name="text">Text to save on clipboard</param>
/// <param name="directCopy">When true it will directly copy the file/folder from the path specified in text</param>
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
/// It will show file/folder/text is copied successfully.
/// Turn this off to show your own notification after copy is done.</param>>
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true);
@ -65,7 +65,7 @@ namespace Flow.Launcher.Plugin
void SavePluginSettings();
/// <summary>
/// Reloads any Plugins that have the
/// Reloads any Plugins that have the
/// IReloadable implemented. It refeshes
/// Plugin's in memory data with new content
/// added by user.
@ -97,7 +97,7 @@ namespace Flow.Launcher.Plugin
/// Show the MainWindow when hiding
/// </summary>
void ShowMainWindow();
/// <summary>
/// Focus the query text box in the main window
/// </summary>
@ -115,7 +115,7 @@ namespace Flow.Launcher.Plugin
bool IsMainWindowVisible();
/// <summary>
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// </summary>
event VisibilityChangedEventHandler VisibilityChanged;
@ -171,7 +171,7 @@ namespace Flow.Launcher.Plugin
string GetTranslation(string key);
/// <summary>
/// Get all loaded plugins
/// Get all loaded plugins
/// </summary>
/// <returns></returns>
List<PluginPair> GetAllPlugins();
@ -229,7 +229,7 @@ namespace Flow.Launcher.Plugin
MatchResult FuzzySearch(string query, string stringToCompare);
/// <summary>
/// Http download the spefic url and return as string
/// Http download the specific url and return as string
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
@ -237,7 +237,7 @@ namespace Flow.Launcher.Plugin
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
/// <summary>
/// Http download the spefic url and return as stream
/// Http download the specific url and return as stream
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
@ -305,8 +305,8 @@ namespace Flow.Launcher.Plugin
void LogError(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow
/// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow
/// </summary>
void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
@ -393,7 +393,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Reloads the query.
/// When current results are from context menu or history, it will go back to query results before requerying.
/// When current results are from context menu or history, it will go back to query results before re-querying.
/// </summary>
/// <param name="reselect">Choose the first result after reload if true; keep the last selected result if false. Default is true.</param>
public void ReQuery(bool reselect = true);
@ -547,8 +547,10 @@ namespace Flow.Launcher.Plugin
/// <param name="zipFilePath">
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
/// </param>
/// <returns></returns>
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
/// <returns>
/// True if the plugin is updated successfully, false otherwise.
/// </returns>
public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
/// <summary>
/// Install a plugin. By default will remove the zip file if installation is from url,
@ -558,7 +560,10 @@ namespace Flow.Launcher.Plugin
/// <param name="zipFilePath">
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
/// </param>
public void InstallPlugin(UserPlugin plugin, string zipFilePath);
/// <returns>
/// True if the plugin is installed successfully, false otherwise.
/// </returns>
public bool InstallPlugin(UserPlugin plugin, string zipFilePath);
/// <summary>
/// Uninstall a plugin
@ -567,8 +572,10 @@ namespace Flow.Launcher.Plugin
/// <param name="removePluginSettings">
/// Plugin has their own settings. If this is set to true, the plugin settings will be removed.
/// </param>
/// <returns></returns>
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
/// <returns>
/// True if the plugin is updated successfully, false otherwise.
/// </returns>
public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
/// <summary>
/// Log debug message of the time taken to execute a method
@ -603,7 +610,7 @@ namespace Flow.Launcher.Plugin
bool IsApplicationDarkTheme();
/// <summary>
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// </summary>
event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged;
}

View file

@ -211,6 +211,9 @@ namespace Flow.Launcher
Http.Proxy = _settings.Proxy;
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
await API.UpdatePluginManifestAsync();
await PluginManager.InitializePluginsAsync();
// Change language after all plugins are initialized because we need to update plugin title based on their api

View file

@ -135,6 +135,10 @@
<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="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>
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>
@ -173,6 +177,12 @@
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="pluginModifiedAlreadyTitle">{0} modified already</system:String>
<system:String x:Key="pluginModifiedAlreadyMessage">Please restart Flow before making any further changes</system:String>
<system:String x:Key="failedToInstallPluginTitle">Fail to install {0}</system:String>
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
<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>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plugin Store</system:String>
@ -188,6 +198,28 @@
<system:String x:Key="LabelNew">New Version</system:String>
<system:String x:Key="LabelNewToolTip">This plugin has been updated within the last 7 days</system:String>
<system:String x:Key="LabelUpdateToolTip">New Update is Available</system:String>
<system:String x:Key="ErrorInstallingPlugin">Error installing plugin</system:String>
<system:String x:Key="ErrorUninstallingPlugin">Error uninstalling plugin</system:String>
<system:String x:Key="ErrorUpdatingPlugin">Error updating plugin</system:String>
<system:String x:Key="KeepPluginSettingsTitle">Keep plugin settings</system:String>
<system:String x:Key="KeepPluginSettingsSubtitle">Do you want to keep the settings of the plugin for the next usage?</system:String>
<system:String x:Key="InstallSuccessNoRestart">Plugin {0} successfully installed. Please restart Flow.</system:String>
<system:String x:Key="UninstallSuccessNoRestart">Plugin {0} successfully uninstalled. Please restart Flow.</system:String>
<system:String x:Key="UpdateSuccessNoRestart">Plugin {0} successfully updated. Please restart Flow.</system:String>
<system:String x:Key="InstallPromptTitle">Plugin install</system:String>
<system:String x:Key="InstallPromptSubtitle">{0} by {1} {2}{2}Would you like to install this plugin?</system:String>
<system:String x:Key="UninstallPromptTitle">Plugin uninstall</system:String>
<system:String x:Key="UninstallPromptSubtitle">{0} by {1} {2}{2}Would you like to uninstall this plugin?</system:String>
<system:String x:Key="UpdatePromptTitle">Plugin update</system:String>
<system:String x:Key="UpdatePromptSubtitle">{0} by {1} {2}{2}Would you like to update this plugin?</system:String>
<system:String x:Key="DownloadingPlugin">Downloading plugin</system:String>
<system:String x:Key="AutoRestartAfterChange">Automatically restart after installing/uninstalling/updating plugins in plugin store</system:String>
<system:String x:Key="ZipFileNotHavePluginJson">Zip file does not have a valid plugin.json configuration</system:String>
<system:String x:Key="InstallFromUnknownSourceTitle">Installing from an unknown source</system:String>
<system:String x:Key="InstallFromUnknownSourceSubtitle">This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window)</system:String>
<system:String x:Key="ZipFiles">Zip files</system:String>
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
<!-- Setting Theme -->
<system:String x:Key="theme">Theme</system:String>

View file

@ -19,32 +19,32 @@ namespace Flow.Launcher
public static async Task ShowAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action cancelProgress = null)
{
ProgressBoxEx prgBox = null;
ProgressBoxEx progressBox = null;
try
{
if (!Application.Current.Dispatcher.CheckAccess())
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
prgBox = new ProgressBoxEx(cancelProgress)
progressBox = new ProgressBoxEx(cancelProgress)
{
Title = caption
};
prgBox.TitleTextBlock.Text = caption;
prgBox.Show();
progressBox.TitleTextBlock.Text = caption;
progressBox.Show();
});
}
else
{
prgBox = new ProgressBoxEx(cancelProgress)
progressBox = new ProgressBoxEx(cancelProgress)
{
Title = caption
};
prgBox.TitleTextBlock.Text = caption;
prgBox.Show();
progressBox.TitleTextBlock.Text = caption;
progressBox.Show();
}
await reportProgressAsync(prgBox.ReportProgress).ConfigureAwait(false);
await reportProgressAsync(progressBox.ReportProgress).ConfigureAwait(false);
}
catch (Exception e)
{
@ -58,12 +58,12 @@ namespace Flow.Launcher
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
prgBox?.Close();
progressBox?.Close();
});
}
else
{
prgBox?.Close();
progressBox?.Close();
}
}
}

View file

@ -567,13 +567,13 @@ namespace Flow.Launcher
public bool PluginModified(string id) => PluginManager.PluginModified(id);
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) =>
public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) =>
PluginManager.UpdatePluginAsync(pluginMetadata, plugin, zipFilePath);
public void InstallPlugin(UserPlugin plugin, string zipFilePath) =>
public bool InstallPlugin(UserPlugin plugin, string zipFilePath) =>
PluginManager.InstallPlugin(plugin, zipFilePath);
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) =>
public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) =>
PluginManager.UninstallPluginAsync(pluginMetadata, removePluginSettings);
public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "") =>

View file

@ -1,7 +1,9 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
@ -96,6 +98,35 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
}
}
[RelayCommand]
private async Task InstallPluginAsync()
{
var file = GetFileFromDialog(
App.API.GetTranslation("SelectZipFile"),
$"{App.API.GetTranslation("ZipFiles")} (*.zip)|*.zip");
if (!string.IsNullOrEmpty(file))
await PluginInstaller.InstallPluginAndCheckRestartAsync(file);
}
private static string GetFileFromDialog(string title, string filter = "")
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\Downloads",
Multiselect = false,
CheckFileExists = true,
CheckPathExists = true,
Title = title,
Filter = filter
};
var result = dlg.ShowDialog();
if (result == true)
return dlg.FileName;
return string.Empty;
}
public bool SatisfiesFilter(PluginStoreItemViewModel plugin)
{
// Check plugin language

View file

@ -225,6 +225,30 @@
</cc:Card>
</cc:CardGroup>
<cc:CardGroup Margin="0 14 0 0">
<cc:Card
Title="{DynamicResource autoRestartAfterChanging}"
Icon="&#xF83E;"
Sub="{DynamicResource autoRestartAfterChangingToolTip}"
Type="First">
<ui:ToggleSwitch
IsOn="{Binding Settings.AutoRestartAfterChanging}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource showUnknownSourceWarning}"
Icon="&#xE7BA;"
Sub="{DynamicResource showUnknownSourceWarningToolTip}"
Type="Last">
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowUnknownSourceWarning}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
</cc:CardGroup>
<cc:ExCard
Title="{DynamicResource searchDelay}"
Margin="0 14 0 0"

View file

@ -92,6 +92,13 @@
</ui:MenuFlyout>
</ui:FlyoutService.Flyout>
</Button>
<Button
Height="34"
Margin="0 0 10 0"
Command="{Binding InstallPluginCommand}"
ToolTip="{DynamicResource installLocalPluginTooltip}">
<ui:FontIcon FontSize="14" Glyph="&#xE8DA;" />
</Button>
<TextBox
Name="PluginStoreFilterTextbox"
Width="150"

View file

@ -1,5 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
@ -9,27 +9,28 @@ namespace Flow.Launcher.ViewModel
{
public partial class PluginStoreItemViewModel : BaseModel
{
private PluginPair PluginManagerData => PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7");
private readonly UserPlugin _newPlugin;
private readonly PluginPair _oldPluginPair;
public PluginStoreItemViewModel(UserPlugin plugin)
{
_plugin = plugin;
_newPlugin = plugin;
_oldPluginPair = PluginManager.GetPluginForId(plugin.ID);
}
private UserPlugin _plugin;
public string ID => _newPlugin.ID;
public string Name => _newPlugin.Name;
public string Description => _newPlugin.Description;
public string Author => _newPlugin.Author;
public string Version => _newPlugin.Version;
public string Language => _newPlugin.Language;
public string Website => _newPlugin.Website;
public string UrlDownload => _newPlugin.UrlDownload;
public string UrlSourceCode => _newPlugin.UrlSourceCode;
public string IcoPath => _newPlugin.IcoPath;
public string ID => _plugin.ID;
public string Name => _plugin.Name;
public string Description => _plugin.Description;
public string Author => _plugin.Author;
public string Version => _plugin.Version;
public string Language => _plugin.Language;
public string Website => _plugin.Website;
public string UrlDownload => _plugin.UrlDownload;
public string UrlSourceCode => _plugin.UrlSourceCode;
public string IcoPath => _plugin.IcoPath;
public bool LabelInstalled => PluginManager.GetPluginForId(_plugin.ID) != null;
public bool LabelUpdate => LabelInstalled && new Version(_plugin.Version) > new Version(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version);
public bool LabelInstalled => _oldPluginPair != null;
public bool LabelUpdate => LabelInstalled && new Version(_newPlugin.Version) > new Version(_oldPluginPair.Metadata.Version);
internal const string None = "None";
internal const string RecentlyUpdated = "RecentlyUpdated";
@ -41,15 +42,15 @@ namespace Flow.Launcher.ViewModel
get
{
string category = None;
if (DateTime.Now - _plugin.LatestReleaseDate < TimeSpan.FromDays(7))
if (DateTime.Now - _newPlugin.LatestReleaseDate < TimeSpan.FromDays(7))
{
category = RecentlyUpdated;
}
if (DateTime.Now - _plugin.DateAdded < TimeSpan.FromDays(7))
if (DateTime.Now - _newPlugin.DateAdded < TimeSpan.FromDays(7))
{
category = NewRelease;
}
if (PluginManager.GetPluginForId(_plugin.ID) != null)
if (_oldPluginPair != null)
{
category = Installed;
}
@ -59,11 +60,22 @@ namespace Flow.Launcher.ViewModel
}
[RelayCommand]
private void ShowCommandQuery(string action)
private async Task ShowCommandQueryAsync(string action)
{
var actionKeyword = PluginManagerData.Metadata.ActionKeywords.Any() ? PluginManagerData.Metadata.ActionKeywords[0] + " " : String.Empty;
App.API.ChangeQuery($"{actionKeyword}{action} {_plugin.Name}");
App.API.ShowMainWindow();
switch (action)
{
case "install":
await PluginInstaller.InstallPluginAndCheckRestartAsync(_newPlugin);
break;
case "uninstall":
await PluginInstaller.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata);
break;
case "update":
await PluginInstaller.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata);
break;
default:
break;
}
}
}
}

View file

@ -1,5 +1,4 @@
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@ -32,21 +31,6 @@ namespace Flow.Launcher.ViewModel
}
}
private static string PluginManagerActionKeyword
{
get
{
var keyword = PluginManager
.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")
.Metadata.ActionKeywords.FirstOrDefault();
return keyword switch
{
null or "*" => string.Empty,
_ => keyword
};
}
}
private async Task LoadIconAsync()
{
Image = await App.API.LoadImageAsync(PluginPair.Metadata.IcoPath);
@ -186,10 +170,9 @@ namespace Flow.Launcher.ViewModel
}
[RelayCommand]
private void OpenDeletePluginWindow()
private async Task OpenDeletePluginWindowAsync()
{
App.API.ChangeQuery($"{PluginManagerActionKeyword} uninstall {PluginPair.Metadata.Name}".Trim(), true);
App.API.ShowMainWindow();
await PluginInstaller.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata);
}
[RelayCommand]

View file

@ -1,6 +1,5 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;

View file

@ -45,10 +45,15 @@
<system:String x:Key="plugin_pluginsmanager_update_success_no_restart">Plugin {0} successfully updated. Please restart Flow.</system:String>
<system:String x:Key="plugin_pluginsmanager_update_all_success_no_restart">{0} plugins successfully updated. Please restart Flow.</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_modified_error">Plugin {0} has already been modified. Please restart Flow before making any further changes.</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_modified_error_title">{0} modified already</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_modified_error_message">Please restart Flow before making any further changes</system:String>
<system:String x:Key="plugin_pluginsmanager_invalid_zip_title">Invalid zip installer file</system:String>
<system:String x:Key="plugin_pluginsmanager_invalid_zip_subtitle">Please check if there is a plugin.json in {0}</system:String>
<!-- Plugin Infos -->
<system:String x:Key="plugin_pluginsmanager_plugin_name">Plugins Manager</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_description">Management of installing, uninstalling or updating Flow Launcher plugins</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_description">Install, uninstall or update Flow Launcher plugins via the search window</system:String>
<system:String x:Key="plugin_pluginsmanager_unknown_author">Unknown Author</system:String>
<!-- Context menu items -->
@ -63,5 +68,5 @@
<!-- Settings menu items -->
<system:String x:Key="plugin_pluginsmanager_plugin_settings_unknown_source">Install from unknown source warning</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_settings_auto_restart">Automatically restart Flow Launcher after installing/uninstalling/updating plugins</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_settings_auto_restart">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugins Manager</system:String>
</ResourceDictionary>

View file

@ -114,6 +114,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
return;
}
if (Context.API.PluginModified(plugin.ID))
{
Context.API.ShowMsgError(
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_title"), plugin.Name),
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_message"));
return;
}
string message;
if (Settings.AutoRestartAfterChanging)
{
@ -158,7 +166,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (cts.IsCancellationRequested)
return;
else
Install(plugin, filePath);
if (!Install(plugin, filePath))
return;
}
catch (HttpRequestException e)
{
@ -196,7 +205,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
}
private async Task DownloadFileAsync(string prgBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
private async Task DownloadFileAsync(string progressBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
{
if (deleteFile && File.Exists(filePath))
File.Delete(filePath);
@ -204,12 +213,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (showProgress)
{
var exceptionHappened = false;
await Context.API.ShowProgressBoxAsync(prgBoxTitle,
await Context.API.ShowProgressBoxAsync(progressBoxTitle,
async (reportProgress) =>
{
if (reportProgress == null)
{
// when reportProgress is null, it means there is expcetion with the progress box
// when reportProgress is null, it means there is exception with the progress box
// so we record it with exceptionHappened and return so that progress box will close instantly
exceptionHappened = true;
return;
@ -242,6 +251,18 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
{
pluginFromLocalPath = Utilities.GetPluginInfoFromZip(search);
if (pluginFromLocalPath == null) return new List<Result>
{
new()
{
Title = Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_title"),
SubTitle = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_subtitle"),
search),
IcoPath = icoPath
}
};
pluginFromLocalPath.LocalInstallPath = search;
updateFromLocalPath = true;
}
@ -261,6 +282,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
select
new
{
existingPlugin.Metadata.ID,
pluginUpdateSource.Name,
pluginUpdateSource.Author,
CurrentVersion = existingPlugin.Metadata.Version,
@ -290,6 +312,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = x.IcoPath,
Action = e =>
{
if (Context.API.PluginModified(x.ID))
{
Context.API.ShowMsgError(
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_title"), x.Name),
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_message"));
return false;
}
string message;
if (Settings.AutoRestartAfterChanging)
{
@ -340,8 +370,11 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
else
{
await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath);
if (!await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath))
{
return;
}
if (Settings.AutoRestartAfterChanging)
{
@ -409,6 +442,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = icoPath,
AsyncAction = async e =>
{
if (resultsForUpdate.All(x => Context.API.PluginModified(x.ID)))
{
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"),
string.Join(" ", resultsForUpdate.Select(x => x.Name))));
return false;
}
string message;
if (Settings.AutoRestartAfterChanging)
{
@ -430,6 +471,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return false;
}
var anyPluginSuccess = false;
await Task.WhenAll(resultsForUpdate.Select(async plugin =>
{
var downloadToFilePath = Path.Combine(Path.GetTempPath(),
@ -447,8 +489,11 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (cts.IsCancellationRequested)
return;
else
await Context.API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
downloadToFilePath);
if (!await Context.API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
downloadToFilePath))
return;
anyPluginSuccess = true;
}
catch (Exception ex)
{
@ -461,6 +506,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
}));
if (!anyPluginSuccess) return false;
if (Settings.AutoRestartAfterChanging)
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
@ -556,6 +603,20 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
var plugin = Utilities.GetPluginInfoFromZip(localPath);
if (plugin == null)
{
return new List<Result>
{
new()
{
Title = Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_title"),
SubTitle = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_subtitle"),
localPath),
IcoPath = icoPath
}
};
}
plugin.LocalInstallPath = localPath;
return new List<Result>
@ -597,14 +658,17 @@ namespace Flow.Launcher.Plugin.PluginsManager
return false;
var author = pieces[3];
var acceptedHost = "github.com";
var acceptedSource = "https://github.com";
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
return url.StartsWith(acceptedSource) &&
Context.API.GetAllPlugins().Any(x =>
!string.IsNullOrEmpty(x.Metadata.Website) &&
x.Metadata.Website.StartsWith(constructedUrlPart)
);
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
return false;
return Context.API.GetAllPlugins().Any(x =>
!string.IsNullOrEmpty(x.Metadata.Website) &&
x.Metadata.Website.StartsWith(constructedUrlPart)
);
}
internal async ValueTask<List<Result>> RequestInstallOrUpdateAsync(string search, CancellationToken token,
@ -644,7 +708,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return Search(results, search);
}
private void Install(UserPlugin plugin, string downloadedFilePath)
private bool Install(UserPlugin plugin, string downloadedFilePath)
{
if (!File.Exists(downloadedFilePath))
throw new FileNotFoundException($"Plugin {plugin.ID} zip file not found at {downloadedFilePath}",
@ -652,10 +716,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
try
{
Context.API.InstallPlugin(plugin, downloadedFilePath);
if (!Context.API.InstallPlugin(plugin, downloadedFilePath))
return false;
if (!plugin.IsFromLocalInstallPath)
File.Delete(downloadedFilePath);
return true;
}
catch (FileNotFoundException e)
{
@ -677,6 +744,8 @@ namespace Flow.Launcher.Plugin.PluginsManager
plugin.Name));
Context.API.LogException(ClassName, e.Message, e);
}
return false;
}
internal List<Result> RequestUninstall(string search)
@ -691,6 +760,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = x.Metadata.IcoPath,
AsyncAction = async e =>
{
if (Context.API.PluginModified(x.Metadata.ID))
{
Context.API.ShowMsgError(
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_title"), x.Metadata.Name),
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error_message"));
return false;
}
string message;
if (Settings.AutoRestartAfterChanging)
{
@ -712,7 +789,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
Context.API.HideMainWindow();
await UninstallAsync(x.Metadata);
if (!await UninstallAsync(x.Metadata))
{
return false;
}
if (Settings.AutoRestartAfterChanging)
{
Context.API.RestartApp();
@ -745,7 +825,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return Search(results, search);
}
private async Task UninstallAsync(PluginMetadata plugin)
private async Task<bool> UninstallAsync(PluginMetadata plugin)
{
try
{
@ -753,13 +833,14 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_subtitle"),
Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_title"),
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
await Context.API.UninstallPluginAsync(plugin, removePluginSettings);
return await Context.API.UninstallPluginAsync(plugin, removePluginSettings);
}
catch (ArgumentException e)
{
Context.API.LogException(ClassName, e.Message, e);
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_error_title"),
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"));
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"), plugin.Name));
return false;
}
}
}

View file

@ -65,9 +65,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
using (ZipArchive archive = System.IO.Compression.ZipFile.OpenRead(filePath))
{
var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json").ToString();
ZipArchiveEntry pluginJsonEntry = archive.GetEntry(pluginJsonPath);
var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json");
if (pluginJsonEntry != null)
{
using Stream stream = pluginJsonEntry.Open();

View file

@ -4,7 +4,7 @@
"pm"
],
"Name": "Plugins Manager",
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
"Description": "Install, uninstall or update Flow Launcher plugins via the search window",
"Author": "Jeremy Wu",
"Version": "1.0.0",
"Language": "csharp",