340 lines
11 KiB
C#
340 lines
11 KiB
C#
using System;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Win32;
|
|
|
|
namespace TimerApp
|
|
{
|
|
public enum MonitorState
|
|
{
|
|
Idle,
|
|
Working,
|
|
Resting
|
|
}
|
|
|
|
public sealed class ActivityMonitor : IDisposable
|
|
{
|
|
private CancellationTokenSource? _cts;
|
|
private readonly object _lock = new object();
|
|
private TimeSpan _accumulatedWorkTime;
|
|
private int _restElapsedSeconds;
|
|
private bool _isPaused;
|
|
private bool _disposed;
|
|
|
|
// 状态检测缓存
|
|
private int _checkTickCounter;
|
|
private const int CheckIntervalTicks = 3; // 每3秒检查一次空闲/媒体状态
|
|
private long _cachedIdleMs;
|
|
private bool _cachedMediaPlaying;
|
|
|
|
// 配置 (默认值)
|
|
public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20);
|
|
public TimeSpan RestDuration { get; set; } = TimeSpan.FromMinutes(1);
|
|
public TimeSpan IdleThreshold { get; set; } = TimeSpan.FromSeconds(30);
|
|
|
|
public MonitorState CurrentState { get; private set; } = MonitorState.Idle;
|
|
public bool IsPaused
|
|
{
|
|
get { lock (_lock) return _isPaused; }
|
|
private set { lock (_lock) _isPaused = value; }
|
|
}
|
|
|
|
// 事件
|
|
public event EventHandler<TimeSpan>? WorkProgressChanged; // 剩余工作时间
|
|
public event EventHandler<TimeSpan>? RestProgressChanged; // 剩余休息时间
|
|
public event EventHandler? RestStarted;
|
|
public event EventHandler? RestEnded;
|
|
public event EventHandler? StateChanged;
|
|
|
|
public ActivityMonitor()
|
|
{
|
|
SystemEvents.PowerModeChanged += OnPowerModeChanged;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
StopInternal();
|
|
|
|
_cts = new CancellationTokenSource();
|
|
var token = _cts.Token;
|
|
|
|
// 启动时立即触发一次检测
|
|
_checkTickCounter = CheckIntervalTicks;
|
|
ResetWork();
|
|
|
|
Task.Run(() => MonitorLoop(token), token);
|
|
}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
StopInternal();
|
|
}
|
|
}
|
|
|
|
private void StopInternal()
|
|
{
|
|
if (_cts != null)
|
|
{
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
_cts = null;
|
|
}
|
|
}
|
|
|
|
private void ResetWork()
|
|
{
|
|
// Must be called within lock
|
|
_accumulatedWorkTime = TimeSpan.Zero;
|
|
ChangeState(MonitorState.Idle);
|
|
}
|
|
|
|
private void ChangeState(MonitorState newState)
|
|
{
|
|
// Must be called within lock
|
|
if (CurrentState != newState)
|
|
{
|
|
CurrentState = newState;
|
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
private async Task MonitorLoop(CancellationToken token)
|
|
{
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(1000, token);
|
|
OnTick();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Error in ActivityMonitor MonitorLoop", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnTick()
|
|
{
|
|
// 在锁外执行耗时的系统检测
|
|
long idleMs = 0;
|
|
bool? newMediaPlaying = null;
|
|
|
|
lock (_lock)
|
|
{
|
|
// 如果处于暂停状态,不处理计时逻辑
|
|
if (_isPaused) return;
|
|
|
|
// 检查是否需要更新状态
|
|
_checkTickCounter++;
|
|
if (_checkTickCounter >= CheckIntervalTicks)
|
|
{
|
|
_checkTickCounter = 0;
|
|
// 需要更新,但在锁外进行
|
|
newMediaPlaying = true;
|
|
}
|
|
|
|
// 如果不需要更新,直接使用缓存值
|
|
if (newMediaPlaying == null)
|
|
{
|
|
idleMs = _cachedIdleMs;
|
|
}
|
|
}
|
|
|
|
// 在锁外执行实际的检测
|
|
if (newMediaPlaying == true)
|
|
{
|
|
idleMs = NativeMethods.GetIdleTime();
|
|
bool playing = NativeMethods.IsMediaPlaying();
|
|
|
|
// 更新缓存
|
|
lock (_lock)
|
|
{
|
|
_cachedIdleMs = idleMs;
|
|
_cachedMediaPlaying = playing;
|
|
}
|
|
}
|
|
|
|
lock (_lock)
|
|
{
|
|
// 再次检查暂停状态
|
|
if (_isPaused) return;
|
|
|
|
// 使用(可能是新更新的)缓存值
|
|
idleMs = _cachedIdleMs;
|
|
bool isMediaPlaying = _cachedMediaPlaying;
|
|
TimeSpan idleTime = TimeSpan.FromMilliseconds(idleMs);
|
|
|
|
if (CurrentState == MonitorState.Resting)
|
|
{
|
|
// 休息模式逻辑
|
|
// 使用计数器而不是时间差,避免秒数跳变
|
|
_restElapsedSeconds++;
|
|
int totalRestSeconds = (int)RestDuration.TotalSeconds;
|
|
int remainingSeconds = totalRestSeconds - _restElapsedSeconds;
|
|
|
|
if (remainingSeconds <= 0)
|
|
{
|
|
// 休息结束
|
|
RestEnded?.Invoke(this, EventArgs.Empty);
|
|
ResetWork(); // 重新开始工作周期
|
|
}
|
|
else
|
|
{
|
|
TimeSpan remainingRest = TimeSpan.FromSeconds(remainingSeconds);
|
|
RestProgressChanged?.Invoke(this, remainingRest);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 工作/空闲模式逻辑
|
|
if (idleTime > IdleThreshold || isMediaPlaying)
|
|
{
|
|
// 用户离开了或正在播放视频
|
|
if (CurrentState == MonitorState.Working)
|
|
{
|
|
// 如果空闲时间超过阈值,状态变为空闲
|
|
ChangeState(MonitorState.Idle);
|
|
}
|
|
|
|
// 如果在 Idle 状态,且空闲时间非常长(比如超过了休息时间),
|
|
// 重置工作计时器
|
|
if (idleTime > RestDuration)
|
|
{
|
|
_accumulatedWorkTime = TimeSpan.Zero;
|
|
}
|
|
|
|
// 如果正在播放视频,不累加工作时间,但保持当前状态
|
|
if (isMediaPlaying && CurrentState == MonitorState.Working)
|
|
{
|
|
TimeSpan remainingWork = WorkDuration - _accumulatedWorkTime;
|
|
WorkProgressChanged?.Invoke(this, remainingWork);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 用户在活动且没有播放视频
|
|
if (CurrentState == MonitorState.Idle)
|
|
{
|
|
// 从空闲变为工作
|
|
ChangeState(MonitorState.Working);
|
|
}
|
|
|
|
// 累加工作时间
|
|
_accumulatedWorkTime += TimeSpan.FromSeconds(1);
|
|
|
|
// 检查是否达到工作时长
|
|
TimeSpan remainingWork = WorkDuration - _accumulatedWorkTime;
|
|
|
|
if (remainingWork <= TimeSpan.Zero)
|
|
{
|
|
// 触发休息
|
|
_restElapsedSeconds = 0; // 重置休息计数器
|
|
ChangeState(MonitorState.Resting);
|
|
RestStarted?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
else
|
|
{
|
|
WorkProgressChanged?.Invoke(this, remainingWork);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RefreshStatus()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (CurrentState == MonitorState.Working)
|
|
{
|
|
TimeSpan remaining = WorkDuration - _accumulatedWorkTime;
|
|
WorkProgressChanged?.Invoke(this, remaining);
|
|
}
|
|
else if (CurrentState == MonitorState.Resting)
|
|
{
|
|
int totalRestSeconds = (int)RestDuration.TotalSeconds;
|
|
int remainingSeconds = totalRestSeconds - _restElapsedSeconds;
|
|
if (remainingSeconds < 0) remainingSeconds = 0;
|
|
TimeSpan remaining = TimeSpan.FromSeconds(remainingSeconds);
|
|
RestProgressChanged?.Invoke(this, remaining);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Restart()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_accumulatedWorkTime = TimeSpan.Zero;
|
|
_isPaused = false;
|
|
|
|
// Ensure task is running
|
|
if (_cts == null || _cts.IsCancellationRequested)
|
|
{
|
|
// Re-start if stopped (though Restart implies it's running)
|
|
// Usually Start() calls ResetWork, so we just reset here
|
|
}
|
|
|
|
// Force state to Working since user manually restarted
|
|
ChangeState(MonitorState.Working);
|
|
|
|
// Immediately refresh UI to show full duration
|
|
RefreshStatus();
|
|
}
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!_isPaused && CurrentState != MonitorState.Idle)
|
|
{
|
|
_isPaused = true;
|
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Resume()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_isPaused)
|
|
{
|
|
_isPaused = false;
|
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
|
RefreshStatus();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
SystemEvents.PowerModeChanged -= OnPowerModeChanged;
|
|
Stop();
|
|
}
|
|
|
|
private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
|
|
{
|
|
if (e.Mode == PowerModes.Resume)
|
|
{
|
|
// 系统唤醒时,强制重置媒体播放检测状态,
|
|
// 避免因检测线程挂起导致一直误报“正在播放”而无法进入工作状态。
|
|
NativeMethods.InvalidateMediaCache();
|
|
}
|
|
}
|
|
}
|
|
}
|