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>
141 lines
6.1 KiB
JavaScript
141 lines
6.1 KiB
JavaScript
'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();
|
||
}
|