Podcasts: diora desktop kann den Browser-Host mitbenutzen (DIORA_PODCAST_HOST), sw v68
All checks were successful
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Successful in 6m39s

Setzt eine Seite window.DIORA_PODCAST_HOST, arbeitet podcasts-web.js trotz
vorhandenem DioraNative als ihr Podcast-Host:
- dock: 'existing'  nimmt native-radio.js' #nui-dock statt einer Leiste in der
  Web-Radioleiste; die Podcast-Seite lädt erst nach DOMContentLoaded
- styles: 'ui-only' nur podcast-ui.css, ohne das Browser-Layout (html.dpw)
- radio: false      die Radio-Hooks (__dioraRadioNow …) kommen von native-radio.js;
  der Sender legt die Folge weiterhin beiseite, Radio-Stopp meldet der Sync weiter
- deviceName/deviceKind  wie der gemeinsame Player das Gerät führt
Ohne die Variable ändert sich im Browser nichts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-02 19:39:54 +02:00
parent 3e67d324c7
commit 461b42a5bf
2 changed files with 73 additions and 52 deletions

View file

@ -23,17 +23,27 @@
* Radio and podcasts never play at the same time: starting an episode stops * Radio and podcasts never play at the same time: starting an episode stops
* the radio (app.js stopPlayback), starting a station puts the episode away. * the radio (app.js stopPlayback), starting a station puts the episode away.
* *
* In the app (and the desktop app) window.DioraNative exists and this file * In the app window.DioraNative exists and this file does nothing. diora
* does nothing. Written without ?. and ?? so old WebViews can still parse it. * desktop has a DioraNative too (books, radio) but no podcast part of its own:
* it sets window.DIORA_PODCAST_HOST and this file is its podcast host, fitted
* into the phone layout it already shows:
* dock: 'existing' use native-radio.js's #nui-dock instead of a bar in the web radio bar
* styles: 'ui-only' only the podcast page's own CSS, not the browser layout
* radio: false native-radio.js provides the radio hooks (__dioraRadioNow …)
* deviceName, deviceKind how the shared player lists this device
* Written without ?. and ?? so old WebViews can still parse it.
*/ */
(function () { (function () {
'use strict'; 'use strict';
if (window.DioraNative) return; const HOST = window.DIORA_PODCAST_HOST || null;
if (window.DioraNative && !HOST) return;
if (typeof IS_AUTHENTICATED === 'undefined' || !IS_AUTHENTICATED) return; if (typeof IS_AUTHENTICATED === 'undefined' || !IS_AUTHENTICATED) return;
if (!document.getElementById('tab-podcasts')) return; if (!document.getElementById('tab-podcasts')) return;
const OPT = Object.assign({dock: 'bar', styles: 'web', radio: true, deviceName: '', deviceKind: 'web'}, HOST || {});
const UI_SCRIPT = '/static/podcasts/native-podcast.js'; const UI_SCRIPT = '/static/podcasts/native-podcast.js';
const UI_STYLES = ['/static/podcasts/podcast-ui.css', '/static/podcasts/podcasts-web.css']; const UI_STYLES = OPT.styles === 'ui-only' ? ['/static/podcasts/podcast-ui.css']
: ['/static/podcasts/podcast-ui.css', '/static/podcasts/podcasts-web.css'];
// ---- storage ------------------------------------------------------------------ // ---- storage ------------------------------------------------------------------
@ -890,31 +900,34 @@
const radioPlaying = () => typeof isPlaying !== 'undefined' && !!isPlaying; const radioPlaying = () => typeof isPlaying !== 'undefined' && !!isPlaying;
window.__dioraRadioNow = function () { // diora desktop: native-radio.js already shows the radio in the shared bar.
const st = typeof currentStation !== 'undefined' ? currentStation : null; if (OPT.radio) {
if (!st) return null; window.__dioraRadioNow = function () {
return { const st = typeof currentStation !== 'undefined' ? currentStation : null;
name: st.name || '', if (!st) return null;
logo: radioLogo(st), return {
track: typeof currentTrack !== 'undefined' ? currentTrack : '', name: st.name || '',
playing: radioPlaying(), logo: radioLogo(st),
buffering: false, track: typeof currentTrack !== 'undefined' ? currentTrack : '',
saved: radioSaved(st.url), playing: radioPlaying(),
buffering: false,
saved: radioSaved(st.url),
};
}; };
}; // Stop keeps the station (like a paused episode): the bar stays with ▶.
// Stop keeps the station (like a paused episode): the bar stays with ▶. window.__dioraRadioToggle = () => (radioPlaying() ? stopPlayback(false) : togglePlayStop());
window.__dioraRadioToggle = () => (radioPlaying() ? stopPlayback(false) : togglePlayStop()); window.__dioraRadioSave = () => saveCurrentStation();
window.__dioraRadioSave = () => saveCurrentStation();
for (const name of ['stopPlayback', 'updateNowPlayingUI', 'saveCurrentStation']) { for (const name of ['stopPlayback', 'updateNowPlayingUI', 'saveCurrentStation']) {
const fn = window[name]; const fn = window[name];
if (typeof fn !== 'function') continue; if (typeof fn !== 'function') continue;
window[name] = function () { window[name] = function () {
const r = fn.apply(this, arguments); const r = fn.apply(this, arguments);
if (r && typeof r.then === 'function') r.then(radioChanged, radioChanged); if (r && typeof r.then === 'function') r.then(radioChanged, radioChanged);
else radioChanged(); else radioChanged();
return r; return r;
}; };
}
} }
// The radio stopping is news for the shared player too. // The radio stopping is news for the shared player too.
if (typeof window.stopPlayback === 'function') { if (typeof window.stopPlayback === 'function') {
@ -931,7 +944,7 @@
// Player sync: the station another device played last becomes the current // Player sync: the station another device played last becomes the current
// one here, without playing (the bar shows it with ▶). // one here, without playing (the bar shows it with ▶).
window.__dioraAdoptStation = function (st) { if (OPT.radio) window.__dioraAdoptStation = function (st) {
if (!st || !st.url || radioPlaying() || typeof currentStation === 'undefined') return; if (!st || !st.url || radioPlaying() || typeof currentStation === 'undefined') return;
currentStation = {url: st.url, name: st.name || '', id: st.id || null}; currentStation = {url: st.url, name: st.name || '', id: st.id || null};
currentTrack = ''; currentTrack = '';
@ -964,7 +977,7 @@
let id = ''; let id = '';
try { id = localStorage.getItem('diora_device_id') || ''; } catch (e) {} try { id = localStorage.getItem('diora_device_id') || ''; } catch (e) {}
if (!id) { if (!id) {
id = 'web-' + Array.from(crypto.getRandomValues(new Uint8Array(12)), b => b.toString(16).padStart(2, '0')).join(''); id = OPT.deviceKind + '-' + Array.from(crypto.getRandomValues(new Uint8Array(12)), b => b.toString(16).padStart(2, '0')).join('');
try { localStorage.setItem('diora_device_id', id); } catch (e) {} try { localStorage.setItem('diora_device_id', id); } catch (e) {}
} }
return id; return id;
@ -981,7 +994,7 @@
const DEVICE_ID = deviceId(); const DEVICE_ID = deviceId();
// Short, it stands in the bar ("läuft auf Chrome · Windows"). // Short, it stands in the bar ("läuft auf Chrome · Windows").
const DEVICE_NAME = browserName(); const DEVICE_NAME = OPT.deviceName || browserName();
const sync = { const sync = {
state: null, // the server's state, as last taken in state: null, // the server's state, as last taken in
@ -1362,7 +1375,7 @@
clearTimeout(retryTimer); clearTimeout(retryTimer);
if (source) source.close(); if (source) source.close();
const seen = Math.max(sync.state ? sync.state.seq : -1, sync.later ? sync.later.seq : -1); const seen = Math.max(sync.state ? sync.state.seq : -1, sync.later ? sync.later.seq : -1);
const q = new URLSearchParams({since: String(seen), device_id: DEVICE_ID, device_name: DEVICE_NAME, device_kind: 'web'}); const q = new URLSearchParams({since: String(seen), device_id: DEVICE_ID, device_name: DEVICE_NAME, device_kind: OPT.deviceKind});
source = new EventSource('/podcasts/player/stream/?' + q); source = new EventSource('/podcasts/player/stream/?' + q);
source.onopen = () => { retryMs = 1000; }; source.onopen = () => { retryMs = 1000; };
source.onmessage = e => heard(parse(e.data, null)); source.onmessage = e => heard(parse(e.data, null));
@ -1603,7 +1616,8 @@
// ---- page set-up ------------------------------------------------------------------------- // ---- page set-up -------------------------------------------------------------------------
document.documentElement.classList.add('dpw'); // The browser layout (podcasts-web.css hangs off html.dpw); diora desktop keeps the phone's.
if (OPT.styles !== 'ui-only') document.documentElement.classList.add('dpw');
for (const href of UI_STYLES) { for (const href of UI_STYLES) {
const link = document.createElement('link'); const link = document.createElement('link');
@ -1615,24 +1629,27 @@
// The dock the page puts its player bar in (native-radio.js builds it in the app): // The dock the page puts its player bar in (native-radio.js builds it in the app):
// here it is part of the radio bar at the bottom, in place of its "now playing", // here it is part of the radio bar at the bottom, in place of its "now playing",
// so radio and podcasts share one bar. // so radio and podcasts share one bar.
const dock = document.createElement('div'); // diora desktop: native-radio.js builds the dock (on DOMContentLoaded) with the tab bar.
dock.id = 'nui-dock'; if (OPT.dock !== 'existing') {
const bar = document.getElementById('now-playing-bar'); const dock = document.createElement('div');
if (bar) { dock.id = 'nui-dock';
bar.insertBefore(dock, bar.firstChild); const bar = document.getElementById('now-playing-bar');
document.documentElement.classList.add('dpw-in-bar'); if (bar) {
} else { bar.insertBefore(dock, bar.firstChild);
document.body.appendChild(dock); document.documentElement.classList.add('dpw-in-bar');
} } else {
document.body.appendChild(dock);
}
// The page keeps clear of the bar, whose height changes with the screen. // The page keeps clear of the bar, whose height changes with the screen.
const placeDock = () => { const placeDock = () => {
const h = bar ? bar.getBoundingClientRect().height : 0; const h = bar ? bar.getBoundingClientRect().height : 0;
document.documentElement.style.setProperty('--dpw-bar-h', Math.round(h) + 'px'); document.documentElement.style.setProperty('--dpw-bar-h', Math.round(h) + 'px');
}; };
placeDock(); placeDock();
if (bar && window.ResizeObserver) new ResizeObserver(placeDock).observe(bar); if (bar && window.ResizeObserver) new ResizeObserver(placeDock).observe(bar);
window.addEventListener('resize', placeDock); window.addEventListener('resize', placeDock);
}
// Escape closes what the podcast page has open: a sheet, the player, notes, // Escape closes what the podcast page has open: a sheet, the player, notes,
// a podcast, choosing several (its back handler) — never the tab itself. // a podcast, choosing several (its back handler) — never the tab itself.
@ -1696,8 +1713,12 @@
if (typeof _currentTopTab === 'undefined' || _currentTopTab !== tab) showTab(tab); if (typeof _currentTopTab === 'undefined' || _currentTopTab !== tab) showTab(tab);
})(); })();
// The page (copied from the app) comes last, once all of the above is in place. // The page (copied from the app) comes last, once all of the above is in place —
// with an existing dock, also after the page's own DOMContentLoaded work built it.
const script = document.createElement('script'); const script = document.createElement('script');
script.src = UI_SCRIPT; script.src = UI_SCRIPT;
document.body.appendChild(script); if (OPT.dock !== 'existing') document.body.appendChild(script);
else if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(() => document.body.appendChild(script), 0));
} else setTimeout(() => document.body.appendChild(script), 0);
})(); })();

View file

@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use. * diora service worker — caches the app shell for offline use.
*/ */
const CACHE = 'diora-v67'; const CACHE = 'diora-v68';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',
'/static/js/app.js', '/static/js/app.js',