| |
| const _t = { |
| mode: 'pomodoro', |
| phase: 'work', |
| secondsElapsed: 0, |
| running: false, |
| intervalId: null, |
| }; |
|
|
| const _TIMER_LS_KEY = 'focus_buddy_timer'; |
|
|
| function _saveTimerState() { |
| const workEl = document.getElementById('work-dur'); |
| const breakEl = document.getElementById('break-dur'); |
| try { |
| localStorage.setItem(_TIMER_LS_KEY, JSON.stringify({ |
| mode: _t.mode, |
| phase: _t.phase, |
| secondsElapsed: _t.secondsElapsed, |
| workDur: workEl?.value || '25', |
| breakDur: breakEl?.value || '5', |
| })); |
| } catch (_) {} |
| } |
|
|
| function _restoreTimerState() { |
| try { |
| const saved = JSON.parse(localStorage.getItem(_TIMER_LS_KEY) || 'null'); |
| if (!saved) return; |
| _t.mode = saved.mode || 'pomodoro'; |
| _t.phase = saved.phase || 'work'; |
| _t.secondsElapsed = saved.secondsElapsed || 0; |
| const workEl = document.getElementById('work-dur'); |
| const breakEl = document.getElementById('break-dur'); |
| if (workEl) workEl.value = saved.workDur || '25'; |
| if (breakEl) breakEl.value = saved.breakDur || '5'; |
| } catch (_) {} |
| } |
|
|
| |
| function _workSecs() { return (parseInt(document.getElementById('work-dur')?.value) || 25) * 60; } |
| function _breakSecs() { return (parseInt(document.getElementById('break-dur')?.value) || 5) * 60; } |
|
|
| function _targetSecs() { |
| if (_t.mode === 'freeflow') return Infinity; |
| return _t.phase === 'work' ? _workSecs() : _breakSecs(); |
| } |
|
|
| function _fmt(secs) { |
| const m = Math.floor(secs / 60).toString().padStart(2, '0'); |
| const s = (secs % 60).toString().padStart(2, '0'); |
| return `${m}:${s}`; |
| } |
|
|
| function _setDisplay() { |
| const elapsed = document.getElementById('timer-elapsed'); |
| const target = document.getElementById('timer-target'); |
| if (elapsed) elapsed.textContent = _fmt(_t.secondsElapsed); |
| if (target) { |
| if (_t.mode === 'pomodoro') { |
| target.textContent = ' / ' + _fmt(_t.phase === 'work' ? _workSecs() : _breakSecs()); |
| } else { |
| target.textContent = ''; |
| } |
| } |
| } |
|
|
| function _setPhaseLabel(text) { |
| const el = document.getElementById('timer-phase'); |
| if (el) el.textContent = text; |
| } |
|
|
| function _setBreakStyle(isBreak) { |
| document.getElementById('timer-display')?.classList.toggle('break-phase', isBreak); |
| } |
|
|
| function _setModeButtonActive(mode) { |
| document.getElementById('btn-pomodoro')?.classList.toggle('active', mode === 'pomodoro'); |
| document.getElementById('btn-freeflow')?.classList.toggle('active', mode === 'freeflow'); |
| } |
|
|
| function _updateStartBtn() { |
| const btn = document.getElementById('btn-start-stop'); |
| if (!btn) return; |
| if (_t.running) { |
| btn.textContent = 'β Stop'; |
| btn.classList.add('stop-btn'); |
| btn.classList.remove('start-btn'); |
| } else { |
| btn.textContent = 'βΆ Start'; |
| btn.classList.remove('stop-btn'); |
| btn.classList.add('start-btn'); |
| } |
| const disabled = _t.running; |
| document.getElementById('btn-pomodoro')?.toggleAttribute('disabled', disabled); |
| document.getElementById('btn-freeflow')?.toggleAttribute('disabled', disabled); |
| |
| if (window.avatarSetBase) { |
| if (_t.running) { |
| window.avatarSetBase(_t.phase === 'break' ? 'rest' : 'working'); |
| } else { |
| window.avatarSetBase('idle'); |
| } |
| } |
| } |
|
|
| |
| function _playChime() { |
| try { |
| const ctx = new (window.AudioContext || window.webkitAudioContext)(); |
| [[880, 0, 0.6], [1109, 0.15, 0.4], [880, 0.4, 0.25]].forEach(([freq, delay, vol]) => { |
| const osc = ctx.createOscillator(); |
| const gain = ctx.createGain(); |
| osc.connect(gain); |
| gain.connect(ctx.destination); |
| osc.type = 'sine'; |
| osc.frequency.value = freq; |
| const t = ctx.currentTime + delay; |
| gain.gain.setValueAtTime(vol, t); |
| gain.gain.exponentialRampToValueAtTime(0.001, t + 1.8); |
| osc.start(t); |
| osc.stop(t + 1.9); |
| }); |
| } catch (_) {} |
| } |
|
|
| |
| function _tick() { |
| _t.secondsElapsed++; |
| _setDisplay(); |
| if (_t.secondsElapsed % 30 === 0) _saveTimerState(); |
|
|
| if (_t.secondsElapsed < _targetSecs()) return; |
|
|
| |
| _stopInterval(); |
| _onComplete(); |
| } |
|
|
| function _stopInterval() { |
| if (_t.intervalId !== null) { |
| clearInterval(_t.intervalId); |
| _t.intervalId = null; |
| } |
| _t.running = false; |
| _updateStartBtn(); |
| _saveTimerState(); |
| } |
|
|
| function _onComplete() { |
| _playChime(); |
| if (window.avatarPlay) window.avatarPlay('celebrate'); |
|
|
| const el = document.getElementById('timer-display'); |
| if (el) { |
| el.classList.add('ringing'); |
| setTimeout(() => el.classList.remove('ringing'), 3000); |
| } |
|
|
| if (_t.mode === 'pomodoro') { |
| if (_t.phase === 'work') { |
| bridgeSend('timer_complete', { mode: 'pomodoro', phase: 'work' }); |
| _t.phase = 'break'; |
| _t.secondsElapsed = 0; |
| _setPhaseLabel('β Break Time'); |
| _setBreakStyle(true); |
| } else { |
| bridgeSend('timer_complete', { mode: 'pomodoro', phase: 'break' }); |
| _t.phase = 'work'; |
| _t.secondsElapsed = 0; |
| _setPhaseLabel('βοΈ Work Session'); |
| _setBreakStyle(false); |
| } |
| } else { |
| bridgeSend('timer_complete', { mode: 'freeflow', phase: 'work' }); |
| _t.secondsElapsed = 0; |
| _setPhaseLabel('π Free Flow'); |
| } |
|
|
| _setDisplay(); |
| } |
|
|
| |
| function timerToggle() { |
| if (_t.running) { |
| |
| const elapsedAtStop = _t.secondsElapsed; |
| const phaseAtStop = _t.phase; |
| const modeAtStop = _t.mode; |
| const targetAtStop = _t.mode === 'pomodoro' |
| ? (phaseAtStop === 'work' ? _workSecs() : _breakSecs()) |
| : null; |
|
|
| _stopInterval(); |
|
|
| |
| if (elapsedAtStop > 0) { |
| if (phaseAtStop === 'work' && window.avatarPlay) window.avatarPlay('celebrate'); |
| const payload = { mode: modeAtStop, phase: phaseAtStop, seconds_elapsed: elapsedAtStop }; |
| if (targetAtStop !== null) payload.target_secs = targetAtStop; |
| bridgeSend('timer_stop', payload); |
| } |
|
|
| |
| _t.phase = 'work'; |
| _t.secondsElapsed = 0; |
| _setBreakStyle(false); |
| _setDisplay(); |
| _setPhaseLabel(modeAtStop === 'pomodoro' ? 'βοΈ Work Session' : 'π Free Flow'); |
| _saveTimerState(); |
| } else { |
| _t.running = true; |
| _t.intervalId = setInterval(_tick, 1000); |
| _updateStartBtn(); |
| _saveTimerState(); |
| } |
| } |
|
|
| function timerReset() { |
| _stopInterval(); |
| _t.phase = 'work'; |
| _t.secondsElapsed = 0; |
| _setBreakStyle(false); |
| _setDisplay(); |
| _setPhaseLabel(_t.mode === 'pomodoro' ? 'βοΈ Work Session' : 'π Free Flow'); |
| _saveTimerState(); |
| } |
|
|
| function setTimerMode(mode) { |
| if (_t.running) _stopInterval(); |
| _t.mode = mode; |
| _t.phase = 'work'; |
| _t.secondsElapsed = 0; |
| _setModeButtonActive(mode); |
| _setBreakStyle(false); |
| _setDisplay(); |
|
|
| const isFreeflow = mode === 'freeflow'; |
| const workConfig = document.getElementById('work-config'); |
| const breakConfig = document.getElementById('break-config'); |
| if (workConfig) workConfig.style.display = isFreeflow ? 'none' : ''; |
| if (breakConfig) breakConfig.style.display = isFreeflow ? 'none' : ''; |
|
|
| _setPhaseLabel(mode === 'pomodoro' ? 'βοΈ Work Session' : 'π Free Flow'); |
| _saveTimerState(); |
| } |
|
|
| |
| function timerStart(durationMinutes, mode) { |
| if (_t.running) _stopInterval(); |
| setTimerMode(mode || 'pomodoro'); |
| if ((mode || 'pomodoro') === 'pomodoro' && durationMinutes) { |
| const el = document.getElementById('work-dur'); |
| if (el) el.value = durationMinutes; |
| } |
| _t.running = true; |
| _t.intervalId = setInterval(_tick, 1000); |
| _updateStartBtn(); |
| } |
|
|
| function timerStop() { |
| if (_t.running) _stopInterval(); |
| } |
|
|
| function timerAddMinutes(mins) { |
| _t.secondsElapsed = Math.max(0, _t.secondsElapsed + mins * 60); |
| _setDisplay(); |
| _saveTimerState(); |
| } |
|
|
| |
| window.timerToggle = timerToggle; |
| window.timerReset = timerReset; |
| window.setTimerMode = setTimerMode; |
| window.timerStart = timerStart; |
| window.timerStop = timerStop; |
| window.timerAddMinutes = timerAddMinutes; |
|
|
| |
| function _initTimer() { |
| if (!document.getElementById('timer-elapsed')) { |
| setTimeout(_initTimer, 150); |
| return; |
| } |
| _restoreTimerState(); |
| _setDisplay(); |
| _setModeButtonActive(_t.mode); |
| _setBreakStyle(_t.phase === 'break'); |
| _setPhaseLabel(_t.mode === 'freeflow' ? 'π Free Flow' : _t.phase === 'break' ? 'β Break Time' : 'βοΈ Work Session'); |
|
|
| const isFreeflow = _t.mode === 'freeflow'; |
| const workConfig = document.getElementById('work-config'); |
| const breakConfig = document.getElementById('break-config'); |
| if (workConfig) workConfig.style.display = isFreeflow ? 'none' : ''; |
| if (breakConfig) breakConfig.style.display = isFreeflow ? 'none' : ''; |
|
|
| _updateStartBtn(); |
| } |
|
|
| _initTimer(); |
|
|