Add pre/post session modes with countdown timer

This commit is contained in:
Kunal1522 2026-06-23 02:04:58 +05:30
parent efad8a948a
commit 3f70dd04af
3 changed files with 177 additions and 67 deletions

View file

@ -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

View file

@ -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 @@
<input type="number" id="session-dur" min="1" max="480" value="25">
</div>
<div class="form-group">
<label for="session-pct">completion</label>
<div class="slider-row">
<input type="range" id="session-pct" min="80" max="100" step="10" value="100">
<span class="slider-val" id="session-pct-val">100%</span>
</div>
<div class="slider-ticks">
<span>80</span>
<span>90</span>
<span>100</span>
<label>when</label>
<div class="mode-toggle">
<button type="button" class="mode-btn active" data-mode="post" id="mode-post">post</button>
<button type="button" class="mode-btn" data-mode="pre" id="mode-pre">pre</button>
</div>
</div>
<div class="form-actions">
@ -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');
});

View file

@ -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"
}