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