mirror of
https://github.com/kunal1522/obsidian-focus-tracker.git
synced 2026-07-22 07:44:51 +00:00
Add pre/post session modes with countdown timer
This commit is contained in:
parent
efad8a948a
commit
3f70dd04af
3 changed files with 177 additions and 67 deletions
|
|
@ -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
|
||||
|
|
|
|||
237
tracker.html
237
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 @@
|
|||
<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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue