diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index 33963c01a5b..a79f4b47ce8 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; @@ -277,6 +278,100 @@ await DownloadFileAsync( } } + /// + /// Updates the plugin to the latest version available from its source. + /// + /// If true, do not show any messages when there is no update available. + /// If true, only use the primary URL for updates. + /// Cancellation token to cancel the update operation. + /// + public static async Task CheckForPluginUpdatesAsync(bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default) + { + // Update the plugin manifest + await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); + + // Get all plugins that can be updated + var resultsForUpdate = ( + from existingPlugin in API.GetAllPlugins() + join pluginUpdateSource in API.GetPluginManifest() + on existingPlugin.Metadata.ID equals pluginUpdateSource.ID + where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version, + StringComparison.InvariantCulture) < + 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest) + && !API.PluginModified(existingPlugin.Metadata.ID) + select + new PluginUpdateInfo() + { + ID = existingPlugin.Metadata.ID, + Name = existingPlugin.Metadata.Name, + Author = existingPlugin.Metadata.Author, + CurrentVersion = existingPlugin.Metadata.Version, + NewVersion = pluginUpdateSource.Version, + IcoPath = existingPlugin.Metadata.IcoPath, + PluginExistingMetadata = existingPlugin.Metadata, + PluginNewUserPlugin = pluginUpdateSource + }).ToList(); + + // No updates + if (!resultsForUpdate.Any()) + { + if (!silentUpdate) + { + API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle")); + } + return; + } + + // If all plugins are modified, just return + if (resultsForUpdate.All(x => API.PluginModified(x.ID))) + { + return; + } + + // Show message box with button to update all plugins + API.ShowMsgWithButton( + API.GetTranslation("updateAllPluginsTitle"), + API.GetTranslation("updateAllPluginsButtonContent"), + () => + { + UpdateAllPlugins(resultsForUpdate); + }, + string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name))); + } + + private static void UpdateAllPlugins(IEnumerable resultsForUpdate) + { + _ = Task.WhenAll(resultsForUpdate.Select(async plugin => + { + var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip"); + + try + { + using var cts = new CancellationTokenSource(); + + await DownloadFileAsync( + $"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}", + plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts); + + // check if user cancelled download before installing plugin + if (cts.IsCancellationRequested) + { + return; + } + + if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath)) + { + return; + } + } + catch (Exception e) + { + API.LogException(ClassName, "Failed to update plugin", e); + API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); + } + })); + } + /// /// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation. /// @@ -350,4 +445,16 @@ private static bool InstallSourceKnown(string url) x.Metadata.Website.StartsWith(constructedUrlPart) ); } + + private record PluginUpdateInfo + { + public string ID { get; init; } + public string Name { get; init; } + public string Author { get; init; } + public string CurrentVersion { get; init; } + public string NewVersion { get; init; } + public string IcoPath { get; init; } + public PluginMetadata PluginExistingMetadata { get; init; } + public UserPlugin PluginNewUserPlugin { get; init; } + } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 271f618da9a..00ecb9bb4fd 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -233,6 +233,7 @@ public bool ShowHistoryResultsForHomePage public bool AutoRestartAfterChanging { get; set; } = false; public bool ShowUnknownSourceWarning { get; set; } = true; + public bool AutoUpdatePlugins { get; set; } = true; public int CustomExplorerIndex { get; set; } = 0; diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7b82748fca3..7e3915b2b5d 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -239,6 +239,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => AutoStartup(); AutoUpdates(); + AutoPluginUpdates(); API.SaveAppAllSettings(); API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------"); @@ -251,7 +252,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => /// Check startup only for Release /// [Conditional("RELEASE")] - private void AutoStartup() + private static void AutoStartup() { // we try to enable auto-startup on first launch, or reenable if it was removed // but the user still has the setting set @@ -272,7 +273,7 @@ private void AutoStartup() } [Conditional("RELEASE")] - private void AutoUpdates() + private static void AutoUpdates() { _ = Task.Run(async () => { @@ -289,6 +290,23 @@ private void AutoUpdates() }); } + private static void AutoPluginUpdates() + { + _ = Task.Run(async () => + { + if (_settings.AutoUpdatePlugins) + { + // check plugin updates every 5 hour + var timer = new PeriodicTimer(TimeSpan.FromHours(5)); + await PluginInstaller.CheckForPluginUpdatesAsync(); + + while (await timer.WaitForNextTickAsync()) + // check updates on startup + await PluginInstaller.CheckForPluginUpdatesAsync(); + } + }); + } + #endregion #region Register Events diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 2fca066059a..725d8d3e1b3 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -93,6 +93,7 @@ Always Start Typing in English Mode Temporarily change your input method to English mode when activating Flow. Auto Update + Automatically check and update the app when available Select Hide Flow Launcher on startup Flow Launcher search window is hidden in the tray after starting up. @@ -117,7 +118,7 @@ Xing Kong Jian Dao Da Niu Xiao Lang - + Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled @@ -150,6 +151,8 @@ Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store Show unknown source warning Show warning when installing plugins from unknown sources + Auto update plugins + Automatically check plugin updates and notify if there are any updates available Search Plugin @@ -231,6 +234,11 @@ Zip files Please select zip file Install plugin from local path + No update available + All plugins are up to date + Plugin updates available + Update all plugins + Check plugin updates Theme diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index efe67d01664..96cd4407233 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -109,6 +109,12 @@ private async Task InstallPluginAsync() await PluginInstaller.InstallPluginAndCheckRestartAsync(file); } + [RelayCommand] + private async Task CheckPluginUpdatesAsync() + { + await PluginInstaller.CheckForPluginUpdatesAsync(silentUpdate: false); + } + private static string GetFileFromDialog(string title, string filter = "") { var dlg = new Microsoft.Win32.OpenFileDialog diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index a879007c349..f539510b0e0 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -182,7 +182,8 @@ + Icon="" + Sub="{DynamicResource autoUpdatesTooltip}"> + Type="Middle"> + + + + +