Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions KeyStats.Windows/KeyStats/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
3 changes: 3 additions & 0 deletions KeyStats.Windows/KeyStats/Properties/Strings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
3 changes: 3 additions & 0 deletions KeyStats.Windows/KeyStats/Properties/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
<data name="Notif_ThresholdTitle" xml:space="preserve"><value>KeyStats Reminder</value></data>
<data name="Notif_KeyThresholdReachedFormat" xml:space="preserve"><value>Today's key presses reached {0:N0}.</value></data>
<data name="Notif_ClickThresholdReachedFormat" xml:space="preserve"><value>Today's clicks reached {0:N0}.</value></data>
<data name="Notif_KeyFatigueBodyFormat" xml:space="preserve"><value>Key {0} today has exceeded 1/3 of its 7-day average ({1:N0} presses).</value></data>
<data name="NotifSettings_FatigueTitle" xml:space="preserve"><value>Key Fatigue Reminder (Top 4 Keys)</value></data>
<data name="NotifSettings_FatigueHint" xml:space="preserve"><value>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.</value></data>

<data name="Tray_OpenMainWindow" xml:space="preserve"><value>Open Main Window</value></data>
<data name="Tray_Settings" xml:space="preserve"><value>Settings</value></data>
Expand Down
3 changes: 3 additions & 0 deletions KeyStats.Windows/KeyStats/Properties/Strings.zh-Hans.resx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
<data name="Notif_ThresholdTitle" xml:space="preserve"><value>统计提醒</value></data>
<data name="Notif_KeyThresholdReachedFormat" xml:space="preserve"><value>今日按键数已达到 {0:N0} 次</value></data>
<data name="Notif_ClickThresholdReachedFormat" xml:space="preserve"><value>今日点击数已达到 {0:N0} 次</value></data>
<data name="Notif_KeyFatigueBodyFormat" xml:space="preserve"><value>按键 {0} 今日已达 {1:N0} 次,超过其近 7 日均值的 1/3,注意用指疲劳</value></data>
<data name="NotifSettings_FatigueTitle" xml:space="preserve"><value>按键疲劳提醒(Top 4 按键)</value></data>
<data name="NotifSettings_FatigueHint" xml:space="preserve"><value>按近 7 天按键总数排名取前 4 个按键,任一键当日次数超过其自身日均值的 1/3 时,当天提醒一次。</value></data>

<data name="Tray_OpenMainWindow" xml:space="preserve"><value>打开主界面</value></data>
<data name="Tray_Settings" xml:space="preserve"><value>设置</value></data>
Expand Down
3 changes: 3 additions & 0 deletions KeyStats.Windows/KeyStats/Properties/Strings.zh-Hant.resx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
<data name="Notif_ThresholdTitle" xml:space="preserve"><value>KeyStats 提醒</value></data>
<data name="Notif_KeyThresholdReachedFormat" xml:space="preserve"><value>今日按鍵數已達 {0:N0} 次。</value></data>
<data name="Notif_ClickThresholdReachedFormat" xml:space="preserve"><value>今日點擊數已達 {0:N0} 次。</value></data>
<data name="Notif_KeyFatigueBodyFormat" xml:space="preserve"><value>按鍵 {0} 今日已達 {1:N0} 次,超過其近 7 日均值的 1/3,注意用指疲勞</value></data>
<data name="NotifSettings_FatigueTitle" xml:space="preserve"><value>按鍵疲勞提醒(Top 4 按鍵)</value></data>
<data name="NotifSettings_FatigueHint" xml:space="preserve"><value>按近 7 天按鍵總數排名取前 4 個按鍵,任一鍵當日次數超過其自身日均值的 1/3 時,當天提醒一次。</value></data>

<data name="Tray_OpenMainWindow" xml:space="preserve"><value>開啟主視窗</value></data>
<data name="Tray_Settings" xml:space="preserve"><value>設定</value></data>
Expand Down
17 changes: 17 additions & 0 deletions KeyStats.Windows/KeyStats/Services/NotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions KeyStats.Windows/KeyStats/Services/StatsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> KeyFatigueExcludedKeys = new(StringComparer.Ordinal) { "Enter", "Space" }; // 排除回车/空格,避免干扰疲劳判断
private readonly Dictionary<string, int> _fatigueThresholds = new(StringComparer.Ordinal); // Top4 键 -> 阈值
private readonly HashSet<string> _fatigueNotifiedKeys = new(StringComparer.Ordinal); // 当日已提醒的键

public DailyStats CurrentStats { get; private set; }
public AppSettings Settings { get; private set; }
public Dictionary<string, DailyStats> History { get; private set; } = new();
Expand Down Expand Up @@ -197,6 +203,7 @@ private void OnKeyPressed(string keyName, string appName, string displayName)
RecordKeyForPeakKPS();
NotifyStatsUpdate();
NotifyKeyPressThresholdIfNeeded();
NotifyKeyFatigueIfNeeded(keyName);
ScheduleSave();
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1282,6 +1290,56 @@ private void NotifyClickThresholdIfNeeded()
}
}

private void UpdateKeyFatigueBaseline()
{
lock (_lock)
{
var today = DateTime.Today;
var totals = new Dictionary<string, int>(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
Expand Down
14 changes: 14 additions & 0 deletions KeyStats.Windows/KeyStats/Views/NotificationSettingsWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@
</StackPanel>
</Border>

<!-- Key fatigue reminder -->
<Border Style="{DynamicResource CardBorder}" Margin="0,0,0,8" Padding="14,12">
<StackPanel>
<CheckBox x:Name="FatigueCheckBox"
Content="{x:Static p:Strings.NotifSettings_FatigueTitle}"
FontSize="13" Margin="0,0,0,6"
Foreground="{DynamicResource TextPrimaryBrush}"
Checked="OnSettingChanged" Unchecked="OnSettingChanged"/>
<TextBlock Text="{x:Static p:Strings.NotifSettings_FatigueHint}"
Foreground="{DynamicResource TextSecondaryBrush}" FontSize="11"
TextWrapping="Wrap"/>
</StackPanel>
</Border>

<!-- Hint -->
<TextBlock Text="{x:Static p:Strings.NotifSettings_Hint}"
Foreground="{DynamicResource TextSecondaryBrush}" FontSize="11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private void LoadSettings()
EnableCheckBox.IsChecked = settings.NotificationsEnabled;
KeyPressThresholdBox.Text = settings.KeyPressNotifyThreshold.ToString();
ClickThresholdBox.Text = settings.ClickNotifyThreshold.ToString();
FatigueCheckBox.IsChecked = settings.KeyFatigueNotifyEnabled;
UpdateFieldsEnabled();
}

Expand Down Expand Up @@ -107,6 +108,8 @@ private void SaveSettings()
settings.ClickNotifyThreshold = clickThreshold;
}

settings.KeyFatigueNotifyEnabled = FatigueCheckBox.IsChecked == true;

StatsManager.Instance.SaveSettings();
}
}