class Timer { constructor(options = {}) { this.onTick = options.onTick || (() => {}) this.onGoalReached = options.onGoalReached || (() => {}) this._duration = 0 this._remaining = 0 this._elapsed = 0 this._startTime = 0 this._pausedTime = 0 this._intervalId = null this._running = false this._paused = false this._goalReached = false this._appShowBound = false // WeChat freezes setInterval while the mini-program is in background. // When the user returns, force an immediate tick so the UI jumps to // the actual elapsed time (instead of staying on the last pre-background // frame) and any missed halfway / 30s / 10s reminders can still fire. this._onAppShow = () => { if (this._running && !this._paused) { this._tick() } } } start(duration) { this._duration = duration this._remaining = duration this._elapsed = 0 this._goalReached = false this._running = true this._paused = false this._startTime = Date.now() this._tick() this._intervalId = setInterval(() => { this._tick() }, 1000) // Register foreground listener (idempotent — we keep a flag so we don't // double-register if start() is called multiple times in one session) if (!this._appShowBound && typeof wx.onAppShow === 'function') { try { wx.onAppShow(this._onAppShow) this._appShowBound = true } catch (e) {} } } pause() { if (!this._running || this._paused) return this._paused = true this._pausedTime = Date.now() clearInterval(this._intervalId) this._intervalId = null } resume() { if (!this._running || !this._paused) return this._startTime += Date.now() - this._pausedTime this._paused = false this._tick() this._intervalId = setInterval(() => { this._tick() }, 1000) } stop() { this._running = false this._paused = false clearInterval(this._intervalId) this._intervalId = null return this._elapsed } _tick() { if (!this._running || this._paused) return this._elapsed = Math.floor((Date.now() - this._startTime) / 1000) this._remaining = Math.max(0, this._duration - this._elapsed) this.onTick({ remaining: this._remaining, elapsed: this._elapsed, duration: this._duration }) // Goal reached: fire once but DON'T stop - the user keeps training past // the target until they manually end. elapsed keeps climbing, so // overtime (elapsed - duration) grows and the UI shows the surplus. if (this._remaining <= 0 && !this._goalReached) { this._goalReached = true this.onGoalReached({ elapsed: this._elapsed, duration: this._duration }) } } get isRunning() { return this._running && !this._paused } get isPaused() { return this._paused } get isGoalReached() { return this._goalReached } get remaining() { return this._remaining } get elapsed() { return this._elapsed } } module.exports = Timer