diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index ce1269a29..024e727ce 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -176,6 +176,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool ShowHistoryResultsForHomePage { get; set; } = false; public int MaxHistoryResultsToShowForHomePage { get; set; } = 5; + public bool AutoRestartAfterChanging { get; set; } = false; + public int CustomExplorerIndex { get; set; } = 0; [JsonIgnore] diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 22ab2016c..24f74e15d 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -131,6 +131,8 @@ Show History Results in Home Page Maximum History Results Shown in Home Page This can only be edited if plugin supports Home feature and Home Page is enabled. + Automatically restart after changing plugins + Automatically restart Flow Launcher after installing/uninstalling/updating plugins Search Plugin @@ -184,6 +186,22 @@ 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 udpate + {0} by {1} {2}{2}Would you like to update this plugin? + Downloading plugin + Automatically restart after installing/uninstalling/updating plugins in plugin store Theme diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index c0c5613de..452e026d7 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -202,6 +202,17 @@ + + + + PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"); + private static readonly string ClassName = nameof(PluginStoreItemViewModel); + + private static readonly Settings Settings = Ioc.Default.GetRequiredService(); + + 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 +51,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 +69,223 @@ 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 InstallPluginAsync(_newPlugin); + break; + case "uninstall": + await UninstallPluginAsync(_oldPluginPair.Metadata); + break; + case "update": + await UpdatePluginAsync(_newPlugin, _oldPluginPair.Metadata); + 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")); + } + + 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")); + } + + 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")); + } + + 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 01fa3d203..bda05a02d 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 PluginStoreItemViewModel.UninstallPluginAsync(PluginPair.Metadata); } [RelayCommand]