1// Visual session timer â shrinking ring, idle pill -> minute presets -> ring. 2// Exposes window.MathTimer. 3 4(function () { 5 const TWO_MIN = 120; 6 7 function ringSvg(size) { 8 return '<svg class="ft-ring" width="' + size + '" height="' + size + '" viewBox="0 0 36 36">' + 9 '<circle class="ft-ring-trk" cx="18" cy="18" r="15.9" pathLength="100"></circle>' + 10 '<circle class="ft-ring-prg" cx="18" cy="18" r="15.9" pathLength="100"></circle></svg>'; 11 } 12 13 function fmt(sec) { 14 const m = Math.floor(sec / 60); 15 const s = sec % 60; 16 return m + ':' + String(s).padStart(2, '0'); 17 } 18 19 function create(minutes, el, callbacks) { 20 const cb = callbacks || {}; 21 const total = Math.max(1, Math.round(minutes * 60)); 22 let remaining = total; 23 let handle = null; 24 let warned = false; 25 26 el.classList.add('ft-timer'); 27 el.innerHTML = ringSvg(el.dataset.size || 96) + '<span class="ft-time"></span>'; 28 const prg = el.querySelector('.ft-ring-prg'); 29 const label = el.querySelector('.ft-time'); 30 31 function paint() { 32 const pct = Math.max(0, (remaining / total) * 100); 33 prg.style.strokeDasharray = pct + ' 100'; 34 label.textContent = fmt(remaining); 35 el.classList.toggle('warn', remaining <= TWO_MIN && remaining > 0); 36 el.classList.toggle('done', remaining <= 0); 37 } 38 39 function tick() { 40 remaining -= 1; 41 if (remaining <= TWO_MIN && !warned) { warned = true; if (cb.onWarn) cb.onWarn(); } 42 if (remaining <= 0) { 43 remaining = 0; 44 paint(); 45 stop(); 46 if (cb.onEnd) cb.onEnd(); 47 return; 48 } 49 paint(); 50 if (cb.onTick) cb.onTick(remaining); 51 } 52 53 function start() { if (!handle) handle = setInterval(tick, 1000); } 54 function stop() { if (handle) { clearInterval(handle); handle = null; } } 55 56 paint(); 57 return { start, stop, get remaining() { return remaining; } }; 58 } 59 60 const CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>'; 61 62 function mountDock() { 63 if (document.getElementById('ftTimerDock')) return; 64 const host = document.querySelector('.topbar-right'); 65 if (!host) return; 66 const dock = document.createElement('div'); 67 dock.id = 'ftTimerDock'; 68 dock.className = 'ft-dock'; 69 host.insertBefore(dock, host.firstChild); 70 71 let ctrl = null; 72 73 function idle() { 74 if (ctrl) { ctrl.stop(); ctrl = null; } 75 dock.innerHTML = '<button class="ft-dock-open" aria-label="Timer">' + CLOCK + ' <span>Timer</span></button>'; 76 dock.querySelector('.ft-dock-open').addEventListener('click', pick); 77 } 78 79 function pick() { 80 dock.innerHTML = '<span class="ft-dock-lbl">min</span>' + 81 [5, 10, 15, 20].map(m => 82 '<button class="ft-dock-min" data-m="' + m + '">' + m + '</button>').join('') + 83 '<button class="ft-dock-x" aria-label="Close">×</button>'; 84 dock.querySelectorAll('.ft-dock-min').forEach(b => b.addEventListener('click', () => run(+b.dataset.m))); 85 dock.querySelector('.ft-dock-x').addEventListener('click', idle); 86 } 87 88 function run(minutes) { 89 dock.innerHTML = '<span class="ft-dock-ring" data-size="34"></span><button class="ft-dock-x" aria-label="Stop timer">×</button>'; 90 const el = dock.querySelector('.ft-dock-ring'); 91 ctrl = create(minutes, el, { onEnd: done }); 92 ctrl.start(); 93 dock.querySelector('.ft-dock-x').addEventListener('click', idle); 94 } 95 96 function done() { 97 ctrl = null; 98 dock.innerHTML = '<span class="ft-dock-done">Time!</span>'; 99 setTimeout(idle, 6000); 100 } 101 102 idle(); 103 } 104 105 if (document.readyState === 'loading') { 106 document.addEventListener('DOMContentLoaded', mountDock); 107 } else { 108 mountDock(); 109 } 110 111 window.MathTimer = { create, fmt }; 112})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.