diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index 5b3419041..d8c99bce9 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -103,3 +103,13 @@ Reloadable
metadatas
WMP
VSTHRD
+CJK
+XiaoHe
+ZiRanMa
+WeiRuan
+ZhiNengABC
+ZiGuangPinYin
+PinYinJiaJia
+XingKongJianDao
+DaNiu
+XiaoLang
diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs
index 33963c01a..a79f4b47c 100644
--- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs
+++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
@@ -277,6 +278,100 @@ 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 update available.
+ /// If true, only use the primary URL for updates.
+ /// Cancellation token to cancel the update operation.
+ ///
+ public static async Task CheckForPluginUpdatesAsync(bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default)
+ {
+ // Update the plugin manifest
+ await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
+
+ // Get all plugins that can be updated
+ var resultsForUpdate = (
+ from existingPlugin in API.GetAllPlugins()
+ join pluginUpdateSource in API.GetPluginManifest()
+ on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
+ where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
+ StringComparison.InvariantCulture) <
+ 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
+ && !API.PluginModified(existingPlugin.Metadata.ID)
+ select
+ new PluginUpdateInfo()
+ {
+ ID = existingPlugin.Metadata.ID,
+ Name = existingPlugin.Metadata.Name,
+ Author = existingPlugin.Metadata.Author,
+ CurrentVersion = existingPlugin.Metadata.Version,
+ NewVersion = pluginUpdateSource.Version,
+ IcoPath = existingPlugin.Metadata.IcoPath,
+ PluginExistingMetadata = existingPlugin.Metadata,
+ PluginNewUserPlugin = pluginUpdateSource
+ }).ToList();
+
+ // No updates
+ if (!resultsForUpdate.Any())
+ {
+ if (!silentUpdate)
+ {
+ API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle"));
+ }
+ return;
+ }
+
+ // If all plugins are modified, just return
+ if (resultsForUpdate.All(x => API.PluginModified(x.ID)))
+ {
+ return;
+ }
+
+ // Show message box with button to update all plugins
+ API.ShowMsgWithButton(
+ API.GetTranslation("updateAllPluginsTitle"),
+ API.GetTranslation("updateAllPluginsButtonContent"),
+ () =>
+ {
+ UpdateAllPlugins(resultsForUpdate);
+ },
+ string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name)));
+ }
+
+ private static void UpdateAllPlugins(IEnumerable resultsForUpdate)
+ {
+ _ = Task.WhenAll(resultsForUpdate.Select(async plugin =>
+ {
+ var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip");
+
+ try
+ {
+ using var cts = new CancellationTokenSource();
+
+ await DownloadFileAsync(
+ $"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}",
+ plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
+
+ // check if user cancelled download before installing plugin
+ if (cts.IsCancellationRequested)
+ {
+ return;
+ }
+
+ if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
+ {
+ return;
+ }
+ }
+ catch (Exception e)
+ {
+ API.LogException(ClassName, "Failed to update plugin", e);
+ API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
+ }
+ }));
+ }
+
///
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
///
@@ -350,4 +445,16 @@ 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; }
+ }
}
diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs
index b5344c7e9..1c0cc6872 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()
@@ -28,10 +25,21 @@ 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):
+ if (_settings.ShouldUsePinyin)
+ {
+ Reload();
+ }
+ break;
+ case nameof(Settings.UseDoublePinyin):
+ case nameof(Settings.DoublePinyinSchema):
+ if (_settings.UseDoublePinyin)
+ {
+ Reload();
+ }
+ break;
}
};
}
@@ -44,105 +52,142 @@ 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) ??
+ throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
+
+ var schemaKey = _settings.DoublePinyinSchema.ToString();
+ if (!table.TryGetValue(schemaKey, out var schemaDict))
{
- throw new ArgumentException("DoublePinyinSchema is invalid or double pinyin table is broken.");
+ throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken.");
}
- currentDoublePinyinTable = new ReadOnlyDictionary(value);
+
+ 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 5b02ae666..b4c6764df 100644
--- a/Flow.Launcher.Infrastructure/TranslationMapping.cs
+++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs
@@ -5,31 +5,30 @@ 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();
+ // 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();
public void AddNewIndex(int translatedIndex, int length)
{
- if (constructed)
- throw new InvalidOperationException("Mapping shouldn't be changed after constructed");
-
- originalToTranslated.Add(translatedIndex + length);
+ if (_isConstructed)
+ throw new InvalidOperationException("Mapping shouldn't be changed after construction");
+ _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;
}
}
}
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 0367bee10..7820d3e5e 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;
@@ -328,7 +329,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
@@ -504,7 +517,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
+ }
+ }
+}
diff --git a/Flow.Launcher.Test/TranslationMappingTest.cs b/Flow.Launcher.Test/TranslationMappingTest.cs
index 10d765f5a..bd3636f0a 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];
}
}
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index d8fd26f61..ac7ab1852 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -248,6 +248,7 @@ namespace Flow.Launcher
AutoStartup();
AutoUpdates();
+ AutoPluginUpdates();
API.SaveAppAllSettings();
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
@@ -260,7 +261,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
@@ -292,7 +293,7 @@ namespace Flow.Launcher
}
[Conditional("RELEASE")]
- private void AutoUpdates()
+ private static void AutoUpdates()
{
_ = Task.Run(async () =>
{
@@ -309,6 +310,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.CheckForPluginUpdatesAsync();
+
+ while (await timer.WaitForNextTickAsync())
+ // check updates on startup
+ await PluginInstaller.CheckForPluginUpdatesAsync();
+ }
+ });
+ }
+
#endregion
#region Register Events
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index ad7c98ecb..6223d2fbe 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -94,6 +94,7 @@
Always Start Typing in English Mode
Temporarily change your input method to English mode when activating Flow.
Auto Update
+ Automatically check and update the app when available
Select
Hide Flow Launcher on startup
Flow Launcher search window is hidden in the tray after starting up.
@@ -118,7 +119,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
@@ -151,6 +152,8 @@
Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store
Show unknown source warning
Show warning when installing plugins from unknown sources
+ Auto update plugins
+ Automatically check plugin updates and notify if there are any updates available
Always run as administrator
Run Flow Launcher as administrator on startup
Administrator Mode Change
@@ -236,6 +239,11 @@
Zip files
Please select zip file
Install plugin from local path
+ No update available
+ All plugins are up to date
+ Plugin updates available
+ Update all plugins
+ Check plugin updates
Theme
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
index efe67d016..96cd44072 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.CheckForPluginUpdatesAsync(silentUpdate: false);
+ }
+
private static string GetFileFromDialog(string title, string filter = "")
{
var dlg = new Microsoft.Win32.OpenFileDialog
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
index f11abac71..75ad10528 100644
--- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
@@ -190,7 +190,8 @@
+ Icon=""
+ Sub="{DynamicResource autoUpdatesTooltip}">
+ Type="Middle">
+
+
+
+
-
-
-
-
-
+
+
+
+
+ Sub="{DynamicResource ShouldUseDoublePinyinToolTip}">
+
-
-
+
+
-
+
+