experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(timers): HUD widget, custom sound URL, list view

- New `timers` HUD widget consumes the existing `timer:hud-update` topic
and shows the running timer's display (or count when >1). HUD config
version bumped to 3 so the widget appears in default layouts.
- `playNotification(timer)` plays `metadata.notification.sound` as audio
when set, falling back to the synth bell. Wired into completion +
interval notifications.
- Timers home grids switched to `view-mode="list"` per spec.

+116 -6
+2 -1
app/hud/hud.js
··· 12 12 const debug = api.debug; 13 13 14 14 const HUD_SHEET_KEY = 'hud_sheet'; 15 - const CURRENT_CONFIG_VERSION = 2; 15 + const CURRENT_CONFIG_VERSION = 3; 16 16 17 17 // Default HUD sheet layout — arranges widget pages vertically. 18 18 const WIDGET_WIDTH = 220; ··· 24 24 { id: 'mode', url: 'peek://hud/widgets/mode.html' }, 25 25 { id: 'izui', url: 'peek://hud/widgets/izui.html' }, 26 26 { id: 'window', url: 'peek://hud/widgets/window.html' }, 27 + { id: 'timers', url: 'peek://hud/widgets/timers.html' }, 27 28 { id: 'stats', url: 'peek://hud/widgets/stats.html', height: STATS_WIDGET_HEIGHT }, 28 29 ]; 29 30
+27
app/hud/widgets/timers.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 + <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 + <title>Timers</title> 8 + <link rel="stylesheet" type="text/css" href="widget.css"> 9 + <style> 10 + .widget-value.timers-value { 11 + font-variant-numeric: tabular-nums; 12 + } 13 + .widget-value.timers-value.has-active { 14 + color: var(--theme-text, #333); 15 + font-weight: 500; 16 + } 17 + </style> 18 + </head> 19 + <body> 20 + <div class="widget"> 21 + <div class="widget-label">Timers</div> 22 + <div id="timers-value" class="widget-value widget-value-truncate timers-value">—</div> 23 + </div> 24 + 25 + <script type="module" src="timers.js"></script> 26 + </body> 27 + </html>
+59
app/hud/widgets/timers.js
··· 1 + /** 2 + * Timers Widget — displays active timer count + display from timer:hud-update. 3 + * 4 + * The timers feature publishes timer:hud-update once per tick with 5 + * { text, count } when timers are running, or { text: '' } when none. 6 + */ 7 + 8 + const api = window.app; 9 + const debug = api.debug; 10 + 11 + const valueEl = document.getElementById('timers-value'); 12 + 13 + const setIdle = () => { 14 + if (!valueEl) return; 15 + valueEl.textContent = '—'; 16 + valueEl.classList.remove('has-active'); 17 + }; 18 + 19 + const setActive = (text) => { 20 + if (!valueEl) return; 21 + valueEl.textContent = text; 22 + valueEl.classList.add('has-active'); 23 + }; 24 + 25 + const handleHudUpdate = (msg) => { 26 + if (!msg || !msg.text) { 27 + setIdle(); 28 + return; 29 + } 30 + setActive(msg.text); 31 + debug && console.log('[hud:timers] Updated:', msg.text); 32 + }; 33 + 34 + const init = async () => { 35 + if (api.initialize) { 36 + await api.initialize(); 37 + } 38 + 39 + setIdle(); 40 + 41 + api.pubsub.subscribe('timer:hud-update', handleHudUpdate); 42 + 43 + // Pull current state on load — background may already have running timers 44 + // and we'll only see ticks once a second; this avoids a flash of "—". 45 + api.pubsub.subscribe('timer:state-response', (msg) => { 46 + if (!msg || !Array.isArray(msg.timers)) return; 47 + const running = msg.timers.filter((t) => t.metadata && t.metadata.status === 'running'); 48 + if (running.length === 0) { 49 + setIdle(); 50 + return; 51 + } 52 + // Background recomputes hud text from tickData, but on first load before a 53 + // tick fires, just show the count so the widget isn't blank. 54 + setActive(running.length === 1 ? '1 timer' : `${running.length} timers`); 55 + }); 56 + api.pubsub.publish('timer:request-state', {}); 57 + }; 58 + 59 + init().catch((error) => console.error('[hud:timers] Init error:', error));
+26 -3
features/timers/background.js
··· 187 187 // ===== Bell Sound ===== 188 188 189 189 /** 190 + * Play a notification sound for a timer. If the timer's notification.sound 191 + * is a URL string, fetch and play it as audio; otherwise play the synth bell. 192 + */ 193 + const playNotification = (timer) => { 194 + const url = timer && timer.metadata && timer.metadata.notification && timer.metadata.notification.sound; 195 + if (typeof url === 'string' && url.length > 0) { 196 + try { 197 + const audio = new Audio(url); 198 + const result = audio.play(); 199 + if (result && typeof result.catch === 'function') { 200 + result.catch((err) => { 201 + debug && console.log('[ext:timers] Custom sound playback failed, falling back to bell:', err); 202 + playBell(); 203 + }); 204 + } 205 + return; 206 + } catch (err) { 207 + debug && console.log('[ext:timers] Custom sound construction failed, falling back to bell:', err); 208 + } 209 + } 210 + playBell(); 211 + }; 212 + 213 + /** 190 214 * Play a bell tone using Web Audio API (no external file needed) 191 215 */ 192 216 const playBell = () => { ··· 324 348 debug && console.log('[ext:timers] Notification failed:', err); 325 349 } 326 350 327 - // Play bell sound 328 - playBell(); 351 + playNotification(timer); 329 352 330 353 // Publish completion event 331 354 api.pubsub.publish('timer:completed', { ··· 352 375 debug && console.log('[ext:timers] Interval notification failed:', err); 353 376 } 354 377 355 - playBell(); 378 + playNotification(timer); 356 379 357 380 api.pubsub.publish('timer:interval', { 358 381 id: timer.id,
+2 -2
features/timers/home.html
··· 38 38 39 39 <div class="section active-section" id="active-section"> 40 40 <div class="section-label">Active</div> 41 - <peek-grid class="cards active-cards" min-item-width="250" gap="10"></peek-grid> 41 + <peek-grid class="cards active-cards" view-mode="list" gap="8"></peek-grid> 42 42 </div> 43 43 44 44 <div class="section" id="completed-section"> 45 45 <div class="section-label">History</div> 46 - <peek-grid class="cards completed-cards" min-item-width="250" gap="10"></peek-grid> 46 + <peek-grid class="cards completed-cards" view-mode="list" gap="6"></peek-grid> 47 47 </div> 48 48 49 49 <script type="module" src="home.js"></script>