diora-web/static/js/app/24-ambient.js

142 lines
6.1 KiB
JavaScript
Raw Permalink Normal View History

'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();
}