Skip to content

Commit 647bc19

Browse files
authored
Merge branch 'dev' into l10n_dev
2 parents c200159 + 8e942c0 commit 647bc19

File tree

14 files changed

+604
-104
lines changed

14 files changed

+604
-104
lines changed

.github/actions/spelling/expect.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,13 @@ Reloadable
103103
metadatas
104104
WMP
105105
VSTHRD
106+
CJK
107+
XiaoHe
108+
ZiRanMa
109+
WeiRuan
110+
ZhiNengABC
111+
ZiGuangPinYin
112+
PinYinJiaJia
113+
XingKongJianDao
114+
DaNiu
115+
XiaoLang

.github/workflows/dotnet.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
runs-on: windows-latest
1818
env:
19-
FlowVersion: 1.19.5
19+
FlowVersion: 1.20.2
2020
NUGET_CERT_REVOCATION_MODE: offline
2121
BUILD_NUMBER: ${{ github.run_number }}
2222
steps:

Flow.Launcher.Core/Plugin/PluginInstaller.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.IO.Compression;
45
using System.Linq;
@@ -277,6 +278,100 @@ await DownloadFileAsync(
277278
}
278279
}
279280

281+
/// <summary>
282+
/// Updates the plugin to the latest version available from its source.
283+
/// </summary>
284+
/// <param name="silentUpdate">If true, do not show any messages when there is no update available.</param>
285+
/// <param name="usePrimaryUrlOnly">If true, only use the primary URL for updates.</param>
286+
/// <param name="token">Cancellation token to cancel the update operation.</param>
287+
/// <returns></returns>
288+
public static async Task CheckForPluginUpdatesAsync(bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default)
289+
{
290+
// Update the plugin manifest
291+
await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
292+
293+
// Get all plugins that can be updated
294+
var resultsForUpdate = (
295+
from existingPlugin in API.GetAllPlugins()
296+
join pluginUpdateSource in API.GetPluginManifest()
297+
on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
298+
where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
299+
StringComparison.InvariantCulture) <
300+
0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
301+
&& !API.PluginModified(existingPlugin.Metadata.ID)
302+
select
303+
new PluginUpdateInfo()
304+
{
305+
ID = existingPlugin.Metadata.ID,
306+
Name = existingPlugin.Metadata.Name,
307+
Author = existingPlugin.Metadata.Author,
308+
CurrentVersion = existingPlugin.Metadata.Version,
309+
NewVersion = pluginUpdateSource.Version,
310+
IcoPath = existingPlugin.Metadata.IcoPath,
311+
PluginExistingMetadata = existingPlugin.Metadata,
312+
PluginNewUserPlugin = pluginUpdateSource
313+
}).ToList();
314+
315+
// No updates
316+
if (!resultsForUpdate.Any())
317+
{
318+
if (!silentUpdate)
319+
{
320+
API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle"));
321+
}
322+
return;
323+
}
324+
325+
// If all plugins are modified, just return
326+
if (resultsForUpdate.All(x => API.PluginModified(x.ID)))
327+
{
328+
return;
329+
}
330+
331+
// Show message box with button to update all plugins
332+
API.ShowMsgWithButton(
333+
API.GetTranslation("updateAllPluginsTitle"),
334+
API.GetTranslation("updateAllPluginsButtonContent"),
335+
() =>
336+
{
337+
UpdateAllPlugins(resultsForUpdate);
338+
},
339+
string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name)));
340+
}
341+
342+
private static void UpdateAllPlugins(IEnumerable<PluginUpdateInfo> resultsForUpdate)
343+
{
344+
_ = Task.WhenAll(resultsForUpdate.Select(async plugin =>
345+
{
346+
var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip");
347+
348+
try
349+
{
350+
using var cts = new CancellationTokenSource();
351+
352+
await DownloadFileAsync(
353+
$"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}",
354+
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
355+
356+
// check if user cancelled download before installing plugin
357+
if (cts.IsCancellationRequested)
358+
{
359+
return;
360+
}
361+
362+
if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
363+
{
364+
return;
365+
}
366+
}
367+
catch (Exception e)
368+
{
369+
API.LogException(ClassName, "Failed to update plugin", e);
370+
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
371+
}
372+
}));
373+
}
374+
280375
/// <summary>
281376
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
282377
/// </summary>
@@ -350,4 +445,16 @@ private static bool InstallSourceKnown(string url)
350445
x.Metadata.Website.StartsWith(constructedUrlPart)
351446
);
352447
}
448+
449+
private record PluginUpdateInfo
450+
{
451+
public string ID { get; init; }
452+
public string Name { get; init; }
453+
public string Author { get; init; }
454+
public string CurrentVersion { get; init; }
455+
public string NewVersion { get; init; }
456+
public string IcoPath { get; init; }
457+
public PluginMetadata PluginExistingMetadata { get; init; }
458+
public UserPlugin PluginNewUserPlugin { get; init; }
459+
}
353460
}

