From 3f70dd04affc799dd449fd79de33fcd209d802a4 Mon Sep 17 00:00:00 2001 From: Kunal1522 Date: Tue, 23 Jun 2026 02:04:58 +0530 Subject: [PATCH] Add pre/post session modes with countdown timer --- manifest.json | 4 +- tracker.html | 237 ++++++++++++++++++++++++++++++++++++-------------- versions.json | 3 +- 3 files changed, 177 insertions(+), 67 deletions(-) diff --git a/manifest.json b/manifest.json index 88c82a1..b0e6348 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "calm-focus-log", "name": "Calm Focus Log", - "version": "1.0.10", + "version": "1.1.0", "minAppVersion": "1.0.0", - "description": "Log focus sessions on a 90-day heatmap. No goals or timers — just a calm record of what you did.", + "description": "Log focus sessions on a 90-day heatmap. Pre or post logging with optional countdown timer.", "author": "Kunal1522", "authorUrl": "https://github.com/Kunal1522", "isDesktopOnly": true diff --git a/tracker.html b/tracker.html index 6618254..241f339 100644 --- a/tracker.html +++ b/tracker.html @@ -211,7 +211,7 @@ } .cell-inner.both .cell-zen { - width: 28%; + width: 34%; } .cell-inner.both .cell-mins { @@ -470,55 +470,49 @@ box-shadow: 0 0 0 3px rgba(196, 105, 90, 0.15); } - .slider-row { + .mode-toggle { display: flex; - align-items: center; - gap: 0.75rem; + gap: 0.35rem; } - .slider-row input[type="range"] { + .mode-btn { flex: 1; - height: 6px; - appearance: none; background: var(--surface-inset); border: 1px solid var(--border); - border-radius: var(--radius-pill); - cursor: pointer; - } - - .slider-row input[type="range"]::-webkit-slider-thumb { - appearance: none; - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--accent); - border: 2px solid var(--surface); - box-shadow: var(--shadow); - } - - .slider-row input[type="range"]::-moz-range-thumb { - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--accent); - border: 2px solid var(--surface); - box-shadow: var(--shadow); - } - - .slider-val { + color: var(--text-dim); + font-family: inherit; font-size: 0.78rem; - font-weight: 600; - color: var(--text-dim); - min-width: 2.5rem; - text-align: right; + font-weight: 500; + padding: 0.42rem 0.65rem; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; } - .slider-ticks { - display: flex; - justify-content: space-between; - padding: 0.25rem 0.5rem 0; - font-size: 0.65rem; - color: var(--text-dim); + .mode-btn:hover { + color: var(--text); + background: var(--surface); + } + + .mode-btn.active { + background: var(--surface); + border-color: var(--accent); + color: var(--accent); + box-shadow: 0 0 0 2px rgba(196, 105, 90, 0.12); + } + + .session-card.timing { + border-color: rgba(107, 143, 94, 0.45); + } + + .session-card.timing .session-dur { + color: var(--accent); + border-color: rgba(196, 105, 90, 0.35); + font-variant-numeric: tabular-nums; + } + + .session-card.timing .session-fill { + opacity: 0.65; } .form-actions { @@ -577,15 +571,10 @@
- -
- - 100% -
-
- 80 - 90 - 100 + +
+ +
@@ -615,6 +604,8 @@ let data = {}; let selectedDate = todayStr(); let lastKnownToday = todayStr(); + let logMode = 'post'; + let timerInterval = null; const inPlugin = window.parent !== window; // ── storage ── @@ -638,7 +629,9 @@ function boot(initial) { data = initial || loadLocal(); + recoverPreSessions(); renderAll(); + if (anyActivePreSessions()) ensureTimerLoop(); } function uid() { @@ -699,6 +692,11 @@ } function sessionPct(s) { + if (isPreActive(s)) { + const total = s.dur * 60; + const elapsed = total - getPreRemaining(s); + return total > 0 ? Math.min(100, (elapsed / total) * 100) : 0; + } if (s.pct != null) return Number(s.pct) || 100; if (s.status === 'incomplete') return 80; return 100; @@ -708,8 +706,97 @@ return s.dur * sessionPct(s) / 100; } + function getPreRemaining(s) { + if (s.mode !== 'pre' || !s.startedAt) return null; + const elapsed = (Date.now() - s.startedAt) / 1000; + return Math.max(0, s.dur * 60 - elapsed); + } + + function isPreActive(s) { + return s.mode === 'pre' && s.startedAt && getPreRemaining(s) > 0; + } + + function anyActivePreSessions() { + for (const dateStr in data) { + if (dateStr === META_MEDITATION) continue; + for (const s of daySessions(dateStr)) { + if (isPreActive(s)) return true; + } + } + return false; + } + + function recoverPreSessions() { + let changed = false; + for (const dateStr in data) { + if (dateStr === META_MEDITATION) continue; + for (const s of daySessions(dateStr)) { + if (s.mode === 'pre' && s.startedAt && getPreRemaining(s) <= 0) { + delete s.mode; + delete s.startedAt; + s.pct = 100; + changed = true; + } + } + } + if (changed) saveData(); + } + + function fmtCountdown(totalSec) { + const m = Math.floor(totalSec / 60); + const s = Math.floor(totalSec % 60); + return `${m}:${String(s).padStart(2, '0')}`; + } + + function updateTimerDisplays() { + document.querySelectorAll('.session-card[data-session-id]').forEach(card => { + const session = daySessions(selectedDate).find(s => s.id === card.dataset.sessionId); + if (!session || !isPreActive(session)) return; + + const rem = getPreRemaining(session); + const durEl = card.querySelector('.session-dur'); + const fill = card.querySelector('.session-fill'); + if (durEl) durEl.textContent = fmtCountdown(rem); + if (fill) fill.style.width = sessionPct(session) + '%'; + }); + } + + function tickTimers() { + let completed = false; + + for (const dateStr in data) { + if (dateStr === META_MEDITATION) continue; + for (const s of daySessions(dateStr)) { + if (s.mode === 'pre' && s.startedAt && getPreRemaining(s) <= 0) { + delete s.mode; + delete s.startedAt; + s.pct = 100; + completed = true; + } + } + } + + if (completed) { + saveData(); + renderAll(); + return; + } + + if (anyActivePreSessions()) updateTimerDisplays(); + } + + function ensureTimerLoop() { + if (timerInterval) return; + timerInterval = setInterval(tickTimers, 1000); + } + + function heatmapDur(s) { + if (s.mode === 'pre') return s.dur; + return effectiveDur(s); + } + function dayMinutes(dateStr) { - return daySessions(dateStr).reduce((sum, s) => sum + effectiveDur(s), 0); + return daySessions(dateStr).reduce((sum, s) => sum + heatmapDur(s), 0); } function maxMinutesInRange(dates) { @@ -895,7 +982,7 @@ function renderDayDetail() { document.getElementById('selected-date-label').textContent = fmtDisplayDate(selectedDate); const sessions = daySessions(selectedDate); - const total = Math.round(sessions.reduce((s, x) => s + effectiveDur(x), 0)); + const total = Math.round(sessions.reduce((s, x) => s + heatmapDur(x), 0)); document.getElementById('day-total').textContent = total > 0 ? `${total} min` : ''; @@ -911,10 +998,12 @@ sessions.forEach(session => { const name = session.label || 'session'; + const active = isPreActive(session); const pct = Math.min(100, Math.max(0, sessionPct(session))); const card = document.createElement('div'); - card.className = 'session-card'; + card.className = 'session-card' + (active ? ' timing' : ''); + card.dataset.sessionId = session.id; const track = document.createElement('div'); track.className = 'session-track'; @@ -944,7 +1033,9 @@ const dur = document.createElement('div'); dur.className = 'session-dur'; - dur.textContent = session.dur + ' min'; + dur.textContent = active + ? fmtCountdown(getPreRemaining(session)) + : session.dur + ' min'; const del = document.createElement('button'); del.className = 'session-delete'; @@ -971,13 +1062,35 @@ // ── add session ── - function addSession(label, dur, pct, dateStr) { + function setLogMode(mode) { + logMode = mode === 'pre' ? 'pre' : 'post'; + document.querySelectorAll('.mode-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.mode === logMode); + }); + } + + function addSession(label, dur, mode, dateStr) { const target = dateStr || getLogDate(); if (!Array.isArray(data[target])) data[target] = []; - data[target].push({ id: uid(), label, dur: Number(dur), pct: Number(pct) }); + + const session = { + id: uid(), + label, + dur: Number(dur) + }; + + if (mode === 'pre') { + session.mode = 'pre'; + session.startedAt = Date.now(); + } else { + session.pct = 100; + } + + data[target].push(session); selectedDate = target; saveData(); renderAll(); + if (mode === 'pre') ensureTimerLoop(); } // ── modals ── @@ -988,18 +1101,15 @@ refreshCalendar(); document.getElementById('session-label').value = ''; document.getElementById('session-dur').value = '25'; - document.getElementById('session-pct').value = '100'; - document.getElementById('session-pct-val').textContent = '100%'; + setLogMode('post'); setDateInputBounds(); syncDateInput(todayStr()); addModal.classList.add('active'); document.getElementById('session-label').focus(); }); - document.getElementById('session-pct').addEventListener('input', () => { - document.getElementById('session-pct-val').textContent = - document.getElementById('session-pct').value + '%'; - }); + document.getElementById('mode-post').addEventListener('click', () => setLogMode('post')); + document.getElementById('mode-pre').addEventListener('click', () => setLogMode('pre')); document.getElementById('session-date').addEventListener('change', () => { const d = getLogDate(); @@ -1013,9 +1123,8 @@ document.getElementById('log-it-btn').addEventListener('click', () => { const label = document.getElementById('session-label').value.trim(); const dur = document.getElementById('session-dur').value; - const pct = document.getElementById('session-pct').value; if (!dur || dur < 1) return; - addSession(label, dur, pct, getLogDate()); + addSession(label, dur, logMode, getLogDate()); addModal.classList.remove('active'); }); diff --git a/versions.json b/versions.json index 589a43a..2bf3fda 100644 --- a/versions.json +++ b/versions.json @@ -9,5 +9,6 @@ "1.0.7": "1.0.0", "1.0.8": "1.0.0", "1.0.9": "1.0.0", - "1.0.10": "1.0.0" + "1.0.10": "1.0.0", + "1.1.0": "1.0.0" }