| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| let focusTimerId = null; |
| let focusRemainingSeconds = 0; |
| let focusTotalSeconds = 0; |
|
|
| function focusStartTimer(durationMinutes) { |
| focusTotalSeconds = durationMinutes * 60; |
| focusRemainingSeconds = focusTotalSeconds; |
| focusUpdateDisplay(); |
| focusShowTimer(); |
|
|
| if (focusTimerId) clearInterval(focusTimerId); |
|
|
| focusTimerId = setInterval(() => { |
| focusRemainingSeconds--; |
| focusUpdateDisplay(); |
|
|
| if (focusRemainingSeconds <= 300) { |
| |
| const timerEl = document.getElementById('ff-focus-timer'); |
| if (timerEl) timerEl.classList.add('warning'); |
| } |
|
|
| if (focusRemainingSeconds <= 0) { |
| focusComplete(); |
| } |
| }, 1000); |
| } |
|
|
| function focusPauseTimer() { |
| if (focusTimerId) { |
| clearInterval(focusTimerId); |
| focusTimerId = null; |
| } |
| } |
|
|
| function focusResumeTimer() { |
| if (focusRemainingSeconds > 0 && !focusTimerId) { |
| focusTimerId = setInterval(() => { |
| focusRemainingSeconds--; |
| focusUpdateDisplay(); |
| if (focusRemainingSeconds <= 0) focusComplete(); |
| }, 1000); |
| } |
| } |
|
|
| function focusStopTimer() { |
| if (focusTimerId) { |
| clearInterval(focusTimerId); |
| focusTimerId = null; |
| } |
| focusHideTimer(); |
| } |
|
|
| function focusUpdateDisplay() { |
| const mins = Math.floor(focusRemainingSeconds / 60); |
| const secs = focusRemainingSeconds % 60; |
| const display = document.getElementById('ff-focus-timer'); |
| if (display) { |
| display.textContent = |
| String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0'); |
| } |
| } |
|
|
| function focusShowTimer() { |
| const timer = document.getElementById('ff-focus-timer'); |
| if (timer) timer.style.display = 'block'; |
| } |
|
|
| function focusHideTimer() { |
| const timer = document.getElementById('ff-focus-timer'); |
| if (timer) { |
| timer.style.display = 'none'; |
| timer.classList.remove('warning'); |
| } |
| } |
|
|
| function focusComplete() { |
| clearInterval(focusTimerId); |
| focusTimerId = null; |
|
|
| const artEl = document.querySelector('.ff-pip-art textarea'); |
| if (artEl) { |
| artEl.classList.add('ff-celebrate'); |
| setTimeout(() => artEl.classList.remove('ff-celebrate'), 1500); |
| } |
|
|
| |
| const event = new CustomEvent('focusfriend:focus-complete', { |
| detail: { duration: focusTotalSeconds / 60 } |
| }); |
| document.dispatchEvent(event); |
| } |
|
|
| function focusGetRemaining() { |
| return focusRemainingSeconds; |
| } |
|
|
| |
| |
| |
|
|
| let breatheIntervalId = null; |
| let breathePhaseIndex = 0; |
| let breatheSecondsInPhase = 0; |
|
|
| const BREATHE_PATTERNS = { |
| '4-7-8': [ |
| { duration: 4, phase: 'in', label: 'Inhale' }, |
| { duration: 7, phase: 'hold', label: 'Hold' }, |
| { duration: 8, phase: 'out', label: 'Exhale' }, |
| ], |
| 'box': [ |
| { duration: 4, phase: 'in', label: 'Inhale' }, |
| { duration: 4, phase: 'hold', label: 'Hold' }, |
| { duration: 4, phase: 'out', label: 'Exhale' }, |
| { duration: 4, phase: 'hold', label: 'Hold' }, |
| ], |
| 'simple': [ |
| { duration: 4, phase: 'in', label: 'Inhale' }, |
| { duration: 6, phase: 'out', label: 'Exhale' }, |
| ], |
| }; |
|
|
| function breatheStart(technique) { |
| breatheStop(); |
| const pattern = BREATHE_PATTERNS[technique] || BREATHE_PATTERNS['4-7-8']; |
| breathePhaseIndex = 0; |
| breatheSecondsInPhase = 0; |
|
|
| const pipEl = document.querySelector('.ff-pip-art textarea'); |
| const guideEl = document.getElementById('ff-breathe-guide'); |
|
|
| breatheIntervalId = setInterval(() => { |
| const phase = pattern[breathePhaseIndex % pattern.length]; |
|
|
| if (breatheSecondsInPhase === 0) { |
| |
| if (pipEl) { |
| pipEl.classList.remove('ff-breathing-in', 'ff-breathing-out'); |
| if (phase.phase === 'in') { |
| pipEl.classList.add('ff-breathing-in'); |
| } else if (phase.phase === 'out') { |
| pipEl.classList.add('ff-breathing-out'); |
| } |
| } |
| |
| if (guideEl) { |
| guideEl.textContent = phase.label + ' (' + phase.duration + 's)'; |
| } |
| } |
|
|
| breatheSecondsInPhase++; |
|
|
| if (breatheSecondsInPhase >= phase.duration) { |
| breatheSecondsInPhase = 0; |
| breathePhaseIndex++; |
| } |
| }, 1000); |
| } |
|
|
| function breatheStop() { |
| if (breatheIntervalId) { |
| clearInterval(breatheIntervalId); |
| breatheIntervalId = null; |
| } |
| const pipEl = document.querySelector('.ff-pip-art textarea'); |
| if (pipEl) { |
| pipEl.classList.remove('ff-breathing-in', 'ff-breathing-out'); |
| } |
| } |
|
|
| |
| |
| |
|
|
| let breakReminderId = null; |
| let lastActivityTime = Date.now(); |
|
|
| function breakReminderStart(intervalMinutes) { |
| breakReminderStop(); |
| breakReminderId = setInterval(() => { |
| const idleMs = Date.now() - lastActivityTime; |
| const idleMinutes = idleMs / 60000; |
| if (idleMinutes >= intervalMinutes) { |
| const event = new CustomEvent('focusfriend:break-reminder', { |
| detail: { idleMinutes: Math.round(idleMinutes) } |
| }); |
| document.dispatchEvent(event); |
| } |
| }, 60000); |
| } |
|
|
| function breakReminderStop() { |
| if (breakReminderId) { |
| clearInterval(breakReminderId); |
| breakReminderId = null; |
| } |
| } |
|
|
| function breakRecordActivity() { |
| lastActivityTime = Date.now(); |
| } |
|
|
| |
| document.addEventListener('click', breakRecordActivity); |
| document.addEventListener('keydown', breakRecordActivity); |
|
|
| |
| |
| |
|
|
| function pipSetMood(mood) { |
| const artEl = document.querySelector('.ff-pip-art textarea'); |
| if (!artEl) return; |
|
|
| |
| artEl.classList.remove( |
| 'ff-breathing-in', 'ff-breathing-out', 'ff-celebrate' |
| ); |
|
|
| |
| if (mood === 'celebrate') { |
| artEl.classList.add('ff-celebrate'); |
| setTimeout(() => artEl.classList.remove('ff-celebrate'), 1500); |
| } |
| } |
|
|
| |
| |
| |
|
|
| document.addEventListener('DOMContentLoaded', () => { |
| console.log('FocusFriend initialized ✦'); |
| breakRecordActivity(); |
| }); |
|
|
| |
| window.focusfriend = { |
| focusStartTimer, |
| focusPauseTimer, |
| focusResumeTimer, |
| focusStopTimer, |
| focusGetRemaining, |
| breatheStart, |
| breatheStop, |
| breakReminderStart, |
| breakReminderStop, |
| pipSetMood, |
| }; |
|
|