diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 177c00fa2..5b3419041 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -99,3 +99,7 @@ pluginsmanager alreadyexists Softpedia img +Reloadable +metadatas +WMP +VSTHRD diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index 5ef8859fc..f308ec599 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -133,3 +133,4 @@ \bPortuguês (Brasil)\b \bčeština\b \bPortuguês\b +\bIoc\b diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs new file mode 100644 index 000000000..33963c01a --- /dev/null +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -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; + +/// +/// Class for installing, updating, and uninstalling plugins. +/// +public static class PluginInstaller +{ + private static readonly string ClassName = nameof(PluginInstaller); + + private static readonly Settings Settings = Ioc.Default.GetRequiredService(); + + // 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(); + + /// + /// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed. + /// + /// The plugin to install. + /// A Task representing the asynchronous install operation. + 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)); + } + } + + /// + /// Installs a plugin from a local zip file and restarts the application if required by settings. Validates the zip and prompts user for confirmation. + /// + /// The path to the plugin zip file. + /// A Task representing the asynchronous install operation. + 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(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); + } + + /// + /// Uninstalls a plugin and restarts the application if required by settings. Prompts user for confirmation and whether to keep plugin settings. + /// + /// The plugin metadata to uninstall. + /// A Task representing the asynchronous uninstall operation. + 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)); + } + } + + /// + /// Updates a plugin to a new version and restarts the application if required by settings. Prompts user for confirmation and handles download if needed. + /// + /// The new plugin version to install. + /// The existing plugin metadata to update. + /// A Task representing the asynchronous update operation. + 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)); + } + } + + /// + /// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation. + /// + /// The title for the progress box. + /// The URL to download from. + /// The local file path to save to. + /// Cancellation token source for cancelling the download. + /// Whether to delete the file if it already exists. + /// Whether to show a progress box during download. + /// A Task representing the asynchronous download operation. + 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); + } + } + + /// + /// Determines if the plugin install source is a known/approved source (e.g., GitHub and matches an existing plugin author). + /// + /// The URL to check. + /// True if the source is known, otherwise false. + 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) + ); + } +} diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 6f8a55dc2..e1af0a869 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -20,7 +20,7 @@ using ISavable = Flow.Launcher.Plugin.ISavable; namespace Flow.Launcher.Core.Plugin { /// - /// The entry for managing Flow Launcher plugins + /// Class for co-ordinating and managing all plugin lifecycle. /// public static class PluginManager { @@ -693,8 +693,13 @@ namespace Flow.Launcher.Core.Plugin private static bool SameOrLesserPluginVersionExists(string metadataPath) { var newMetadata = JsonSerializer.Deserialize(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 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 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 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 diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 88c4cfb81..a8e58f0fc 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -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] diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 1827354d0..cfa813d3f 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -23,8 +23,8 @@ namespace Flow.Launcher.Plugin /// /// query text /// - /// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one. - /// Set this to 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 to force Flow Launcher re-querying /// void ChangeQuery(string query, bool requery = false); @@ -49,7 +49,7 @@ namespace Flow.Launcher.Plugin /// /// Text to save on clipboard /// When true it will directly copy the file/folder from the path specified in text - /// Whether to show the default notification from this method after copy is done. + /// 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.> public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true); @@ -65,7 +65,7 @@ namespace Flow.Launcher.Plugin void SavePluginSettings(); /// - /// 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 /// void ShowMainWindow(); - + /// /// Focus the query text box in the main window /// @@ -115,7 +115,7 @@ namespace Flow.Launcher.Plugin bool IsMainWindowVisible(); /// - /// 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. /// event VisibilityChangedEventHandler VisibilityChanged; @@ -171,7 +171,7 @@ namespace Flow.Launcher.Plugin string GetTranslation(string key); /// - /// Get all loaded plugins + /// Get all loaded plugins /// /// List GetAllPlugins(); @@ -229,7 +229,7 @@ namespace Flow.Launcher.Plugin MatchResult FuzzySearch(string query, string stringToCompare); /// - /// Http download the spefic url and return as string + /// Http download the specific url and return as string /// /// URL to call Http Get /// Cancellation Token @@ -237,7 +237,7 @@ namespace Flow.Launcher.Plugin Task HttpGetStringAsync(string url, CancellationToken token = default); /// - /// Http download the spefic url and return as stream + /// Http download the specific url and return as stream /// /// URL to call Http Get /// Cancellation Token @@ -305,8 +305,8 @@ namespace Flow.Launcher.Plugin void LogError(string className, string message, [CallerMemberName] string methodName = ""); /// - /// 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 /// void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = ""); @@ -393,7 +393,7 @@ namespace Flow.Launcher.Plugin /// /// 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. /// /// Choose the first result after reload if true; keep the last selected result if false. Default is true. public void ReQuery(bool reselect = true); @@ -547,8 +547,10 @@ namespace Flow.Launcher.Plugin /// /// 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); + /// + /// True if the plugin is updated successfully, false otherwise. + /// + public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath); /// /// Install a plugin. By default will remove the zip file if installation is from url, @@ -558,7 +560,10 @@ namespace Flow.Launcher.Plugin /// /// 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); + /// + /// True if the plugin is installed successfully, false otherwise. + /// + public bool InstallPlugin(UserPlugin plugin, string zipFilePath); /// /// Uninstall a plugin @@ -567,8 +572,10 @@ namespace Flow.Launcher.Plugin /// /// 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); + /// + /// True if the plugin is updated successfully, false otherwise. + /// + public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false); /// /// Log debug message of the time taken to execute a method @@ -603,7 +610,7 @@ namespace Flow.Launcher.Plugin bool IsApplicationDarkTheme(); /// - /// 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. /// event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged; } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index e99f5e643..7b82748fc 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -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 diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 96a310052..4e3dee888 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -135,6 +135,10 @@ This can only be edited if plugin supports Home feature and Home Page is enabled. Show Search Window at Foremost Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position. + Restart after modifying plugin via Plugin Store + Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store + Show unknown source warning + Show warning when installing plugins from unknown sources Search Plugin @@ -173,6 +177,12 @@ 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 + {0} modified already + Please restart Flow before making any further changes + Fail to install {0} + Fail to uninstall {0} + Unable to find plugin.json from the extracted zip file, or this path {0} does not exist + A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin Plugin Store @@ -188,6 +198,28 @@ New Version This plugin has been updated within the last 7 days New Update is Available + Error installing plugin + Error uninstalling plugin + Error updating plugin + Keep plugin settings + Do you want to keep the settings of the plugin for the next usage? + Plugin {0} successfully installed. Please restart Flow. + Plugin {0} successfully uninstalled. Please restart Flow. + Plugin {0} successfully updated. Please restart Flow. + Plugin install + {0} by {1} {2}{2}Would you like to install this plugin? + Plugin uninstall + {0} by {1} {2}{2}Would you like to uninstall this plugin? + Plugin update + {0} by {1} {2}{2}Would you like to update this plugin? + Downloading plugin + Automatically restart after installing/uninstalling/updating plugins in plugin store + Zip file does not have a valid plugin.json configuration + Installing from an unknown source + 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) + Zip files + Please select zip file + Install plugin from local path Theme diff --git a/Flow.Launcher/ProgressBoxEx.xaml.cs b/Flow.Launcher/ProgressBoxEx.xaml.cs index 840c8bade..119463348 100644 --- a/Flow.Launcher/ProgressBoxEx.xaml.cs +++ b/Flow.Launcher/ProgressBoxEx.xaml.cs @@ -19,32 +19,32 @@ namespace Flow.Launcher public static async Task ShowAsync(string caption, Func, 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(); } } } diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index a2e5f1f85..d865a087b 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -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 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 UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) => PluginManager.UninstallPluginAsync(pluginMetadata, removePluginSettings); public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "") => diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index 07df0682d..efe67d016 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -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 diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index d114736d5..ac27c3b40 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -225,6 +225,30 @@ + + + + + + + + + + + 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; + } } } } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 01fa3d203..ea222d023 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -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] diff --git a/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs b/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs index 1eee6dba5..67bbbd930 100644 --- a/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs +++ b/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs @@ -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; diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml index 573ca9051..fa2e65240 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml @@ -45,10 +45,15 @@ Plugin {0} successfully updated. Please restart Flow. {0} plugins successfully updated. Please restart Flow. Plugin {0} has already been modified. Please restart Flow before making any further changes. + {0} modified already + Please restart Flow before making any further changes + + Invalid zip installer file + Please check if there is a plugin.json in {0} Plugins Manager - Management of installing, uninstalling or updating Flow Launcher plugins + Install, uninstall or update Flow Launcher plugins via the search window Unknown Author @@ -63,5 +68,5 @@ Install from unknown source warning - Automatically restart Flow Launcher after installing/uninstalling/updating plugins + Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugins Manager \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index a0990f5e3..ae7773187 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -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 + { + 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 + { + 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 @@ -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> 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 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 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; } } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs index 4bb78f6ff..d76ce40c4 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs @@ -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(); diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index 327011ac3..949e9e9db 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -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",