From 79f81a6274e7bdaa3a62ff8afa012f56dca40c43 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 15:35:23 +0800 Subject: [PATCH 01/18] Improve code quality --- Flow.Launcher/App.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7b82748fc..ad7b44f10 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -251,7 +251,7 @@ namespace Flow.Launcher /// Check startup only for Release /// [Conditional("RELEASE")] - private void AutoStartup() + private static void AutoStartup() { // we try to enable auto-startup on first launch, or reenable if it was removed // but the user still has the setting set @@ -272,7 +272,7 @@ namespace Flow.Launcher } [Conditional("RELEASE")] - private void AutoUpdates() + private static void AutoUpdates() { _ = Task.Run(async () => { From c0c7546cfc07c68478300572f6dee21a21b91967 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 15:48:15 +0800 Subject: [PATCH 02/18] Add auto plugin update setting & UI --- .../UserSettings/Settings.cs | 1 + Flow.Launcher/Languages/en.xaml | 4 ++- .../Views/SettingsPaneGeneral.xaml | 27 +++++++++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 6b10d693d..54c313146 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -233,6 +233,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool AutoRestartAfterChanging { get; set; } = false; public bool ShowUnknownSourceWarning { get; set; } = true; + public bool AutoUpdatePlugins { get; set; } = true; public int CustomExplorerIndex { get; set; } = 0; diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 2fca06605..57ad1ce9a 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -117,7 +117,7 @@ Xing Kong Jian Dao Da Niu Xiao Lang - + Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled @@ -150,6 +150,8 @@ Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store Show unknown source warning Show warning when installing plugins from unknown sources + Auto update plugins + Automatically check plugin updates every 1 hour and notify if there are any updates available Search Plugin diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index df0243ce8..78b6d8db0 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -241,12 +241,23 @@ Title="{DynamicResource showUnknownSourceWarning}" Icon="" Sub="{DynamicResource showUnknownSourceWarningToolTip}" - Type="Last"> + Type="Middle"> + + + + - + Type="Middle" + Visibility="{ext:VisibleWhen {Binding ShouldUsePinyin}, + IsEqualToBool=True}"> + Type="Last" + Visibility="{ext:VisibleWhen {Binding UseDoublePinyin}, + IsEqualToBool=True}"> Date: Mon, 14 Jul 2025 16:14:13 +0800 Subject: [PATCH 03/18] Update string resource --- 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 57ad1ce9a..7a5dc0c07 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -151,7 +151,7 @@ Show unknown source warning Show warning when installing plugins from unknown sources Auto update plugins - Automatically check plugin updates every 1 hour and notify if there are any updates available + Automatically check plugin updates and notify if there are any updates available Search Plugin From 6eb52b8961882f0d1e723c733cecad3408b94840 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 16:18:22 +0800 Subject: [PATCH 04/18] Check plugin updates in the background --- Flow.Launcher.Core/Plugin/PluginInstaller.cs | 91 ++++++++++++++++++++ Flow.Launcher/App.xaml.cs | 18 ++++ Flow.Launcher/Languages/en.xaml | 4 + 3 files changed, 113 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index 33963c01a..692fe3ce1 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -277,6 +277,97 @@ public static class PluginInstaller } } + /// + /// Updates the plugin to the latest version available from its source. + /// + /// If true, do not show any messages when there is no udpate available. + /// If true, only use the primary URL for updates. + /// Cancellation token to cancel the update operation. + /// + public static async Task UpdatePluginAsync(bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default) + { + // Update the plugin manifest + await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); + + // Get all plugins that can be updated + var resultsForUpdate = ( + from existingPlugin in API.GetAllPlugins() + join pluginUpdateSource in API.GetPluginManifest() + on existingPlugin.Metadata.ID equals pluginUpdateSource.ID + where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version, + StringComparison.InvariantCulture) < + 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest) + && !API.PluginModified(existingPlugin.Metadata.ID) + select + new + { + existingPlugin.Metadata.ID, + pluginUpdateSource.Name, + pluginUpdateSource.Author, + CurrentVersion = existingPlugin.Metadata.Version, + NewVersion = pluginUpdateSource.Version, + existingPlugin.Metadata.IcoPath, + PluginExistingMetadata = existingPlugin.Metadata, + PluginNewUserPlugin = pluginUpdateSource + }).ToList(); + + // No updates + if (!resultsForUpdate.Any()) + { + if (!silentUpdate) + { + API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle")); + } + return; + } + + // If all plugins are modified, just return + if (resultsForUpdate.All(x => API.PluginModified(x.ID))) + { + return; + } + + if (API.ShowMsgBox( + string.Format(API.GetTranslation("updateAllPluginsSubtitle"), + Environment.NewLine, string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name))), + API.GetTranslation("updateAllPluginsTitle"), + MessageBoxButton.YesNo) == MessageBoxResult.No) + { + return; + } + + // Update all plugins + await Task.WhenAll(resultsForUpdate.Select(async plugin => + { + var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip"); + + try + { + using var cts = new CancellationTokenSource(); + + await DownloadFileAsync( + $"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}", + plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts); + + // check if user cancelled download before installing plugin + if (cts.IsCancellationRequested) + { + return; + } + + if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath)) + { + return; + } + } + catch (Exception e) + { + API.LogException(ClassName, "Failed to update plugin", e); + API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); + } + })); + } + /// /// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation. /// diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index ad7b44f10..6b04f11d9 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -239,6 +239,7 @@ namespace Flow.Launcher AutoStartup(); AutoUpdates(); + AutoPluginUpdates(); API.SaveAppAllSettings(); API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------"); @@ -289,6 +290,23 @@ namespace Flow.Launcher }); } + private static void AutoPluginUpdates() + { + _ = Task.Run(async () => + { + if (_settings.AutoUpdatePlugins) + { + // check plugin updates every 5 hour + var timer = new PeriodicTimer(TimeSpan.FromHours(5)); + await PluginInstaller.UpdatePluginAsync(); + + while (await timer.WaitForNextTickAsync()) + // check updates on startup + await PluginInstaller.UpdatePluginAsync(); + } + }); + } + #endregion #region Register Events diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 7a5dc0c07..efbb1de3b 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -233,6 +233,10 @@ Zip files Please select zip file Install plugin from local path + No update available + All plugins are up to date + Update all plugins + Would you like to update these plugins?{0}{0}{1} Theme From b393d361865bf66688e0cb5ecf83f38ff468571f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 16:24:09 +0800 Subject: [PATCH 05/18] Add check plugin update button --- Flow.Launcher/Languages/en.xaml | 1 + .../ViewModels/SettingsPanePluginStoreViewModel.cs | 6 ++++++ .../SettingPages/Views/SettingsPanePluginStore.xaml | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index efbb1de3b..ee500324e 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -237,6 +237,7 @@ All plugins are up to date Update all plugins Would you like to update these plugins?{0}{0}{1} + Check plugin updates Theme diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index efe67d016..bfec08c52 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -109,6 +109,12 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel await PluginInstaller.InstallPluginAndCheckRestartAsync(file); } + [RelayCommand] + private async Task CheckPluginUpdatesAsync() + { + await PluginInstaller.UpdatePluginAsync(silentUpdate: false); + } + private static string GetFileFromDialog(string title, string filter = "") { var dlg = new Microsoft.Win32.OpenFileDialog diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml index 68f78d46c..21290bb62 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml @@ -99,6 +99,13 @@ ToolTip="{DynamicResource installLocalPluginTooltip}"> + Date: Mon, 14 Jul 2025 16:24:46 +0800 Subject: [PATCH 06/18] Rename function name --- Flow.Launcher.Core/Plugin/PluginInstaller.cs | 2 +- Flow.Launcher/App.xaml.cs | 4 ++-- .../ViewModels/SettingsPanePluginStoreViewModel.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index 692fe3ce1..4c551f993 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -284,7 +284,7 @@ public static class PluginInstaller /// If true, only use the primary URL for updates. /// Cancellation token to cancel the update operation. /// - public static async Task UpdatePluginAsync(bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default) + public static async Task CheckForPluginUpdatesAsync(bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default) { // Update the plugin manifest await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 6b04f11d9..7e3915b2b 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -298,11 +298,11 @@ namespace Flow.Launcher { // check plugin updates every 5 hour var timer = new PeriodicTimer(TimeSpan.FromHours(5)); - await PluginInstaller.UpdatePluginAsync(); + await PluginInstaller.CheckForPluginUpdatesAsync(); while (await timer.WaitForNextTickAsync()) // check updates on startup - await PluginInstaller.UpdatePluginAsync(); + await PluginInstaller.CheckForPluginUpdatesAsync(); } }); } diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index bfec08c52..96cd44072 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -112,7 +112,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel [RelayCommand] private async Task CheckPluginUpdatesAsync() { - await PluginInstaller.UpdatePluginAsync(silentUpdate: false); + await PluginInstaller.CheckForPluginUpdatesAsync(silentUpdate: false); } private static string GetFileFromDialog(string title, string filter = "") From 69d5e33150c83ceaf455aec48a1b83004efc83ad Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 16:31:09 +0800 Subject: [PATCH 07/18] Show message box with button instead --- Flow.Launcher.Core/Plugin/PluginInstaller.cs | 22 ++++++++++++-------- Flow.Launcher/Languages/en.xaml | 4 ++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index 4c551f993..84f0f1fd9 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; @@ -327,17 +328,20 @@ public static class PluginInstaller return; } - if (API.ShowMsgBox( - string.Format(API.GetTranslation("updateAllPluginsSubtitle"), - Environment.NewLine, string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name))), + // Show message box with button to update all plugins + API.ShowMsgWithButton( API.GetTranslation("updateAllPluginsTitle"), - MessageBoxButton.YesNo) == MessageBoxResult.No) - { - return; - } + API.GetTranslation("updateAllPluginsButtonContent"), + () => + { + UpdateAllPlugins(resultsForUpdate); + }, + string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name))); + } - // Update all plugins - await Task.WhenAll(resultsForUpdate.Select(async plugin => + private static void UpdateAllPlugins(IEnumerable resultsForUpdate) + { + _ = Task.WhenAll(resultsForUpdate.Select(async plugin => { var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip"); diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index ee500324e..53f26c5f4 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -235,8 +235,8 @@ Install plugin from local path No update available All plugins are up to date - Update all plugins - Would you like to update these plugins?{0}{0}{1} + Plugin updates available + Update all plugins Check plugin updates From 44fbc6eed5763b089c78cc8f9a2eab3e8cb33185 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 16:35:48 +0800 Subject: [PATCH 08/18] Add auto update subtitle --- Flow.Launcher/Languages/en.xaml | 1 + Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 53f26c5f4..3905df7c6 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -93,6 +93,7 @@ Always Start Typing in English Mode Temporarily change your input method to English mode when activating Flow. Auto Update + Automatically check app updates and notify if there are any updates available Select Hide Flow Launcher on startup Flow Launcher search window is hidden in the tray after starting up. diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 78b6d8db0..cfb292633 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -182,7 +182,8 @@ + Icon="" + Sub="{DynamicResource autoUpdatesTooltip}"> Date: Mon, 14 Jul 2025 16:56:12 +0800 Subject: [PATCH 09/18] Replace dynamic type with a strongly-typed model --- Flow.Launcher.Core/Plugin/PluginInstaller.cs | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index 84f0f1fd9..c00c83d9e 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -300,14 +300,14 @@ public static class PluginInstaller 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest) && !API.PluginModified(existingPlugin.Metadata.ID) select - new + new PluginUpdateInfo() { - existingPlugin.Metadata.ID, - pluginUpdateSource.Name, - pluginUpdateSource.Author, + ID = existingPlugin.Metadata.ID, + Name = existingPlugin.Metadata.Name, + Author = existingPlugin.Metadata.Author, CurrentVersion = existingPlugin.Metadata.Version, NewVersion = pluginUpdateSource.Version, - existingPlugin.Metadata.IcoPath, + IcoPath = existingPlugin.Metadata.IcoPath, PluginExistingMetadata = existingPlugin.Metadata, PluginNewUserPlugin = pluginUpdateSource }).ToList(); @@ -339,7 +339,7 @@ public static class PluginInstaller string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name))); } - private static void UpdateAllPlugins(IEnumerable resultsForUpdate) + private static void UpdateAllPlugins(IEnumerable resultsForUpdate) { _ = Task.WhenAll(resultsForUpdate.Select(async plugin => { @@ -445,4 +445,16 @@ public static class PluginInstaller x.Metadata.Website.StartsWith(constructedUrlPart) ); } + + private record PluginUpdateInfo + { + public string ID { get; init; } + public string Name { get; init; } + public string Author { get; init; } + public string CurrentVersion { get; init; } + public string NewVersion { get; init; } + public string IcoPath { get; init; } + public PluginMetadata PluginExistingMetadata { get; init; } + public UserPlugin PluginNewUserPlugin { get; init; } + } } From fd4efe009cf2839b5eb01c95aaceb025a84edda2 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:07:25 +0800 Subject: [PATCH 10/18] Hide double pin card when use pinyin is false --- Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 38fc8df54..a879007c3 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -386,6 +386,8 @@ Date: Mon, 14 Jul 2025 21:21:47 +0800 Subject: [PATCH 11/18] Update spell check --- .github/actions/spelling/expect.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 0fea6d9ab..d8c99bce9 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -104,3 +104,12 @@ metadatas WMP VSTHRD CJK +XiaoHe +ZiRanMa +WeiRuan +ZhiNengABC +ZiGuangPinYin +PinYinJiaJia +XingKongJianDao +DaNiu +XiaoLang From 970aa5eefe89d3a3009753ed76a8bec64cd1d827 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:24:11 +0800 Subject: [PATCH 12/18] Fix typo --- .../UserSettings/Settings.cs | 2 +- .../ChineseDetectionPerformanceTest.cs | 265 ++++++++++++++++++ 2 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 726a0023b..271f618da 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -514,7 +514,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings { var list = FixedHotkeys(); - // Customizeable hotkeys + // Customizable hotkeys if (!string.IsNullOrEmpty(Hotkey)) list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = "")); if (!string.IsNullOrEmpty(PreviewHotkey)) diff --git a/Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs b/Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs new file mode 100644 index 000000000..1747f2b4a --- /dev/null +++ b/Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using Flow.Launcher.Infrastructure; +using ToolGood.Words.Pinyin; + +namespace Flow.Launcher.Test +{ + /// + /// Performance test comparing ContainsChinese() vs WordsHelper.HasChinese() + /// + /// This test verifies: + /// 1. Both methods produce identical results (correctness) + /// 2. Performance characteristics of both implementations + /// 3. Memory allocation patterns + /// + /// The ContainsChinese() method uses optimized Unicode range checking with ReadOnlySpan + /// while WordsHelper.HasChinese() uses the ToolGood.Words library implementation. + /// + [TestFixture] + public class ChineseDetectionPerformanceTest + { + private readonly List _testStrings = new() + { + // Pure English - should return false + "Hello World", + "Visual Studio Code", + "Microsoft Office 2023", + "Adobe Photoshop Creative Suite", + "Google Chrome Browser Application", + + // Pure Chinese - should return true + "你好世界", + "微软办公软件", + "谷歌浏览器", + "北京大学计算机科学与技术学院", + "中华人民共和国国家发展和改革委员会", + + // Mixed content - should return true + "Hello 世界", + "Visual Studio 代码编辑器", + "QQ音乐 Music Player", + "Windows 10 操作系统", + "GitHub 代码仓库管理平台", + + // Edge cases + "", + " ", + "123456", + "!@#$%^&*()", + "café résumé naïve", // Accented characters (not Chinese) + + // Long strings for performance testing + "This is a very long English string that contains no Chinese characters but is designed to test performance with longer text content that might appear in file names or application descriptions", + "这是一个非常长的中文字符串,包含了很多汉字,用来测试在处理较长中文文本时的性能表现,比如可能出现在文件名或应用程序描述中的文本内容", + "This is a mixed 混合内容的字符串 that contains both English and Chinese characters 中英文混合 to test performance with 复杂的文本内容 in real-world scenarios 真实场景中的应用" + }; + + [Test] + public void ContainsChinese_CorrectnessTest() + { + // Verify ContainsChinese works correctly for known cases + ClassicAssert.IsFalse(ContainsChinese("Hello World"), "Pure English should return false"); + ClassicAssert.IsTrue(ContainsChinese("你好世界"), "Pure Chinese should return true"); + ClassicAssert.IsTrue(ContainsChinese("Hello 世界"), "Mixed content should return true"); + ClassicAssert.IsFalse(ContainsChinese(""), "Empty string should return false"); + ClassicAssert.IsFalse(ContainsChinese("123456"), "Numbers should return false"); + ClassicAssert.IsFalse(ContainsChinese("café résumé"), "Accented characters should return false"); + } + + [Test] + public void WordsHelper_CorrectnessTest() + { + // Verify WordsHelper.HasChinese works correctly for known cases + ClassicAssert.IsFalse(WordsHelper.HasChinese("Hello World"), "Pure English should return false"); + ClassicAssert.IsTrue(WordsHelper.HasChinese("你好世界"), "Pure Chinese should return true"); + ClassicAssert.IsTrue(WordsHelper.HasChinese("Hello 世界"), "Mixed content should return true"); + ClassicAssert.IsFalse(WordsHelper.HasChinese(""), "Empty string should return false"); + ClassicAssert.IsFalse(WordsHelper.HasChinese("123456"), "Numbers should return false"); + ClassicAssert.IsFalse(WordsHelper.HasChinese("café résumé"), "Accented characters should return false"); + } + + [Test] + public void BothMethods_ShouldProduceSameResults() + { + // Critical test: verify both methods produce identical results for all test cases + foreach (var testString in _testStrings) + { + var wordsHelperResult = WordsHelper.HasChinese(testString); + var containsChineseResult = ContainsChinese(testString); + + ClassicAssert.AreEqual(wordsHelperResult, containsChineseResult, + $"Results differ for string: '{testString}'. WordsHelper: {wordsHelperResult}, ContainsChinese: {containsChineseResult}"); + } + + Console.WriteLine($"✓ Both methods produce identical results for all {_testStrings.Count} test cases"); + } + + [Test] + public void PerformanceComparison_BasicBenchmark() + { + const int iterations = 1000000; + + Console.WriteLine("=== CHINESE CHARACTER DETECTION PERFORMANCE TEST ==="); + Console.WriteLine($"Test iterations: {iterations:N0}"); + Console.WriteLine($"Test strings: {_testStrings.Count}"); + Console.WriteLine($"Total operations: {iterations * _testStrings.Count:N0}"); + Console.WriteLine(); + + // Warmup to ensure JIT compilation + Console.WriteLine("Warming up..."); + for (int i = 0; i < 1000; i++) + { + foreach (var testString in _testStrings) + { + _ = ContainsChinese(testString); + _ = WordsHelper.HasChinese(testString); + } + } + + // Benchmark ContainsChinese method + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var sw1 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + { + foreach (var testString in _testStrings) + { + _ = ContainsChinese(testString); + } + } + sw1.Stop(); + + // Benchmark WordsHelper.HasChinese method + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var sw2 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + { + foreach (var testString in _testStrings) + { + _ = WordsHelper.HasChinese(testString); + } + } + sw2.Stop(); + + // Calculate and display results + var containsChineseMs = sw1.Elapsed.TotalMilliseconds; + var wordsHelperMs = sw2.Elapsed.TotalMilliseconds; + var speedRatio = wordsHelperMs / containsChineseMs; + var timeDifference = wordsHelperMs - containsChineseMs; + + Console.WriteLine("RESULTS:"); + Console.WriteLine($"ContainsChinese(): {containsChineseMs:F3} ms"); + Console.WriteLine($"WordsHelper.HasChinese(): {wordsHelperMs:F3} ms"); + Console.WriteLine($"Time difference: {timeDifference:F3} ms"); + Console.WriteLine($"Speed improvement: {speedRatio:F2}x"); + Console.WriteLine($"Performance gain: {((speedRatio - 1) * 100):F1}%"); + Console.WriteLine(); + + if (speedRatio > 1.0) + { + Console.WriteLine($"✓ ContainsChinese() is {speedRatio:F2}x faster than WordsHelper.HasChinese()"); + } + else + { + Console.WriteLine($"⚠ WordsHelper.HasChinese() is {(1/speedRatio):F2}x faster than ContainsChinese()"); + } + + // Test always passes - this is a measurement test + ClassicAssert.IsTrue(true); + } + + [Test] + public void PerformanceComparison_ByStringType() + { + Console.WriteLine("=== PERFORMANCE BY STRING TYPE ==="); + + var categories = new Dictionary> + { + ["Pure English"] = _testStrings.Where(s => !ContainsChinese(s) && s.All(c => c <= 127)).ToList(), + ["Pure Chinese"] = _testStrings.Where(s => ContainsChinese(s) && s.All(c => IsChineseCharacter(c) || char.IsWhiteSpace(c))).ToList(), + ["Mixed Content"] = _testStrings.Where(s => ContainsChinese(s) && s.Any(c => c <= 127 && char.IsLetter(c))).ToList(), + ["Edge Cases"] = _testStrings.Where(s => string.IsNullOrWhiteSpace(s) || s.All(c => !char.IsLetter(c))).ToList() + }; + + foreach (var category in categories) + { + if (category.Value.Count == 0) continue; + + Console.WriteLine($"\n{category.Key} ({category.Value.Count} strings):"); + + var sample = category.Value.First(); + var displayText = sample.Length > 40 ? sample.Substring(0, 40) + "..." : sample; + Console.WriteLine($" Sample: '{displayText}'"); + + const int categoryIterations = 5000; + + // Test each method + var sw1 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < categoryIterations; i++) + { + foreach (var str in category.Value) + { + _ = ContainsChinese(str); + } + } + sw1.Stop(); + + var sw2 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < categoryIterations; i++) + { + foreach (var str in category.Value) + { + _ = WordsHelper.HasChinese(str); + } + } + sw2.Stop(); + + var ratio = (double)sw2.ElapsedTicks / sw1.ElapsedTicks; + Console.WriteLine($" Performance: ContainsChinese is {ratio:F2}x faster"); + } + + ClassicAssert.IsTrue(true); + } + + /// + /// Optimized Chinese character detection using comprehensive CJK Unicode ranges + /// This method uses ReadOnlySpan for better performance and covers all CJK character ranges + /// + private static bool ContainsChinese(ReadOnlySpan text) + { + foreach (var c in text) + { + if (IsChineseCharacter(c)) + return true; + } + return false; + } + + /// + /// Check if a character is a Chinese character using comprehensive Unicode ranges + /// Covers CJK Unified Ideographs and all extension blocks + /// + private static bool IsChineseCharacter(char c) + { + return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs (most common Chinese characters) + (c >= 0x3400 && c <= 0x4DBF) || // CJK Extension A + (c >= 0x20000 && c <= 0x2A6DF) || // CJK Extension B + (c >= 0x2A700 && c <= 0x2B73F) || // CJK Extension C + (c >= 0x2B740 && c <= 0x2B81F) || // CJK Extension D + (c >= 0x2B820 && c <= 0x2CEAF) || // CJK Extension E + (c >= 0x2CEB0 && c <= 0x2EBEF) || // CJK Extension F + (c >= 0xF900 && c <= 0xFAFF) || // CJK Compatibility Ideographs + (c >= 0x2F800 && c <= 0x2FA1F); // CJK Compatibility Supplement + } + } +} From a858aa8f5554b7e87d01c4c3d563516625ffdbc2 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Tue, 15 Jul 2025 19:58:12 +0800 Subject: [PATCH 13/18] Update auto update desc Co-authored-by: Jeremy Wu --- 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 3905df7c6..725d8d3e1 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -93,7 +93,7 @@ Always Start Typing in English Mode Temporarily change your input method to English mode when activating Flow. Auto Update - Automatically check app updates and notify if there are any updates available + Automatically check and update the app when available Select Hide Flow Launcher on startup Flow Launcher search window is hidden in the tray after starting up. From 37d6cea2d32822c9b26ede0ee6f2b1a1e4cf5f63 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Tue, 15 Jul 2025 22:10:07 +1000 Subject: [PATCH 14/18] fix typo --- Flow.Launcher.Core/Plugin/PluginInstaller.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index c00c83d9e..a79f4b47c 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -281,7 +281,7 @@ public static class PluginInstaller /// /// Updates the plugin to the latest version available from its source. /// - /// If true, do not show any messages when there is no udpate available. + /// If true, do not show any messages when there is no update available. /// If true, only use the primary URL for updates. /// Cancellation token to cancel the update operation. /// From a34b8f2630a93c439dfea4107a9f27eeb5b3c464 Mon Sep 17 00:00:00 2001 From: WayneFerdon Date: Tue, 15 Jul 2025 21:22:19 +0800 Subject: [PATCH 15/18] [Plugin.Sys Enhancement] Support returning all usable commands while query is empty; ChangeQuery by ThemeSelector Action with ActionKeyword at the front as well --- Plugins/Flow.Launcher.Plugin.Sys/Main.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 39bf49654..09581709f 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -70,13 +70,19 @@ namespace Flow.Launcher.Plugin.Sys return _themeSelector.Query(query); } - var commands = Commands(); + var commands = Commands(query); var results = new List(); + var isEmptyQuery = string.IsNullOrEmpty(query.Search) || string.IsNullOrWhiteSpace(query.Search); foreach (var c in commands) { var command = _settings.Commands.First(x => x.Key == c.Title); c.Title = command.Name; c.SubTitle = command.Description; + if (isEmptyQuery) + { + results.Add(c); + continue; + } // Match from localized title & localized subtitle & keyword var titleMatch = _context.API.FuzzySearch(query.Search, c.Title); @@ -188,7 +194,7 @@ namespace Flow.Launcher.Plugin.Sys } } - private List Commands() + private List Commands(Query query) { var results = new List(); var recycleBinFolder = "shell:RecycleBinFolder"; @@ -491,7 +497,7 @@ namespace Flow.Launcher.Plugin.Sys Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue790"), Action = c => { - _context.API.ChangeQuery($"{ThemeSelector.Keyword} "); + _context.API.ChangeQuery($"{query.ActionKeyword}{ (string.IsNullOrEmpty(query.ActionKeyword) ? string.Empty : Plugin.Query.ActionKeywordSeparator)}{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}"); return false; } } From aece80390560dec310327817f6c26618a8bacabc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Jul 2025 21:50:41 +0800 Subject: [PATCH 16/18] Simplify logic --- Plugins/Flow.Launcher.Plugin.Sys/Main.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 09581709f..fc0770375 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -72,7 +72,7 @@ namespace Flow.Launcher.Plugin.Sys var commands = Commands(query); var results = new List(); - var isEmptyQuery = string.IsNullOrEmpty(query.Search) || string.IsNullOrWhiteSpace(query.Search); + var isEmptyQuery = string.IsNullOrWhiteSpace(query.Search); foreach (var c in commands) { var command = _settings.Commands.First(x => x.Key == c.Title); From 539a8523636678b8b10daa60181b04a13bc3cc25 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 15 Jul 2025 21:59:40 +0800 Subject: [PATCH 17/18] Improve logic --- Plugins/Flow.Launcher.Plugin.Sys/Main.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index fc0770375..d2dcf6e5a 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -497,7 +497,15 @@ namespace Flow.Launcher.Plugin.Sys Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue790"), Action = c => { - _context.API.ChangeQuery($"{query.ActionKeyword}{ (string.IsNullOrEmpty(query.ActionKeyword) ? string.Empty : Plugin.Query.ActionKeywordSeparator)}{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}"); + if (string.IsNullOrEmpty(query.ActionKeyword)) + { + _context.API.ChangeQuery($"{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}"); + } + else + { + _context.API.ChangeQuery($"{query.ActionKeyword}{Plugin.Query.ActionKeywordSeparator}{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}"); + + } return false; } } From 363c0fb2a0984d7616c6fe7b496f1bef5ed7935e Mon Sep 17 00:00:00 2001 From: Kevin Zhang <45326534+taooceros@users.noreply.github.com> Date: Tue, 15 Jul 2025 18:11:16 -0500 Subject: [PATCH 18/18] Update dotnet.yml --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 7498262de..812a56257 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: runs-on: windows-latest env: - FlowVersion: 1.19.5 + FlowVersion: 1.20.2 NUGET_CERT_REVOCATION_MODE: offline BUILD_NUMBER: ${{ github.run_number }} steps: