From 2316c36a051034b5dba35aa36651b811f7eb2280 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:29:41 +0800 Subject: [PATCH 01/22] Improve performance and readablility --- .../PinyinAlphabet.cs | 123 ++++++++++++------ .../TranslationMapping.cs | 20 +-- 2 files changed, 90 insertions(+), 53 deletions(-) diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index b5344c7e9..3850a3bcb 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure { public class PinyinAlphabet : IAlphabet { - private ConcurrentDictionary _pinyinCache = - new(); - + private readonly ConcurrentDictionary _pinyinCache = new(); private readonly Settings _settings; - private ReadOnlyDictionary currentDoublePinyinTable; public PinyinAlphabet() @@ -44,105 +41,145 @@ namespace Flow.Launcher.Infrastructure private void CreateDoublePinyinTableFromStream(Stream jsonStream) { - Dictionary> table = JsonSerializer.Deserialize>>(jsonStream); - string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string - if (!table.TryGetValue(schemaKey, out var value)) + var table = JsonSerializer.Deserialize>>(jsonStream); + if (table == null) { - throw new ArgumentException("DoublePinyinSchema is invalid or double pinyin table is broken."); + throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null"); } - currentDoublePinyinTable = new ReadOnlyDictionary(value); + + var schemaKey = _settings.DoublePinyinSchema.ToString(); + if (!table.TryGetValue(schemaKey, out var schemaDict)) + { + throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken."); + } + + currentDoublePinyinTable = new ReadOnlyDictionary(schemaDict); } private void LoadDoublePinyinTable() { - if (_settings.UseDoublePinyin) + if (!_settings.UseDoublePinyin) { - var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json"); - try - { - using var fs = File.OpenRead(tablePath); - CreateDoublePinyinTableFromStream(fs); - } - catch (System.Exception e) - { - Log.Exception(nameof(PinyinAlphabet), "Failed to load double pinyin table from file: " + tablePath, e); - currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); - } + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + return; } - else + + var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json"); + try { + using var fs = File.OpenRead(tablePath); + CreateDoublePinyinTableFromStream(fs); + } + catch (FileNotFoundException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (DirectoryNotFoundException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (UnauthorizedAccessException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (System.Exception e) + { + Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e); currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); } } public bool ShouldTranslate(string stringToTranslate) { - // If a string has Chinese characters, we don't need to translate it to pinyin. - return _settings.ShouldUsePinyin && !WordsHelper.HasChinese(stringToTranslate); + // If the query (stringToTranslate) does NOT contain Chinese characters, + // we should translate the target string to pinyin for matching + return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate); } public (string translation, TranslationMapping map) Translate(string content) { - if (!_settings.ShouldUsePinyin || !WordsHelper.HasChinese(content)) + if (!_settings.ShouldUsePinyin || !ContainsChinese(content)) return (content, null); - return _pinyinCache.TryGetValue(content, out var value) - ? value - : BuildCacheFromContent(content); + return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content); } private (string translation, TranslationMapping map) BuildCacheFromContent(string content) { var resultList = WordsHelper.GetPinyinList(content); - - var resultBuilder = new StringBuilder(); + var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity var map = new TranslationMapping(); var previousIsChinese = false; for (var i = 0; i < resultList.Length; i++) { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) + if (IsChineseCharacter(content[i])) { - string translated = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i]; + var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i]; + if (i > 0) { resultBuilder.Append(' '); } + map.AddNewIndex(resultBuilder.Length, translated.Length); resultBuilder.Append(translated); previousIsChinese = true; } else { + // Add space after Chinese characters before non-Chinese characters if (previousIsChinese) { previousIsChinese = false; resultBuilder.Append(' '); } + map.AddNewIndex(resultBuilder.Length, resultList[i].Length); resultBuilder.Append(resultList[i]); } } - map.endConstruct(); + map.EndConstruct(); - var key = resultBuilder.ToString(); - - return _pinyinCache[content] = (key, map); + var translation = resultBuilder.ToString(); + var result = (translation, map); + + return _pinyinCache[content] = result; } - #region Double Pinyin - - private string ToDoublePin(string fullPinyin) + /// + /// Optimized Chinese character detection using the comprehensive CJK Unicode ranges + /// + private static bool ContainsChinese(ReadOnlySpan text) { - if (currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)) + foreach (var c in text) { - return doublePinyinValue; + if (IsChineseCharacter(c)) + return true; } - return fullPinyin; + return false; } - #endregion + /// + /// Check if a character is a Chinese character using comprehensive Unicode ranges + /// Covers CJK Unified Ideographs, Extension A + /// + private static bool IsChineseCharacter(char c) + { + return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs + (c >= 0x3400 && c <= 0x4DBF); // CJK Extension A + } + + private string ToDoublePinyin(string fullPinyin) + { + return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue) + ? doublePinyinValue + : fullPinyin; + } } } diff --git a/Flow.Launcher.Infrastructure/TranslationMapping.cs b/Flow.Launcher.Infrastructure/TranslationMapping.cs index 951979fa7..13d8342ac 100644 --- a/Flow.Launcher.Infrastructure/TranslationMapping.cs +++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs @@ -6,31 +6,31 @@ namespace Flow.Launcher.Infrastructure { public class TranslationMapping { - private bool constructed; + private bool _isConstructed; // Assuming one original item maps to multi translated items // list[i] is the last translated index + 1 of original index i - private readonly List originalToTranslated = new List(); + private readonly List _originalToTranslated = new(); public void AddNewIndex(int translatedIndex, int length) { - if (constructed) - throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); + if (_isConstructed) + throw new InvalidOperationException("Mapping shouldn't be changed after construction"); - originalToTranslated.Add(translatedIndex + length); + _originalToTranslated.Add(translatedIndex + length); } public int MapToOriginalIndex(int translatedIndex) { - int loc = originalToTranslated.BinarySearch(translatedIndex); - return loc >= 0 ? loc : ~loc; + var searchResult = _originalToTranslated.BinarySearch(translatedIndex); + return searchResult >= 0 ? searchResult : ~searchResult; } - public void endConstruct() + public void EndConstruct() { - if (constructed) + if (_isConstructed) throw new InvalidOperationException("Mapping has already been constructed"); - constructed = true; + _isConstructed = true; } } } From 071b75bcb585ae9109e65d525b72a5612570a597 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 09:12:14 +0800 Subject: [PATCH 02/22] Improve code quality --- Flow.Launcher.Infrastructure/PinyinAlphabet.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 3850a3bcb..cc4eccdc5 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -41,11 +41,8 @@ namespace Flow.Launcher.Infrastructure private void CreateDoublePinyinTableFromStream(Stream jsonStream) { - var table = JsonSerializer.Deserialize>>(jsonStream); - if (table == null) - { + var table = JsonSerializer.Deserialize>>(jsonStream) ?? throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null"); - } var schemaKey = _settings.DoublePinyinSchema.ToString(); if (!table.TryGetValue(schemaKey, out var schemaDict)) From d7d3549c828f8845dccdc62ff1b233b0caad0d45 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 09:17:32 +0800 Subject: [PATCH 03/22] Improve code quality --- .../TranslationMapping.cs | 2 -- Flow.Launcher.Test/TranslationMappingTest.cs | 17 +++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher.Infrastructure/TranslationMapping.cs b/Flow.Launcher.Infrastructure/TranslationMapping.cs index 395b4a4b2..b4c6764df 100644 --- a/Flow.Launcher.Infrastructure/TranslationMapping.cs +++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs @@ -11,12 +11,10 @@ namespace Flow.Launcher.Infrastructure // list[i] is the last translated index + 1 of original index i private readonly List _originalToTranslated = new(); - public void AddNewIndex(int translatedIndex, int length) { if (_isConstructed) throw new InvalidOperationException("Mapping shouldn't be changed after construction"); - _originalToTranslated.Add(translatedIndex + length); } diff --git a/Flow.Launcher.Test/TranslationMappingTest.cs b/Flow.Launcher.Test/TranslationMappingTest.cs index 10d765f5a..829dbee81 100644 --- a/Flow.Launcher.Test/TranslationMappingTest.cs +++ b/Flow.Launcher.Test/TranslationMappingTest.cs @@ -1,4 +1,6 @@ -using Flow.Launcher.Infrastructure; +using System.Collections.Generic; +using System.Reflection; +using Flow.Launcher.Infrastructure; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -34,22 +36,21 @@ namespace Flow.Launcher.Test mapping.AddNewIndex(2, 2); mapping.AddNewIndex(5, 3); - var result = mapping.MapToOriginalIndex(translatedIndex); ClassicAssert.AreEqual(expectedOriginalIndex, result); } - private int GetOriginalToTranslatedCount(TranslationMapping mapping) + private static int GetOriginalToTranslatedCount(TranslationMapping mapping) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var list = (System.Collections.Generic.List)field.GetValue(mapping); + var field = typeof(TranslationMapping).GetField("originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var list = (List)field.GetValue(mapping); return list.Count; } - private int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) + private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var list = (System.Collections.Generic.List)field.GetValue(mapping); + var field = typeof(TranslationMapping).GetField("originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var list = (List)field.GetValue(mapping); return list[index]; } } From 88a6e59f46c2789d85d2b4cdf81c0a6ef317c1a7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 09:19:46 +0800 Subject: [PATCH 04/22] Fix test project field issue --- Flow.Launcher.Test/TranslationMappingTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Test/TranslationMappingTest.cs b/Flow.Launcher.Test/TranslationMappingTest.cs index 829dbee81..bd3636f0a 100644 --- a/Flow.Launcher.Test/TranslationMappingTest.cs +++ b/Flow.Launcher.Test/TranslationMappingTest.cs @@ -42,14 +42,14 @@ namespace Flow.Launcher.Test private static int GetOriginalToTranslatedCount(TranslationMapping mapping) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); var list = (List)field.GetValue(mapping); return list.Count; } private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); var list = (List)field.GetValue(mapping); return list[index]; } From bdf5ccd3a97459ae69f25ffe9023a63d9e94e1a0 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:28:52 +0800 Subject: [PATCH 05/22] Add word --- .github/actions/spelling/expect.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 5b3419041..0fea6d9ab 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -103,3 +103,4 @@ Reloadable metadatas WMP VSTHRD +CJK 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 06/22] 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 07/22] 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 08/22] 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 09/22] 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 10/22] 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 11/22] 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 12/22] 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 13/22] 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:49:04 +0800 Subject: [PATCH 14/22] Change double pinyin panel design --- .../Views/SettingsPaneGeneral.xaml | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index df0243ce8..38fc8df54 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -371,44 +371,37 @@ OnContent="{DynamicResource enable}" /> - - - - - + + + + + + - - + + - + Date: Mon, 14 Jul 2025 16:56:12 +0800 Subject: [PATCH 15/22] 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 6317d0eec6f42b788324fdcf9e6a2c2fdea388f4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 19:29:12 +0800 Subject: [PATCH 16/22] Reload on all settings change --- Flow.Launcher.Infrastructure/PinyinAlphabet.cs | 13 ++++++++++--- .../UserSettings/Settings.cs | 14 +++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index cc4eccdc5..0f6d00014 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -25,10 +25,17 @@ namespace Flow.Launcher.Infrastructure _settings.PropertyChanged += (sender, e) => { - if (e.PropertyName == nameof(Settings.UseDoublePinyin) || - e.PropertyName == nameof(Settings.DoublePinyinSchema)) + switch (e.PropertyName) { - Reload(); + case nameof(Settings.ShouldUsePinyin): + Reload(); + break; + case nameof(Settings.UseDoublePinyin): + Reload(); + break; + case nameof(Settings.DoublePinyinSchema): + Reload(); + break; } }; } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 6b10d693d..726a0023b 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -328,7 +328,19 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// /// when false Alphabet static service will always return empty results /// - public bool ShouldUsePinyin { get; set; } = false; + private bool _useAlphabet = true; + public bool ShouldUsePinyin + { + get => _useAlphabet; + set + { + if (_useAlphabet != value) + { + _useAlphabet = value; + OnPropertyChanged(); + } + } + } private bool _useDoublePinyin = false; public bool UseDoublePinyin From 8c56c0bddf4a4b7b361b26a3a6b32038144c2f35 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 14 Jul 2025 19:30:16 +0800 Subject: [PATCH 17/22] Fix logic --- Flow.Launcher.Infrastructure/PinyinAlphabet.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 0f6d00014..1c0cc6872 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -27,14 +27,18 @@ namespace Flow.Launcher.Infrastructure { switch (e.PropertyName) { - case nameof(Settings.ShouldUsePinyin): - Reload(); + case nameof (Settings.ShouldUsePinyin): + if (_settings.ShouldUsePinyin) + { + Reload(); + } break; case nameof(Settings.UseDoublePinyin): - Reload(); - break; case nameof(Settings.DoublePinyinSchema): - Reload(); + if (_settings.UseDoublePinyin) + { + Reload(); + } break; } }; 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 18/22] 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 19/22] 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 20/22] 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 21/22] 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 22/22] 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. ///