From b7a81f3e26c83282220ce41f6a609015e58c4fdc Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sat, 3 Oct 2026 23:08:57 +0200 Subject: [PATCH] =?UTF-8?q?Neuer=20Tab=20=E2=80=9EAmbient=E2=80=9C:=20Drif?= =?UTF-8?q?t,=20ein=20generativer=20Ambient-Synthesizer;=20sw=20v73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - static/ambient/index.html: Drift (aus der Session "Ambient-Synthesizer-Webapp", eine Datei, Web Audio, offline), eingebettet per iframe (?embed=1&bg=#000) - app.js: Tab 'ambient', iframe lädt beim ersten Öffnen und füllt den Platz bis zur Leiste; Radio/Podcast und Ambient wechseln sich ab (postMessage {type:'ambient', cmd|state}) - sw.js: Ambient im Offline-Cache (ignoreSearch für die Shell-Dateien) Co-Authored-By: Claude Opus 5.5 --- static/ambient/index.html | 778 ++++++++++++++++++++++++++++++ static/css/app.css | 9 + static/js/app.js | 51 +- static/js/sw.js | 6 +- static/podcasts/native-podcast.js | 9 +- templates/radio/player.html | 9 + 6 files changed, 858 insertions(+), 4 deletions(-) create mode 100644 static/ambient/index.html diff --git a/static/ambient/index.html b/static/ambient/index.html new file mode 100644 index 0000000..fd541e0 --- /dev/null +++ b/static/ambient/index.html @@ -0,0 +1,778 @@ + + + + + +Drift – Ambient Synth + + + + +
+
+

Drift

+
Generativer Ambient-Synthesizer · alles wird live im Browser erzeugt
+
+ +
+ – + Noch still. +
+
+ +
+ + + + + + +
+ + + + + +
+
+ + + + diff --git a/static/css/app.css b/static/css/app.css index 050ed16..e6d8b7b 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -389,6 +389,15 @@ a:hover { min-height: 200px; } +/* Ambient: the synthesizer's frame fills the room down to the player bar (app.js _ambientSize). */ +.ambient-frame { + display: block; + width: 100%; + height: 70vh; + border: 0; + background: #000; +} + .sub-tabs { border-bottom-color: transparent; margin-bottom: 1rem; diff --git a/static/js/app.js b/static/js/app.js index 6016b1b..6e6a0cd 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -158,6 +158,7 @@ function customPrompt(message, defaultValue = '') { function playStation(url, name, stationId) { stopPlayback(false); + if (window.__dioraAmbientStop) window.__dioraAmbientStop(); // A secure page can't load plain-http streams (mixed content). Native shells // that play the stream themselves set window.DIORA_PLAYS_HTTP_STREAMS. @@ -1177,7 +1178,7 @@ async function editNotes(pk, current) { // Tabs // --------------------------------------------------------------------------- -const TOP_TABS = ['radio', 'focus', 'podcasts', 'books']; +const TOP_TABS = ['radio', 'focus', 'podcasts', 'books', 'ambient']; const RADIO_SUB_TABS = ['search', 'saved', 'history']; // --------------------------------------------------------------------------- @@ -1234,8 +1235,56 @@ function showTab(name) { _syncHash(); if (name === 'books') loadBookList(); + if (name === 'ambient') _ambientLoad(); } +// --------------------------------------------------------------------------- +// 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; + +function _ambientFrame() { + return $('ambient-frame'); +} + +function _ambientLoad() { + const f = _ambientFrame(); + if (!f) return; + if (!f.getAttribute('src')) 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 radio or a podcast starts: Ambient stops. +window.__dioraAmbientStop = function () { + const f = _ambientFrame(); + if (_ambientPlaying && f && f.contentWindow) f.contentWindow.postMessage({type: 'ambient', cmd: 'stop'}, '*'); +}; + +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) 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}`); diff --git a/static/js/sw.js b/static/js/sw.js index 8c93abc..0f6ab15 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v72'; +const CACHE = 'diora-v73'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', @@ -11,6 +11,7 @@ const SHELL = [ '/static/podcasts/podcast-ui.css', '/static/podcasts/podcasts-web.css', '/static/manifest.json', + '/static/ambient/index.html', '/static/fonts/SourceSerif4-normal-latin.woff2', '/static/fonts/SourceSerif4-normal-latin-ext.woff2', '/static/fonts/SourceSerif4-italic-latin.woff2', @@ -77,7 +78,8 @@ self.addEventListener('fetch', function (event) { const isShell = SHELL.some(function (s) { return url.pathname === s; }); if (isShell) { event.respondWith( - caches.match(event.request).then(function (cached) { + // ignoreSearch: the Ambient frame loads with ?embed=1&bg=… + caches.match(event.request, {ignoreSearch: true}).then(function (cached) { return cached || fetch(event.request); }) ); diff --git a/static/podcasts/native-podcast.js b/static/podcasts/native-podcast.js index 48023e6..0ad47fc 100644 --- a/static/podcasts/native-podcast.js +++ b/static/podcasts/native-podcast.js @@ -1,4 +1,4 @@ -// Copied from diora-android 370e653 2026-10-03: web-native/static/native-podcast.js +// Copied from diora-android d323614 2026-10-03: web-native/static/native-podcast.js // by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android // and copy again. The browser host is static/js/podcasts-web.js. @@ -4294,6 +4294,8 @@ window.__dioraPlayer = function (p) { const before = player; player = p || {kind: null}; + // Something starts playing here (an episode, the radio): Ambient stops (app.js). + if (player.playing && !before.playing && !player.remote && window.__dioraAmbientStop) window.__dioraAmbientStop(); renderMini(); renderPlayer(); // The current episode changed: the queue view marks it. @@ -4302,6 +4304,11 @@ if (epScreen && (before.episodeId !== player.episodeId || !!before.playing !== !!player.playing)) episodeScreenParts(); }; + // Ambient starts (app.js): an episode playing here pauses. + window.__dioraPausePlayer = function () { + if (player.kind === 'podcast' && player.playing && !player.remote && N.podcastToggle) N.podcastToggle(); + }; + // The app loads this in ; the browser may load it after the page is parsed. function start(fn) { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); diff --git a/templates/radio/player.html b/templates/radio/player.html index 9d57b91..78ee240 100644 --- a/templates/radio/player.html +++ b/templates/radio/player.html @@ -51,6 +51,7 @@ + @@ -254,6 +255,14 @@ {% endif %} + +{% comment %}"Drift", a generative ambient synthesizer (static/ambient/), in a frame of its own: its + script and styles stay apart from the page's. Loaded the first time the tab opens.{% endcomment %} + +