mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into code_quality
This commit is contained in:
commit
7cc0d232be
6 changed files with 146 additions and 95 deletions
1
.github/actions/spelling/expect.txt
vendored
1
.github/actions/spelling/expect.txt
vendored
|
|
@ -103,3 +103,4 @@ Reloadable
|
||||||
metadatas
|
metadatas
|
||||||
WMP
|
WMP
|
||||||
VSTHRD
|
VSTHRD
|
||||||
|
CJK
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
public class PinyinAlphabet : IAlphabet
|
public class PinyinAlphabet : IAlphabet
|
||||||
{
|
{
|
||||||
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
|
private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
|
||||||
new();
|
|
||||||
|
|
||||||
private readonly Settings _settings;
|
private readonly Settings _settings;
|
||||||
|
|
||||||
private ReadOnlyDictionary<string, string> currentDoublePinyinTable;
|
private ReadOnlyDictionary<string, string> currentDoublePinyinTable;
|
||||||
|
|
||||||
public PinyinAlphabet()
|
public PinyinAlphabet()
|
||||||
|
|
@ -28,10 +25,21 @@ namespace Flow.Launcher.Infrastructure
|
||||||
|
|
||||||
_settings.PropertyChanged += (sender, e) =>
|
_settings.PropertyChanged += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(Settings.UseDoublePinyin) ||
|
switch (e.PropertyName)
|
||||||
e.PropertyName == nameof(Settings.DoublePinyinSchema))
|
|
||||||
{
|
{
|
||||||
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)
|
private void CreateDoublePinyinTableFromStream(Stream jsonStream)
|
||||||
{
|
{
|
||||||
Dictionary<string, Dictionary<string, string>> table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream);
|
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
|
||||||
string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string
|
throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
|
||||||
if (!table.TryGetValue(schemaKey, out var value))
|
|
||||||
|
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()
|
private void LoadDoublePinyinTable()
|
||||||
{
|
{
|
||||||
if (_settings.UseDoublePinyin)
|
if (!_settings.UseDoublePinyin)
|
||||||
{
|
{
|
||||||
var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
|
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||||
try
|
return;
|
||||||
{
|
|
||||||
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>());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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>());
|
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldTranslate(string stringToTranslate)
|
public bool ShouldTranslate(string stringToTranslate)
|
||||||
{
|
{
|
||||||
// If a string has Chinese characters, we don't need to translate it to pinyin.
|
// If the query (stringToTranslate) does NOT contain Chinese characters,
|
||||||
return _settings.ShouldUsePinyin && !WordsHelper.HasChinese(stringToTranslate);
|
// we should translate the target string to pinyin for matching
|
||||||
|
return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public (string translation, TranslationMapping map) Translate(string content)
|
public (string translation, TranslationMapping map) Translate(string content)
|
||||||
{
|
{
|
||||||
if (!_settings.ShouldUsePinyin || !WordsHelper.HasChinese(content))
|
if (!_settings.ShouldUsePinyin || !ContainsChinese(content))
|
||||||
return (content, null);
|
return (content, null);
|
||||||
|
|
||||||
return _pinyinCache.TryGetValue(content, out var value)
|
return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content);
|
||||||
? value
|
|
||||||
: BuildCacheFromContent(content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
|
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
|
||||||
{
|
{
|
||||||
var resultList = WordsHelper.GetPinyinList(content);
|
var resultList = WordsHelper.GetPinyinList(content);
|
||||||
|
var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity
|
||||||
var resultBuilder = new StringBuilder();
|
|
||||||
var map = new TranslationMapping();
|
var map = new TranslationMapping();
|
||||||
|
|
||||||
var previousIsChinese = false;
|
var previousIsChinese = false;
|
||||||
|
|
||||||
for (var i = 0; i < resultList.Length; i++)
|
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)
|
if (i > 0)
|
||||||
{
|
{
|
||||||
resultBuilder.Append(' ');
|
resultBuilder.Append(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
map.AddNewIndex(resultBuilder.Length, translated.Length);
|
map.AddNewIndex(resultBuilder.Length, translated.Length);
|
||||||
resultBuilder.Append(translated);
|
resultBuilder.Append(translated);
|
||||||
previousIsChinese = true;
|
previousIsChinese = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Add space after Chinese characters before non-Chinese characters
|
||||||
if (previousIsChinese)
|
if (previousIsChinese)
|
||||||
{
|
{
|
||||||
previousIsChinese = false;
|
previousIsChinese = false;
|
||||||
resultBuilder.Append(' ');
|
resultBuilder.Append(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
|
map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
|
||||||
resultBuilder.Append(resultList[i]);
|
resultBuilder.Append(resultList[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
map.endConstruct();
|
map.EndConstruct();
|
||||||
|
|
||||||
var key = resultBuilder.ToString();
|
var translation = resultBuilder.ToString();
|
||||||
|
var result = (translation, map);
|
||||||
return _pinyinCache[content] = (key, map);
|
|
||||||
|
return _pinyinCache[content] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Double Pinyin
|
/// <summary>
|
||||||
|
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
|
||||||
private string ToDoublePin(string fullPinyin)
|
/// </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
|
public class TranslationMapping
|
||||||
{
|
{
|
||||||
private bool constructed;
|
private bool _isConstructed;
|
||||||
|
|
||||||
// Assuming one original item maps to multi translated items
|
// Assuming one original item maps to multi translated items
|
||||||
// list[i] is the last translated index + 1 of original index i
|
// list[i] is the last translated index + 1 of original index i
|
||||||
private readonly List<int> originalToTranslated = new();
|
private readonly List<int> _originalToTranslated = new();
|
||||||
|
|
||||||
public void AddNewIndex(int translatedIndex, int length)
|
public void AddNewIndex(int translatedIndex, int length)
|
||||||
{
|
{
|
||||||
if (constructed)
|
if (_isConstructed)
|
||||||
throw new InvalidOperationException("Mapping shouldn't be changed after constructed");
|
throw new InvalidOperationException("Mapping shouldn't be changed after construction");
|
||||||
|
_originalToTranslated.Add(translatedIndex + length);
|
||||||
originalToTranslated.Add(translatedIndex + length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int MapToOriginalIndex(int translatedIndex)
|
public int MapToOriginalIndex(int translatedIndex)
|
||||||
{
|
{
|
||||||
int loc = originalToTranslated.BinarySearch(translatedIndex);
|
var searchResult = _originalToTranslated.BinarySearch(translatedIndex);
|
||||||
return loc >= 0 ? loc : ~loc;
|
return searchResult >= 0 ? searchResult : ~searchResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void endConstruct()
|
public void EndConstruct()
|
||||||
{
|
{
|
||||||
if (constructed)
|
if (_isConstructed)
|
||||||
throw new InvalidOperationException("Mapping has already been constructed");
|
throw new InvalidOperationException("Mapping has already been constructed");
|
||||||
constructed = true;
|
_isConstructed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,19 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// when false Alphabet static service will always return empty results
|
/// when false Alphabet static service will always return empty results
|
||||||
/// </summary>
|
/// </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;
|
private bool _useDoublePinyin = false;
|
||||||
public bool UseDoublePinyin
|
public bool UseDoublePinyin
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
|
|
||||||
|
|
@ -34,22 +36,21 @@ namespace Flow.Launcher.Test
|
||||||
mapping.AddNewIndex(2, 2);
|
mapping.AddNewIndex(2, 2);
|
||||||
mapping.AddNewIndex(5, 3);
|
mapping.AddNewIndex(5, 3);
|
||||||
|
|
||||||
|
|
||||||
var result = mapping.MapToOriginalIndex(translatedIndex);
|
var result = mapping.MapToOriginalIndex(translatedIndex);
|
||||||
ClassicAssert.AreEqual(expectedOriginalIndex, result);
|
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 field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
var list = (System.Collections.Generic.List<int>)field.GetValue(mapping);
|
var list = (List<int>)field.GetValue(mapping);
|
||||||
return list.Count;
|
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 field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
var list = (System.Collections.Generic.List<int>)field.GetValue(mapping);
|
var list = (List<int>)field.GetValue(mapping);
|
||||||
return list[index];
|
return list[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -371,44 +371,37 @@
|
||||||
OnContent="{DynamicResource enable}" />
|
OnContent="{DynamicResource enable}" />
|
||||||
</cc:Card>
|
</cc:Card>
|
||||||
|
|
||||||
<cc:CardGroup Margin="0 4 0 0">
|
<cc:Card
|
||||||
<cc:Card
|
Title="{DynamicResource ShouldUsePinyin}"
|
||||||
Title="{DynamicResource ShouldUsePinyin}"
|
Margin="0 4 0 0"
|
||||||
Icon=""
|
Icon=""
|
||||||
Sub="{DynamicResource ShouldUsePinyinToolTip}"
|
Sub="{DynamicResource ShouldUsePinyinToolTip}">
|
||||||
Type="First">
|
<ui:ToggleSwitch
|
||||||
<ui:ToggleSwitch
|
IsOn="{Binding ShouldUsePinyin}"
|
||||||
IsOn="{Binding ShouldUsePinyin}"
|
OffContent="{DynamicResource disable}"
|
||||||
OffContent="{DynamicResource disable}"
|
OnContent="{DynamicResource enable}"
|
||||||
OnContent="{DynamicResource enable}"
|
ToolTip="{DynamicResource ShouldUsePinyinToolTip}" />
|
||||||
ToolTip="{DynamicResource ShouldUsePinyinToolTip}" />
|
</cc:Card>
|
||||||
</cc:Card>
|
|
||||||
<cc:Card
|
<cc:ExCard
|
||||||
Visibility="{ext:VisibleWhen {Binding ShouldUsePinyin},
|
Title="{DynamicResource ShouldUseDoublePinyin}"
|
||||||
IsEqualToBool=True}"
|
Icon=""
|
||||||
Title="{DynamicResource ShouldUseDoublePinyin}"
|
Sub="{DynamicResource ShouldUseDoublePinyinToolTip}">
|
||||||
Icon=""
|
<cc:ExCard.SideContent>
|
||||||
Sub="{DynamicResource ShouldUseDoublePinyinToolTip}"
|
|
||||||
Type="Middle">
|
|
||||||
<ui:ToggleSwitch
|
<ui:ToggleSwitch
|
||||||
IsOn="{Binding UseDoublePinyin}"
|
IsOn="{Binding UseDoublePinyin}"
|
||||||
OffContent="{DynamicResource disable}"
|
OffContent="{DynamicResource disable}"
|
||||||
OnContent="{DynamicResource enable}"
|
OnContent="{DynamicResource enable}"
|
||||||
ToolTip="{DynamicResource ShouldUseDoublePinyinToolTip}" />
|
ToolTip="{DynamicResource ShouldUseDoublePinyinToolTip}" />
|
||||||
</cc:Card>
|
</cc:ExCard.SideContent>
|
||||||
<cc:Card
|
<cc:Card Title="{DynamicResource DoublePinyinSchema}" Type="InsideFit">
|
||||||
Visibility="{ext:VisibleWhen {Binding UseDoublePinyin},
|
|
||||||
IsEqualToBool=True}"
|
|
||||||
Title="{DynamicResource DoublePinyinSchema}"
|
|
||||||
Sub="{DynamicResource DoublePinyinSchemaToolTip}"
|
|
||||||
Type="Last">
|
|
||||||
<ComboBox
|
<ComboBox
|
||||||
DisplayMemberPath="Display"
|
DisplayMemberPath="Display"
|
||||||
ItemsSource="{Binding DoublePinyinSchemas}"
|
ItemsSource="{Binding DoublePinyinSchemas}"
|
||||||
SelectedValue="{Binding Settings.DoublePinyinSchema}"
|
SelectedValue="{Binding Settings.DoublePinyinSchema}"
|
||||||
SelectedValuePath="Value" />
|
SelectedValuePath="Value" />
|
||||||
</cc:Card>
|
</cc:Card>
|
||||||
</cc:CardGroup>
|
</cc:ExCard>
|
||||||
|
|
||||||
<cc:Card
|
<cc:Card
|
||||||
Title="{DynamicResource language}"
|
Title="{DynamicResource language}"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue