From 73a6fb6f65e97d823c4d2e0ebe7f6195f237bfa7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 15:48:08 +0800 Subject: [PATCH] Move codes to new place --- Flow.Launcher.Core/Plugin/PluginManager.cs | 264 ---------------- .../Helper/PluginInstallationHelper.cs | 283 ++++++++++++++++++ .../SettingsPanePluginStoreViewModel.cs | 4 +- .../ViewModel/PluginStoreItemViewModel.cs | 7 +- Flow.Launcher/ViewModel/PluginViewModel.cs | 3 +- 5 files changed, 291 insertions(+), 270 deletions(-) create mode 100644 Flow.Launcher/Helper/PluginInstallationHelper.cs diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index f7a2461ab..9b525f331 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -2,12 +2,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; 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.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; @@ -26,8 +24,6 @@ namespace Flow.Launcher.Core.Plugin { private static readonly string ClassName = nameof(PluginManager); - private static readonly Settings FlowSettings = Ioc.Default.GetRequiredService(); - private static IEnumerable _contextMenuPlugins; private static IEnumerable _homePlugins; @@ -561,213 +557,6 @@ namespace Flow.Launcher.Core.Plugin await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true); } - public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin) - { - 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; - } - else - { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath); - } - - API.InstallPlugin(newPlugin, filePath); - - if (!newPlugin.IsFromLocalInstallPath) - { - File.Delete(filePath); - } - } - } - catch (Exception e) - { - API.LogException(ClassName, "Failed to install plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin")); - return; // don’t restart on failure - } - - if (FlowSettings.AutoRestartAfterChanging) - { - API.RestartApp(); - } - else - { - API.ShowMsg( - API.GetTranslation("installbtn"), - string.Format( - API.GetTranslation( - "InstallSuccessNoRestart"), - newPlugin.Name)); - } - } - - public static async Task InstallPluginAndCheckRestartAsync(string filePath) - { - UserPlugin plugin; - try - { - using ZipArchive archive = ZipFile.OpenRead(filePath); - var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ?? - throw new FileNotFoundException("The zip file does not contain a plugin.json file."); - var pluginJsonEntry = archive.GetEntry(pluginJsonPath.ToString()) ?? - 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 (FlowSettings.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); - } - - public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin) - { - 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 - { - await API.UninstallPluginAsync(oldPlugin, removePluginSettings); - } - catch (Exception e) - { - API.LogException(ClassName, "Failed to uninstall plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin")); - return; // don’t restart on failure - } - - if (FlowSettings.AutoRestartAfterChanging) - { - API.RestartApp(); - } - else - { - API.ShowMsg( - API.GetTranslation("uninstallbtn"), - string.Format( - API.GetTranslation( - "UninstallSuccessNoRestart"), - oldPlugin.Name)); - } - } - - 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; - } - else - { - await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath); - } - } - catch (Exception e) - { - API.LogException(ClassName, "Failed to update plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); - return; // don’t restart on failure - } - - if (FlowSettings.AutoRestartAfterChanging) - { - API.RestartApp(); - } - else - { - API.ShowMsg( - API.GetTranslation("updatebtn"), - string.Format( - API.GetTranslation( - "UpdateSuccessNoRestart"), - newPlugin.Name)); - } - } - #endregion #region Internal functions @@ -915,59 +704,6 @@ namespace Flow.Launcher.Core.Plugin } } - internal static async Task DownloadFileAsync(string prgBoxTitle, 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(prgBoxTitle, - async (reportProgress) => - { - if (reportProgress == null) - { - // when reportProgress is null, it means there is expcetion 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); - } - } - - private static bool InstallSourceKnown(string url) - { - var pieces = url.Split('/'); - - if (pieces.Length < 4) - return false; - - var author = pieces[3]; - var acceptedSource = "https://github.com"; - var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author); - - return url.StartsWith(acceptedSource) && - API.GetAllPlugins().Any(x => - !string.IsNullOrEmpty(x.Metadata.Website) && - x.Metadata.Website.StartsWith(constructedUrlPart) - ); - } - #endregion } } diff --git a/Flow.Launcher/Helper/PluginInstallationHelper.cs b/Flow.Launcher/Helper/PluginInstallationHelper.cs new file mode 100644 index 000000000..570c5d34c --- /dev/null +++ b/Flow.Launcher/Helper/PluginInstallationHelper.cs @@ -0,0 +1,283 @@ +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.Helper; + +/// +/// Helper class for installing, updating, and uninstalling plugins. +/// +public static class PluginInstallationHelper +{ + private static readonly string ClassName = nameof(PluginInstallationHelper); + + private static readonly Settings Settings = Ioc.Default.GetRequiredService(); + + public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin) + { + if (App.API.ShowMsgBox( + string.Format( + App.API.GetTranslation("InstallPromptSubtitle"), + newPlugin.Name, newPlugin.Author, Environment.NewLine), + App.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( + $"{App.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; + } + else + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath); + } + + App.API.InstallPlugin(newPlugin, filePath); + + if (!newPlugin.IsFromLocalInstallPath) + { + File.Delete(filePath); + } + } + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to install plugin", e); + App.API.ShowMsgError(App.API.GetTranslation("ErrorInstallingPlugin")); + return; // don’t restart on failure + } + + if (Settings.AutoRestartAfterChanging) + { + App.API.RestartApp(); + } + else + { + App.API.ShowMsg( + App.API.GetTranslation("installbtn"), + string.Format( + App.API.GetTranslation( + "InstallSuccessNoRestart"), + newPlugin.Name)); + } + } + + public static async Task InstallPluginAndCheckRestartAsync(string filePath) + { + UserPlugin plugin; + try + { + using ZipArchive archive = ZipFile.OpenRead(filePath); + var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ?? + throw new FileNotFoundException("The zip file does not contain a plugin.json file."); + var pluginJsonEntry = archive.GetEntry(pluginJsonPath.ToString()) ?? + 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) + { + App.API.LogException(ClassName, "Failed to validate zip file", e); + App.API.ShowMsgError(App.API.GetTranslation("ZipFileNotHavePluginJson")); + return; + } + + if (Settings.ShowUnknownSourceWarning) + { + if (!InstallSourceKnown(plugin.Website) + && App.API.ShowMsgBox(string.Format( + App.API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine), + App.API.GetTranslation("InstallFromUnknownSourceTitle"), + MessageBoxButton.YesNo) == MessageBoxResult.No) + return; + } + + await InstallPluginAndCheckRestartAsync(plugin); + } + + public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin) + { + if (App.API.ShowMsgBox( + string.Format( + App.API.GetTranslation("UninstallPromptSubtitle"), + oldPlugin.Name, oldPlugin.Author, Environment.NewLine), + App.API.GetTranslation("UninstallPromptTitle"), + button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; + + var removePluginSettings = App.API.ShowMsgBox( + App.API.GetTranslation("KeepPluginSettingsSubtitle"), + App.API.GetTranslation("KeepPluginSettingsTitle"), + button: MessageBoxButton.YesNo) == MessageBoxResult.No; + + try + { + await App.API.UninstallPluginAsync(oldPlugin, removePluginSettings); + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to uninstall plugin", e); + App.API.ShowMsgError(App.API.GetTranslation("ErrorUninstallingPlugin")); + return; // don’t restart on failure + } + + if (Settings.AutoRestartAfterChanging) + { + App.API.RestartApp(); + } + else + { + App.API.ShowMsg( + App.API.GetTranslation("uninstallbtn"), + string.Format( + App.API.GetTranslation( + "UninstallSuccessNoRestart"), + oldPlugin.Name)); + } + } + + public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin) + { + if (App.API.ShowMsgBox( + string.Format( + App.API.GetTranslation("UpdatePromptSubtitle"), + oldPlugin.Name, oldPlugin.Author, Environment.NewLine), + App.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( + $"{App.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; + } + else + { + await App.API.UpdatePluginAsync(oldPlugin, newPlugin, filePath); + } + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to update plugin", e); + App.API.ShowMsgError(App.API.GetTranslation("ErrorUpdatingPlugin")); + return; // don’t restart on failure + } + + if (Settings.AutoRestartAfterChanging) + { + App.API.RestartApp(); + } + else + { + App.API.ShowMsg( + App.API.GetTranslation("updatebtn"), + string.Format( + App.API.GetTranslation( + "UpdateSuccessNoRestart"), + newPlugin.Name)); + } + } + + private static async Task DownloadFileAsync(string prgBoxTitle, 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 App.API.ShowProgressBoxAsync(prgBoxTitle, + async (reportProgress) => + { + if (reportProgress == null) + { + // when reportProgress is null, it means there is expcetion with the progress box + // so we record it with exceptionHappened and return so that progress box will close instantly + exceptionHappened = true; + return; + } + else + { + await App.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 App.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + } + else + { + await App.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + } + } + + private static bool InstallSourceKnown(string url) + { + var pieces = url.Split('/'); + + if (pieces.Length < 4) + return false; + + var author = pieces[3]; + var acceptedSource = "https://github.com"; + var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author); + + return url.StartsWith(acceptedSource) && + App.API.GetAllPlugins().Any(x => + !string.IsNullOrEmpty(x.Metadata.Website) && + x.Metadata.Website.StartsWith(constructedUrlPart) + ); + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index b9b7c12fa..bce7201b8 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; using CommunityToolkit.Mvvm.Input; -using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Helper; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; @@ -107,7 +107,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel $"{App.API.GetTranslation("ZipFiles")} (*.zip)|*.zip"); if (!string.IsNullOrEmpty(file)) - await PluginManager.InstallPluginAndCheckRestartAsync(file); + await PluginInstallationHelper.InstallPluginAndCheckRestartAsync(file); } private static string GetFileFromDialog(string title, string filter = "") diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs index a504b7a05..f03d2740e 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Helper; using Flow.Launcher.Plugin; using Version = SemanticVersioning.Version; @@ -65,13 +66,13 @@ namespace Flow.Launcher.ViewModel switch (action) { case "install": - await PluginManager.InstallPluginAndCheckRestartAsync(_newPlugin); + await PluginInstallationHelper.InstallPluginAndCheckRestartAsync(_newPlugin); break; case "uninstall": - await PluginManager.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata); + await PluginInstallationHelper.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata); break; case "update": - await PluginManager.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata); + await PluginInstallationHelper.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata); break; default: break; diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index f902fb037..131972e85 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -5,6 +5,7 @@ using System.Windows.Media; using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -172,7 +173,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private async Task OpenDeletePluginWindowAsync() { - await PluginManager.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata); + await PluginInstallationHelper.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata); } [RelayCommand]