From 794dbadbf55503323a06280bc6811c7166f03b20 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sat, 3 Oct 2026 23:22:00 +0200 Subject: [PATCH] =?UTF-8?q?Ambient:=20Sleep-Timer=20z=C3=A4hlt=20und=20sto?= =?UTF-8?q?ppt=20Drift=20(Browser=20und=20App),=20=E2=98=BE=20im=20Ambient?= =?UTF-8?q?-Tab;=20sw=20v74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app.js: __dioraAmbientCmd, meldet Start/Stopp an die App (DioraNative.ambientState, sie hält Drift bei ausgeschaltetem Bildschirm am Laufen) und an den Browser-Host - podcasts-web.js: der Sleep-Timer zählt auch, solange Ambient spielt, stoppt es beim Ablauf; Nachtautomatik auch beim Ambient-Start Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 13 ++++++++-- static/js/podcasts-web.js | 13 ++++++++-- static/js/sw.js | 2 +- static/podcasts/native-podcast.js | 41 ++++++++++++++++++++++--------- static/podcasts/podcast-ui.css | 18 +++++++++++++- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 6e6a0cd..35ba6f4 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1268,17 +1268,26 @@ function _ambientSize() { } window.addEventListener('resize', () => requestAnimationFrame(_ambientSize)); +// A command for Drift ('start' | 'stop'): the app's notification, the sleep timer. +window.__dioraAmbientCmd = function (cmd) { + const f = _ambientFrame(); + if (f && f.contentWindow && f.getAttribute('src')) f.contentWindow.postMessage({type: 'ambient', cmd}, '*'); +}; + // The radio or a podcast starts: Ambient stops. window.__dioraAmbientStop = function () { - const f = _ambientFrame(); - if (_ambientPlaying && f && f.contentWindow) f.contentWindow.postMessage({type: 'ambient', cmd: 'stop'}, '*'); + if (_ambientPlaying) window.__dioraAmbientCmd('stop'); }; +window.__dioraAmbientPlaying = () => _ambientPlaying; window.addEventListener('message', e => { const f = _ambientFrame(); const d = e.data; if (!f || e.source !== f.contentWindow || !d || d.type !== 'ambient' || !d.state) return; _ambientPlaying = d.state === 'playing'; + // The app keeps it going with the screen off (AmbientService); the browser's sleep timer counts it. + if (window.DioraNative && DioraNative.ambientState) DioraNative.ambientState(_ambientPlaying); + if (window.__dioraOnAmbient) window.__dioraOnAmbient(_ambientPlaying); if (!_ambientPlaying) return; // Ambient starts: the radio and a podcast here stop. if (typeof isPlaying !== 'undefined' && isPlaying) stopPlayback(false); diff --git a/static/js/podcasts-web.js b/static/js/podcasts-web.js index becb8da..83c8c8e 100644 --- a/static/js/podcasts-web.js +++ b/static/js/podcasts-web.js @@ -544,6 +544,7 @@ setFade(1); pause(); stopRadio(); // the radio sleeps too (it plays in app.js's element) + if (window.__dioraAmbientStop) window.__dioraAmbientStop(); // and Ambient sleepChanged(); return; } @@ -574,6 +575,14 @@ }, }; + const ambientPlaying = () => !!(window.__dioraAmbientPlaying && window.__dioraAmbientPlaying()); + + // Ambient starts or stops (app.js): at night the timer may switch itself on; the page shows it. + window.__dioraOnAmbient = function (on) { + if (on) sleep.maybeAutoEnable(); + setTimeout(report, 100); + }; + // Set, changed or done here: the other devices show it (when this one is the active one). function sleepChanged() { if (typeof sync !== 'undefined' && sync.state && sync.state.device_id === DEVICE_ID) syncSoon(); @@ -924,8 +933,8 @@ if (playing && Date.now() - lastSaved > 10000) saveNow(id, true); if (playing) skipOutro(id); } - // The sleep timer counts while either plays (radio and podcasts share the player). - sleep.tick(playing || radioPlaying()); + // The sleep timer counts while any of them plays (radio and podcasts share the player; Ambient). + sleep.tick(playing || radioPlaying() || ambientPlaying()); // While playing, the shared player hears from us every 10 s (its place, and that we're alive). if ((playing || radioPlaying()) && Date.now() - sync.lastSent >= REPORT_EVERY_MS && !busy()) syncReport(); // Another device's place moves on in the bar too. diff --git a/static/js/sw.js b/static/js/sw.js index 0f6ab15..611a144 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v73'; +const CACHE = 'diora-v74'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', diff --git a/static/podcasts/native-podcast.js b/static/podcasts/native-podcast.js index 0ad47fc..df2a096 100644 --- a/static/podcasts/native-podcast.js +++ b/static/podcasts/native-podcast.js @@ -1,4 +1,4 @@ -// Copied from diora-android d323614 2026-10-03: web-native/static/native-podcast.js +// Copied from diora-android 67a915e 2026-10-03: web-native/static/native-podcast.js // by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android // and copy again. The browser host is static/js/podcasts-web.js. @@ -2825,6 +2825,22 @@ } } + // Ambient (app.js's #tab-ambient): its sleep timer, a ☾ at the frame's top right corner. + function ambientSleepButton() { + const tab = $id('tab-ambient'); + if (!tab || !N.podcastSleep || $id('nui-amb-sleep')) return; + const b = button('☾', 'Sleep-Timer', () => sleepMenu(true, true), 'nui-pod-sleep'); + b.id = 'nui-amb-sleep'; + tab.append(b); + } + + function renderAmbientSleep() { + const b = $id('nui-amb-sleep'); + if (!b) return; + b.textContent = sleepLabel(true) || '☾'; + b.classList.toggle('on', !!sleepLabel(true)); + } + // The bar names a running sleep timer: "… · ☾ 12 min". function sleepSuffix() { const l = sleepLabel(); @@ -2854,6 +2870,7 @@ } function renderMini() { + renderAmbientSleep(); const r = remote(); const isPod = player.kind === 'podcast'; document.documentElement.classList.toggle('nui-pod-playing', isPod || !!r); @@ -3516,14 +3533,15 @@ // ---- sleep timer (runs in the app, also with the screen off; episodes and the radio) ---- - // The timer that counts: the other device's while that one is the active one, else ours. - function sleepState() { - const r = remote(); + // The timer that counts: the other device's while that one is the active one, else ours + // (`local`: ours anyway — Ambient's button, Ambient plays here). + function sleepState(local) { + const r = local ? null : remote(); return (r ? r.sleep : player.sleep) || {}; } - function sleepLabel() { - const t = sleepState(); + function sleepLabel(local) { + const t = sleepState(local); if (t.episodes > 0) return t.episodes === 1 ? '☾ Folgenende' : `☾ ${t.episodes} Folgen`; if (t.leftMs > 0) return `☾ ${Math.max(1, Math.ceil(t.leftMs / 60000))} min`; return ''; @@ -3562,18 +3580,18 @@ // Minutes count only while playing; shaking the phone starts them over. With another // device playing, the timer is that device's (set there by the player sync). - function sleepMenu(radio) { - const r = remote(); + function sleepMenu(radio, local) { + const r = local ? null : remote(); const set = r ? (m => remoteCmd('sleep', m)) : N.podcastSleep ? (m => N.podcastSleep(m)) : null; if (!set) return; const onRadio = r ? r.kind === 'radio' : radio === true; - const t = sleepState(); + const t = sleepState(local); const own = player.sleep || {}; - const on = !!sleepLabel(); + const on = !!sleepLabel(local); const items = []; if (r) items.push({custom: el('div', 'nui-pod-meta nui-pod-devnote', `Der Sleep-Timer gilt für ${remoteName(r)}.`)}); if (on) { - items.push({icon: '✕', label: 'Ausschalten', hint: sleepLabel().replace('☾ ', 'läuft: '), action: () => set(0)}); + items.push({icon: '✕', label: 'Ausschalten', hint: sleepLabel(local).replace('☾ ', 'läuft: '), action: () => set(0)}); if (t.leftMs > 0) { for (const m of [5, 10]) { items.push({icon: '+', label: `${m} Minuten länger`, action: () => (!r && N.podcastSleepExtend @@ -4322,6 +4340,7 @@ window.refreshAllFeeds = async function () {}; build(); buildMini(); + ambientSleepButton(); player = parse(N.playerState(), {kind: null}); moveFeedListsToServer(); render(); diff --git a/static/podcasts/podcast-ui.css b/static/podcasts/podcast-ui.css index d040f06..7422bc7 100644 --- a/static/podcasts/podcast-ui.css +++ b/static/podcasts/podcast-ui.css @@ -1,4 +1,4 @@ -/* Copied from diora-android abda34a 2026-10-03: the podcast parts of web-native/static/native.css +/* Copied from diora-android 67a915e 2026-10-03: the podcast parts of web-native/static/native.css (bottom sheet, podcasts) by tools/sync_podcast_ui.py. Do not edit here. The web's own additions are in podcasts-web.css. */ @@ -780,3 +780,19 @@ body.eink .nui-pod-facts a { text-decoration-color: #000; } sheet comes above both (app.css has it at 500/501, under them). */ .modal-overlay { z-index: 990; } .modal-dialog { z-index: 991; } + +/* Ambient's sleep timer: a ☾ on the synthesizer's frame, top right (native-podcast.js). */ +#tab-ambient { position: relative; } +#nui-amb-sleep { + position: absolute; + top: 10px; + right: 10px; + z-index: 2; + min-width: 44px; + height: 36px; + padding: 0 12px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(0, 0, 0, 0.55); + color: #ddd; +}