From fcb6c6fc9c07faf9b572d7fc56135433506b0a67 Mon Sep 17 00:00:00 2001 From: slowpy <16513084+slowpy@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:55:14 +0800 Subject: [PATCH 1/3] feat(notification): add key fatigue reminder for top 4 keys --- .../KeyStats/Models/AppSettings.cs | 3 + .../KeyStats/Properties/Strings.cs | 3 + .../KeyStats/Properties/Strings.resx | 3 + .../KeyStats/Properties/Strings.zh-Hans.resx | 3 + .../KeyStats/Properties/Strings.zh-Hant.resx | 3 + .../KeyStats/Services/NotificationService.cs | 17 ++++++ .../KeyStats/Services/StatsManager.cs | 55 +++++++++++++++++++ .../Views/NotificationSettingsWindow.xaml | 14 +++++ .../Views/NotificationSettingsWindow.xaml.cs | 3 + 9 files changed, 104 insertions(+) diff --git a/KeyStats.Windows/KeyStats/Models/AppSettings.cs b/KeyStats.Windows/KeyStats/Models/AppSettings.cs index 37d45f6..65e2f34 100644 --- a/KeyStats.Windows/KeyStats/Models/AppSettings.cs +++ b/KeyStats.Windows/KeyStats/Models/AppSettings.cs @@ -16,6 +16,9 @@ public class AppSettings [JsonPropertyName("clickNotifyThreshold")] public int ClickNotifyThreshold { get; set; } = 1000; + [JsonPropertyName("keyFatigueNotifyEnabled")] + public bool KeyFatigueNotifyEnabled { get; set; } + [JsonPropertyName("launchAtStartup")] public bool LaunchAtStartup { get; set; } diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.cs b/KeyStats.Windows/KeyStats/Properties/Strings.cs index ef22dee..f5dd025 100644 --- a/KeyStats.Windows/KeyStats/Properties/Strings.cs +++ b/KeyStats.Windows/KeyStats/Properties/Strings.cs @@ -27,6 +27,9 @@ public static class Strings public static string Notif_ThresholdTitle => Get(nameof(Notif_ThresholdTitle)); public static string Notif_KeyThresholdReachedFormat => Get(nameof(Notif_KeyThresholdReachedFormat)); public static string Notif_ClickThresholdReachedFormat => Get(nameof(Notif_ClickThresholdReachedFormat)); + public static string Notif_KeyFatigueBodyFormat => Get(nameof(Notif_KeyFatigueBodyFormat)); + public static string NotifSettings_FatigueTitle => Get(nameof(NotifSettings_FatigueTitle)); + public static string NotifSettings_FatigueHint => Get(nameof(NotifSettings_FatigueHint)); public static string Tray_OpenMainWindow => Get(nameof(Tray_OpenMainWindow)); public static string Tray_Settings => Get(nameof(Tray_Settings)); diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.resx b/KeyStats.Windows/KeyStats/Properties/Strings.resx index 6be7731..b713e22 100644 --- a/KeyStats.Windows/KeyStats/Properties/Strings.resx +++ b/KeyStats.Windows/KeyStats/Properties/Strings.resx @@ -66,6 +66,9 @@ KeyStats Reminder Today's key presses reached {0:N0}. Today's clicks reached {0:N0}. + Key {0} today has exceeded 1/3 of its 7-day average ({1:N0} presses). + Key Fatigue Reminder (Top 4 Keys) + Ranks keys by total presses over the last 7 days. If any of the top 4 keys exceeds 1/3 of its own daily average today, you'll be reminded once. Open Main Window Settings diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx index cbd3005..3fb4b8a 100644 --- a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx +++ b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx @@ -66,6 +66,9 @@ 统计提醒 今日按键数已达到 {0:N0} 次 今日点击数已达到 {0:N0} 次 + 按键 {0} 今日已达 {1:N0} 次,超过其近 7 日均值的 1/3,注意用指疲劳 + 按键疲劳提醒(Top 4 按键) + 按近 7 天按键总数排名取前 4 个按键,任一键当日次数超过其自身日均值的 1/3 时,当天提醒一次。 打开主界面 设置 diff --git a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx index d6fc94b..176e565 100644 --- a/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx +++ b/KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx @@ -66,6 +66,9 @@ KeyStats 提醒 今日按鍵數已達 {0:N0} 次。 今日點擊數已達 {0:N0} 次。 + 按鍵 {0} 今日已達 {1:N0} 次,超過其近 7 日均值的 1/3,注意用指疲勞 + 按鍵疲勞提醒(Top 4 按鍵) + 按近 7 天按鍵總數排名取前 4 個按鍵,任一鍵當日次數超過其自身日均值的 1/3 時,當天提醒一次。 開啟主視窗 設定 diff --git a/KeyStats.Windows/KeyStats/Services/NotificationService.cs b/KeyStats.Windows/KeyStats/Services/NotificationService.cs index e3625f8..a88427c 100644 --- a/KeyStats.Windows/KeyStats/Services/NotificationService.cs +++ b/KeyStats.Windows/KeyStats/Services/NotificationService.cs @@ -38,6 +38,23 @@ public void SendThresholdNotification(Metric metric, int count) } } + public void SendKeyFatigueNotification(string keyName, int threshold) + { + var body = string.Format(KeyStats.Properties.Strings.Notif_KeyFatigueBodyFormat, keyName, threshold); + + try + { + new ToastContentBuilder() + .AddText(KeyStats.Properties.Strings.Notif_ThresholdTitle) + .AddText(body) + .Show(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error showing notification: {ex.Message}"); + } + } + public void ClearNotifications() { try diff --git a/KeyStats.Windows/KeyStats/Services/StatsManager.cs b/KeyStats.Windows/KeyStats/Services/StatsManager.cs index 55c64e3..39c31ba 100644 --- a/KeyStats.Windows/KeyStats/Services/StatsManager.cs +++ b/KeyStats.Windows/KeyStats/Services/StatsManager.cs @@ -57,6 +57,11 @@ public enum StatsUpdateKind private int _lastNotifiedKeyPresses; private int _lastNotifiedClicks; + private const int KeyFatigueTopCount = 4; + private const double KeyFatigueRatio = 1.0 / 3.0; + private readonly Dictionary _fatigueThresholds = new(StringComparer.Ordinal); // Top4 键 -> 阈值 + private readonly HashSet _fatigueNotifiedKeys = new(StringComparer.Ordinal); // 当日已提醒的键 + public DailyStats CurrentStats { get; private set; } public AppSettings Settings { get; private set; } public Dictionary History { get; private set; } = new(); @@ -197,6 +202,7 @@ private void OnKeyPressed(string keyName, string appName, string displayName) RecordKeyForPeakKPS(); NotifyStatsUpdate(); NotifyKeyPressThresholdIfNeeded(); + NotifyKeyFatigueIfNeeded(keyName); ScheduleSave(); } @@ -1238,6 +1244,7 @@ private void UpdateNotificationBaselines() { _lastNotifiedKeyPresses = NormalizedBaseline(CurrentStats.KeyPresses, Settings.KeyPressNotifyThreshold); _lastNotifiedClicks = NormalizedBaseline(CurrentStats.TotalClicks, Settings.ClickNotifyThreshold); + UpdateKeyFatigueBaseline(); } private int NormalizedBaseline(int count, int threshold) @@ -1282,6 +1289,54 @@ private void NotifyClickThresholdIfNeeded() } } + private void UpdateKeyFatigueBaseline() + { + lock (_lock) + { + var today = DateTime.Today; + var totals = new Dictionary(StringComparer.Ordinal); + for (var date = today.AddDays(-7); date < today; date = date.AddDays(1)) + { + MergeKeyCounts(GetDailyStats(date).KeyPressCounts, totals); + } + + var topKeys = totals + .Where(x => x.Value > 0) + .OrderByDescending(x => x.Value) + .ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase) + .Take(KeyFatigueTopCount); + + _fatigueThresholds.Clear(); + foreach (var (key, total) in topKeys) + { + var dailyAvg = total / 7.0; + var threshold = (int)Math.Floor(dailyAvg * KeyFatigueRatio); + _fatigueThresholds[key] = Math.Max(1, threshold); + } + + _fatigueNotifiedKeys.Clear(); + } + } + + private void NotifyKeyFatigueIfNeeded(string keyName) + { + if (!Settings.KeyFatigueNotifyEnabled) return; + if (string.IsNullOrWhiteSpace(keyName)) return; + if (_fatigueNotifiedKeys.Contains(keyName)) return; + + int threshold; + lock (_lock) + { + if (_fatigueNotifiedKeys.Contains(keyName)) return; + if (!_fatigueThresholds.TryGetValue(keyName, out threshold)) return; + var todayCount = CurrentStats.KeyPressCounts.TryGetValue(keyName, out var count) ? count : 0; + if (todayCount <= threshold) return; + _fatigueNotifiedKeys.Add(keyName); + } + + NotificationService.Instance.SendKeyFatigueNotification(keyName, threshold); + } + #endregion #region Formatting diff --git a/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml b/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml index e1a2ee6..38ceaa9 100644 --- a/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml +++ b/KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml @@ -63,6 +63,20 @@ + + + + + + + + Date: Thu, 6 Aug 2026 14:55:14 +0800 Subject: [PATCH 2/3] fix(build): avoid KeyValuePair deconstruction on .NET Framework 4.8 --- KeyStats.Windows/KeyStats/Services/StatsManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/KeyStats.Windows/KeyStats/Services/StatsManager.cs b/KeyStats.Windows/KeyStats/Services/StatsManager.cs index 39c31ba..c9d9228 100644 --- a/KeyStats.Windows/KeyStats/Services/StatsManager.cs +++ b/KeyStats.Windows/KeyStats/Services/StatsManager.cs @@ -1307,11 +1307,11 @@ private void UpdateKeyFatigueBaseline() .Take(KeyFatigueTopCount); _fatigueThresholds.Clear(); - foreach (var (key, total) in topKeys) + foreach (var pair in topKeys) { - var dailyAvg = total / 7.0; + var dailyAvg = pair.Value / 7.0; var threshold = (int)Math.Floor(dailyAvg * KeyFatigueRatio); - _fatigueThresholds[key] = Math.Max(1, threshold); + _fatigueThresholds[pair.Key] = Math.Max(1, threshold); } _fatigueNotifiedKeys.Clear(); From 10d8eb8434258eebea2eeef735d3a94df7a56bc0 Mon Sep 17 00:00:00 2001 From: slowpy <16513084+slowpy@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:55:14 +0800 Subject: [PATCH 3/3] feat(notification): exclude Enter/Space from fatigue Top 4 and log exceeded key --- KeyStats.Windows/KeyStats/Services/StatsManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/KeyStats.Windows/KeyStats/Services/StatsManager.cs b/KeyStats.Windows/KeyStats/Services/StatsManager.cs index c9d9228..fac74d1 100644 --- a/KeyStats.Windows/KeyStats/Services/StatsManager.cs +++ b/KeyStats.Windows/KeyStats/Services/StatsManager.cs @@ -59,6 +59,7 @@ public enum StatsUpdateKind private const int KeyFatigueTopCount = 4; private const double KeyFatigueRatio = 1.0 / 3.0; + private static readonly HashSet KeyFatigueExcludedKeys = new(StringComparer.Ordinal) { "Enter", "Space" }; // 排除回车/空格,避免干扰疲劳判断 private readonly Dictionary _fatigueThresholds = new(StringComparer.Ordinal); // Top4 键 -> 阈值 private readonly HashSet _fatigueNotifiedKeys = new(StringComparer.Ordinal); // 当日已提醒的键 @@ -1301,7 +1302,7 @@ private void UpdateKeyFatigueBaseline() } var topKeys = totals - .Where(x => x.Value > 0) + .Where(x => x.Value > 0 && !KeyFatigueExcludedKeys.Contains(x.Key)) .OrderByDescending(x => x.Value) .ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .Take(KeyFatigueTopCount); @@ -1332,6 +1333,8 @@ private void NotifyKeyFatigueIfNeeded(string keyName) var todayCount = CurrentStats.KeyPressCounts.TryGetValue(keyName, out var count) ? count : 0; if (todayCount <= threshold) return; _fatigueNotifiedKeys.Add(keyName); + + Console.WriteLine($"Key fatigue reminder: key '{keyName}' exceeded threshold today ({todayCount} presses > {threshold}), notification sent."); } NotificationService.Instance.SendKeyFatigueNotification(keyName, threshold);