From af19ffb7368c999b97395ce4266ace0f7a6aede3 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sat, 4 Apr 2026 22:16:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=B8=80=E8=A8=80API=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/MainWindow_cs/MW_Settings.cs | 176 ++++++++++++++++-- Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs | 29 ++- 2 files changed, 183 insertions(+), 22 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index 0749adcd..02cf401e 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -1,9 +1,11 @@ using Hardcodet.Wpf.TaskbarNotification; using Ink_Canvas.Helpers; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OSVersionExtension; using System; using System.Collections.Generic; +using System.Globalization; using System.Diagnostics; using System.IO; using System.Linq; @@ -1209,17 +1211,14 @@ namespace Ink_Canvas return; } - // 构建API URL,包含选中的分类参数 - var categories = Settings.Appearance.HitokotoCategories; - if (categories == null || categories.Count == 0) - { - // 如果没有选中任何分类,默认全选 - categories = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; - Settings.Appearance.HitokotoCategories = categories; - } + // 构建 API URL:仅用于本次请求;null/空列表时本地采用默认全部分类,不写回 Settings,避免启动或拉取一言时触发无意义的配置持久化 + var stored = Settings.Appearance.HitokotoCategories; + var categoriesForRequest = (stored == null || stored.Count == 0) + ? new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" } + : stored; var urlBuilder = new StringBuilder("https://v1.hitokoto.cn/?encode=text"); - foreach (var category in categories) + foreach (var category in categoriesForRequest) { urlBuilder.Append($"&c={category}"); } @@ -1267,7 +1266,11 @@ namespace Ink_Canvas private async void ComboBoxChickenSoupSource_SelectionChanged(object sender, RoutedEventArgs e) { if (!isLoaded) return; - Settings.Appearance.ChickenSoupSource = ComboBoxChickenSoupSource.SelectedIndex; + int idx = ComboBoxChickenSoupSource.SelectedIndex; + if (Settings.Appearance.ChickenSoupSource == idx) + return; + + Settings.Appearance.ChickenSoupSource = idx; if (BtnHitokotoCustomize != null) { @@ -1317,6 +1320,9 @@ namespace Ink_Canvas // 存储各个分类的复选框 var categoryCheckBoxes = new Dictionary(); + var savedHitokoto = Settings.Appearance.HitokotoCategories; + bool implicitAllCategories = savedHitokoto == null || savedHitokoto.Count == 0; + // 创建分类复选框 foreach (var category in categories) { @@ -1326,7 +1332,7 @@ namespace Ink_Canvas Tag = category.Key, FontSize = 13, FontFamily = new FontFamily("Microsoft YaHei UI"), - IsChecked = Settings.Appearance.HitokotoCategories.Contains(category.Key), + IsChecked = implicitAllCategories || savedHitokoto.Contains(category.Key), Margin = new Thickness(0, 0, 0, 8) }; categoryCheckBoxes[category.Key] = checkBox; @@ -1335,7 +1341,7 @@ namespace Ink_Canvas // 全选复选框逻辑 bool isUpdatingSelectAll = false; - selectAllCheckBox.IsChecked = Settings.Appearance.HitokotoCategories.Count == categories.Count; + selectAllCheckBox.IsChecked = implicitAllCategories || savedHitokoto.Count == categories.Count; selectAllCheckBox.Checked += (s, args) => { @@ -5137,14 +5143,142 @@ namespace Ink_Canvas #endregion + /// + /// 将 JSON 树归一化后再比较:统一数值为 double、对象属性按名称排序、纯字符串数组按字典序排序(如 hitokotoCategories 顺序与文件不一致时仍视为相同)。 + /// + private static JToken NormalizeJsonForSettingsCompare(JToken token) + { + if (token == null || token.Type == JTokenType.Null) + return JValue.CreateNull(); + + switch (token.Type) + { + case JTokenType.Integer: + case JTokenType.Float: + return new JValue(token.Value()); + case JTokenType.Date: + return new JValue(token.Value().ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)); + case JTokenType.Object: + var o = (JObject)token; + var sorted = new JObject(); + foreach (var p in o.Properties().OrderBy(x => x.Name, StringComparer.Ordinal)) + sorted[p.Name] = NormalizeJsonForSettingsCompare(p.Value); + return sorted; + case JTokenType.Array: + var arr = (JArray)token; + var items = arr.Select(NormalizeJsonForSettingsCompare).ToList(); + if (items.Count > 0) + { + if (items.TrueForAll(x => x.Type == JTokenType.String)) + return new JArray(items.Cast().OrderBy(x => x.Value(), StringComparer.Ordinal)); + if (items.TrueForAll(x => x.Type == JTokenType.Integer || x.Type == JTokenType.Float)) + return new JArray(items.OrderBy(x => x.Value())); + } + return new JArray(items); + default: + return token.DeepClone(); + } + } + + private static bool SettingsFileContentSemanticallyEquals(string newJson, string existingJson) + { + if (string.IsNullOrWhiteSpace(existingJson)) + return false; + var a = NormalizeJsonForSettingsCompare(JToken.Parse(newJson)); + var b = NormalizeJsonForSettingsCompare(JToken.Parse(existingJson)); + return JToken.DeepEquals(a, b); + } + + /// 一言 API 官方分类字母,顺序固定,与自定义对话框一致。 + private static readonly string[] HitokotoCategoryCanonicalOrder = + { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; + + private static readonly HashSet HitokotoCategoryKnownKeys = + new HashSet(HitokotoCategoryCanonicalOrder, StringComparer.Ordinal); + + /// + /// 将 规范为固定顺序并去重,避免 JSON 仅因数组顺序/重复项变化而反复重写。 + /// null 或空列表表示「未持久化、运行时按全部分类」语义,不改为非空列表。 + /// + private static void StabilizeAppearanceHitokotoCategories() + { + var appearance = Settings?.Appearance; + if (appearance == null) + return; + + var list = appearance.HitokotoCategories; + if (list == null || list.Count == 0) + return; + + var normalizedTokens = list + .Select(x => x?.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + var canonical = new List(); + foreach (var key in HitokotoCategoryCanonicalOrder) + { + if (normalizedTokens.Any(x => string.Equals(x, key, StringComparison.Ordinal))) + canonical.Add(key); + } + + foreach (var key in normalizedTokens + .Where(x => !HitokotoCategoryKnownKeys.Contains(x)) + .Distinct(StringComparer.Ordinal) + .OrderBy(x => x, StringComparer.Ordinal)) + { + canonical.Add(key); + } + + if (canonical.Count == 0) + return; + + if (list.Count != canonical.Count || !list.SequenceEqual(canonical, StringComparer.Ordinal)) + appearance.HitokotoCategories = canonical; + } + /// /// 将当前内存中的 Settings 序列化为格式化的 JSON 并写入应用程序配置文件(位于 App.RootPath 下的 Configs 目录或根设置文件)。 /// /// - /// 在写入前会确保目标目录/文件具有写入权限(使用 ProcessProtectionManager)。任何写入失败或异常都会被吞掉,调用方不会收到异常抛出。 + /// 在写入前会确保目标目录/文件具有写入权限(使用 ProcessProtectionManager)。若与磁盘已有内容语义一致则跳过写入,避免启动或其它路径多次调用时重复刷盘。 + /// 在 LoadSettings 执行期间会延迟到加载结束再统一写盘,避免启动流程中多次保存。 + /// 任何写入失败或异常都会被吞掉,调用方不会收到异常抛出。 /// + private static int _settingsLoadReentrancyDepth; + private static bool _settingsSavePendingDuringLoad; + + /// 配对,在 LoadSettings 的 try/finally 中使用。 + private static void BeginDeferredSettingsSaveDuringLoad() + { + _settingsLoadReentrancyDepth++; + } + + private static void EndDeferredSettingsSaveDuringLoad() + { + if (_settingsLoadReentrancyDepth > 0) + _settingsLoadReentrancyDepth--; + if (_settingsLoadReentrancyDepth == 0 && _settingsSavePendingDuringLoad) + { + _settingsSavePendingDuringLoad = false; + SaveSettingsToFileCore(); + } + } + public static void SaveSettingsToFile() { + if (_settingsLoadReentrancyDepth > 0) + { + _settingsSavePendingDuringLoad = true; + return; + } + + SaveSettingsToFileCore(); + } + + private static void SaveSettingsToFileCore() + { + StabilizeAppearanceHitokotoCategories(); var text = JsonConvert.SerializeObject(Settings, Formatting.Indented); try { @@ -5155,6 +5289,22 @@ namespace Ink_Canvas } var path = App.RootPath + settingsFileName; + if (File.Exists(path)) + { + try + { + string existing = File.ReadAllText(path); + if (existing.Length > 0 && existing[0] == '\uFEFF') + existing = existing.TrimStart('\uFEFF'); + if (!string.IsNullOrWhiteSpace(existing) && SettingsFileContentSemanticallyEquals(text, existing)) + return; + } + catch + { + // 无法比较或解析失败时仍写入,避免丢失修复机会 + } + } + ProcessProtectionManager.WithWriteAccess(path, () => File.WriteAllText(path, text)); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index c9616ec1..069920e3 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -37,6 +37,9 @@ namespace Ink_Canvas /// 指示是否跳过自动更新检查;为 true 时不会在加载设置后执行自动更新检测。 private void LoadSettings(bool isStartup = false, bool skipAutoUpdateCheck = false) { + BeginDeferredSettingsSaveDuringLoad(); + try + { AppVersionTextBlock.Text = Assembly.GetExecutingAssembly().GetName().Version.ToString(); try { @@ -450,12 +453,6 @@ namespace Ink_Canvas : Visibility.Collapsed; } - // 初始化HitokotoCategories,如果为空则默认全选 - if (Settings.Appearance.HitokotoCategories == null || Settings.Appearance.HitokotoCategories.Count == 0) - { - Settings.Appearance.HitokotoCategories = new List { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; - } - ToggleSwitchEnableQuickPanel.IsOn = Settings.Appearance.IsShowQuickPanel; ToggleSwitchEnableSplashScreen.IsOn = Settings.Appearance.EnableSplashScreen; @@ -636,12 +633,14 @@ namespace Ink_Canvas } else { + int prev = Settings.PowerPointSettings.PPTButtonsDisplayOption; Settings.PowerPointSettings.PPTButtonsDisplayOption = 2222; CheckboxEnableLBPPTButton.IsChecked = true; CheckboxEnableRBPPTButton.IsChecked = true; CheckboxEnableLSPPTButton.IsChecked = true; CheckboxEnableRSPPTButton.IsChecked = true; - SaveSettingsToFile(); + if (prev != 2222) + SaveSettingsToFile(); } var sops = Settings.PowerPointSettings.PPTSButtonsOption.ToString(); @@ -655,11 +654,13 @@ namespace Ink_Canvas } else { + int prev = Settings.PowerPointSettings.PPTSButtonsOption; Settings.PowerPointSettings.PPTSButtonsOption = 221; CheckboxSPPTDisplayPage.IsChecked = true; CheckboxSPPTHalfOpacity.IsChecked = true; CheckboxSPPTBlackBackground.IsChecked = false; - SaveSettingsToFile(); + if (prev != 221) + SaveSettingsToFile(); } var bops = Settings.PowerPointSettings.PPTBButtonsOption.ToString(); @@ -673,11 +674,13 @@ namespace Ink_Canvas } else { + int prev = Settings.PowerPointSettings.PPTBButtonsOption; Settings.PowerPointSettings.PPTBButtonsOption = 121; CheckboxBPPTDisplayPage.IsChecked = false; CheckboxBPPTHalfOpacity.IsChecked = true; CheckboxBPPTBlackBackground.IsChecked = false; - SaveSettingsToFile(); + if (prev != 121) + SaveSettingsToFile(); } PPTButtonLeftPositionValueSlider.Value = Settings.PowerPointSettings.PPTLSButtonPosition; @@ -1322,6 +1325,14 @@ namespace Ink_Canvas // 刷新配置文件列表 try { RefreshConfigProfileList(); } catch (Exception ex) { LogHelper.WriteLogToFile($"刷新配置文件列表失败: {ex.Message}", LogHelper.LogType.Warning); } + + // 一言分类数组固定为 a–l 顺序并去重,避免仅顺序/重复导致配置反复变化 + StabilizeAppearanceHitokotoCategories(); + } + finally + { + EndDeferredSettingsSaveDuringLoad(); + } } ///