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]