This commit is contained in:
2026-09-12 13:49:44 +08:00
commit a8b1decc48
62 changed files with 16061 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
// src/renderer/secondTick.ts
// 统一的「每显示秒重画一次」调度器,替代各面板自己的 rAF / setTimeout(250) 循环。
//
// 为什么不是简单的 setInterval(1000)
// 重新导出浮窗需要的日期工具,避免浮窗再绕到 shared/constants 拉(仅模块内部使用)。
export { localDateKey } from '../shared/constants';
// 倒计时显示的是 Math.ceil((endsAt - now) / 1000),它的变化时刻锁在 endsAt 的相位上,
// 跟墙上时钟的整秒边界没有关系。用整秒对齐的定时器驱动会产生 0~999ms 的固定滞后
// (最坏情况下 00:00 会比铃声晚将近一秒才出现)——比它要替换掉的 240ms 闸门更差。
// 所以这里睡到「显示值下一次真正改变的那一刻」:唤醒次数是 1 次/秒,而重画延迟
// 反而从 ≤240ms 降到 ≤1 帧。
//
// 为什么必须处理 visibilitychange
// rAF 在窗口隐藏时天然停摆、显示时立即恢复setTimeout 不是 —— 隐藏页面的定时器会被
// Chromium 对齐到 1s约 5 分钟后进一步降到 1 次/分钟。若不管,从托盘恢复主窗时会看到
// 最长一分钟的陈旧读数。这里隐藏时直接停表、显示时立刻重画并重排,
// 既复刻了 rAF 原本的语义,也比它更省(隐藏期间零唤醒)。
export interface SecondTicker {
/** 状态变化后重排下一次唤醒endsAt 变了、开始/暂停/重置了)。 */
restart(): void;
stop(): void;
}
/**
* @param draw 重画回调
* @param nextAt 返回「显示值下一次改变」的绝对时间戳;返回 null 表示当前状态下
* 显示值不会自己变化idle / paused此时不装定时器。
*/
export function createSecondTicker(draw: () => void, nextAt: () => number | null): SecondTicker {
let timer = 0;
const clear = (): void => {
if (timer) { window.clearTimeout(timer); timer = 0; }
};
const schedule = (): void => {
clear();
if (document.hidden) return; // 隐藏期间不唤醒show 时由 onVisibility 补画
const at = nextAt();
if (at === null) return;
// clamp 到 [4, 1000]:即使 nextAt() 的相位算错,最坏也退化成 1Hz 轮询,
// 不会比「每秒刷新一次」更差。
const delay = Math.min(1000, Math.max(4, at - Date.now()));
timer = window.setTimeout(() => {
timer = 0;
draw();
schedule();
}, delay);
};
const onVisibility = (): void => {
if (document.hidden) clear();
else { draw(); schedule(); }
};
document.addEventListener('visibilitychange', onVisibility);
schedule();
return {
restart: schedule,
stop: () => {
clear();
document.removeEventListener('visibilitychange', onVisibility);
}
};
}
/**
* 倒数显示 Math.ceil(remain/1000) 下一次变化的绝对时刻。
* 例endsAt = now+5500 → 当前显示 6将在 now+504 变成 5。
* +4ms 让 ceil 稳定跨过边界,避免在临界点上重复算出同一个时刻。
*/
export function nextCeilBoundary(endsAt: number, now: number = Date.now()): number {
const remain = Math.max(0, endsAt - now);
const shown = Math.ceil(remain / 1000);
return shown <= 0 ? now + 1000 : endsAt - (shown - 1) * 1000 + 4;
}