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