mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into administrator_mode
This commit is contained in:
commit
9a320bc193
12 changed files with 586 additions and 100 deletions
10
.github/actions/spelling/expect.txt
vendored
10
.github/actions/spelling/expect.txt
vendored
|
|
@ -103,3 +103,13 @@ Reloadable
|
|||
metadatas
|
||||
WMP
|
||||
VSTHRD
|
||||
CJK
|
||||
XiaoHe
|
||||
ZiRanMa
|
||||
WeiRuan
|
||||
ZhiNengABC
|
||||
ZiGuangPinYin
|
||||
PinYinJiaJia
|
||||
XingKongJianDao
|
||||
DaNiu
|
||||
XiaoLang
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the plugin to the latest version available from its source.
|
||||
/// </summary>
|
||||
/// <param name="silentUpdate">If true, do not show any messages when there is no update available.</param>
|
||||
/// <param name="usePrimaryUrlOnly">If true, only use the primary URL for updates.</param>
|
||||
/// <param name="token">Cancellation token to cancel the update operation.</param>
|
||||
/// <returns></returns>
|
||||
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<PluginUpdateInfo> 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"));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
|
||||
/// </summary>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure
|
|||
{
|
||||
public class PinyinAlphabet : IAlphabet
|
||||
{
|
||||
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
|
||||
new();
|
||||
|
||||
private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
|
||||
private readonly Settings _settings;
|
||||
|
||||
private ReadOnlyDictionary<string, string> 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<string, Dictionary<string, string>> table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream);
|
||||
string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string
|
||||
if (!table.TryGetValue(schemaKey, out var value))
|
||||
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(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<string, string>(value);
|
||||
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(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<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
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<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
{
|
||||
Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e);
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e);
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e);
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
/// <summary>
|
||||
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
|
||||
/// </summary>
|
||||
private static bool ContainsChinese(ReadOnlySpan<char> 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
|
||||
/// <summary>
|
||||
/// Check if a character is a Chinese character using comprehensive Unicode ranges
|
||||
/// Covers CJK Unified Ideographs, Extension A
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<int> 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<int> _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// <summary>
|
||||
/// when false Alphabet static service will always return empty results
|
||||
/// </summary>
|
||||
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))
|
||||
|
|
|
|||
265
Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs
Normal file
265
Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ChineseDetectionPerformanceTest
|
||||
{
|
||||
private readonly List<string> _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<string, List<string>>
|
||||
{
|
||||
["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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized Chinese character detection using comprehensive CJK Unicode ranges
|
||||
/// This method uses ReadOnlySpan for better performance and covers all CJK character ranges
|
||||
/// </summary>
|
||||
private static bool ContainsChinese(ReadOnlySpan<char> text)
|
||||
{
|
||||
foreach (var c in text)
|
||||
{
|
||||
if (IsChineseCharacter(c))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a character is a Chinese character using comprehensive Unicode ranges
|
||||
/// Covers CJK Unified Ideographs and all extension blocks
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int>)field.GetValue(mapping);
|
||||
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var list = (List<int>)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<int>)field.GetValue(mapping);
|
||||
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var list = (List<int>)field.GetValue(mapping);
|
||||
return list[index];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// </summary>
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
<system:String x:Key="typingStartEn">Always Start Typing in English Mode</system:String>
|
||||
<system:String x:Key="typingStartEnTooltip">Temporarily change your input method to English mode when activating Flow.</system:String>
|
||||
<system:String x:Key="autoUpdates">Auto Update</system:String>
|
||||
<system:String x:Key="autoUpdatesTooltip">Automatically check and update the app when available</system:String>
|
||||
<system:String x:Key="select">Select</system:String>
|
||||
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
|
||||
<system:String x:Key="hideOnStartupToolTip">Flow Launcher search window is hidden in the tray after starting up.</system:String>
|
||||
|
|
@ -118,7 +119,7 @@
|
|||
<system:String x:Key="DoublePinyinSchemasXingKongJianDao">Xing Kong Jian Dao</system:String>
|
||||
<system:String x:Key="DoublePinyinSchemasDaNiu">Da Niu</system:String>
|
||||
<system:String x:Key="DoublePinyinSchemasXiaoLang">Xiao Lang</system:String>
|
||||
|
||||
|
||||
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
|
||||
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
|
||||
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
|
||||
|
|
@ -151,6 +152,8 @@
|
|||
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
|
||||
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
|
||||
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
|
||||
<system:String x:Key="autoUpdatePlugins">Auto update plugins</system:String>
|
||||
<system:String x:Key="autoUpdatePluginsToolTip">Automatically check plugin updates and notify if there are any updates available</system:String>
|
||||
<system:String x:Key="alwaysRunAsAdministrator">Always run as administrator</system:String>
|
||||
<system:String x:Key="alwaysRunAsAdministratorToolTip">Run Flow Launcher as administrator on startup</system:String>
|
||||
<system:String x:Key="runAsAdministratorChange">Administrator Mode Change</system:String>
|
||||
|
|
@ -236,6 +239,11 @@
|
|||
<system:String x:Key="ZipFiles">Zip files</system:String>
|
||||
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
|
||||
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
|
||||
<system:String x:Key="updateNoResultTitle">No update available</system:String>
|
||||
<system:String x:Key="updateNoResultSubtitle">All plugins are up to date</system:String>
|
||||
<system:String x:Key="updateAllPluginsTitle">Plugin updates available</system:String>
|
||||
<system:String x:Key="updateAllPluginsButtonContent">Update all plugins</system:String>
|
||||
<system:String x:Key="checkPluginUpdatesTooltip">Check plugin updates</system:String>
|
||||
|
||||
<!-- Setting Theme -->
|
||||
<system:String x:Key="theme">Theme</system:String>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -190,7 +190,8 @@
|
|||
<cc:Card
|
||||
Title="{DynamicResource autoUpdates}"
|
||||
Margin="0 14 0 0"
|
||||
Icon="">
|
||||
Icon=""
|
||||
Sub="{DynamicResource autoUpdatesTooltip}">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding AutoUpdates}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
|
|
@ -249,12 +250,23 @@
|
|||
Title="{DynamicResource showUnknownSourceWarning}"
|
||||
Icon=""
|
||||
Sub="{DynamicResource showUnknownSourceWarningToolTip}"
|
||||
Type="Last">
|
||||
Type="Middle">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.ShowUnknownSourceWarning}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
|
||||
<cc:Card
|
||||
Title="{DynamicResource autoUpdatePlugins}"
|
||||
Icon=""
|
||||
Sub="{DynamicResource autoUpdatePluginsToolTip}"
|
||||
Type="Last">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.AutoUpdatePlugins}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
</cc:CardGroup>
|
||||
|
||||
<cc:ExCard
|
||||
|
|
@ -379,44 +391,39 @@
|
|||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
|
||||
<cc:CardGroup Margin="0 4 0 0">
|
||||
<cc:Card
|
||||
Title="{DynamicResource ShouldUsePinyin}"
|
||||
Icon=""
|
||||
Sub="{DynamicResource ShouldUsePinyinToolTip}"
|
||||
Type="First">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding ShouldUsePinyin}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}"
|
||||
ToolTip="{DynamicResource ShouldUsePinyinToolTip}" />
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Visibility="{ext:VisibleWhen {Binding ShouldUsePinyin},
|
||||
<cc:Card
|
||||
Title="{DynamicResource ShouldUsePinyin}"
|
||||
Margin="0 4 0 0"
|
||||
Icon=""
|
||||
Sub="{DynamicResource ShouldUsePinyinToolTip}">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding ShouldUsePinyin}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}"
|
||||
ToolTip="{DynamicResource ShouldUsePinyinToolTip}" />
|
||||
</cc:Card>
|
||||
|
||||
<cc:ExCard
|
||||
Title="{DynamicResource ShouldUseDoublePinyin}"
|
||||
Icon=""
|
||||
Visibility="{ext:VisibleWhen {Binding ShouldUsePinyin},
|
||||
IsEqualToBool=True}"
|
||||
Title="{DynamicResource ShouldUseDoublePinyin}"
|
||||
Icon=""
|
||||
Sub="{DynamicResource ShouldUseDoublePinyinToolTip}"
|
||||
Type="Middle">
|
||||
Sub="{DynamicResource ShouldUseDoublePinyinToolTip}">
|
||||
<cc:ExCard.SideContent>
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding UseDoublePinyin}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}"
|
||||
ToolTip="{DynamicResource ShouldUseDoublePinyinToolTip}" />
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Visibility="{ext:VisibleWhen {Binding UseDoublePinyin},
|
||||
IsEqualToBool=True}"
|
||||
Title="{DynamicResource DoublePinyinSchema}"
|
||||
Sub="{DynamicResource DoublePinyinSchemaToolTip}"
|
||||
Type="Last">
|
||||
</cc:ExCard.SideContent>
|
||||
<cc:Card Title="{DynamicResource DoublePinyinSchema}" Type="InsideFit">
|
||||
<ComboBox
|
||||
DisplayMemberPath="Display"
|
||||
ItemsSource="{Binding DoublePinyinSchemas}"
|
||||
SelectedValue="{Binding Settings.DoublePinyinSchema}"
|
||||
SelectedValuePath="Value" />
|
||||
</cc:Card>
|
||||
</cc:CardGroup>
|
||||
</cc:ExCard>
|
||||
|
||||
<cc:Card
|
||||
Title="{DynamicResource language}"
|
||||
|
|
|
|||
|
|
@ -99,6 +99,13 @@
|
|||
ToolTip="{DynamicResource installLocalPluginTooltip}">
|
||||
<ui:FontIcon FontSize="14" Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
Height="34"
|
||||
Margin="0 0 10 0"
|
||||
Command="{Binding CheckPluginUpdatesCommand}"
|
||||
ToolTip="{DynamicResource checkPluginUpdatesTooltip}">
|
||||
<ui:FontIcon FontSize="14" Glyph="" />
|
||||
</Button>
|
||||
<TextBox
|
||||
Name="PluginStoreFilterTextbox"
|
||||
Width="150"
|
||||
|
|
|
|||
Loading…
Reference in a new issue