Focus timer with live progress, focus/break sliders, phase notices, and sounds.
window.ggbro. This is the complete plugin.
{
"id": "com.ggbro.plugin.pomodoro",
"name": "Pomodoro Focus",
"version": "3.0.0",
"author": "ggbro",
"description": "Focus timer with live progress, focus/break sliders, phase notices, and sounds.",
"main": "plugin.js",
"styles": "styles.css",
"permissions": ["ui.toast", "ui.panel", "ui.sound", "storage"]
}
/**
* Pomodoro Focus v3 — live progress, sliders, phase notices.
*/
(async function () {
var workMin = 25;
var breakMin = 5;
var phase = 'idle'; // idle | work | break | paused
var pausedFrom = 'work';
var cycles = 0;
var timer = null;
var tick = null;
var endsAt = 0;
var remainingMs = 0;
var totalMs = 0;
var soundOn = true;
try {
cycles = parseInt((await ggbro.storage.get('cycles')).value || '0', 10) || 0;
var savedWork = parseInt((await ggbro.storage.get('workMin')).value || '25', 10);
if (savedWork >= 5 && savedWork <= 60) workMin = savedWork;
var savedBreak = parseInt((await ggbro.storage.get('breakMin')).value || '5', 10);
if (savedBreak >= 1 && savedBreak <= 30) breakMin = savedBreak;
soundOn = ((await ggbro.storage.get('soundOn')).value || '1') !== '0';
} catch (e) {
ggbro.log('storage', e && e.message);
}
function clearTimer() {
if (timer) clearTimeout(timer);
timer = null;
}
function clearTick() {
if (tick) clearInterval(tick);
tick = null;
}
function pctLeft() {
if (!totalMs || phase === 'idle') return 0;
var left = phase === 'paused' ? remainingMs : Math.max(0, endsAt - Date.now());
return Math.round(((totalMs - left) / totalMs) * 100);
}
function clockLeft() {
var left = phase === 'paused' ? remainingMs : Math.max(0, endsAt - Date.now());
if (phase === 'idle') return '—';
var s = Math.ceil(left / 1000);
var m = Math.floor(s / 60);
var r = s % 60;
return m + ':' + (r < 10 ? '0' : '') + r;
}
function statusLine() {
if (phase === 'idle') return 'Idle — press Start focus';
if (phase === 'paused') return 'Paused · ' + clockLeft() + ' left (' + pausedFrom + ')';
return (phase === 'work' ? 'Focus' : 'Break') + ' · ' + clockLeft() + ' left';
}
function phaseNotice() {
if (phase === 'work') return { text: 'Focus mode — notifications can wait.', tone: 'success' };
if (phase === 'break') return { text: 'Break time — stretch, water, look away.', tone: 'info' };
if (phase === 'paused') return { text: 'Paused — resume when you’re ready.', tone: 'warning' };
return { text: 'Pick a focus length, then Start. Progress updates live.', tone: 'info' };
}
async function paint() {
var notice = phaseNotice();
await ggbro.ui.registerPanel({
id: 'timer',
title: 'Pomodoro Focus',
schema: [
{ type: 'notice', id: 'phase', text: notice.text, tone: notice.tone },
{ type: 'section', id: 's0', title: 'Status' },
{ type: 'text', id: 'status', label: 'Now', value: statusLine() },
{ type: 'progress', id: 'prog', label: 'Phase progress', value: pctLeft(), max: 100 },
{ type: 'text', id: 'cycles', label: 'Completed focuses', value: String(cycles) },
{ type: 'section', id: 's1', title: 'Durations' },
{
type: 'slider',
id: 'workMin',
label: 'Focus (minutes)',
value: workMin,
min: 5,
max: 60,
step: 5,
},
{
type: 'slider',
id: 'breakMin',
label: 'Break (minutes)',
value: breakMin,
min: 1,
max: 30,
step: 1,
},
{ type: 'toggle', id: 'sound', label: 'Play sound on phase change', value: soundOn },
{ type: 'section', id: 's2', title: 'Controls' },
{ type: 'button', id: 'start', label: phase === 'paused' ? 'Resume' : 'Start focus' },
{ type: 'button', id: 'pause', label: 'Pause' },
{ type: 'button', id: 'skip', label: 'Skip phase' },
{ type: 'button', id: 'reset', label: 'Reset timer' },
],
});
}
function ding(id) {
if (!soundOn) return;
ggbro.playSound({ id: id || 'notification' }).catch(function () {});
}
function armTick() {
clearTick();
tick = setInterval(function () {
if (phase === 'work' || phase === 'break') paint().catch(function () {});
}, 1000);
}
function schedule(ms, fn) {
clearTimer();
totalMs = ms;
endsAt = Date.now() + ms;
remainingMs = ms;
timer = setTimeout(fn, ms);
armTick();
}
function startWork() {
phase = 'work';
ding('voiceJoin');
ggbro.toast({ message: 'Focus ' + workMin + ' min — you’ve got this.', type: 'success' });
schedule(workMin * 60 * 1000, onWorkDone);
paint();
}
function onWorkDone() {
cycles += 1;
ggbro.storage.set('cycles', String(cycles)).catch(function () {});
phase = 'break';
ding('notification');
ggbro.toast({
message: 'Focus #' + cycles + ' done — ' + breakMin + ' min break.',
type: 'success',
});
schedule(breakMin * 60 * 1000, onBreakDone);
paint();
}
function onBreakDone() {
phase = 'idle';
clearTimer();
clearTick();
remainingMs = 0;
totalMs = 0;
ding('boing');
ggbro.toast({ message: 'Break over — start another focus when ready.', type: 'info' });
paint();
}
await paint();
await ggbro.toast({
message: 'Pomodoro v3 ready — live countdown in Settings → Plugins (or Apps).',
type: 'info',
});
ggbro.onEvent(function (event, payload) {
if (event !== 'panelAction' || !payload) return;
var id = payload.fieldId;
if (id === 'workMin') {
var w = Math.max(5, Math.min(60, Number(payload.value) || 25));
workMin = Math.round(w / 5) * 5 || 25;
ggbro.storage.set('workMin', String(workMin)).catch(function () {});
paint();
ggbro.toast({ message: 'Focus length set to ' + workMin + ' min', type: 'info' });
return;
}
if (id === 'breakMin') {
breakMin = Math.max(1, Math.min(30, Number(payload.value) || 5));
ggbro.storage.set('breakMin', String(breakMin)).catch(function () {});
paint();
ggbro.toast({ message: 'Break set to ' + breakMin + ' min', type: 'info' });
return;
}
if (id === 'sound') {
soundOn = !!payload.value;
ggbro.storage.set('soundOn', soundOn ? '1' : '0').catch(function () {});
return;
}
if (id === 'start') {
if (phase === 'paused' && remainingMs > 0) {
phase = pausedFrom;
schedule(remainingMs, phase === 'work' ? onWorkDone : onBreakDone);
ggbro.toast({ message: 'Resumed ' + phase, type: 'info' });
paint();
return;
}
if (phase === 'work' || phase === 'break') {
ggbro.toast({ message: 'Already running — Pause or Skip first.', type: 'warning' });
return;
}
startWork();
return;
}
if (id === 'pause') {
if (phase !== 'work' && phase !== 'break') {
ggbro.toast({ message: 'Nothing to pause', type: 'warning' });
return;
}
remainingMs = Math.max(0, endsAt - Date.now());
pausedFrom = phase;
clearTimer();
clearTick();
phase = 'paused';
ggbro.toast({ message: 'Paused', type: 'warning' });
paint();
return;
}
if (id === 'skip') {
clearTimer();
clearTick();
if (phase === 'work' || (phase === 'paused' && pausedFrom === 'work')) {
onWorkDone();
} else if (phase === 'break' || (phase === 'paused' && pausedFrom === 'break')) {
onBreakDone();
} else {
ggbro.toast({ message: 'Nothing to skip', type: 'warning' });
}
return;
}
if (id === 'reset') {
clearTimer();
clearTick();
phase = 'idle';
remainingMs = 0;
totalMs = 0;
ggbro.toast({ message: 'Timer reset', type: 'info' });
paint();
}
});
})();
:root {
--ggbro-plugin-pomodoro: 1;
}
# Pomodoro Focus (v2)
Interactive focus timer — does **not** auto-spam a 25m cycle on load.
Use the panel under **Settings → Plugins**:
- Start / Pause / Resume / Skip / Reset
- Focus length 15 / 25 / 45
- Optional phase-change sounds
- Completed focus count persisted locally