Flow.Launcher.Infrastructure/PinyinAlphabet.cs

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure
1414
{
1515
public class PinyinAlphabet : IAlphabet
1616
{
17-
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
18-
new();
19-
17+
private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
2018
private readonly Settings _settings;
21-
2219
private ReadOnlyDictionary<string, string> currentDoublePinyinTable;
2320

2421
public PinyinAlphabet()
@@ -28,10 +25,21 @@ public PinyinAlphabet()
2825

2926
_settings.PropertyChanged += (sender, e) =>
3027
{
31-
if (e.PropertyName == nameof(Settings.UseDoublePinyin) ||
32-
e.PropertyName == nameof(Settings.DoublePinyinSchema))
28+
switch (e.PropertyName)
3329
{
34-
Reload();
30+
case nameof (Settings.ShouldUsePinyin):
31+
if (_settings.ShouldUsePinyin)
32+
{
33+
Reload();
34+
}
35+
break;
36+
case nameof(Settings.UseDoublePinyin):
37+
case nameof(Settings.DoublePinyinSchema):
38+
if (_settings.UseDoublePinyin)
39+
{
40+
Reload();
41+
}
42+
break;
3543
}
3644
};
3745
}
@@ -44,105 +52,142 @@ public void Reload()
4452

4553
private void CreateDoublePinyinTableFromStream(Stream jsonStream)
4654
{
47-
Dictionary<string, Dictionary<string, string>> table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream);
48-
string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string
49-
if (!table.TryGetValue(schemaKey, out var value))
55+
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
56+
throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
57+
58+
var schemaKey = _settings.DoublePinyinSchema.ToString();
59+
if (!table.TryGetValue(schemaKey, out var schemaDict))
5060
{
51-
throw new ArgumentException("DoublePinyinSchema is invalid or double pinyin table is broken.");
61+
throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken.");
5262
}
53-
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(value);
63+
64+
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(schemaDict);
5465
}
5566

5667
private void LoadDoublePinyinTable()
5768
{
58-
if (_settings.UseDoublePinyin)
69+
if (!_settings.UseDoublePinyin)
5970
{
60-
var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
61-
try
62-
{
63-
using var fs = File.OpenRead(tablePath);
64-
CreateDoublePinyinTableFromStream(fs);
65-
}
66-
catch (System.Exception e)
67-
{
68-
Log.Exception(nameof(PinyinAlphabet), "Failed to load double pinyin table from file: " + tablePath, e);
69-
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
70-
}
71+
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
72+
return;
73+
}
74+
75+
var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
76+
try
77+
{
78+
using var fs = File.OpenRead(tablePath);
79+
CreateDoublePinyinTableFromStream(fs);
80+
}
81+
catch (FileNotFoundException e)
82+
{
83+
Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e);
84+
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
7185
}
72-
else
86+
catch (DirectoryNotFoundException e)
7387
{
88+
Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e);
89+
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
90+
}
91+
catch (UnauthorizedAccessException e)
92+
{
93+
Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e);
94+
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
95+
}
96+
catch (System.Exception e)
97+
{
98+
Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e);
7499
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
75100
}
76101
}
77102

78103
public bool ShouldTranslate(string stringToTranslate)
79104
{
80-
// If a string has Chinese characters, we don't need to translate it to pinyin.
81-
return _settings.ShouldUsePinyin && !WordsHelper.HasChinese(stringToTranslate);
105+
// If the query (stringToTranslate) does NOT contain Chinese characters,
106+
// we should translate the target string to pinyin for matching
107+
return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate);
82108
}
83109

84110
public (string translation, TranslationMapping map) Translate(string content)
85111
{
86-
if (!_settings.ShouldUsePinyin || !WordsHelper.HasChinese(content))
112+
if (!_settings.ShouldUsePinyin || !ContainsChinese(content))
87113
return (content, null);
88114

89-
return _pinyinCache.TryGetValue(content, out var value)
90-
? value
91-
: BuildCacheFromContent(content);
115+
return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content);
92116
}
93117

94118
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
95119
{
96120
var resultList = WordsHelper.GetPinyinList(content);
97-
98-
var resultBuilder = new StringBuilder();
121+
var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity
99122
var map = new TranslationMapping();
100123

101124
var previousIsChinese = false;
102125

103126
for (var i = 0; i < resultList.Length; i++)
104127
{
105-
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
128+
if (IsChineseCharacter(content[i]))
106129
{
107-
string translated = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i];
130+
var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i];
131+
108132
if (i > 0)
109133
{
110134
resultBuilder.Append(' ');
111135
}
136+
112137
map.AddNewIndex(resultBuilder.Length, translated.Length);
113138
resultBuilder.Append(translated);
114139
previousIsChinese = true;
115140
}
116141
else
117142
{
143+
// Add space after Chinese characters before non-Chinese characters
118144
if (previousIsChinese)
119145
{
120146
previousIsChinese = false;
121147
resultBuilder.Append(' ');
122148
}
149+
123150
map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
124151
resultBuilder.Append(resultList[i]);
125152
}
126153
}
127154

128-
map.endConstruct();
155+
map.EndConstruct();
129156

130-
var key = resultBuilder.ToString();
131-
132-
return _pinyinCache[content] = (key, map);
157+
var translation = resultBuilder.ToString();
158+
var result = (translation, map);
159+
160+
return _pinyinCache[content] = result;
133161
}
134162

135-
#region Double Pinyin
136-
137-
private string ToDoublePin(string fullPinyin)
163+
/// <summary>
164+
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
165+
/// </summary>
166+
private static bool ContainsChinese(ReadOnlySpan<char> text)
138167
{
139-
if (currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue))
168+
foreach (var c in text)
140169
{
141-
return doublePinyinValue;
170+
if (IsChineseCharacter(c))
171+
return true;
142172
}
143-
return fullPinyin;
173+
return false;
144174
}
145175

146-
#endregion
176+
/// <summary>
177+
/// Check if a character is a Chinese character using comprehensive Unicode ranges
178+
/// Covers CJK Unified Ideographs, Extension A
179+
/// </summary>
180+
private static bool IsChineseCharacter(char c)
181+
{
182+
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs
183+
(c >= 0x3400 && c <= 0x4DBF); // CJK Extension A
184+
}
185+
186+
private string ToDoublePinyin(string fullPinyin)
187+
{
188+
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
189+
? doublePinyinValue
190+
: fullPinyin;
191+
}
147192
}
148193
}

0 commit comments

Comments
 (0)