/** * @fileoverview Pomodoro focus timer — frontend-only countdown using start/stop commands. * * Uses the same backend timer (start_timer/stop_timer) but adds a JS countdown * overlay. On countdown end: notification sound, auto-stop, break prompt. * Break timer is pure JS (no DB tracking). */ (function() { 'use strict'; const PRESETS = { standard: { work: 25, break: 5, label: '25/5' }, deep: { work: 50, break: 10, label: '50/10' }, }; let countdownInterval = null; let remainingSeconds = 0; let currentTaskId = null; let currentPreset = 'standard'; let customWorkMinutes = null; let customBreakMinutes = null; let isBreak = false; // ============ Focus Mode UI ============ function createOverlay() { if (document.getElementById('focus-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'focus-overlay'; overlay.className = 'focus-overlay hidden'; overlay.innerHTML = `
`; document.body.appendChild(overlay); overlay.querySelector('.focus-stop-btn').addEventListener('click', stopFocus); overlay.querySelector('.focus-cancel-btn').addEventListener('click', cancelFocus); overlay.querySelectorAll('.focus-preset-btn').forEach(btn => { btn.addEventListener('click', () => { if (!currentTaskId || isBreak) return; selectPreset(btn.dataset.preset); }); }); } function selectPreset(preset) { currentPreset = preset; const overlay = document.getElementById('focus-overlay'); if (!overlay) return; overlay.querySelectorAll('.focus-preset-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.preset === preset); }); } function showOverlay(taskDescription) { const overlay = document.getElementById('focus-overlay'); if (!overlay) return; overlay.querySelector('.focus-task-name').textContent = taskDescription; overlay.querySelector('.focus-label').textContent = isBreak ? 'Break Time' : 'Focus Mode'; overlay.classList.remove('hidden'); selectPreset(currentPreset); updateCountdownDisplay(); } function hideOverlay() { const overlay = document.getElementById('focus-overlay'); if (overlay) overlay.classList.add('hidden'); } function updateCountdownDisplay() { const overlay = document.getElementById('focus-overlay'); if (!overlay) return; const m = Math.floor(remainingSeconds / 60); const s = remainingSeconds % 60; overlay.querySelector('.focus-countdown').textContent = `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; const preset = currentPreset ? (PRESETS[currentPreset] || PRESETS.standard) : null; const totalSeconds = isBreak ? (customBreakMinutes || preset?.break || 5) * 60 : (customWorkMinutes || preset?.work || 25) * 60; const progress = totalSeconds > 0 ? ((totalSeconds - remainingSeconds) / totalSeconds) * 100 : 0; overlay.querySelector('.focus-progress-fill').style.width = `${progress}%`; } // ============ Timer Logic ============ /** * Start a focus mode session (Pomodoro) for a task. * Starts the backend timer and shows a countdown overlay. * @param {string} taskId - Task ID to focus on * @param {Object} [options] - Optional settings * @param {number} [options.workMinutes] - Custom work duration in minutes * @param {number} [options.breakMinutes] - Custom break duration in minutes * @param {string} [options.preset] - Named preset ('standard' or 'deep') */ async function start(taskId, options = {}) { currentTaskId = taskId; isBreak = false; // Support custom work/break minutes or a named preset if (options.workMinutes) { customWorkMinutes = options.workMinutes; customBreakMinutes = options.breakMinutes || Math.round(options.workMinutes / 5); currentPreset = null; remainingSeconds = customWorkMinutes * 60; } else { const presetName = (typeof options === 'string') ? options : (options.preset || 'standard'); currentPreset = presetName; customWorkMinutes = null; customBreakMinutes = null; const preset = PRESETS[currentPreset] || PRESETS.standard; remainingSeconds = preset.work * 60; } try { // Start the backend timer await GoingsOn.api.timeTracking.startTimer(taskId); } catch (err) { // Timer may already be running — that's OK for focus mode const msg = GoingsOn.utils.getErrorMessage(err); if (!msg.includes('already running')) { GoingsOn.ui.showToast(msg, 'error'); return; } } createOverlay(); // Get task description for display try { const active = await GoingsOn.api.timeTracking.getActive(); showOverlay(active?.taskDescription || 'Task'); } catch { showOverlay('Task'); } startCountdown(); GoingsOn.timeTracking.checkActive(); } function startCountdown() { if (countdownInterval) clearInterval(countdownInterval); countdownInterval = setInterval(() => { remainingSeconds--; updateCountdownDisplay(); if (remainingSeconds <= 0) { clearInterval(countdownInterval); countdownInterval = null; onCountdownEnd(); } }, 1000); } async function onCountdownEnd() { if (isBreak) { // Break finished hideOverlay(); GoingsOn.ui.showToast('Break over. Ready to focus again!', 'info'); return; } // Work session finished — auto-stop the backend timer try { if (currentTaskId) { const session = await GoingsOn.api.timeTracking.stopTimer(currentTaskId); if (session) { const mins = session.durationMinutes || 0; GoingsOn.ui.showToast(`Focus session complete! Tracked ${mins}m`, 'success'); } } } catch (err) { console.error('Failed to stop timer on focus end:', err); } GoingsOn.timeTracking.checkActive(); if (GoingsOn.tasks?.load) GoingsOn.tasks.load(); // Prompt for break promptBreak(); } function promptBreak() { isBreak = true; if (customBreakMinutes) { remainingSeconds = customBreakMinutes * 60; } else { const preset = PRESETS[currentPreset] || PRESETS.standard; remainingSeconds = preset.break * 60; } showOverlay('Break Time'); startCountdown(); } /** * Stop the active focus session, recording tracked time to the backend. */ async function stopFocus() { if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } if (!isBreak && currentTaskId) { try { const session = await GoingsOn.api.timeTracking.stopTimer(currentTaskId); if (session) { const mins = session.durationMinutes || 0; const display = mins >= 60 ? `${Math.floor(mins / 60)}h ${mins % 60}m` : `${mins}m`; GoingsOn.ui.showToast(`Focus stopped. Tracked ${display}`, 'success'); } } catch (err) { console.error('Failed to stop focus timer:', err); } GoingsOn.timeTracking.checkActive(); if (GoingsOn.tasks?.load) GoingsOn.tasks.load(); } hideOverlay(); currentTaskId = null; isBreak = false; } /** * Cancel the active focus session, discarding any tracked time. */ async function cancelFocus() { if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } if (!isBreak && currentTaskId) { try { await GoingsOn.api.timeTracking.discardTimer(currentTaskId); } catch (err) { console.error('Failed to discard focus timer:', err); } GoingsOn.timeTracking.checkActive(); if (GoingsOn.tasks?.load) GoingsOn.tasks.load(); } hideOverlay(); currentTaskId = null; isBreak = false; GoingsOn.ui.showToast('Focus session cancelled', 'info'); } // ============ Namespace ============ GoingsOn.focusTimer = { start, stop: stopFocus, cancel: cancelFocus, PRESETS, }; })();