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..fac74d1 100644 --- a/KeyStats.Windows/KeyStats/Services/StatsManager.cs +++ b/KeyStats.Windows/KeyStats/Services/StatsManager.cs @@ -57,6 +57,12 @@ public enum StatsUpdateKind private int _lastNotifiedKeyPresses; private int _lastNotifiedClicks; + 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); // 当日已提醒的键 + public DailyStats CurrentStats { get; private set; } public AppSettings Settings { get; private set; } public Dictionary History { get; private set; } = new(); @@ -197,6 +203,7 @@ private void OnKeyPressed(string keyName, string appName, string displayName) RecordKeyForPeakKPS(); NotifyStatsUpdate(); NotifyKeyPressThresholdIfNeeded(); + NotifyKeyFatigueIfNeeded(keyName); ScheduleSave(); } @@ -1238,6 +1245,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 +1290,56 @@ 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 && !KeyFatigueExcludedKeys.Contains(x.Key)) + .OrderByDescending(x => x.Value) + .ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase) + .Take(KeyFatigueTopCount); + + _fatigueThresholds.Clear(); + foreach (var pair in topKeys) + { + var dailyAvg = pair.Value / 7.0; + var threshold = (int)Math.Floor(dailyAvg * KeyFatigueRatio); + _fatigueThresholds[pair.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); + + Console.WriteLine($"Key fatigue reminder: key '{keyName}' exceeded threshold today ({todayCount} presses > {threshold}), notification sent."); + } + + 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 @@ + + + + + + + +