diora-web/static/js/app/24-ambient.js
mrwnslz 656520fe8e
Some checks failed
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 8m23s
Test / e2e (push) Failing after 1m54s
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.

Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00

141 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
// ---------------------------------------------------------------------------
// Ambient: "Drift" (static/ambient/), a generative synthesizer in a frame of its
// own. It and the radio / podcast player take turns — one starting stops the
// other — by postMessage: {type: 'ambient', cmd: 'start'|'stop'} to the frame,
// {type: 'ambient', state: 'playing'|'stopped'} from it.
// ---------------------------------------------------------------------------
let _ambientPlaying = false;
let _ambientLast = false; // Ambient was started last: the player bar shows it (native-podcast.js)
function _ambientFrame() {
return $('ambient-frame');
}
function _ambientLoad() {
const f = _ambientFrame();
if (!f) return;
if (!f.getAttribute('src')) {
// Drift takes the page's volume before its first sound; the bar follows its sound world.
f.addEventListener('load', () => {
_ambientVolume();
try {
const sel = f.contentDocument.getElementById('mode');
// The sound world chosen last comes back (Drift itself starts with its default).
let last = '';
try { last = localStorage.getItem('diora_ambient_mode') || ''; } catch (e) {}
if (last && last !== sel.value && [...sel.options].some(o => o.value === last)) {
f.contentWindow.postMessage({type: 'ambient', cmd: 'mode', mode: last}, '*');
}
sel.addEventListener('change', () => {
try {
localStorage.setItem('diora_ambient_mode', sel.value);
localStorage.setItem('diora_ambient_mode_label', sel.selectedOptions[0] ? sel.selectedOptions[0].text : '');
} catch (e) {}
if (window.__dioraAmbientChanged) window.__dioraAmbientChanged();
});
} catch (e) {}
});
f.src = f.dataset.src;
}
requestAnimationFrame(_ambientSize);
}
// The frame fills the room between the tabs and the player bar at the bottom.
function _ambientSize() {
const f = _ambientFrame();
if (!f || _currentTopTab !== 'ambient') return;
const root = getComputedStyle(document.documentElement);
const bar = parseFloat(root.getPropertyValue('--nui-dock-h')) || parseFloat(root.getPropertyValue('--dpw-bar-h')) || 0;
const top = f.getBoundingClientRect().top + window.scrollY;
f.style.height = Math.max(320, Math.floor(window.innerHeight - top - bar)) + 'px';
}
window.addEventListener('resize', () => requestAnimationFrame(_ambientSize));
// The bar at the bottom grows or shrinks (the player appears, a second line …): its height
// comes as --nui-dock-h / --dpw-bar-h on <html>, set inline — measure again then.
new MutationObserver(() => requestAnimationFrame(_ambientSize))
.observe(document.documentElement, {attributes: true, attributeFilter: ['style']});
// A command for Drift ('start' | 'stop'): the app's notification, the sleep timer.
window.__dioraAmbientCmd = function (cmd, value) {
const f = _ambientFrame();
if (f && f.contentWindow && f.getAttribute('src')) f.contentWindow.postMessage({type: 'ambient', cmd, value}, '*');
};
// Start Drift from elsewhere (Home): load the frame if needed, then start it — with the
// sound world chosen last, which the frame's load handler above sets.
window.__dioraAmbientStart = function () {
const f = _ambientFrame();
if (!f) return;
const go = () => window.__dioraAmbientCmd('start');
let ready = false;
try { ready = !!f.getAttribute('src') && f.contentDocument.readyState === 'complete' && !!f.contentDocument.getElementById('mode'); } catch (e) {}
if (ready) { go(); return; }
f.addEventListener('load', () => setTimeout(go, 0), {once: true});
_ambientLoad();
};
// The radio or a podcast starts: Ambient stops, and the player bar is theirs again.
window.__dioraAmbientStop = function () {
if (_ambientPlaying) window.__dioraAmbientCmd('stop');
if (_ambientLast) {
_ambientLast = false;
if (window.__dioraAmbientChanged) window.__dioraAmbientChanged();
}
};
window.__dioraAmbientPlaying = () => _ambientPlaying;
// For the player bar: playing, started last, the sound world's name (the frame is ours).
window.__dioraAmbientInfo = function () {
const f = _ambientFrame();
let mode = '';
try {
const sel = f && f.contentDocument && f.contentDocument.getElementById('mode');
if (sel && sel.selectedOptions[0]) mode = sel.selectedOptions[0].text;
} catch (e) {}
return {playing: _ambientPlaying, last: _ambientLast, mode};
};
// The page's volume (0–255, the bar's slider) is Drift's too; its own slider hides when
// embedded. Not in the Android app: there the volume keys set it, as for everything.
function _ambientVolume(val) {
if (window.DioraNative && DioraNative.ambientState) return;
if (val == null) {
const s = $('volume');
val = s ? +s.value : +(localStorage.getItem('diora_volume') || 102);
}
window.__dioraAmbientCmd && window.__dioraAmbientCmd('volume', val / 255);
}
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';
if (_ambientPlaying) _ambientLast = true;
if (window.__dioraAmbientChanged) window.__dioraAmbientChanged();
// 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);
if (window.__dioraPausePlayer) window.__dioraPausePlayer();
});
function showRadioTab(name) {
RADIO_SUB_TABS.forEach(p => {
const panel = $(`tab-${p}`);
if (panel) panel.style.display = (p === name) ? '' : 'none';
});
document.querySelectorAll('#radio-sub-tabs .tab-btn').forEach((btn, i) => {
btn.classList.toggle('active', RADIO_SUB_TABS[i] === name);
});
localStorage.setItem('diora_active_radio_tab', name);
_syncHash();
if (name === 'saved') loadRecommendations();
}