mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-04 10:40:23 -06:00
- Command palette (Ctrl/Cmd+K) with quick nav (g d/p/r/t), start/stop timer, theme toggle - Adds modal to base layout and global shortcuts - Files: app/templates/base.html, app/static/commands.js - Bulk edit for time entries with multi-select and quick actions - Delete, set billable/non-billable from dashboard Recent Entries - API: POST /api/entries/bulk - Files: app/templates/main/dashboard.html, app/routes/api.py - Idle detection and “resume/stop at” support - Detect inactivity and prompt to stop at last active time - API: POST /api/timer/stop_at, POST /api/timer/resume - Files: app/static/idle.js, app/templates/base.html, app/routes/api.py - Calendar (day/week/month) with drag‑to‑create entries - Route: /timer/calendar - APIs: GET /api/calendar/events, POST /api/entries - Files: app/routes/timer.py, templates/timer/calendar.html, app/routes/api.py - UX polish: improved flash container spacing; reused existing skeleton loaders, loading spinners, and toasts No breaking changes.
65 lines
2.5 KiB
JavaScript
65 lines
2.5 KiB
JavaScript
// Idle detection: when user is inactive, offer to stop timer at last active time
|
|
(function(){
|
|
if (window.__ttIdleLoaded) return; window.__ttIdleLoaded = true;
|
|
const IDLE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
|
const CHECK_INTERVAL_MS = 60 * 1000; // 1 minute
|
|
|
|
let lastActivity = Date.now();
|
|
let promptShown = false;
|
|
|
|
function markActive(){
|
|
lastActivity = Date.now();
|
|
promptShown = false;
|
|
}
|
|
|
|
['mousemove','keydown','scroll','click','touchstart','visibilitychange'].forEach(evt =>
|
|
document.addEventListener(evt, markActive, { passive: true })
|
|
);
|
|
|
|
async function getTimer(){
|
|
try {
|
|
const r = await fetch('/api/timer/status');
|
|
if (!r.ok) return null; const j = await r.json();
|
|
return j && j.active ? j.timer : null;
|
|
} catch(e){ return null; }
|
|
}
|
|
|
|
function formatTime(d){
|
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
async function stopAt(ts){
|
|
try {
|
|
const r = await fetch('/api/timer/stop_at', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stop_time: new Date(ts).toISOString() }) });
|
|
if (r.ok){ showToast('Timer stopped due to inactivity', 'warning'); location.reload(); }
|
|
} catch(e) {}
|
|
}
|
|
|
|
function showIdlePrompt(stopTs){
|
|
if (promptShown) return; promptShown = true;
|
|
// Create a lightweight inline prompt toast
|
|
const t = document.createElement('div');
|
|
t.className = 'toast align-items-center text-white bg-warning border-0 fade show';
|
|
t.innerHTML = `<div class="d-flex"><div class="toast-body">You seem inactive since ${formatTime(new Date(stopTs))}. Stop the timer at that time?</div><div class="d-flex gap-2 align-items-center me-2"><button class="btn btn-sm btn-light" data-act="stop">Stop</button><button class="btn btn-sm btn-outline-light" data-act="dismiss">Dismiss</button></div></div>`;
|
|
const container = document.getElementById('toast-container') || document.body;
|
|
container.appendChild(t);
|
|
t.querySelector('[data-act="stop"]').addEventListener('click', () => { t.remove(); stopAt(stopTs); });
|
|
t.querySelector('[data-act="dismiss"]').addEventListener('click', () => { t.remove(); });
|
|
setTimeout(() => { try { t.remove(); } catch(e){} }, 60_000);
|
|
}
|
|
|
|
async function tick(){
|
|
const active = await getTimer();
|
|
if (!active) return;
|
|
const idleFor = Date.now() - lastActivity;
|
|
if (idleFor >= IDLE_THRESHOLD_MS){
|
|
const stopTs = Date.now() - idleFor; // last active time
|
|
showIdlePrompt(stopTs);
|
|
}
|
|
}
|
|
|
|
setInterval(tick, CHECK_INTERVAL_MS);
|
|
})();
|
|
|
|
|