diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index aae8dd764..5b14ad0b7 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -6,6 +6,7 @@ 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; @@ -24,6 +25,8 @@ 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; @@ -547,6 +550,177 @@ 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 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 @@ -694,6 +868,41 @@ 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); + } + } + #endregion } } diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs index 6b2cf6eed..a504b7a05 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -1,12 +1,7 @@ using System; -using System.IO; -using System.Threading; using System.Threading.Tasks; -using System.Windows; -using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Version = SemanticVersioning.Version; @@ -14,10 +9,6 @@ namespace Flow.Launcher.ViewModel { public partial class PluginStoreItemViewModel : BaseModel { - private static readonly string ClassName = nameof(PluginStoreItemViewModel); - - private static readonly Settings Settings = Ioc.Default.GetRequiredService(); - private readonly UserPlugin _newPlugin; private readonly PluginPair _oldPluginPair; @@ -74,223 +65,17 @@ namespace Flow.Launcher.ViewModel switch (action) { case "install": - await InstallPluginAsync(_newPlugin); + await PluginManager.InstallPluginAndCheckRestartAsync(_newPlugin); break; case "uninstall": - await UninstallPluginAsync(_oldPluginPair.Metadata); + await PluginManager.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata); break; case "update": - await UpdatePluginAsync(_newPlugin, _oldPluginPair.Metadata); + await PluginManager.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata); break; default: break; } } - - internal static async Task InstallPluginAsync(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)); - } - } - - internal static async Task UninstallPluginAsync(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)); - } - } - - internal static async Task UpdatePluginAsync(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); - } - } } } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index bda05a02d..f902fb037 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -172,7 +172,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private async Task OpenDeletePluginWindowAsync() { - await PluginStoreItemViewModel.UninstallPluginAsync(PluginPair.Metadata); + await PluginManager.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata); } [RelayCommand]