diff --git a/ActivityMonitor.cs b/ActivityMonitor.cs index 086e4de..3e5ebc5 100644 --- a/ActivityMonitor.cs +++ b/ActivityMonitor.cs @@ -10,17 +10,13 @@ namespace TimerApp Resting } - public class ActivityMonitor + public sealed class ActivityMonitor : IDisposable { - private System.Windows.Forms.Timer _timer; - private DateTime _lastWorkStartTime; + private readonly System.Windows.Forms.Timer _timer; private TimeSpan _accumulatedWorkTime; - private DateTime _restStartTime; - private int _restElapsedSeconds; // 休息已过秒数(用于避免时间计算导致的跳变) - private bool _isPaused = false; // 暂停状态 - private DateTime _pauseStartTime; // 暂停开始时间 - private TimeSpan _pauseDuration = TimeSpan.Zero; // 累计暂停时间(用于工作计时) - private int _restPauseStartSeconds = 0; // 休息暂停时的已过秒数 + private int _restElapsedSeconds; + private bool _isPaused; + private bool _disposed; // 配置 (默认值) public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20); @@ -31,11 +27,11 @@ namespace TimerApp public bool IsPaused { get; private set; } = false; // 事件 - public event EventHandler WorkProgressChanged; // 剩余工作时间 - public event EventHandler RestProgressChanged; // 剩余休息时间 - public event EventHandler RestStarted; - public event EventHandler RestEnded; - public event EventHandler StateChanged; + public event EventHandler? WorkProgressChanged; // 剩余工作时间 + public event EventHandler? RestProgressChanged; // 剩余休息时间 + public event EventHandler? RestStarted; + public event EventHandler? RestEnded; + public event EventHandler? StateChanged; public ActivityMonitor() { @@ -58,7 +54,6 @@ namespace TimerApp private void ResetWork() { _accumulatedWorkTime = TimeSpan.Zero; - _lastWorkStartTime = DateTime.Now; ChangeState(MonitorState.Idle); } @@ -71,7 +66,7 @@ namespace TimerApp } } - private void Timer_Tick(object sender, EventArgs e) + private void Timer_Tick(object? sender, EventArgs e) { // 如果处于暂停状态,不处理计时逻辑 if (_isPaused) @@ -142,7 +137,6 @@ namespace TimerApp { // 从空闲变为工作 ChangeState(MonitorState.Working); - _lastWorkStartTime = DateTime.Now; } // 累加工作时间 @@ -155,7 +149,6 @@ namespace TimerApp if (remainingWork <= TimeSpan.Zero) { // 触发休息 - _restStartTime = DateTime.Now; _restElapsedSeconds = 0; // 重置休息计数器 ChangeState(MonitorState.Resting); RestStarted?.Invoke(this, EventArgs.Empty); @@ -171,7 +164,6 @@ namespace TimerApp // 用于强制重置或测试 public void ForceRest() { - _restStartTime = DateTime.Now; _restElapsedSeconds = 0; // 重置休息计数器 ChangeState(MonitorState.Resting); RestStarted?.Invoke(this, EventArgs.Empty); @@ -198,10 +190,8 @@ namespace TimerApp public void Restart() { _accumulatedWorkTime = TimeSpan.Zero; - _lastWorkStartTime = DateTime.Now; _isPaused = false; IsPaused = false; - _pauseDuration = TimeSpan.Zero; // Ensure timer is running if (!_timer.Enabled) _timer.Start(); @@ -219,13 +209,6 @@ namespace TimerApp { _isPaused = true; IsPaused = true; - _pauseStartTime = DateTime.Now; - - // 如果正在休息,记录当前已过秒数 - if (CurrentState == MonitorState.Resting) - { - _restPauseStartSeconds = _restElapsedSeconds; - } StateChanged?.Invoke(this, EventArgs.Empty); } @@ -238,25 +221,19 @@ namespace TimerApp _isPaused = false; IsPaused = false; - // 计算暂停时长 - TimeSpan pauseTime = DateTime.Now - _pauseStartTime; - - // 如果正在工作,将暂停时间累加到暂停总时长中 - // 这样工作时间就不会因为暂停而减少 - if (CurrentState == MonitorState.Working) - { - _pauseDuration += pauseTime; - } - // 如果正在休息,调整已过秒数,使剩余时间保持不变 - else if (CurrentState == MonitorState.Resting) - { - // 保持已过秒数不变,这样恢复后剩余时间不会变化 - // _restElapsedSeconds 保持不变 - } - StateChanged?.Invoke(this, EventArgs.Empty); RefreshStatus(); } } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + _timer.Stop(); + _timer.Tick -= Timer_Tick; + _timer.Dispose(); + } } } diff --git a/AppSettings.cs b/AppSettings.cs index cd3d5c4..29726bf 100644 --- a/AppSettings.cs +++ b/AppSettings.cs @@ -11,16 +11,26 @@ namespace TimerApp public int IdleThresholdSeconds { get; set; } = 30; public bool IsDarkMode { get; set; } = true; - private static string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); + private static string LegacyConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); + + private static string ConfigPath + { + get + { + string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TimerApp"); + return Path.Combine(dir, "settings.json"); + } + } public static AppSettings Load() { try { - if (File.Exists(ConfigPath)) + string path = File.Exists(ConfigPath) ? ConfigPath : LegacyConfigPath; + if (File.Exists(path)) { - string json = File.ReadAllText(ConfigPath); - return JsonSerializer.Deserialize(json); + string json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json) ?? new AppSettings(); } } catch @@ -34,6 +44,7 @@ namespace TimerApp { try { + Directory.CreateDirectory(Path.GetDirectoryName(ConfigPath)!); string json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(ConfigPath, json); } diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs index 772120a..01e2ee5 100644 --- a/MainForm.Designer.cs +++ b/MainForm.Designer.cs @@ -13,9 +13,11 @@ namespace TimerApp /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { - if (disposing && (components != null)) + if (disposing) { - components.Dispose(); + _monitor?.Dispose(); + _restForm?.Dispose(); + components?.Dispose(); } base.Dispose(disposing); } diff --git a/MainForm.cs b/MainForm.cs index dfca223..780f861 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -7,9 +7,9 @@ namespace TimerApp { public partial class MainForm : Form { - private ActivityMonitor _monitor; + private ActivityMonitor _monitor = null!; private AppSettings _settings; - private RestForm _restForm; + private RestForm? _restForm; // Colors private Color _darkBg = Color.FromArgb(30, 30, 30); @@ -106,8 +106,8 @@ namespace TimerApp // Manual input validation txtWork.KeyPress += ValidateDigitInput; txtRest.KeyPress += ValidateDigitInput; - txtWork.Leave += (s, ev) => ValidateRange((TextBox)s, 1, 120); - txtRest.Leave += (s, ev) => ValidateRange((TextBox)s, 1, 30); + txtWork.Leave += (s, ev) => ValidateRange((TextBox)s!, 1, 120); + txtRest.Leave += (s, ev) => ValidateRange((TextBox)s!, 1, 30); // Focus handling (remove custom caret) txtWork.KeyDown += TextBox_KeyDown; @@ -163,7 +163,7 @@ namespace TimerApp } } - private void ValidateDigitInput(object sender, KeyPressEventArgs e) + private void ValidateDigitInput(object? sender, KeyPressEventArgs e) { // Allow control keys (backspace, etc.) and digits if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar)) @@ -216,7 +216,7 @@ namespace TimerApp // CustomCaret removed to use system caret with centered text - private void TextBox_KeyDown(object sender, KeyEventArgs e) + private void TextBox_KeyDown(object? sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) { @@ -407,11 +407,11 @@ namespace TimerApp _monitor.IdleThreshold = TimeSpan.FromSeconds(_settings.IdleThresholdSeconds); } - private void Monitor_StateChanged(object sender, EventArgs e) + private void Monitor_StateChanged(object? sender, EventArgs e) { if (InvokeRequired) { - Invoke(new Action(Monitor_StateChanged), sender, e); + Invoke(new Action(Monitor_StateChanged), sender, e); return; } UpdateStatusUI(); @@ -456,11 +456,11 @@ namespace TimerApp } } - private void Monitor_WorkProgressChanged(object sender, TimeSpan remaining) + private void Monitor_WorkProgressChanged(object? sender, TimeSpan remaining) { if (InvokeRequired) { - Invoke(new Action(Monitor_WorkProgressChanged), sender, remaining); + Invoke(new Action(Monitor_WorkProgressChanged), sender, remaining); return; } lblTimeLeft.Text = $"{remaining.Minutes:D2}:{remaining.Seconds:D2}"; @@ -476,11 +476,11 @@ namespace TimerApp } } - private void Monitor_RestStarted(object sender, EventArgs e) + private void Monitor_RestStarted(object? sender, EventArgs e) { if (InvokeRequired) { - Invoke(new Action(Monitor_RestStarted), sender, e); + Invoke(new Action(Monitor_RestStarted), sender, e); return; } @@ -494,13 +494,13 @@ namespace TimerApp _restForm.Show(); } - private void RestForm_SkipRequested(object sender, EventArgs e) + private void RestForm_SkipRequested(object? sender, EventArgs e) { _monitor.Stop(); _monitor.Start(); } - private void Monitor_RestProgressChanged(object sender, TimeSpan remaining) + private void Monitor_RestProgressChanged(object? sender, TimeSpan remaining) { if (_restForm != null && !_restForm.IsDisposed && _restForm.Visible) { @@ -508,11 +508,11 @@ namespace TimerApp } } - private void Monitor_RestEnded(object sender, EventArgs e) + private void Monitor_RestEnded(object? sender, EventArgs e) { if (InvokeRequired) { - Invoke(new Action(Monitor_RestEnded), sender, e); + Invoke(new Action(Monitor_RestEnded), sender, e); return; } @@ -555,12 +555,12 @@ namespace TimerApp } } - private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e) + private void notifyIcon1_MouseDoubleClick(object? sender, MouseEventArgs e) { ShowForm(); } - private void toolStripMenuItemShow_Click(object sender, EventArgs e) + private void toolStripMenuItemShow_Click(object? sender, EventArgs e) { ShowForm(); } diff --git a/NativeMethods.cs b/NativeMethods.cs index b668bde..bc9637c 100644 --- a/NativeMethods.cs +++ b/NativeMethods.cs @@ -1,5 +1,7 @@ using System; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Windows.Media.Control; namespace TimerApp @@ -26,22 +28,17 @@ namespace TimerApp /// public static long GetIdleTime() { - LASTINPUTINFO lastInputInfo = new LASTINPUTINFO(); - lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo); - lastInputInfo.dwTime = 0; + LASTINPUTINFO lastInputInfo = new LASTINPUTINFO + { + cbSize = (uint)Marshal.SizeOf(), + dwTime = 0 + }; if (GetLastInputInfo(ref lastInputInfo)) { - // Environment.TickCount 可能会在大约 24.9 天后翻转为负数, - // 但 GetLastInputInfo 返回的也是 uint (DWORD),所以我们统一转为 long 处理差值 - // 或者直接使用 unchecked 减法处理溢出 - // 更稳健的做法是使用 GetTickCount64 (Vista+),但 Environment.TickCount 在 .NET Core 3.1+ 已经是 64位了(Environment.TickCount64) - // 这里为了兼容性,我们简单处理。注意 GetLastInputInfo 返回的是 uint 毫秒数。 - - long envTicks = Environment.TickCount; - // 处理 TickCount 翻转问题 (Environment.TickCount 是 int,GetLastInputInfo 是 uint) - // 简单的做法: - return (long)GetTickCount() - (long)lastInputInfo.dwTime; + uint tickCount = GetTickCount(); + uint idleMs = unchecked(tickCount - lastInputInfo.dwTime); + return idleMs; } return 0; @@ -56,27 +53,57 @@ namespace TimerApp /// 如果有媒体正在播放返回 true,否则返回 false public static bool IsMediaPlaying() { - try - { - var sessionManager = GlobalSystemMediaTransportControlsSessionManager.RequestAsync().GetAwaiter().GetResult(); - var sessions = sessionManager.GetSessions(); + EnsureMediaPlaybackStatusFresh(); + return Volatile.Read(ref _mediaPlaying); + } - foreach (var session in sessions) + private static bool _mediaPlaying; + private static long _mediaLastUpdateMs = -1; + private static int _mediaUpdateInProgress; + + private static void EnsureMediaPlaybackStatusFresh() + { + long now = Environment.TickCount64; + long last = Interlocked.Read(ref _mediaLastUpdateMs); + if (last >= 0 && now - last < 3000) + { + return; + } + + if (Interlocked.Exchange(ref _mediaUpdateInProgress, 1) == 1) + { + return; + } + + _ = Task.Run(async () => + { + bool playing = false; + try { - var playbackInfo = session.GetPlaybackInfo(); - if (playbackInfo.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) + var sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); + var sessions = sessionManager.GetSessions(); + + foreach (var session in sessions) { - return true; + var playbackInfo = session.GetPlaybackInfo(); + if (playbackInfo.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) + { + playing = true; + break; + } } } - } - catch - { - // 如果 API 调用失败,返回 false(保守处理) - return false; - } - - return false; + catch + { + playing = false; + } + finally + { + Volatile.Write(ref _mediaPlaying, playing); + Interlocked.Exchange(ref _mediaLastUpdateMs, Environment.TickCount64); + Interlocked.Exchange(ref _mediaUpdateInProgress, 0); + } + }); } } } diff --git a/RestForm.cs b/RestForm.cs index 4f5a2ef..d54811a 100644 --- a/RestForm.cs +++ b/RestForm.cs @@ -6,11 +6,11 @@ namespace TimerApp { public class RestForm : Form { - private Label lblMessage; - private Label lblTimer; - private Button btnSkip; + private Label lblMessage = null!; + private Label lblTimer = null!; + private Button btnSkip = null!; - public event EventHandler SkipRequested; + public event EventHandler? SkipRequested; public RestForm() { @@ -99,12 +99,12 @@ namespace TimerApp this.PerformLayout(); } - private void RestForm_Load(object sender, EventArgs e) + private void RestForm_Load(object? sender, EventArgs e) { CenterControls(); } - private void RestForm_Resize(object sender, EventArgs e) + private void RestForm_Resize(object? sender, EventArgs e) { CenterControls(); } @@ -153,7 +153,7 @@ namespace TimerApp } } - private void btnSkip_Click(object sender, EventArgs e) + private void btnSkip_Click(object? sender, EventArgs e) { SkipRequested?.Invoke(this, EventArgs.Empty); this.Close(); diff --git a/SingleInstanceManager.cs b/SingleInstanceManager.cs index 21115d7..c1db749 100644 --- a/SingleInstanceManager.cs +++ b/SingleInstanceManager.cs @@ -11,12 +11,16 @@ namespace TimerApp private const string MutexName = "Local\\TimerApp.SingleInstance"; private const string PipeName = "TimerApp.SingleInstancePipe"; - private readonly Mutex _mutex; + private readonly Mutex? _mutex; + private readonly bool _ownsMutex; + private readonly bool _enableIpc; private readonly CancellationTokenSource _cts = new(); - private SingleInstanceManager(Mutex mutex) + private SingleInstanceManager(Mutex? mutex, bool ownsMutex, bool enableIpc) { _mutex = mutex; + _ownsMutex = ownsMutex; + _enableIpc = enableIpc; } public static bool TryAcquire(out SingleInstanceManager? manager) @@ -32,11 +36,12 @@ namespace TimerApp return false; } - manager = new SingleInstanceManager(mutex); + manager = new SingleInstanceManager(mutex, ownsMutex: true, enableIpc: true); return true; } catch { + manager = new SingleInstanceManager(mutex: null, ownsMutex: false, enableIpc: false); return true; } } @@ -57,6 +62,7 @@ namespace TimerApp public void StartServer(Action onShowRequested) { + if (!_enableIpc) return; Task.Run(() => ServerLoop(onShowRequested, _cts.Token)); } @@ -104,15 +110,17 @@ namespace TimerApp try { - _mutex.ReleaseMutex(); + if (_ownsMutex) + { + _mutex?.ReleaseMutex(); + } } catch { } - _mutex.Dispose(); + _mutex?.Dispose(); _cts.Dispose(); } } } -