From 949344a51e9062828e800149adb91165cc8882c3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 22 May 2025 17:32:33 +0800 Subject: [PATCH 01/75] Add internal model for plugin management --- .../UserSettings/Settings.cs | 2 + Flow.Launcher/Languages/en.xaml | 18 ++ .../Views/SettingsPaneGeneral.xaml | 11 + .../ViewModel/PluginStoreItemViewModel.cs | 270 ++++++++++++++++-- Flow.Launcher/ViewModel/PluginViewModel.cs | 23 +- 5 files changed, 280 insertions(+), 44 deletions(-) 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] From 76736b785091873534770d4806572f2641f20adf Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Thu, 22 May 2025 17:37:27 +0800 Subject: [PATCH 02/75] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 24f74e15d..2166bdd8c 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -198,7 +198,7 @@ {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 + Plugin update {0} by {1} {2}{2}Would you like to update this plugin? Downloading plugin Automatically restart after installing/uninstalling/updating plugins in plugin store From c6c7ff882e6745216c828559191966753553e839 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 22 May 2025 17:40:26 +0800 Subject: [PATCH 03/75] Handle default --- Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs index 3e823d635..a69c0dbd7 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -82,6 +82,8 @@ namespace Flow.Launcher.ViewModel case "update": await UpdatePluginAsync(_newPlugin, _oldPluginPair.Metadata); break; + default: + break; } } From 6044f87e806c97cd96f9d62beb6fb3a868ba1efe Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 22 May 2025 17:41:13 +0800 Subject: [PATCH 04/75] Do not restart on failure --- Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs index a69c0dbd7..6b2cf6eed 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -142,6 +142,7 @@ namespace Flow.Launcher.ViewModel { 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) @@ -181,6 +182,7 @@ namespace Flow.Launcher.ViewModel { 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) @@ -238,6 +240,7 @@ namespace Flow.Launcher.ViewModel { 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) From 383c0aeffc9afc1351f3c008d62aa0915ddc0da4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 23 May 2025 13:41:59 +0800 Subject: [PATCH 05/75] Improve code quality --- Flow.Launcher.Core/Plugin/PluginManager.cs | 209 +++++++++++++++++ .../ViewModel/PluginStoreItemViewModel.cs | 221 +----------------- Flow.Launcher/ViewModel/PluginViewModel.cs | 2 +- 3 files changed, 213 insertions(+), 219 deletions(-) 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] From 6bf7f00f0a7c268ff2e5c62bcbe791ff84328157 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 1 Jun 2025 16:28:16 +0800 Subject: [PATCH 06/75] Add unknown source warning setting --- .../UserSettings/Settings.cs | 1 + Flow.Launcher/Languages/en.xaml | 2 ++ .../Views/SettingsPaneGeneral.xaml | 31 +++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 0b2b042d4..9f8e51047 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -191,6 +191,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public int MaxHistoryResultsToShowForHomePage { get; set; } = 5; public bool AutoRestartAfterChanging { get; set; } = false; + public bool ShowUnknownSourceWarning { get; set; } = true; public int CustomExplorerIndex { get; set; } = 0; diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 7f00926f1..2b42b8f84 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -133,6 +133,8 @@ 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 + Show unknown source warning + Show warning when installing plugins from unknown sources Search Plugin diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 452e026d7..1966c4c0d 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -202,16 +202,27 @@ - - - + + + + + + + + + Date: Mon, 9 Jun 2025 20:28:18 +0800 Subject: [PATCH 07/75] Support installing from local path --- Flow.Launcher.Core/Plugin/PluginManager.cs | 55 +++++++++++++++++++ Flow.Launcher/Languages/en.xaml | 6 ++ .../SettingsPanePluginStoreViewModel.cs | 35 +++++++++++- .../Views/SettingsPanePluginStore.xaml | 7 +++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index ef831e940..f7a2461ab 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -2,6 +2,7 @@ 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; @@ -633,6 +634,42 @@ namespace Flow.Launcher.Core.Plugin } } + 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( @@ -913,6 +950,24 @@ namespace Flow.Launcher.Core.Plugin } } + 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/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index b3fdd6892..6ce5d17d0 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -204,6 +204,12 @@ {0} by {1} {2}{2}Would you like to update this plugin? Downloading plugin Automatically restart after installing/uninstalling/updating plugins in plugin store + Zip file does not have a valid plugin.json configuration + Installing from an unknown source + This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window) + Zip files + Please select zip file + Install plugin from local path Theme diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index 07df0682d..b9b7c12fa 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Windows.Forms; using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; @@ -96,6 +99,36 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel } } + [RelayCommand] + private async Task InstallPluginAsync() + { + var file = GetFileFromDialog( + App.API.GetTranslation("SelectZipFile"), + $"{App.API.GetTranslation("ZipFiles")} (*.zip)|*.zip"); + + if (!string.IsNullOrEmpty(file)) + await PluginManager.InstallPluginAndCheckRestartAsync(file); + } + + private static string GetFileFromDialog(string title, string filter = "") + { + var dlg = new OpenFileDialog + { + InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\Downloads", + Multiselect = false, + CheckFileExists = true, + CheckPathExists = true, + Title = title, + Filter = filter + }; + + return dlg.ShowDialog() switch + { + DialogResult.OK => dlg.FileName, + _ => string.Empty + }; + } + public bool SatisfiesFilter(PluginStoreItemViewModel plugin) { // Check plugin language diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml index 9312b0c2d..68f78d46c 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml @@ -92,6 +92,13 @@ + Date: Sat, 21 Jun 2025 21:34:49 +1000 Subject: [PATCH 08/75] simplify assignee by using filtered PR list --- .github/update_release_pr.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/update_release_pr.py b/.github/update_release_pr.py index ccea511b3..bf5f9a15e 100644 --- a/.github/update_release_pr.py +++ b/.github/update_release_pr.py @@ -107,14 +107,12 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all") return pr_list -def get_prs_assignees(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[str]: +def get_prs_assignees(pull_request_items: list[dict]) -> list[str]: """ - Returns a list of pull request assignees after applying the label and state filters, excludes jjw24. + Returns a list of pull request assignees, excludes jjw24. Args: - pull_request_items (list[dict]): List of PR items. - label (str): The label name. Filter is not applied when empty string. - state (str): State of PR, e.g. open, closed, all + pull_request_items (list[dict]): List of PR items to get the assignees from. Returns: list: A list of strs, where each string is an assignee name. List is not distinct, so can contain @@ -123,10 +121,9 @@ def get_prs_assignees(pull_request_items: list[dict], label: str = "", state: st """ assignee_list = [] for pr in pull_request_items: - if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]): - [assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24" ] + [assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24" ] - print(f"Found {len(assignee_list)} assignees with {label if label else 'no filter on'} label and state as {state}") + print(f"Found {len(assignee_list)} assignees") return assignee_list @@ -230,7 +227,7 @@ if __name__ == "__main__": description_content += f"## Features\n{get_pr_descriptions(enhancement_prs)}" if enhancement_prs else "" description_content += f"## Bug fixes\n{get_pr_descriptions(bug_fix_prs)}" if bug_fix_prs else "" - assignees = list(set(get_prs_assignees(pull_requests, "enhancement", "closed") + get_prs_assignees(pull_requests, "bug", "closed"))) + assignees = list(set(get_prs_assignees(enhancement_prs) + get_prs_assignees(bug_fix_prs))) assignees.sort(key=str.lower) description_content += f"### Authors:\n{', '.join(assignees)}" From 4e57e3a66cebec0af365c2bc6b7e462644d5c92d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 21 Jun 2025 21:37:12 +1000 Subject: [PATCH 09/75] determine milestone from release PR instead of querying milestones --- .github/update_release_pr.py | 77 ++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/.github/update_release_pr.py b/.github/update_release_pr.py index bf5f9a15e..0ae83151d 100644 --- a/.github/update_release_pr.py +++ b/.github/update_release_pr.py @@ -1,11 +1,12 @@ from os import getenv +from typing import Optional import requests def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: str = "all") -> list[dict]: """ - Fetches pull requests from a GitHub repository that match a given milestone and label. + Fetches pull requests from a GitHub repository that match a given label and state. Args: token (str): GitHub token. @@ -23,39 +24,10 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st "Accept": "application/vnd.github.v3+json", } - milestone_id = None - milestone_url = f"https://api.github.com/repos/{owner}/{repo}/milestones" - params = {"state": "open"} - - try: - response = requests.get(milestone_url, headers=headers, params=params) - response.raise_for_status() - milestones = response.json() - - if len(milestones) > 2: - print("More than two milestones found, unable to determine the milestone required.") - exit(1) - - # milestones.pop() - for ms in milestones: - if ms["title"] != "Future": - milestone_id = ms["number"] - print(f"Gathering PRs with milestone {ms['title']}...") - break - - if not milestone_id: - print(f"No suitable milestone found in repository '{owner}/{repo}'.") - exit(1) - - except requests.exceptions.RequestException as e: - print(f"Error fetching milestones: {e}") - exit(1) - - # This endpoint allows filtering by milestone and label. A PR in GH's perspective is a type of issue. + # This endpoint allows filtering by label(and milestone). A PR in GH's perspective is a type of issue. prs_url = f"https://api.github.com/repos/{owner}/{repo}/issues" params = { "state": state, - "milestone": milestone_id, "labels": label, "per_page": 100, } @@ -83,7 +55,7 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st return all_prs -def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[dict]: +def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all", milestone_number: Optional[int] = None) -> list[dict]: """ Returns a list of pull requests after applying the label and state filters. @@ -91,6 +63,7 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all") pull_request_items (list[dict]): List of PR items. label (str): The label name. Filter is not applied when empty string. state (str): State of PR, e.g. open, closed, all + milestone_number (Optional[int]): The milestone number to filter by. If None, no milestone filtering is applied. Returns: list: A list of dictionaries, where each dictionary represents a pull request. @@ -99,11 +72,20 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all") pr_list = [] count = 0 for pr in pull_request_items: - if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]): - pr_list.append(pr) - count += 1 + if state not in [pr["state"], "all"]: + continue - print(f"Found {count} PRs with {label if label else 'no filter on'} label and state as {state}") + if label and not [item for item in pr["labels"] if item["name"] == label]: + continue + + if milestone_number: + if not pr.get("milestone") or pr["milestone"]["number"] != milestone_number: + continue + + pr_list.append(pr) + count += 1 + + print(f"Found {count} PRs with {label if label else 'no filter on'} label, state as {state}, and milestone {pr.get("milestone",{}).get("number","None")}") return pr_list @@ -204,15 +186,16 @@ if __name__ == "__main__": print(f"Fetching {state} PRs for {repository_owner}/{repository_name} ...") - pull_requests = get_github_prs(github_token, repository_owner, repository_name) + # First, get all PRs to find the release PR and determine the milestone + all_pull_requests = get_github_prs(github_token, repository_owner, repository_name) - if not pull_requests: - print("No matching pull requests found") + if not all_pull_requests: + print("No pull requests found") exit(1) - print(f"\nFound total of {len(pull_requests)} pull requests") + print(f"\nFound total of {len(all_pull_requests)} pull requests") - release_pr = get_prs(pull_requests, "release", "open") + release_pr = get_prs(all_pull_requests, "release", "open") if len(release_pr) != 1: print(f"Unable to find the exact release PR. Returned result: {release_pr}") @@ -220,8 +203,16 @@ if __name__ == "__main__": print(f"Found release PR: {release_pr[0]['title']}") - enhancement_prs = get_prs(pull_requests, "enhancement", "closed") - bug_fix_prs = get_prs(pull_requests, "bug", "closed") + release_milestone_number = release_pr[0].get("milestone",{}).get("number",None) + + if not release_milestone_number: + print("Release PR does not have a milestone assigned.") + exit(1) + + print(f"Using milestone number: {release_milestone_number}") + + enhancement_prs = get_prs(all_pull_requests, "enhancement", "closed", release_milestone_number) + bug_fix_prs = get_prs(all_pull_requests, "bug", "closed", release_milestone_number) description_content = "# Release notes\n" description_content += f"## Features\n{get_pr_descriptions(enhancement_prs)}" if enhancement_prs else "" From 60d59668636db3bbef2488b0291328a78eacc655 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Sat, 21 Jun 2025 11:46:12 +0000 Subject: [PATCH 10/75] formatting --- .github/update_release_pr.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/update_release_pr.py b/.github/update_release_pr.py index 0ae83151d..68e4c0659 100644 --- a/.github/update_release_pr.py +++ b/.github/update_release_pr.py @@ -55,7 +55,9 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st return all_prs -def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all", milestone_number: Optional[int] = None) -> list[dict]: +def get_prs( + pull_request_items: list[dict], label: str = "", state: str = "all", milestone_number: Optional[int] = None +) -> list[dict]: """ Returns a list of pull requests after applying the label and state filters. @@ -85,10 +87,13 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all", pr_list.append(pr) count += 1 - print(f"Found {count} PRs with {label if label else 'no filter on'} label, state as {state}, and milestone {pr.get("milestone",{}).get("number","None")}") + print( + f"Found {count} PRs with {label if label else 'no filter on'} label, state as {state}, and milestone {pr.get("milestone",{}).get("number","None")}" + ) return pr_list + def get_prs_assignees(pull_request_items: list[dict]) -> list[str]: """ Returns a list of pull request assignees, excludes jjw24. @@ -103,12 +108,13 @@ def get_prs_assignees(pull_request_items: list[dict]) -> list[str]: """ assignee_list = [] for pr in pull_request_items: - [assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24" ] + [assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24"] print(f"Found {len(assignee_list)} assignees") return assignee_list + def get_pr_descriptions(pull_request_items: list[dict]) -> str: """ Returns the concatenated string of pr title and number in the format of @@ -203,7 +209,7 @@ if __name__ == "__main__": print(f"Found release PR: {release_pr[0]['title']}") - release_milestone_number = release_pr[0].get("milestone",{}).get("number",None) + release_milestone_number = release_pr[0].get("milestone", {}).get("number", None) if not release_milestone_number: print("Release PR does not have a milestone assigned.") From 9b02e1f74e180c906a7b0f13cde1cfb3c38904de Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Sat, 21 Jun 2025 21:52:54 +1000 Subject: [PATCH 11/75] fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/update_release_pr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/update_release_pr.py b/.github/update_release_pr.py index 68e4c0659..d637a3275 100644 --- a/.github/update_release_pr.py +++ b/.github/update_release_pr.py @@ -88,7 +88,7 @@ def get_prs( count += 1 print( - f"Found {count} PRs with {label if label else 'no filter on'} label, state as {state}, and milestone {pr.get("milestone",{}).get("number","None")}" + f"Found {count} PRs with {label if label else 'no filter on'} label, state as {state}, and milestone {pr.get('milestone', {}).get('number', 'None')}" ) return pr_list From 7c3c7680c0b8b2180ba6393300c65b5ac558a542 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 15:47:46 +0800 Subject: [PATCH 12/75] Add type for card elements inside card group --- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index d831774fb..ac27c3b40 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -229,7 +229,8 @@ + Sub="{DynamicResource autoRestartAfterChangingToolTip}" + Type="First"> + Sub="{DynamicResource showUnknownSourceWarningToolTip}" + Type="Last"> Date: Sun, 29 Jun 2025 15:48:08 +0800 Subject: [PATCH 13/75] 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] From 135fd03f88d78e85f2b3471396c58be0c8740de1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 15:58:52 +0800 Subject: [PATCH 14/75] Improve code quality --- .../Helper/PluginInstallationHelper.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/Flow.Launcher/Helper/PluginInstallationHelper.cs b/Flow.Launcher/Helper/PluginInstallationHelper.cs index 570c5d34c..d7ce2934c 100644 --- a/Flow.Launcher/Helper/PluginInstallationHelper.cs +++ b/Flow.Launcher/Helper/PluginInstallationHelper.cs @@ -57,19 +57,17 @@ public static class PluginInstallationHelper { return; } - else + + if (!File.Exists(filePath)) { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath); - } + throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath); + } - App.API.InstallPlugin(newPlugin, filePath); + App.API.InstallPlugin(newPlugin, filePath); - if (!newPlugin.IsFromLocalInstallPath) - { - File.Delete(filePath); - } + if (!newPlugin.IsFromLocalInstallPath) + { + File.Delete(filePath); } } catch (Exception e) @@ -201,10 +199,8 @@ public static class PluginInstallationHelper { return; } - else - { - await App.API.UpdatePluginAsync(oldPlugin, newPlugin, filePath); - } + + await App.API.UpdatePluginAsync(oldPlugin, newPlugin, filePath); } catch (Exception e) { From c5dd19ef300acc396c2a0d037ebb52f5aa288864 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 16:03:19 +0800 Subject: [PATCH 15/75] Use Microsoft.Win32.OpenFileDialog instead --- .../ViewModels/SettingsPanePluginStoreViewModel.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index bce7201b8..2b12ba70f 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Windows.Forms; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Helper; using Flow.Launcher.Plugin; @@ -112,7 +111,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel private static string GetFileFromDialog(string title, string filter = "") { - var dlg = new OpenFileDialog + var dlg = new Microsoft.Win32.OpenFileDialog { InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "\\Downloads", Multiselect = false, @@ -121,12 +120,11 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel Title = title, Filter = filter }; + var result = dlg.ShowDialog(); + if (result == true) + return dlg.FileName; - return dlg.ShowDialog() switch - { - DialogResult.OK => dlg.FileName, - _ => string.Empty - }; + return string.Empty; } public bool SatisfiesFilter(PluginStoreItemViewModel plugin) From 104b4b26805196bfb6c43327dac9bdf05b0d0266 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 16:09:06 +0800 Subject: [PATCH 16/75] Improve code quality --- Flow.Launcher/Helper/PluginInstallationHelper.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Flow.Launcher/Helper/PluginInstallationHelper.cs b/Flow.Launcher/Helper/PluginInstallationHelper.cs index d7ce2934c..0d3d2df67 100644 --- a/Flow.Launcher/Helper/PluginInstallationHelper.cs +++ b/Flow.Launcher/Helper/PluginInstallationHelper.cs @@ -98,9 +98,7 @@ public static class PluginInstallationHelper 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()) ?? + var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ?? throw new FileNotFoundException("The zip file does not contain a plugin.json file."); using Stream stream = pluginJsonEntry.Open(); From 3e9e91d71c8b0f9d63a9c7b97acb89aa6cd4d394 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 16:24:21 +0800 Subject: [PATCH 17/75] Fix possible exception when extracting zip file --- .../Languages/en.xaml | 3 +++ .../PluginsManager.cs | 26 +++++++++++++++++++ .../Utilities.cs | 4 +-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml index 573ca9051..bb0d6e5fb 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml @@ -46,6 +46,9 @@ {0} plugins successfully updated. Please restart Flow. Plugin {0} has already been modified. Please restart Flow before making any further changes. + Invalid zip installer file + Please check if there is plugin.json in {0} + Plugins Manager Management of installing, uninstalling or updating Flow Launcher plugins diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 25182f6d3..6ccd781c3 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -242,6 +242,18 @@ namespace Flow.Launcher.Plugin.PluginsManager if (FilesFolders.IsZipFilePath(search, checkFileExists: true)) { pluginFromLocalPath = Utilities.GetPluginInfoFromZip(search); + + if (pluginFromLocalPath == null) return new List + { + new() + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_title"), + SubTitle = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_subtitle"), + search), + IcoPath = icoPath + } + }; + pluginFromLocalPath.LocalInstallPath = search; updateFromLocalPath = true; } @@ -559,6 +571,20 @@ namespace Flow.Launcher.Plugin.PluginsManager { var plugin = Utilities.GetPluginInfoFromZip(localPath); + if (plugin == null) + { + return new List + { + new() + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_title"), + SubTitle = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_invalid_zip_subtitle"), + localPath), + IcoPath = icoPath + } + }; + } + plugin.LocalInstallPath = localPath; return new List diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs index 4bb78f6ff..d76ce40c4 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs @@ -65,9 +65,7 @@ namespace Flow.Launcher.Plugin.PluginsManager using (ZipArchive archive = System.IO.Compression.ZipFile.OpenRead(filePath)) { - var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json").ToString(); - ZipArchiveEntry pluginJsonEntry = archive.GetEntry(pluginJsonPath); - + var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json"); if (pluginJsonEntry != null) { using Stream stream = pluginJsonEntry.Open(); From a3a0c59fa3e2c3372a7c08f51d522a4a6e49149e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 16:29:45 +0800 Subject: [PATCH 18/75] Check url nullability --- Flow.Launcher/Helper/PluginInstallationHelper.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/Helper/PluginInstallationHelper.cs b/Flow.Launcher/Helper/PluginInstallationHelper.cs index 0d3d2df67..0e94566b8 100644 --- a/Flow.Launcher/Helper/PluginInstallationHelper.cs +++ b/Flow.Launcher/Helper/PluginInstallationHelper.cs @@ -259,6 +259,9 @@ public static class PluginInstallationHelper private static bool InstallSourceKnown(string url) { + if (string.IsNullOrEmpty(url)) + return false; + var pieces = url.Split('/'); if (pieces.Length < 4) From bdb3616977529f256d312486b23c54687603a3f7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 16:34:18 +0800 Subject: [PATCH 19/75] Improve string resource --- Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml index bb0d6e5fb..742d5d8b9 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml @@ -47,7 +47,7 @@ Plugin {0} has already been modified. Please restart Flow before making any further changes. Invalid zip installer file - Please check if there is plugin.json in {0} + Please check if there is a plugin.json in {0} Plugins Manager From 8e6a410cfcd6ebe56b893797bc4ba646e73c8ba7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 17:06:29 +0800 Subject: [PATCH 20/75] Use url host --- Flow.Launcher/Helper/PluginInstallationHelper.cs | 13 ++++++++----- .../PluginsManager.cs | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher/Helper/PluginInstallationHelper.cs b/Flow.Launcher/Helper/PluginInstallationHelper.cs index 0e94566b8..ea5f07bda 100644 --- a/Flow.Launcher/Helper/PluginInstallationHelper.cs +++ b/Flow.Launcher/Helper/PluginInstallationHelper.cs @@ -268,13 +268,16 @@ public static class PluginInstallationHelper return false; var author = pieces[3]; + var acceptedHost = "github.com"; 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) - ); + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost) + return false; + + return App.API.GetAllPlugins().Any(x => + !string.IsNullOrEmpty(x.Metadata.Website) && + x.Metadata.Website.StartsWith(constructedUrlPart) + ); } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 6ccd781c3..c7c3ff3a2 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -626,14 +626,17 @@ namespace Flow.Launcher.Plugin.PluginsManager return false; var author = pieces[3]; + var acceptedHost = "github.com"; var acceptedSource = "https://github.com"; var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author); - return url.StartsWith(acceptedSource) && - Context.API.GetAllPlugins().Any(x => - !string.IsNullOrEmpty(x.Metadata.Website) && - x.Metadata.Website.StartsWith(constructedUrlPart) - ); + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost) + return false; + + return Context.API.GetAllPlugins().Any(x => + !string.IsNullOrEmpty(x.Metadata.Website) && + x.Metadata.Website.StartsWith(constructedUrlPart) + ); } internal async ValueTask> RequestInstallOrUpdateAsync(string search, CancellationToken token, From 19cb3eaf6a52f651986301b06de0246ed6e9ee90 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 18:20:47 +0800 Subject: [PATCH 21/75] Fix typos --- Flow.Launcher/Helper/PluginInstallationHelper.cs | 2 +- Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/Helper/PluginInstallationHelper.cs b/Flow.Launcher/Helper/PluginInstallationHelper.cs index ea5f07bda..ea8195e57 100644 --- a/Flow.Launcher/Helper/PluginInstallationHelper.cs +++ b/Flow.Launcher/Helper/PluginInstallationHelper.cs @@ -235,7 +235,7 @@ public static class PluginInstallationHelper { if (reportProgress == null) { - // when reportProgress is null, it means there is expcetion with the progress box + // when reportProgress is null, it means there is exception with the progress box // so we record it with exceptionHappened and return so that progress box will close instantly exceptionHappened = true; return; diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index c7c3ff3a2..c1d3a81a2 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -209,7 +209,7 @@ namespace Flow.Launcher.Plugin.PluginsManager { if (reportProgress == null) { - // when reportProgress is null, it means there is expcetion with the progress box + // when reportProgress is null, it means there is exception with the progress box // so we record it with exceptionHappened and return so that progress box will close instantly exceptionHappened = true; return; From 9e868e7e3fec02ac340b79cc1f9d505d33616176 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 29 Jun 2025 22:35:14 +0800 Subject: [PATCH 22/75] Move plugin installer location --- .../Plugin/PluginInstaller.cs | 100 +++++++++--------- .../SettingsPanePluginStoreViewModel.cs | 4 +- .../ViewModel/PluginStoreItemViewModel.cs | 6 +- Flow.Launcher/ViewModel/PluginViewModel.cs | 3 +- .../ViewModel/SelectBrowserViewModel.cs | 1 - 5 files changed, 58 insertions(+), 56 deletions(-) rename Flow.Launcher/Helper/PluginInstallationHelper.cs => Flow.Launcher.Core/Plugin/PluginInstaller.cs (71%) diff --git a/Flow.Launcher/Helper/PluginInstallationHelper.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs similarity index 71% rename from Flow.Launcher/Helper/PluginInstallationHelper.cs rename to Flow.Launcher.Core/Plugin/PluginInstaller.cs index ea8195e57..a69ab322e 100644 --- a/Flow.Launcher/Helper/PluginInstallationHelper.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -10,24 +10,28 @@ using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -namespace Flow.Launcher.Helper; +namespace Flow.Launcher.Core.Plugin; /// /// Helper class for installing, updating, and uninstalling plugins. /// -public static class PluginInstallationHelper +public static class PluginInstaller { - private static readonly string ClassName = nameof(PluginInstallationHelper); + private static readonly string ClassName = nameof(PluginInstaller); private static readonly Settings Settings = Ioc.Default.GetRequiredService(); + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin) { - if (App.API.ShowMsgBox( + if (API.ShowMsgBox( string.Format( - App.API.GetTranslation("InstallPromptSubtitle"), + API.GetTranslation("InstallPromptSubtitle"), newPlugin.Name, newPlugin.Author, Environment.NewLine), - App.API.GetTranslation("InstallPromptTitle"), + API.GetTranslation("InstallPromptTitle"), button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; try @@ -44,7 +48,7 @@ public static class PluginInstallationHelper if (!newPlugin.IsFromLocalInstallPath) { await DownloadFileAsync( - $"{App.API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", + $"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", newPlugin.UrlDownload, filePath, cts); } else @@ -63,7 +67,7 @@ public static class PluginInstallationHelper throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath); } - App.API.InstallPlugin(newPlugin, filePath); + API.InstallPlugin(newPlugin, filePath); if (!newPlugin.IsFromLocalInstallPath) { @@ -72,21 +76,21 @@ public static class PluginInstallationHelper } catch (Exception e) { - App.API.LogException(ClassName, "Failed to install plugin", e); - App.API.ShowMsgError(App.API.GetTranslation("ErrorInstallingPlugin")); + API.LogException(ClassName, "Failed to install plugin", e); + API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin")); return; // don’t restart on failure } if (Settings.AutoRestartAfterChanging) { - App.API.RestartApp(); + API.RestartApp(); } else { - App.API.ShowMsg( - App.API.GetTranslation("installbtn"), + API.ShowMsg( + API.GetTranslation("installbtn"), string.Format( - App.API.GetTranslation( + API.GetTranslation( "InstallSuccessNoRestart"), newPlugin.Name)); } @@ -108,17 +112,17 @@ public static class PluginInstallationHelper } catch (Exception e) { - App.API.LogException(ClassName, "Failed to validate zip file", e); - App.API.ShowMsgError(App.API.GetTranslation("ZipFileNotHavePluginJson")); + API.LogException(ClassName, "Failed to validate zip file", e); + API.ShowMsgError(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"), + && API.ShowMsgBox(string.Format( + API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine), + API.GetTranslation("InstallFromUnknownSourceTitle"), MessageBoxButton.YesNo) == MessageBoxResult.No) return; } @@ -128,39 +132,39 @@ public static class PluginInstallationHelper public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin) { - if (App.API.ShowMsgBox( + if (API.ShowMsgBox( string.Format( - App.API.GetTranslation("UninstallPromptSubtitle"), + API.GetTranslation("UninstallPromptSubtitle"), oldPlugin.Name, oldPlugin.Author, Environment.NewLine), - App.API.GetTranslation("UninstallPromptTitle"), + API.GetTranslation("UninstallPromptTitle"), button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; - var removePluginSettings = App.API.ShowMsgBox( - App.API.GetTranslation("KeepPluginSettingsSubtitle"), - App.API.GetTranslation("KeepPluginSettingsTitle"), + var removePluginSettings = API.ShowMsgBox( + API.GetTranslation("KeepPluginSettingsSubtitle"), + API.GetTranslation("KeepPluginSettingsTitle"), button: MessageBoxButton.YesNo) == MessageBoxResult.No; try { - await App.API.UninstallPluginAsync(oldPlugin, removePluginSettings); + await API.UninstallPluginAsync(oldPlugin, removePluginSettings); } catch (Exception e) { - App.API.LogException(ClassName, "Failed to uninstall plugin", e); - App.API.ShowMsgError(App.API.GetTranslation("ErrorUninstallingPlugin")); + API.LogException(ClassName, "Failed to uninstall plugin", e); + API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin")); return; // don’t restart on failure } if (Settings.AutoRestartAfterChanging) { - App.API.RestartApp(); + API.RestartApp(); } else { - App.API.ShowMsg( - App.API.GetTranslation("uninstallbtn"), + API.ShowMsg( + API.GetTranslation("uninstallbtn"), string.Format( - App.API.GetTranslation( + API.GetTranslation( "UninstallSuccessNoRestart"), oldPlugin.Name)); } @@ -168,11 +172,11 @@ public static class PluginInstallationHelper public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin) { - if (App.API.ShowMsgBox( + if (API.ShowMsgBox( string.Format( - App.API.GetTranslation("UpdatePromptSubtitle"), + API.GetTranslation("UpdatePromptSubtitle"), oldPlugin.Name, oldPlugin.Author, Environment.NewLine), - App.API.GetTranslation("UpdatePromptTitle"), + API.GetTranslation("UpdatePromptTitle"), button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; try @@ -184,7 +188,7 @@ public static class PluginInstallationHelper if (!newPlugin.IsFromLocalInstallPath) { await DownloadFileAsync( - $"{App.API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", + $"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", newPlugin.UrlDownload, filePath, cts); } else @@ -198,25 +202,25 @@ public static class PluginInstallationHelper return; } - await App.API.UpdatePluginAsync(oldPlugin, newPlugin, filePath); + await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath); } catch (Exception e) { - App.API.LogException(ClassName, "Failed to update plugin", e); - App.API.ShowMsgError(App.API.GetTranslation("ErrorUpdatingPlugin")); + API.LogException(ClassName, "Failed to update plugin", e); + API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); return; // don’t restart on failure } if (Settings.AutoRestartAfterChanging) { - App.API.RestartApp(); + API.RestartApp(); } else { - App.API.ShowMsg( - App.API.GetTranslation("updatebtn"), + API.ShowMsg( + API.GetTranslation("updatebtn"), string.Format( - App.API.GetTranslation( + API.GetTranslation( "UpdateSuccessNoRestart"), newPlugin.Name)); } @@ -230,7 +234,7 @@ public static class PluginInstallationHelper if (showProgress) { var exceptionHappened = false; - await App.API.ShowProgressBoxAsync(prgBoxTitle, + await API.ShowProgressBoxAsync(prgBoxTitle, async (reportProgress) => { if (reportProgress == null) @@ -242,18 +246,18 @@ public static class PluginInstallationHelper } else { - await App.API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false); + 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 App.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); } else { - await App.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); } } @@ -275,7 +279,7 @@ public static class PluginInstallationHelper if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost) return false; - return App.API.GetAllPlugins().Any(x => + return 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 2b12ba70f..efe67d016 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; -using Flow.Launcher.Helper; +using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; @@ -106,7 +106,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel $"{App.API.GetTranslation("ZipFiles")} (*.zip)|*.zip"); if (!string.IsNullOrEmpty(file)) - await PluginInstallationHelper.InstallPluginAndCheckRestartAsync(file); + await PluginInstaller.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 f03d2740e..a985ca7ff 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -66,13 +66,13 @@ namespace Flow.Launcher.ViewModel switch (action) { case "install": - await PluginInstallationHelper.InstallPluginAndCheckRestartAsync(_newPlugin); + await PluginInstaller.InstallPluginAndCheckRestartAsync(_newPlugin); break; case "uninstall": - await PluginInstallationHelper.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata); + await PluginInstaller.UninstallPluginAndCheckRestartAsync(_oldPluginPair.Metadata); break; case "update": - await PluginInstallationHelper.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata); + await PluginInstaller.UpdatePluginAndCheckRestartAsync(_newPlugin, _oldPluginPair.Metadata); break; default: break; diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 131972e85..ea222d023 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -5,7 +5,6 @@ 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; @@ -173,7 +172,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private async Task OpenDeletePluginWindowAsync() { - await PluginInstallationHelper.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata); + await PluginInstaller.UninstallPluginAndCheckRestartAsync(PluginPair.Metadata); } [RelayCommand] diff --git a/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs b/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs index 1eee6dba5..67bbbd930 100644 --- a/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs +++ b/Flow.Launcher/ViewModel/SelectBrowserViewModel.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using System.Linq; -using System.Windows; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; From 0290675e1100fa2eab509cd4e2227efe68172e30 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 30 Jun 2025 12:12:47 +0800 Subject: [PATCH 23/75] Fix explorer plugin preview margin --- .../Views/PreviewPanel.xaml | 143 +++++++++--------- 1 file changed, 69 insertions(+), 74 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml index e200a187f..284ad32ad 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml @@ -8,10 +8,7 @@ d:DesignHeight="300" d:DesignWidth="300" mc:Ignorable="d"> - + @@ -90,78 +87,76 @@ - - - - - - - - - - - - - + + + + + + + + + + + + - - + + - - - - + + + From dafb0caac4a4a7745e26c5e49e1a94933e1089c0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 30 Jun 2025 12:42:09 +0800 Subject: [PATCH 24/75] Remove unused using --- Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs index a985ca7ff..f5523212e 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -2,7 +2,6 @@ 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; From ea25a661eec75486ce7b8ad3dad41addac97b2fe Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 30 Jun 2025 12:46:41 +0800 Subject: [PATCH 25/75] Fix an issue that after uninstalling pm, store no longer fetches plugin until clicking on refresh. --- Flow.Launcher/App.xaml.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 5df1f88ae..6d1499f2d 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -208,6 +208,9 @@ namespace Flow.Launcher Http.Proxy = _settings.Proxy; + // Initialize plugin manifest before initializing plugins so that they can use the manifest instantly + await API.UpdatePluginManifestAsync(); + await PluginManager.InitializePluginsAsync(); // Change language after all plugins are initialized because we need to update plugin title based on their api From 5b8b84a34c97b2503732311e3fd9b00a5b89b2a0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 30 Jun 2025 12:58:44 +0800 Subject: [PATCH 26/75] Fix code comments --- Flow.Launcher.Core/Plugin/PluginInstaller.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index a69ab322e..781ad3ff0 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -78,7 +78,7 @@ public static class PluginInstaller { API.LogException(ClassName, "Failed to install plugin", e); API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin")); - return; // don’t restart on failure + return; // do not restart on failure } if (Settings.AutoRestartAfterChanging) @@ -152,7 +152,7 @@ public static class PluginInstaller { API.LogException(ClassName, "Failed to uninstall plugin", e); API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin")); - return; // don’t restart on failure + return; // don not restart on failure } if (Settings.AutoRestartAfterChanging) @@ -208,7 +208,7 @@ public static class PluginInstaller { API.LogException(ClassName, "Failed to update plugin", e); API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); - return; // don’t restart on failure + return; // do not restart on failure } if (Settings.AutoRestartAfterChanging) From 01e749ac88e2aa4f9bc5e5bcc8ee5920ec68e9ee Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 30 Jun 2025 13:07:19 +0800 Subject: [PATCH 27/75] Fix an issue that store install/uninstall same plugin without restart shows error message error, should say already installed/uninstalled --- Flow.Launcher.Core/Plugin/PluginManager.cs | 24 +++++++++++++++------- Flow.Launcher/Languages/en.xaml | 5 +++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 9b525f331..5b74d80f0 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -542,7 +542,8 @@ namespace Flow.Launcher.Core.Plugin public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) { - InstallPlugin(newVersion, zipFilePath, checkModified:false); + var success = InstallPlugin(newVersion, zipFilePath, checkModified:false); + if (!success) return; await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); _modifiedPlugins.Add(existingVersion.ID); } @@ -561,12 +562,13 @@ namespace Flow.Launcher.Core.Plugin #region Internal functions - internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified) + internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified) { if (checkModified && PluginModified(plugin.ID)) { - // Distinguish exception from installing same or less version - throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin)); + API.ShowMsg(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), + API.GetTranslation("pluginModifiedAlreadyMessage")); + return false; } // Unzip plugin files to temp folder @@ -584,12 +586,16 @@ namespace Flow.Launcher.Core.Plugin if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath)) { - throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist"); + API.ShowMsg(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), + string.Format(API.GetTranslation("fileNotFoundMessage"), pluginFolderPath)); + return false; } if (SameOrLesserPluginVersionExists(metadataJsonFilePath)) { - throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}"); + API.ShowMsg(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), + API.GetTranslation("pluginExistAlreadyMessage")); + return false; } var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}"; @@ -633,13 +639,17 @@ namespace Flow.Launcher.Core.Plugin { _modifiedPlugins.Add(plugin.ID); } + + return true; } internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) { if (checkModified && PluginModified(plugin.ID)) { - throw new ArgumentException($"Plugin {plugin.Name} has been modified"); + API.ShowMsg(string.Format(API.GetTranslation("failedToUninstallPluginTitle"), plugin.Name), + API.GetTranslation("pluginModifiedAlreadyMessage")); + return; } if (removePluginSettings || removePluginFromSettings) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index e71ece19d..1cc03d6b1 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -175,6 +175,11 @@ Plugins: {0} - Fail to remove plugin settings files, please remove them manually Fail to remove plugin cache Plugins: {0} - Fail to remove plugin cache files, please remove them manually + Fail to install {0} + Fail to uninstall {0} + This plugin has been installed or uninstalled already, please restart Flow + Unable to find plugin.json from the extracted zip file, or this path {0} does not exist + A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin Plugin Store From 6318bbe1878d54ebc2c19670e6a9d06e1035b95b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 30 Jun 2025 13:16:59 +0800 Subject: [PATCH 28/75] Fix an issue that pm install/uninstall same plugin without restart says correct message but another message also pops up to say it's successfully installed --- Flow.Launcher.Core/Plugin/PluginInstaller.cs | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index 781ad3ff0..4cdab09f9 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -27,6 +27,13 @@ public static class PluginInstaller public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin) { + if (API.PluginModified(newPlugin.ID)) + { + API.ShowMsg(string.Format(API.GetTranslation("failedToInstallPluginTitle"), newPlugin.Name), + API.GetTranslation("pluginModifiedAlreadyMessage")); + return; + } + if (API.ShowMsgBox( string.Format( API.GetTranslation("InstallPromptSubtitle"), @@ -117,6 +124,13 @@ public static class PluginInstaller return; } + if (API.PluginModified(plugin.ID)) + { + API.ShowMsg(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), + API.GetTranslation("pluginModifiedAlreadyMessage")); + return; + } + if (Settings.ShowUnknownSourceWarning) { if (!InstallSourceKnown(plugin.Website) @@ -132,6 +146,13 @@ public static class PluginInstaller public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin) { + if (API.PluginModified(oldPlugin.ID)) + { + API.ShowMsg(string.Format(API.GetTranslation("failedToUninstallPluginTitle"), oldPlugin.Name), + API.GetTranslation("pluginModifiedAlreadyMessage")); + return; + } + if (API.ShowMsgBox( string.Format( API.GetTranslation("UninstallPromptSubtitle"), From 0c1fcad06f8df0a654b4b476fcf9c22321db41b5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 30 Jun 2025 14:39:29 +0800 Subject: [PATCH 29/75] Fix grid row issue --- Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml index 284ad32ad..0daa36e63 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/PreviewPanel.xaml @@ -44,7 +44,7 @@ TextWrapping="Wrap" /> - +