Ambient: Sleep-Timer zählt und stoppt Drift (Browser und App), ☾ im Ambient-Tab; sw v74
All checks were successful
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 6m55s

- 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 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-03 23:22:00 +02:00
parent b7a81f3e26
commit 794dbadbf5
5 changed files with 70 additions and 17 deletions

View file

@ -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);

View file

@ -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.

View file

@ -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',

View file

@ -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();

View file

@ -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;
}