Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// Copied from diora-android 304abf0 2026-10-02: web-native/static/native-podcast.js
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// 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.
// Podcasts for the phone, the AntennaPod way: the queue comes first.
//
// The app keeps the podcast state (Podcasts.kt: feeds, queue, inbox and
// settings, fetched from the server in one request and kept for offline use)
// and plays it (RadioService: the queue is the playlist, it keeps the place
// and moves on by itself). This page only shows that state and hands what
// the user does to DioraNative.podcast*. The web app's own podcast tab stays
// hidden; its endpoints are the server side of all this.
//
// The browser runs this same file (diora-web copies it, tools/sync_podcast_ui.py):
// there window.DioraPodcastHost (diora-web static/js/podcasts-web.js) plays the
// part of the app, with the same calls.
( function ( ) {
'use strict' ;
const N = window . DioraPodcastHost || window . DioraNative ;
if ( ! N || ! N . podcastState ) return ;
// E-readers have no speaker; they stay a pure book app.
if ( N . isEinkDevice && N . isEinkDevice ( ) ) return ;
document . documentElement . classList . add ( 'nui-podcasts' ) ;
const $id = id => document . getElementById ( id ) ;
const VIEWS = [
{ id : 'queue' , label : 'Warteschlange' } ,
{ id : 'inbox' , label : 'Neu' } ,
{ id : 'episodes' , label : 'Folgen' } ,
{ id : 'feeds' , label : 'Abos' } ,
{ id : 'search' , label : 'Suche' } ,
] ;
const POSITIONS = [
{ value : 'back' , label : 'Ans Ende' } ,
{ value : 'front' , label : 'An den Anfang' } ,
{ value : 'after_current' , label : 'Nach der laufenden Folge' } ,
] ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// The speed menu's choices while the account has no presets of its own (settings().speed_presets).
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const SPEEDS = [ 0.8 , 0.9 , 1 , 1.1 , 1.2 , 1.3 , 1.5 , 1.75 , 2 ] ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
const MIN _SPEED = 0.5 ;
const MAX _SPEED = 3.5 ;
const MAX _PRESETS = 12 ;
// Skip lengths back and forward (seconds), as AntennaPod offers them.
const SKIPS = [ 5 , 10 , 15 , 20 , 30 , 45 , 60 ] ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function parse ( s , fallback ) {
try { return JSON . parse ( s ) ; } catch ( e ) { return fallback ; }
}
let state = parse ( N . podcastState ( ) , { } ) ;
let player = { kind : null } ;
let view = 'queue' ;
let feedView = null ; // {feed, episodes} while one podcast is open
let searchResults = null ;
2026-10-02 16:29:15 +02:00
let preview = null ; // {id, url, data | choices | error} while a feed is looked at before subscribing
let previewSeq = 0 ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
let refreshing = false ;
let selected = null ; // Set of episode ids while choosing several (queue or inbox)
let locked = false ; // queue locked: no dragging
try { locked = localStorage . getItem ( 'nui_pod_locked' ) === '1' ; } catch ( e ) { }
try { view = localStorage . getItem ( 'nui_pod_view' ) || 'queue' ; } catch ( e ) { }
if ( ! VIEWS . some ( v => v . id === view ) ) view = 'queue' ;
const queue = ( ) => state . queue || [ ] ;
const inbox = ( ) => state . inbox || [ ] ;
const feeds = ( ) => state . feeds || [ ] ;
const settings = ( ) => state . settings || { } ;
const currentId = ( ) => ( player . kind === 'podcast' ? player . episodeId : 0 ) ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// The account's speed presets (null there: the default list).
const presets = ( ) => {
const p = settings ( ) . speed _presets ;
return Array . isArray ( p ) && p . length ? p . slice ( ) . sort ( ( a , b ) => a - b ) : SPEEDS ;
} ;
// Skipping to the next episode keeps the skipped one (AntennaPod's "keep skipped episodes").
const keepSkipped = ( ) => settings ( ) . keep _skipped !== false ;
// "Zeiten an das Tempo anpassen": lengths and remaining times as long as they take at the speed.
const bySpeed = ( ) => ! ! settings ( ) . times _by _speed ;
let prefsSeen = null ;
let prefsAt = 0 ;
function speedOf ( feedId ) {
if ( ! prefsSeen || Date . now ( ) - prefsAt > 2000 ) { prefsSeen = devicePrefs ( ) ; prefsAt = Date . now ( ) ; }
const f = feedId ? feeds ( ) . find ( x => x . id === feedId ) : null ;
return f && f . playback _speed > 0 ? f . playback _speed : prefsSeen . speed > 0 ? prefsSeen . speed : 1 ;
}
// Seconds as shown: at the podcast's speed (or `speed`), when so set.
function shown ( seconds , feedId , speed ) {
if ( ! bySpeed ( ) ) return seconds ;
const s = speed > 0 ? speed : speedOf ( feedId ) ;
return s > 0 ? seconds / s : seconds ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// ---- small helpers ---------------------------------------------------------------
function el ( tag , cls , text ) {
const e = document . createElement ( tag ) ;
if ( cls ) e . className = cls ;
if ( text != null ) e . textContent = text ;
return e ;
}
function button ( label , title , onClick , cls ) {
const b = el ( 'button' , 'btn-icon nui-icon' + ( cls ? ' ' + cls : '' ) , label ) ;
b . type = 'button' ;
b . title = title ;
b . setAttribute ( 'aria-label' , title ) ;
b . addEventListener ( 'click' , e => { e . stopPropagation ( ) ; onClick ( e ) ; } ) ;
return b ;
}
function openSheet ( title , items ) {
if ( window . DioraUI && DioraUI . openSheet ) DioraUI . openSheet ( title , items ) ;
}
function clock ( seconds ) {
seconds = Math . max ( 0 , Math . round ( seconds || 0 ) ) ;
const h = Math . floor ( seconds / 3600 ) ;
const m = Math . floor ( ( seconds % 3600 ) / 60 ) ;
const s = seconds % 60 ;
return h ? ` ${ h } : ${ String ( m ) . padStart ( 2 , '0' ) } : ${ String ( s ) . padStart ( 2 , '0' ) } ` : ` ${ m } : ${ String ( s ) . padStart ( 2 , '0' ) } ` ;
}
function minutes ( seconds ) {
const m = Math . round ( ( seconds || 0 ) / 60 ) ;
if ( m < 60 ) return ` ${ m } min ` ;
return ` ${ Math . floor ( m / 60 ) } h ${ String ( m % 60 ) . padStart ( 2 , '0' ) } min ` ;
}
function day ( iso ) {
if ( ! iso ) return '' ;
const d = new Date ( iso ) ;
if ( isNaN ( d ) ) return '' ;
const diff = Math . floor ( ( Date . now ( ) - d ) / 86400000 ) ;
if ( diff <= 0 ) return 'heute' ;
if ( diff === 1 ) return 'gestern' ;
if ( diff < 7 ) return ` vor ${ diff } Tagen ` ;
return d . toLocaleDateString ( 'de-DE' , { day : 'numeric' , month : 'short' , year : diff > 300 ? 'numeric' : undefined } ) ;
}
let toastTimer = 0 ;
function toast ( text ) {
let t = $id ( 'nui-radio-toast' ) ;
if ( ! t ) {
t = el ( 'div' ) ;
t . id = 'nui-radio-toast' ;
document . body . appendChild ( t ) ;
}
t . textContent = text ;
t . classList . add ( 'nui-show' ) ;
clearTimeout ( toastTimer ) ;
toastTimer = setTimeout ( ( ) => t . classList . remove ( 'nui-show' ) , 2600 ) ;
}
// Feed titles sometimes arrive with HTML entities still in them (&).
const decoder = document . createElement ( 'textarea' ) ;
function plain ( s ) {
if ( ! s || s . indexOf ( '&' ) < 0 ) return s || '' ;
decoder . innerHTML = s ;
return decoder . value ;
}
// Episodes come in two shapes: queue entries (episode__*) and inbox/feed
// episodes. One view of both:
function ep ( o ) {
const e = shape ( o ) ;
e . title = plain ( e . title ) ;
e . feed = plain ( e . feed ) ;
return e ;
}
function shape ( o ) {
if ( o . episode _ _id != null ) {
return {
id : o . episode _ _id , title : o . episode _ _title , feed : o . episode _ _feed _ _title , feedId : o . episode _ _feed _ _id ,
art : o . episode _ _artwork _url || o . episode _ _feed _ _artwork _url , duration : o . episode _ _duration _seconds ,
pos : o . position _seconds , date : o . episode _ _pub _date , raw : o ,
} ;
}
return {
id : o . id , title : o . title , feed : o . feed _ _title || ( feedView && feedView . feed . title ) , feedId : o . feed _ _id ,
art : o . artwork _url || o . feed _ _artwork _url || ( feedView && feedView . feed . artwork _url ) , duration : o . duration _seconds ,
pos : o . position _seconds , date : o . pub _date , played : o . played , inQueue : o . in _queue , raw : o ,
} ;
}
function art ( src , size ) {
const box = el ( 'div' , 'nui-pod-art' + ( size ? ' nui-pod-art-' + size : '' ) ) ;
if ( src ) {
const img = el ( 'img' ) ;
img . loading = 'lazy' ;
img . alt = '' ;
img . src = src . replace ( /^http:/ , 'https:' ) ;
img . addEventListener ( 'error' , ( ) => img . remove ( ) ) ;
box . append ( img ) ;
}
return box ;
}
function inQueue ( id ) {
return queue ( ) . some ( q => q . episode _ _id === id ) ;
}
// ---- actions ---------------------------------------------------------------------------
function play ( e ) {
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
const r = remote ( ) ;
if ( r ? r . kind === 'podcast' && r . episodeId === e . id : currentId ( ) === e . id ) { openPlayer ( ) ; return ; }
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
N . podcastPlayEpisode ( JSON . stringify ( e . raw ) , currentId ( ) ) ;
}
function enqueue ( e , where ) {
N . podcastEnqueue ( JSON . stringify ( [ e . raw ] ) , where || '' , currentId ( ) ) ;
const label = POSITIONS . find ( p => p . value === ( where || settings ( ) . enqueue _position ) ) ;
toast ( 'In der Warteschlange' + ( label ? ': ' + label . label . toLowerCase ( ) : '' ) ) ;
}
// Move within the queue: "next" is right after the episode that plays.
function moveTo ( e , where ) {
const ids = queue ( ) . map ( q => q . episode _ _id ) . filter ( id => id !== e . id ) ;
let index = where === 'top' ? 0 : ids . length ;
if ( where === 'next' ) {
const at = ids . indexOf ( currentId ( ) ) ;
index = at >= 0 ? at + 1 : 0 ;
}
N . podcastMove ( e . id , index ) ;
}
function markPlayed ( e ) {
N . podcastMarkPlayed ( e . id , true ) ;
toast ( 'Als gehört markiert' ) ;
}
function queueMenu ( e ) {
const playing = currentId ( ) === e . id ;
openSheet ( e . title , [
{ icon : '▶' , label : playing ? 'Player öffnen' : 'Abspielen' , action : ( ) => play ( e ) } ,
! playing && { icon : '☰' , label : 'Shownotes und Kapitel' , action : ( ) => openNotes ( e ) } ,
favoriteItem ( e ) ,
! playing && { icon : '⤴' , label : 'Als Nächstes' , hint : 'direkt nach der laufenden Folge' , action : ( ) => moveTo ( e , 'next' ) } ,
{ icon : '⇈' , label : 'Ganz nach oben' , action : ( ) => moveTo ( e , 'top' ) } ,
{ icon : '⇊' , label : 'Ganz nach unten' , action : ( ) => moveTo ( e , 'bottom' ) } ,
{ icon : '✓' , label : 'Als gehört markieren' , hint : 'verlässt die Warteschlange' , action : ( ) => markPlayed ( e ) } ,
... dlMenuItems ( e ) ,
{ icon : '✕' , label : 'Aus der Warteschlange nehmen' , danger : true , action : ( ) => N . podcastDequeue ( e . id ) } ,
] ) ;
}
function episodeMenu ( e ) {
const queued = inQueue ( e . id ) ;
openSheet ( e . title , [
{ icon : '▶' , label : 'Abspielen' , hint : queued ? null : 'kommt dafür in die Warteschlange' , action : ( ) => play ( e ) } ,
{ icon : '☰' , label : 'Shownotes und Kapitel' , action : ( ) => openNotes ( e ) } ,
favoriteItem ( e ) ,
! queued && { icon : '⤴' , label : 'Als Nächstes' , action : ( ) => enqueue ( e , 'after_current' ) } ,
! queued && { icon : '⇈' , label : 'An den Anfang der Warteschlange' , action : ( ) => enqueue ( e , 'front' ) } ,
! queued && { icon : '⇊' , label : 'Ans Ende der Warteschlange' , action : ( ) => enqueue ( e , 'back' ) } ,
queued && { icon : '✕' , label : 'Aus der Warteschlange nehmen' , action : ( ) => N . podcastDequeue ( e . id ) } ,
! e . played && { icon : '✓' , label : 'Als gehört markieren' , action : ( ) => markPlayed ( e ) } ,
e . played && { icon : '↺' , label : 'Als ungehört markieren' , action : ( ) => N . podcastMarkPlayed ( e . id , false ) } ,
... dlMenuItems ( e ) ,
{ icon : '⊘' , label : 'Ausblenden' , hint : 'aus „Neu“ entfernen, ohne es als gehört zu zählen' ,
action : ( ) => N . podcastDismiss ( JSON . stringify ( [ e . id ] ) ) } ,
] ) ;
}
function settingsMenu ( ) {
const s = settings ( ) ;
const choose = ( name , title ) => openSheet ( title , POSITIONS . map ( p => ( {
icon : s [ name ] === p . value ? '●' : '○' ,
label : p . label ,
active : s [ name ] === p . value ,
action : ( ) => N . podcastSettings ( JSON . stringify ( { [ name ] : p . value } ) ) ,
} ) ) ) ;
const label = v => ( POSITIONS . find ( p => p . value === v ) || POSITIONS [ 0 ] ) . label ;
const prefs = devicePrefs ( ) ;
// Grouped like AntennaPod's settings; each group opens its own list.
const group = ( icon , title , hint , items ) => ( { icon , label : title , hint , action : ( ) => openSheet ( title , items ( ) ) } ) ;
openSheet ( 'Podcasts' , [
2026-10-02 10:30:00 +02:00
{ icon : '⟳' , label : 'Alle Feeds jetzt abrufen' , hint : 'der Server holt sie, hier kommt nur das Ergebnis an' ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
action : ( ) => refresh ( true ) } ,
group ( '≡' , 'Warteschlange' , 'wohin neue Folgen kommen, gehörte entfernen' , ( ) => [
{ icon : '+' , label : 'Neue Folgen von Hand einreihen' , hint : label ( s . enqueue _position ) ,
action : ( ) => choose ( 'enqueue_position' , 'Von Hand eingereihte Folgen kommen …' ) } ,
{ icon : '↻' , label : 'Automatisch eingereihte Folgen' , hint : label ( s . auto _queue _position ) ,
action : ( ) => choose ( 'auto_queue_position' , 'Neue Folgen von Podcasts mit „Automatisch einreihen“ kommen …' ) } ,
removePlayedItem ( ) ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
keepSkippedItem ( ) ,
deleteDequeueItem ( ) ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
] ) ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
group ( '▶' , 'Abspielen' , [ 'Sprungweiten' , 'speed_presets' in s && 'Tempo' , 'skip_silence' in prefs && 'Stille' , '„gehört“' ,
'times_by_speed' in s && 'Zeiten' , 'headset_next' in prefs && 'Kopfhörer' ,
'notify_buttons' in prefs && 'Benachrichtigung' ] . filter ( Boolean ) . join ( ', ' ) , ( ) => [
skipItem ( 'skip_back' , 'Zurückspringen' , SKIPS ) ,
skipItem ( 'skip_forward' , 'Vorspringen' , SKIPS ) ,
'speed_presets' in s && { icon : '»' , label : 'Tempo-Voreinstellungen' , hint : presets ( ) . map ( speedLabel ) . join ( ' · ' ) ,
action : ( ) => presetsEditor ( ) } ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
silenceItem ( ) ,
playedItem ( ) ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
timesItem ( ) ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
headsetItem ( ) ,
2026-10-02 16:29:15 +02:00
notifyItem ( ) ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
] ) ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
mobileGroup ( ) ,
2026-10-02 12:44:49 +02:00
DL && { icon : '↓' , label : 'Downloads' , hint : dlSummary ( ) , action : openDownloads } ,
DL && group ( '⚙︎' , 'Download-Einstellungen' , 'automatisch, gleichzeitig, Limit, aufräumen' , ( ) => [
2026-10-02 11:46:37 +02:00
// Account-wide (server): new subscriptions start with auto-download on or off.
'auto_download_default' in s && { icon : s . auto _download _default ? '☑' : '☐' , label : 'Neue Abos laden von selbst' ,
hint : s . auto _download _default ? 'an: neue Folgen neuer Abos werden geladen' : 'aus: pro Podcast einschaltbar' ,
action : ( ) => N . podcastSettings ( JSON . stringify ( { auto _download _default : ! s . auto _download _default } ) ) } ,
... dlSettingsItems ( ) ,
] ) ,
2026-10-02 16:36:00 +02:00
backgroundSyncItem ( ) ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
group ( '⇅' , 'Abos umziehen' , 'OPML exportieren und importieren' , ( ) => [
{ icon : '⇩' , label : 'Abos exportieren (OPML)' , hint : 'zum Umziehen in eine andere App' , action : exportOpml } ,
{ icon : '⇧' , label : 'Abos importieren (OPML)' , action : importOpml } ,
] ) ,
{ icon : '▤' , label : 'Hörstatistik' , action : openStats } ,
] ) ;
}
2026-10-02 16:36:00 +02:00
// The app's background job (BackgroundSync.kt): new episodes, their notifications and downloads with the app closed.
const BG _SYNC = [
{ value : 0 , label : 'Aus' , hint : 'nur wenn die App offen ist' } ,
{ value : 15 , label : 'Alle 15 Minuten' } ,
{ value : 60 , label : 'Jede Stunde' , hint : 'Standard' } ,
{ value : 240 , label : 'Alle 4 Stunden' } ,
] ;
function backgroundSyncItem ( ) {
if ( ! N . podcastBackgroundSync || ! N . podcastSetBackgroundSync ) return null ;
const now = N . podcastBackgroundSync ( ) ;
const label = ( BG _SYNC . find ( c => c . value === now ) || BG _SYNC [ 2 ] ) . label ;
return { icon : '⟲' , label : 'Im Hintergrund abgleichen' , hint : label + ' · neue Folgen, Mitteilungen, Downloads' ,
action : ( ) => openSheet ( 'Im Hintergrund abgleichen' , BG _SYNC . map ( c => ( {
icon : c . value === now ? '●' : '○' , label : c . label , hint : c . hint , active : c . value === now ,
action : ( ) => { N . podcastSetBackgroundSync ( c . value ) ; settingsMenu ( ) ; } ,
} ) ) ) } ;
}
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// Skip lengths and "counts as played" belong to the account where the server knows them
// (the host sends them there, PlaybackPrefs.set); else to this device.
const where = name => ( name in settings ( ) ? 'für alle Geräte' : 'auf diesem Gerät' ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function skipItem ( name , label , choices ) {
if ( ! N . podcastSetPrefValue ) return null ;
const now = devicePrefs ( ) [ name ] ;
return { icon : name === 'skip_back' ? '↺' : '↻' , label , hint : ` ${ now } Sekunden ` ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
action : ( ) => openSheet ( ` ${ label } ( ${ where ( name ) } ) ` , choices . map ( c => ( {
icon : c === now ? '●' : '○' , label : ` ${ c } Sekunden ` , active : c === now ,
action : ( ) => { N . podcastSetPrefValue ( name , c ) ; toast ( ` ${ label } : ${ c } Sekunden ` ) ; } ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} ) ) ) } ;
}
function playedItem ( ) {
if ( ! N . podcastSetPrefValue ) return null ;
const now = devicePrefs ( ) . smart _played ;
if ( now == null ) return null ;
const label = v => ( v ? ` ${ v >= 60 ? v / 60 + ' min' : v + ' s' } vor dem Ende ` : 'erst ganz am Ende' ) ;
return { icon : '✓' , label : 'Als gehört zählen' , hint : label ( now ) ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
action : ( ) => openSheet ( ` Eine Folge zählt als gehört … ( ${ where ( 'smart_played' ) } ) ` ,
[ 0 , 15 , 30 , 45 , 60 , 120 , 300 , 600 ] . map ( v => ( {
icon : v === now ? '●' : '○' , label : label ( v ) , active : v === now ,
action : ( ) => N . podcastSetPrefValue ( 'smart_played' , v ) ,
} ) ) ) } ;
}
// An account setting (server: POST /podcasts/settings/), through the host.
function setAccount ( name , value , text ) {
if ( ! N . podcastSettings ) return ;
N . podcastSettings ( JSON . stringify ( { [ name ] : value } ) ) ;
if ( text ) toast ( text ) ;
}
function accountSwitch ( name , label , onHint , offHint , def ) {
const s = settings ( ) ;
if ( ! ( name in s ) ) return null ;
const on = s [ name ] == null ? def : ! ! s [ name ] ;
return { icon : on ? '☑' : '☐' , label , hint : on ? onHint : offHint ,
action : ( ) => setAccount ( name , ! on , ` ${ label } : ${ on ? 'aus' : 'an' } ` ) } ;
}
// AntennaPod's "keep skipped episodes": ⏭, "Folge überspringen", headset and car go on without marking.
function keepSkippedItem ( ) {
return accountSwitch ( 'keep_skipped' , 'Übersprungene Folgen behalten' ,
'an: wer zur nächsten springt, lässt die Folge ungehört in der Warteschlange' ,
'aus: übersprungene Folgen zählen als gehört' , true ) ;
}
// AntennaPod's "delete removes from queue".
function deleteDequeueItem ( ) {
if ( ! DL ) return null ;
return accountSwitch ( 'delete_dequeue' , 'Löschen nimmt aus der Warteschlange' ,
'an: wer einen Download löscht, nimmt die Folge auch aus der Warteschlange' , 'aus: die Folge bleibt eingereiht' , true ) ;
}
// AntennaPod's "adjust media info to playback speed".
function timesItem ( ) {
return accountSwitch ( 'times_by_speed' , 'Zeiten an das Tempo anpassen' ,
'an: Längen und Restzeiten so, wie lange sie beim eingestellten Tempo dauern' , 'aus: Längen wie bei 1× ' , false ) ;
}
// What may use mobile data: downloads by hand (account), automatic ones (this device), streaming (account).
const STREAMING = [
{ value : 'allow' , label : 'Erlauben' } ,
{ value : 'ask' , label : 'Nachfragen' , hint : 'einmal pro Folge' } ,
{ value : 'never' , label : 'Nie' , hint : 'nur WLAN oder heruntergeladene Folgen' } ,
] ;
function mobileGroup ( ) {
const s = settings ( ) ;
const items = ( ) => {
const now = settings ( ) ;
const dls = DL ? parse ( N . podcastDownloadSettings ( ) , dlSettings ) : { } ;
const streaming = STREAMING . find ( c => c . value === now . mobile _streaming ) || STREAMING [ 0 ] ;
return [
DL && accountSwitch ( 'mobile_downloads' , 'Downloads per Hand über mobile Daten' ,
'an: was du von Hand lädst, lädt auch unterwegs' , 'aus: wartet aufs WLAN, wie automatische Downloads' , true ) ,
DL && { icon : dls . mobile ? '☑' : '☐' , label : 'Automatische Downloads über mobile Daten' ,
hint : ( dls . mobile ? 'an' : 'aus: nur im WLAN' ) + ' · dieses Gerät' , action : ( ) => dlSet ( { mobile : ! dls . mobile } ) } ,
N . podcastAllowStream && 'mobile_streaming' in now && { icon : '⇵' , label : 'Streamen über mobile Daten' ,
hint : streaming . label + ' · Radio ist davon nicht betroffen' ,
action : ( ) => openSheet ( 'Folgen über mobile Daten streamen' , STREAMING . map ( c => ( {
icon : c . value === streaming . value ? '●' : '○' , label : c . label , hint : c . hint || null , active : c . value === streaming . value ,
action : ( ) => setAccount ( 'mobile_streaming' , c . value , ` Streamen über mobile Daten: ${ c . label } ` ) ,
} ) ) ) } ,
] . filter ( Boolean ) ;
} ;
if ( ! ( 'mobile_downloads' in s || 'mobile_streaming' in s ) || ! items ( ) . length ) return null ;
return { icon : '📶' , label : 'Mobile Daten' , hint : 'Downloads und Streamen unterwegs' , action : ( ) => openSheet ( 'Mobile Daten' , items ( ) ) } ;
}
// ---- speed presets (the account's; AntennaPod's speed presets, up to 3.5× ) ----
const roundSpeed = v => Math . round ( v * 100 ) / 100 ;
function savePresets ( list ) {
setAccount ( 'speed_presets' , list ) ;
setTimeout ( ( ) => presetsEditor ( list || SPEEDS ) , 60 ) ;
}
function presetsEditor ( list ) {
const now = ( list || presets ( ) ) . slice ( ) . sort ( ( a , b ) => a - b ) ;
const own = Array . isArray ( settings ( ) . speed _presets ) || ! ! list ;
openSheet ( 'Tempo-Voreinstellungen' , now . map ( v => ( {
icon : '✕' , label : speedLabel ( v ) , hint : now . length > 1 ? 'antippen zum Entfernen' : 'eine bleibt immer' ,
action : ( ) => { if ( now . length > 1 ) savePresets ( now . filter ( x => x !== v ) ) ; else toast ( 'Eine Voreinstellung bleibt immer' ) ; } ,
} ) ) . concat ( [
{ icon : '+' , label : 'Tempo hinzufügen' , hint : ` ${ speedLabel ( MIN _SPEED ) } bis ${ speedLabel ( MAX _SPEED ) } ` ,
action : ( ) => addPreset ( now ) } ,
own && { icon : '↺' , label : 'Standardliste' , hint : SPEEDS . map ( speedLabel ) . join ( ' · ' ) , action : ( ) => savePresets ( null ) } ,
] . filter ( Boolean ) ) ) ;
}
function addPreset ( now ) {
if ( now . length >= MAX _PRESETS ) { toast ( ` Höchstens ${ MAX _PRESETS } Voreinstellungen ` ) ; return ; }
const add = v => savePresets ( now . concat ( roundSpeed ( v ) ) . sort ( ( a , b ) => a - b ) ) ;
const choices = [ ] ;
for ( let v = MIN _SPEED ; v <= MAX _SPEED + 0.001 ; v += 0.25 ) {
if ( ! now . some ( x => Math . abs ( x - v ) < 0.001 ) ) choices . push ( roundSpeed ( v ) ) ;
}
openSheet ( 'Tempo hinzufügen' , choices . map ( v => ( { icon : '+' , label : speedLabel ( v ) , action : ( ) => add ( v ) } ) )
. concat ( typeof customPrompt === 'function' ? [ { icon : '✎' , label : 'Eigener Wert …' , hint : 'z. B. 1,65' , action : async ( ) => {
const t = await customPrompt ( ` Tempo ( ${ speedLabel ( MIN _SPEED ) } bis ${ speedLabel ( MAX _SPEED ) } ): ` , '' ) ;
if ( t == null ) return ;
const v = roundSpeed ( parseFloat ( String ( t ) . replace ( ',' , '.' ) . replace ( /[× x]/g , '' ) ) ) ;
if ( ! ( v >= MIN _SPEED && v <= MAX _SPEED ) ) { toast ( ` Bitte eine Zahl von ${ speedLabel ( MIN _SPEED ) } bis ${ speedLabel ( MAX _SPEED ) } ` ) ; return ; }
if ( now . some ( x => Math . abs ( x - v ) < 0.001 ) ) { toast ( ` ${ speedLabel ( v ) } ist schon dabei ` ) ; return ; }
add ( v ) ;
} } ] : [ ] ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
// A host without headset buttons or silence skipping (the browser) leaves them out of its prefs.
function headsetItem ( ) {
if ( ! N . podcastSetPref || ! ( 'headset_next' in devicePrefs ( ) ) ) return null ;
const next = ! ! devicePrefs ( ) . headset _next ;
return { icon : '🎧' , label : 'Kopfhörer: zweimal tippen' , hint : next ? 'nächste Folge' : 'vorspulen (dreimal: zurück)' ,
action : ( ) => openSheet ( 'Kopfhörer zweimal tippen …' , [
{ icon : ! next ? '●' : '○' , label : 'Vorspulen' , hint : 'dreimal tippen springt zurück' , action : ( ) => N . podcastSetPref ( 'headset_next' , false ) } ,
{ icon : next ? '●' : '○' , label : 'Nächste Folge' , action : ( ) => N . podcastSetPref ( 'headset_next' , true ) } ,
] ) } ;
}
2026-10-02 16:29:15 +02:00
// The notification's two extra buttons (app only: NotifyButtons.kt), like AntennaPod's.
const NOTIFY = [
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
{ key : 'next' , label : 'Folge überspringen' ,
hint : ( ) => ( keepSkipped ( ) ? 'die nächste spielt, diese bleibt ungehört eingereiht' : 'zählt als gehört, die nächste spielt' ) } ,
{ key : 'chapter' , label : 'Nächstes Kapitel' , hint : ( ) => 'wenn die Folge Kapitel hat' } ,
// The account's presets; without any, the app's own list.
{ key : 'speed' , label : 'Tempo' , hint : ( ) => ( Array . isArray ( settings ( ) . speed _presets ) && settings ( ) . speed _presets . length
? presets ( ) : [ 1 , 1.2 , 1.5 , 1.75 , 2 ] ) . map ( speedLabel ) . join ( ' · ' ) } ,
{ key : 'sleep' , label : 'Sleep-Timer' , hint : ( ) => '15 Minuten an / aus' } ,
2026-10-02 16:29:15 +02:00
] ;
function notifyItem ( ) {
if ( ! N . podcastSetNotifyButtons || ! Array . isArray ( devicePrefs ( ) . notify _buttons ) ) return null ;
const now = devicePrefs ( ) . notify _buttons ;
const names = keys => keys . map ( k => ( NOTIFY . find ( n => n . key === k ) || { } ) . label ) . filter ( Boolean ) . join ( ', ' ) || 'keine' ;
const sheet = chosen => openSheet ( 'Zwei Knöpfe in der Benachrichtigung' , NOTIFY . map ( n => ( {
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
icon : chosen . includes ( n . key ) ? '☑' : '☐' , label : n . label , hint : n . hint ( ) ,
2026-10-02 16:29:15 +02:00
// A third one replaces the one chosen first.
action : ( ) => {
const next = chosen . includes ( n . key ) ? chosen . filter ( k => k !== n . key ) : chosen . concat ( n . key ) . slice ( - 2 ) ;
N . podcastSetNotifyButtons ( next . join ( ',' ) ) ;
toast ( 'Benachrichtigung: ' + names ( next ) ) ;
} ,
} ) ) ) ;
return { icon : '⊞' , label : 'Knöpfe in der Benachrichtigung' , hint : names ( now ) , action : ( ) => sheet ( now ) } ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function silenceItem ( ) {
if ( ! N . podcastSetPref || ! ( 'skip_silence' in devicePrefs ( ) ) ) return null ;
const on = ! ! devicePrefs ( ) . skip _silence ;
return { icon : on ? '☑' : '☐' , label : 'Stille überspringen' ,
hint : on ? 'an: Pausen in Folgen werden übersprungen' : 'aus' ,
action : ( ) => N . podcastSetPref ( 'skip_silence' , ! on ) } ;
}
function devicePrefs ( ) {
return parse ( N . podcastPrefs ? N . podcastPrefs ( ) : '{}' , { } ) ;
}
function removePlayedItem ( ) {
if ( ! N . podcastSetPref ) return null ;
const on = devicePrefs ( ) . remove _played !== false ;
return { icon : on ? '☑' : '☐' , label : 'Gehörte Folgen aus der Warteschlange nehmen' ,
hint : on ? 'an: fertige Folgen verlassen die Warteschlange' : 'aus: fertige Folgen bleiben stehen' ,
action : ( ) => { N . podcastSetPref ( 'remove_played' , ! on ) ; toast ( on ? 'Gehörte Folgen bleiben jetzt stehen' : 'Gehörte Folgen verlassen jetzt die Warteschlange' ) ; } } ;
}
// ---- sorting, locking, choosing several ----------------------------------------------
const SORTS = [
{ id : 'new' , label : 'Neueste zuerst' , key : e => - ( Date . parse ( e . date ) || 0 ) } ,
{ id : 'old' , label : 'Älteste zuerst' , key : e => Date . parse ( e . date ) || 0 } ,
{ id : 'short' , label : 'Kürzeste zuerst' , key : e => Math . max ( 0 , ( e . duration || 0 ) - ( e . pos || 0 ) ) } ,
{ id : 'long' , label : 'Längste zuerst' , key : e => - Math . max ( 0 , ( e . duration || 0 ) - ( e . pos || 0 ) ) } ,
{ id : 'feed' , label : 'Nach Podcast' , key : e => ( e . feed || '' ) . toLowerCase ( ) + ' ' + String ( 9e15 - ( Date . parse ( e . date ) || 0 ) ) } ,
] ;
// The episode that plays stays on top; everything else is sorted.
function sortQueue ( sort ) {
const items = queue ( ) . map ( ep ) ;
const now = items . filter ( e => e . id === currentId ( ) ) ;
const rest = items . filter ( e => e . id !== currentId ( ) ) ;
if ( sort === 'shuffle' ) {
for ( let i = rest . length - 1 ; i > 0 ; i -- ) {
const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
[ rest [ i ] , rest [ j ] ] = [ rest [ j ] , rest [ i ] ] ;
}
} else {
const s = SORTS . find ( x => x . id === sort ) ;
rest . sort ( ( a , b ) => {
const ka = s . key ( a ) , kb = s . key ( b ) ;
return ka < kb ? - 1 : ka > kb ? 1 : 0 ;
} ) ;
}
N . podcastReorder ( JSON . stringify ( now . concat ( rest ) . map ( e => e . id ) ) ) ;
}
const keepSorted = ( ) => settings ( ) . keep _sorted || '' ;
function sortMenu ( ) {
const kept = keepSorted ( ) ;
const keptLabel = kept ? ( SORTS . find ( x => x . id === kept ) || { } ) . label : '' ;
openSheet ( 'Warteschlange sortieren' , SORTS . map ( x => ( { icon : '⇅' , label : x . label , action : ( ) => sortQueue ( x . id ) } ) )
. concat ( [ { icon : '⤨' , label : 'Mischen' , action : ( ) => sortQueue ( 'shuffle' ) } ] )
. concat ( 'keep_sorted' in settings ( ) ? [ { icon : kept ? '☑' : '☐' , label : 'Immer sortiert halten' ,
hint : kept ? ` an: ${ keptLabel } ` : 'aus: Reihenfolge bestimmst du' ,
action : ( ) => openSheet ( 'Immer sortiert halten' , [ { id : '' , label : 'Aus' } ] . concat ( SORTS ) . map ( x => ( {
icon : kept === x . id ? '●' : '○' , label : x . label , hint : x . id ? 'auch neu eingereihte Folgen' : null ,
action : ( ) => N . podcastSettings ( JSON . stringify ( { keep _sorted : x . id } ) ) ,
} ) ) ) } ] : [ ] ) ) ;
}
function toggleLock ( ) {
locked = ! locked ;
try { localStorage . setItem ( 'nui_pod_locked' , locked ? '1' : '0' ) ; } catch ( e ) { }
toast ( locked ? 'Warteschlange gesperrt' : 'Warteschlange entsperrt' ) ;
render ( ) ;
}
function startSelecting ( id ) {
selected = new Set ( id ? [ id ] : [ ] ) ;
render ( ) ;
}
function stopSelecting ( ) {
if ( ! selected ) return false ;
selected = null ;
render ( ) ;
return true ;
}
function toggleSelected ( id ) {
if ( selected . has ( id ) ) selected . delete ( id ) ; else selected . add ( id ) ;
render ( ) ;
}
// The bar while choosing: count, all/none, and what to do with the chosen ones.
function selectionBar ( items , actions ) {
const bar = el ( 'div' , 'nui-pod-selbar' ) ;
const all = items . length > 0 && items . every ( e => selected . has ( e . id ) ) ;
bar . append (
button ( '✕' , 'Auswahl beenden' , stopSelecting ) ,
el ( 'span' , 'nui-pod-selcount' , selected . size ? ` ${ selected . size } ausgewählt ` : 'Folgen antippen' ) ,
button ( all ? '☑' : '☐' , all ? 'Keine auswählen' : 'Alle auswählen' , ( ) => {
selected = new Set ( all ? [ ] : items . map ( e => e . id ) ) ;
render ( ) ;
} ) ,
) ;
const acts = el ( 'div' , 'nui-pod-selactions' ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
for ( const a of actions . filter ( Boolean ) ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const b = el ( 'button' , 'btn btn-sm' + ( a . danger ? ' btn-danger' : '' ) , a . label ) ;
b . type = 'button' ;
b . disabled = ! selected . size ;
b . addEventListener ( 'click' , ( ) => {
const ids = items . filter ( e => selected . has ( e . id ) ) . map ( e => e . id ) ;
if ( ids . length ) a . run ( ids ) ;
} ) ;
acts . append ( b ) ;
}
bar . append ( acts ) ;
return bar ;
}
function listTools ( buttons ) {
const t = el ( 'div' , 'nui-pod-tools' ) ;
for ( const b of buttons ) if ( b ) t . append ( b ) ;
return t ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
const nEpisodes = n => ` ${ n } ${ n === 1 ? 'Folge' : 'Folgen' } ` ;
// Choosing several in a list of episodes (Folgen, one podcast): what to do with them.
function episodeSelection ( items ) {
const pick = ids => items . filter ( e => ids . includes ( e . id ) ) ;
const fresh = new Set ( inbox ( ) . map ( o => o . id ) ) ;
return selectionBar ( items , [
{ label : 'Einreihen' , run : ids => enqueueMany ( pick ( ids ) ) } ,
{ label : 'Gehört' , run : ids => markMany ( pick ( ids ) , true ) } ,
{ label : 'Ungehört' , run : ids => markMany ( pick ( ids ) , false ) } ,
DL && { label : 'Laden' , run : ids => downloadMany ( pick ( ids ) ) } ,
{ label : 'Favorit' , run : ids => openSheet ( nEpisodes ( ids . length ) , [
{ icon : '★' , label : 'Als Favorit markieren' , action : ( ) => favoriteMany ( pick ( ids ) , true ) } ,
{ icon : '☆' , label : 'Kein Favorit mehr' , action : ( ) => favoriteMany ( pick ( ids ) , false ) } ,
] ) } ,
// Only where some of them are new.
items . some ( e => fresh . has ( e . id ) ) && { label : 'Aus „Neu“' , run : ids => {
const gone = ids . filter ( id => fresh . has ( id ) ) ;
if ( gone . length ) N . podcastDismiss ( JSON . stringify ( gone ) ) ;
toast ( gone . length ? ` ${ nEpisodes ( gone . length ) } aus „Neu“ entfernt ` : 'Keine davon ist in „Neu“' ) ;
stopSelecting ( ) ;
} } ,
] . filter ( Boolean ) ) ;
}
function enqueueMany ( list ) {
const todo = list . filter ( e => ! inQueue ( e . id ) ) ;
if ( ! todo . length ) { toast ( 'Schon alle in der Warteschlange' ) ; stopSelecting ( ) ; return ; }
const add = where => {
N . podcastEnqueue ( JSON . stringify ( todo . map ( e => e . raw ) ) , where , currentId ( ) ) ;
for ( const e of todo ) e . raw . in _queue = true ;
toast ( ` ${ nEpisodes ( todo . length ) } eingereiht ` ) ;
stopSelecting ( ) ;
} ;
openSheet ( ` ${ nEpisodes ( todo . length ) } einreihen ` , [
{ icon : '+' , label : 'Wie eingestellt' , hint : ( POSITIONS . find ( p => p . value === settings ( ) . enqueue _position ) || POSITIONS [ 0 ] ) . label ,
action : ( ) => add ( '' ) } ,
{ icon : '⤴' , label : 'Als Nächstes' , action : ( ) => add ( 'after_current' ) } ,
{ icon : '⇈' , label : 'An den Anfang' , action : ( ) => add ( 'front' ) } ,
{ icon : '⇊' , label : 'Ans Ende' , action : ( ) => add ( 'back' ) } ,
] ) ;
}
function markMany ( list , played ) {
const ids = list . map ( e => e . id ) ;
if ( played ) {
N . podcastMarkPlayedAll ( JSON . stringify ( ids ) , true ) ;
} else {
// "Ungehört" one by one fetches the state again each time: all but the
// last in one go; downloaded ones singly, the app has to know of them.
const single = ids . filter ( id => DL && downloads [ id ] ) ;
const rest = ids . filter ( id => ! ( DL && downloads [ id ] ) ) ;
if ( ! single . length ) single . push ( rest . pop ( ) ) ;
if ( rest . length ) N . podcastMarkPlayedAll ( JSON . stringify ( rest ) , false ) ;
for ( const id of single ) N . podcastMarkPlayed ( id , false ) ;
}
for ( const e of list ) {
e . raw . played = played ;
if ( played ) e . raw . position _seconds = 0 ;
}
toast ( ` ${ nEpisodes ( ids . length ) } als ${ played ? 'gehört' : 'ungehört' } markiert ` ) ;
stopSelecting ( ) ;
}
function downloadMany ( list ) {
const todo = list . filter ( e => ( e . raw . audio _url || e . raw . episode _ _audio _url ) &&
! ( downloads [ e . id ] && downloads [ e . id ] . state !== 'failed' ) ) ;
const waiting = { } ;
for ( const e of todo ) {
N . podcastDownload ( JSON . stringify ( e . raw ) ) ;
waiting [ e . id ] = { state : 'queued' , progress : 0 , bytes : 0 , total : 0 , title : e . title , reason : 'queue' } ;
}
toast ( todo . length ? ` ${ nEpisodes ( todo . length ) } ${ todo . length === 1 ? 'wird' : 'werden' } geladen ` : 'Schon alle geladen' ) ;
selected = null ;
window . _ _dioraDownloads ( Object . assign ( { } , downloads , waiting ) ) ;
render ( ) ;
}
async function favoriteMany ( list , star ) {
stopSelecting ( ) ;
try {
const r = await fetch ( '/podcasts/episodes/favorite/' , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { episode _ids : list . map ( e => e . id ) , favorite : star } ) ,
} ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
for ( const e of list ) {
e . raw . favorite = star ;
if ( N . podcastFavoriteChanged ) N . podcastFavoriteChanged ( e . id , star ) ;
const cached = storedDetails ( e . id ) ;
if ( cached ) keepDetails ( e . id , Object . assign ( { } , cached , { favorite : star } ) ) ;
}
toast ( star ? ` ★ ${ nEpisodes ( list . length ) } als Favorit markiert ` : ` ${ nEpisodes ( list . length ) } : kein Favorit mehr ` ) ;
} catch ( err ) {
toast ( navigator . onLine ? 'Favoriten ließen sich nicht setzen' : 'Favoriten brauchen eine Verbindung' ) ;
}
render ( ) ;
}
// ---- filters and order of episode lists (Folgen, one podcast) -------------------------
const FILTERS = [
{ id : 'unplayed' , label : 'Ungehört' , test : o => ! o . played , none : 'Keine ungehörten Folgen.' } ,
{ id : 'in_progress' , label : 'Angefangen' , test : o => ! o . played && ( o . position _seconds || 0 ) > 0 ,
none : 'Keine angefangenen Folgen.' } ,
{ id : 'favorites' , label : 'Favoriten' , test : o => ! ! o . favorite ,
none : 'Noch keine Favoriten. Im ⋯-Menü einer Folge markierst du sie mit ★.' } ,
{ id : 'queued' , label : 'Warteschlange' , test : o => inQueue ( o . id ) , none : 'Keine davon steht in der Warteschlange.' } ,
{ id : 'downloaded' , label : 'Geladen' , app : true , test : o => dlDone ( o . id ) , none : 'Keine Folgen auf dem Gerät.' } ,
{ id : 'history' , label : 'Verlauf' , all : true , test : o => ! ! o . played || ( o . position _seconds || 0 ) > 0 ,
none : 'Noch nichts gehört.' } ,
{ id : 'played' , label : 'Gehört' , test : o => ! ! o . played , none : 'Noch keine gehörten Folgen.' } ,
] ;
// Filters that can't both be on: choosing one takes the others off.
const EXCLUDES = { unplayed : [ 'played' ] , in _progress : [ 'played' ] , played : [ 'unplayed' , 'in_progress' ] } ;
const EP _SORTS = [
{ id : 'new' , label : 'Neueste zuerst' , key : o => - ( Date . parse ( o . pub _date ) || - Infinity ) } ,
{ id : 'old' , label : 'Älteste zuerst' , key : o => Date . parse ( o . pub _date ) || Infinity } ,
{ id : 'long' , label : 'Längste zuerst' , key : o => ( o . duration _seconds ? - o . duration _seconds : Infinity ) } ,
{ id : 'short' , label : 'Kürzeste zuerst' , key : o => o . duration _seconds || Infinity } ,
{ id : 'title' , label : 'Nach Titel' , key : o => plain ( o . title ) . toLocaleLowerCase ( 'de' ) } ,
{ id : 'played' , label : 'Zuletzt gehört' , all : true } ,
] ;
const sortLabel = id => ( EP _SORTS . find ( s => s . id === id ) || EP _SORTS [ 0 ] ) . label ;
function dlDone ( id ) {
return DL && ! ! downloads [ id ] && downloads [ id ] . state === 'done' ;
}
function matchesAll ( o , filters ) {
return filters . every ( id => { const f = FILTERS . find ( x => x . id === id ) ; return ! f || f . test ( o ) ; } ) ;
}
function sortEpisodes ( list , sort ) {
const s = EP _SORTS . find ( x => x . id === sort ) ;
if ( ! s || ! s . key ) return list ;
const keyed = list . map ( ( o , i ) => [ s . key ( o ) , i , o ] ) ;
keyed . sort ( ( a , b ) => ( typeof a [ 0 ] === 'string' ? a [ 0 ] . localeCompare ( b [ 0 ] , 'de' ) : a [ 0 ] < b [ 0 ] ? - 1 : a [ 0 ] > b [ 0 ] ? 1 : 0 ) ||
a [ 1 ] - b [ 1 ] ) ;
return keyed . map ( k => k [ 2 ] ) ;
}
// One row of chips: "Alle" and the filters, combinable.
function filterRow ( active , onChange , allEpisodes ) {
const chips = el ( 'div' , 'nui-pod-filters' ) ;
const chip = ( label , on , onClick ) => {
const b = el ( 'button' , 'nui-pod-chip' + ( on ? ' on' : '' ) , label ) ;
b . type = 'button' ;
b . setAttribute ( 'aria-pressed' , on ? 'true' : 'false' ) ;
b . addEventListener ( 'click' , onClick ) ;
chips . append ( b ) ;
} ;
chip ( 'Alle' , ! active . length , ( ) => onChange ( [ ] ) ) ;
for ( const f of FILTERS ) {
if ( ( f . app && ! DL ) || ( f . all && ! allEpisodes ) ) continue ;
const on = active . includes ( f . id ) ;
chip ( f . label , on , ( ) => onChange ( on ? active . filter ( x => x !== f . id )
: active . filter ( x => ! ( EXCLUDES [ f . id ] || [ ] ) . includes ( x ) ) . concat ( f . id ) ) ) ;
}
return chips ;
}
function sortMenuFor ( current , allEpisodes , onPick ) {
openSheet ( 'Folgen sortieren' , EP _SORTS . filter ( s => allEpisodes || ! s . all ) . map ( s => ( {
icon : s . id === current ? '●' : '○' , label : s . label , active : s . id === current , action : ( ) => onPick ( s . id ) ,
} ) ) ) ;
}
function noneText ( filters ) {
const f = filters . length === 1 && FILTERS . find ( x => x . id === filters [ 0 ] ) ;
return f ? f . none : filters . length ? 'Keine Folgen mit diesen Filtern.' : 'Keine Folgen.' ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function refresh ( fetchFeeds ) {
refreshing = true ;
render ( ) ;
N . podcastRefresh ( ! ! fetchFeeds ) ;
}
// ---- rendering ---------------------------------------------------------------------------
let root = null ;
// The page scrolls inside .main-content (app.css), not the window.
function scroller ( ) {
for ( let e = root && root . parentElement ; e && e !== document . body ; e = e . parentElement ) {
const o = getComputedStyle ( e ) . overflowY ;
if ( o === 'auto' || o === 'scroll' ) return e ;
}
return document . scrollingElement || document . documentElement ;
}
function scrollTop ( y ) {
const s = scroller ( ) ;
if ( y == null ) return s . scrollTop ;
s . scrollTop = y ;
return y ;
}
function build ( ) {
const tab = $id ( 'tab-podcasts' ) ;
if ( ! tab || $id ( 'nui-pod' ) ) return false ;
root = el ( 'div' ) ;
root . id = 'nui-pod' ;
tab . prepend ( root ) ;
return true ;
}
function render ( ) {
if ( ! root ) return ;
const scroll = scrollTop ( ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
// Search results arriving while typing: the field keeps its text and the cursor.
const q = $id ( 'nui-pod-q' ) ;
const typing = q && document . activeElement === q ? [ q . selectionStart , q . selectionEnd ] : null ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
root . textContent = '' ;
const bar = el ( 'div' , 'nui-tab-bar' ) ;
bar . id = 'nui-pod-bar' ;
const text = el ( 'div' , 'nui-bar-text' ) ;
text . append ( el ( 'span' , 'native-brand' , 'Podcasts' ) ) ;
if ( refreshing ) text . append ( el ( 'span' , 'muted nui-pod-status' , 'Wird abgerufen …' ) ) ;
else if ( state . server _time ) text . append ( el ( 'span' , 'muted nui-pod-status' , 'Stand ' + day ( state . server _time ) + ', ' +
new Date ( state . server _time ) . toLocaleTimeString ( 'de-DE' , { hour : '2-digit' , minute : '2-digit' } ) ) ) ;
2026-10-02 12:44:49 +02:00
bar . append ( ... [ text , dlIndicator ( ) , button ( '⟳' , 'Alle Feeds abrufen' , ( ) => refresh ( true ) ) ,
button ( '⋮' , 'Einstellungen' , settingsMenu ) ] . filter ( Boolean ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
root . append ( bar ) ;
2026-10-02 16:29:15 +02:00
if ( preview ) {
renderPreview ( ) ;
} else if ( feedView ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
renderFeed ( ) ;
} else {
const tabs = el ( 'div' , 'nui-pod-tabs' ) ;
for ( const v of VIEWS ) {
const b = el ( 'button' , 'nui-pod-tab' + ( v . id === view ? ' active' : '' ) ) ;
b . type = 'button' ;
b . append ( el ( 'span' , null , v . label ) ) ;
const count = v . id === 'queue' ? queue ( ) . length : v . id === 'inbox' ? inbox ( ) . length : 0 ;
if ( count ) b . append ( el ( 'span' , 'nui-pod-count' , String ( count ) ) ) ;
b . addEventListener ( 'click' , ( ) => {
selected = null ;
view = v . id ;
try { localStorage . setItem ( 'nui_pod_view' , view ) ; } catch ( e ) { }
render ( ) ;
scrollTop ( 0 ) ;
} ) ;
tabs . append ( b ) ;
}
root . append ( tabs ) ;
2026-10-02 12:44:49 +02:00
const offer = importOffer ( ) ;
if ( offer ) root . append ( offer ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( view === 'queue' ) renderQueue ( ) ;
else if ( view === 'inbox' ) renderInbox ( ) ;
else if ( view === 'episodes' ) renderEpisodes ( ) ;
else if ( view === 'feeds' ) renderFeeds ( ) ;
else renderSearch ( ) ;
}
scrollTop ( scroll ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
const q2 = typing && $id ( 'nui-pod-q' ) ;
if ( q2 ) {
q2 . focus ( ) ;
try { q2 . setSelectionRange ( typing [ 0 ] , typing [ 1 ] ) ; } catch ( e ) { }
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
2026-10-02 12:44:49 +02:00
// AntennaPod had episodes on the phone: offered once to fetch them again (Wi-Fi only).
function importOffer ( ) {
const o = state . import _downloads ;
if ( ! o || ! o . count || ! N . podcastImportDownloads || ! N . podcastDownloadSettings ) return null ;
const box = el ( 'div' , 'nui-pod-offer' ) ;
const s = parse ( N . podcastDownloadSettings ( ) , { } ) ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
const needed = ( Object . keys ( downloads ) . length || 0 ) + o . count + ( s . auto ? ( s . count === 0 ? queue ( ) . length : s . count || 0 ) : 0 ) ;
2026-10-02 12:44:49 +02:00
const raise = s . limit > 0 && needed > s . limit
? ` „Höchstens behalten“ geht dafür von ${ s . limit } auf ${ [ 5 , 10 , 25 , 50 , 100 , 500 ] . find ( n => n >= needed ) || 'unbegrenzt' } . ` : '' ;
box . append ( el ( 'div' , null , ` ${ o . count } ${ o . count === 1 ? 'Folge war' : 'Folgen waren' } in AntennaPod heruntergeladen ` +
( o . bytes ? ` ( ${ size ( o . bytes ) } ) ` : '' ) + '. Hier noch einmal laden? Nur im WLAN; sie kommen nicht in die Warteschlange.' + raise ) ) ;
const row = el ( 'div' , 'nui-pod-offer-row' ) ;
row . append ( button ( 'Laden' , 'Im WLAN herunterladen' , ( ) => { N . podcastImportDownloads ( true ) ; box . remove ( ) ; } , 'btn' ) ,
button ( 'Nein danke' , 'Nicht laden' , ( ) => { N . podcastImportDownloads ( false ) ; box . remove ( ) ; } , 'btn' ) ) ;
box . append ( row ) ;
return box ;
}
window . _ _dioraImportDownloads = function ( r ) {
if ( ! r || ! r . ok ) toast ( 'Die Liste aus AntennaPod kam nicht an — später noch einmal' ) ;
else if ( r . count ) toast ( ` ${ r . count } ${ r . count === 1 ? 'Folge wird' : 'Folgen werden' } im WLAN geladen ` +
( r . limit != null ? ` · Höchstens behalten: ${ r . limit || 'unbegrenzt' } ` : '' ) ) ;
render ( ) ;
} ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function empty ( text ) {
root . append ( el ( 'div' , 'nui-pod-empty' , text ) ) ;
}
function progressBar ( pos , duration ) {
if ( ! pos || ! duration ) return null ;
const b = el ( 'div' , 'nui-pod-progress' ) ;
const f = el ( 'span' ) ;
f . style . width = Math . min ( 100 , Math . round ( pos / duration * 100 ) ) + '%' ;
b . append ( f ) ;
return b ;
}
function episodeRow ( e , opts ) {
const row = el ( 'div' , 'nui-pod-row' + ( currentId ( ) === e . id ? ' nui-pod-current' : '' ) + ( e . played ? ' nui-pod-played' : '' ) ) ;
row . dataset . id = e . id ;
row . append ( art ( e . art ) ) ;
const main = el ( 'div' , 'nui-pod-main' ) ;
main . append ( el ( 'div' , 'nui-pod-title' , e . title ) ) ;
// Length first: podcast names are long and get cut off.
const meta = [ ] ;
// A played episode, or a place past the feed's (often wrong) length: just the length.
const started = e . pos > 30 && ! e . played && e . pos < e . duration - 30 ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
if ( e . duration ) meta . push ( started ? minutes ( shown ( e . duration - e . pos , e . feedId ) ) + ' übrig' : minutes ( shown ( e . duration , e . feedId ) ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( opts . showDate && e . date ) meta . push ( day ( e . date ) ) ;
if ( opts . showFeed && e . feed ) meta . push ( e . feed ) ;
main . append ( el ( 'div' , 'nui-pod-meta' , meta . join ( ' · ' ) ) ) ;
const p = e . played ? null : progressBar ( e . pos , e . duration ) ;
if ( p ) main . append ( p ) ;
row . append ( main ) ;
if ( selected ) {
row . classList . add ( 'nui-pod-selecting' ) ;
const box = el ( 'span' , 'nui-pod-check' + ( selected . has ( e . id ) ? ' on' : '' ) , selected . has ( e . id ) ? '☑' : '☐' ) ;
row . prepend ( box ) ;
row . addEventListener ( 'click' , ( ) => toggleSelected ( e . id ) ) ;
} else {
dlAppend ( row , e ) ;
row . addEventListener ( 'click' , ( ) => play ( e ) ) ;
}
if ( ! selected && ! opts . noSelect ) {
// Long press starts choosing several, like in AntennaPod.
let timer = 0 ;
row . addEventListener ( 'pointerdown' , ev => {
if ( ev . target . closest ( 'button, .nui-pod-handle' ) ) return ;
timer = setTimeout ( ( ) => { timer = 0 ; startSelecting ( e . id ) ; } , 550 ) ;
} ) ;
const cancel = ( ) => { if ( timer ) clearTimeout ( timer ) ; timer = 0 ; } ;
row . addEventListener ( 'pointerup' , cancel ) ;
row . addEventListener ( 'pointerleave' , cancel ) ;
row . addEventListener ( 'pointercancel' , cancel ) ;
row . addEventListener ( 'contextmenu' , ev => ev . preventDefault ( ) ) ;
}
return row ;
}
function renderQueue ( ) {
const items = queue ( ) . map ( ep ) ;
if ( ! items . length ) {
empty ( 'Die Warteschlange ist leer. Unter „Neu“ oder bei einem Abo tippst du auf „+“, um Folgen einzureihen.' ) ;
return ;
}
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
const total = items . reduce ( ( n , e ) => n + shown ( Math . max ( 0 , ( e . duration || 0 ) - ( e . pos || 0 ) ) , e . feedId ) , 0 ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( selected ) {
root . append ( selectionBar ( items , [
{ label : 'Nach oben' , run : ids => N . podcastReorder ( JSON . stringify ( ids . concat ( queue ( ) . map ( q => q . episode _ _id ) . filter ( id => ! ids . includes ( id ) ) ) ) ) } ,
{ label : 'Nach unten' , run : ids => N . podcastReorder ( JSON . stringify ( queue ( ) . map ( q => q . episode _ _id ) . filter ( id => ! ids . includes ( id ) ) . concat ( ids ) ) ) } ,
{ label : 'Gehört' , run : ids => { N . podcastMarkPlayedAll ( JSON . stringify ( ids ) , true ) ; stopSelecting ( ) ; } } ,
{ label : 'Entfernen' , danger : true , run : ids => { N . podcastDequeueAll ( JSON . stringify ( ids ) ) ; stopSelecting ( ) ; } } ,
] ) ) ;
} else {
const head = el ( 'div' , 'nui-pod-head' ) ;
const kept = keepSorted ( ) ? ' · ' + ( ( SORTS . find ( x => x . id === keepSorted ( ) ) || { } ) . label || '' ) . toLowerCase ( ) : '' ;
head . append ( el ( 'div' , 'nui-pod-summary' , ` ${ items . length } ${ items . length === 1 ? 'Folge' : 'Folgen' } · ${ minutes ( total ) } ${ kept } ` ) ,
listTools ( [
button ( '⇅' , 'Sortieren oder mischen' , sortMenu ) ,
button ( '☐' , 'Mehrere auswählen' , ( ) => startSelecting ( ) ) ,
button ( locked ? '🔒' : '🔓' , locked ? 'Warteschlange entsperren' : 'Warteschlange sperren' , toggleLock ,
locked ? 'nui-pod-lock on' : 'nui-pod-lock' ) ,
] ) ) ;
root . append ( head ) ;
}
const list = el ( 'div' , 'nui-pod-list nui-pod-queue' ) ;
for ( const e of items ) {
const row = episodeRow ( e , { showFeed : true } ) ;
if ( ! selected ) {
row . append ( button ( '⋯' , 'Mehr' , ( ) => queueMenu ( e ) ) ) ;
if ( ! locked && ! keepSorted ( ) ) {
const handle = el ( 'div' , 'nui-pod-handle' , '≡' ) ;
handle . title = 'Ziehen zum Umsortieren' ;
handle . addEventListener ( 'click' , ev => ev . stopPropagation ( ) ) ;
dragSort ( handle , row , list ) ;
row . append ( handle ) ;
}
}
list . append ( row ) ;
}
root . append ( list ) ;
}
function renderInbox ( ) {
const items = inbox ( ) . map ( ep ) ;
if ( ! items . length ) {
empty ( feeds ( ) . length
? 'Keine neuen Folgen. Neue Folgen deiner Abos erscheinen hier, sobald der Server sie gefunden hat.'
: 'Noch keine Abos. Unter „Suche“ findest du Podcasts.' ) ;
return ;
}
if ( selected ) {
const chosen = ids => items . filter ( e => ids . includes ( e . id ) ) ;
const add = ( ids , where ) => {
N . podcastEnqueue ( JSON . stringify ( chosen ( ids ) . map ( e => e . raw ) ) , where , currentId ( ) ) ;
toast ( ` ${ ids . length } ${ ids . length === 1 ? 'Folge' : 'Folgen' } eingereiht ` ) ;
stopSelecting ( ) ;
} ;
root . append ( selectionBar ( items , [
{ label : 'Einreihen' , run : ids => openSheet ( ` ${ ids . length } ${ ids . length === 1 ? 'Folge' : 'Folgen' } einreihen ` , [
{ icon : '+' , label : 'Wie eingestellt' , hint : ( POSITIONS . find ( p => p . value === settings ( ) . enqueue _position ) || POSITIONS [ 0 ] ) . label ,
action : ( ) => add ( ids , '' ) } ,
{ icon : '⤴' , label : 'Als Nächstes' , action : ( ) => add ( ids , 'after_current' ) } ,
{ icon : '⇈' , label : 'An den Anfang' , action : ( ) => add ( ids , 'front' ) } ,
{ icon : '⇊' , label : 'Ans Ende' , action : ( ) => add ( ids , 'back' ) } ,
] ) } ,
{ label : 'Gehört' , run : ids => { N . podcastMarkPlayedAll ( JSON . stringify ( ids ) , true ) ; stopSelecting ( ) ; } } ,
{ label : 'Ausblenden' , run : ids => { N . podcastDismiss ( JSON . stringify ( ids ) ) ; stopSelecting ( ) ; } } ,
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
DL && { label : 'Laden' , run : ids => downloadMany ( chosen ( ids ) ) } ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
] ) ) ;
} else {
const head = el ( 'div' , 'nui-pod-head' ) ;
head . append ( el ( 'div' , 'nui-pod-summary' , ` ${ items . length } neue ${ items . length === 1 ? 'Folge' : 'Folgen' } ` ) ,
listTools ( [ button ( '☐' , 'Mehrere auswählen' , ( ) => startSelecting ( ) ) ] ) ) ;
root . append ( head ) ;
}
const list = el ( 'div' , 'nui-pod-list' ) ;
for ( const e of items ) {
const row = episodeRow ( e , { showFeed : true , showDate : true } ) ;
if ( ! selected ) {
row . append (
e . inQueue || inQueue ( e . id )
? button ( '✓' , 'In der Warteschlange' , ( ) => toast ( 'Schon in der Warteschlange' ) , 'nui-pod-queued' )
: button ( '+' , 'Einreihen' , ( ) => enqueue ( e ) ) ,
button ( '⋯' , 'Mehr' , ( ) => episodeMenu ( e ) ) ) ;
}
list . append ( row ) ;
}
root . append ( list ) ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
// The subscriptions: sorted, each with a number (new, unplayed or downloaded
// episodes), both remembered on this device.
const byDate = key => ( a , b ) => String ( b [ key ] || '' ) . localeCompare ( String ( a [ key ] || '' ) ) ;
const byTitle = ( a , b ) => plain ( a . title ) . localeCompare ( plain ( b . title ) , 'de' , { sensitivity : 'base' } ) ;
const FEED _SORTS = [
{ id : 'latest' , label : 'Neueste Folge zuerst' , cmp : byDate ( 'latest_episode_at' ) } ,
{ id : 'title' , label : 'Nach Titel' , cmp : byTitle } ,
{ id : 'count' , label : 'Nach Zähler' , hint : 'die meisten zuerst' ,
cmp : ( a , b ) => ( feedCount ( b ) || 0 ) - ( feedCount ( a ) || 0 ) || byTitle ( a , b ) } ,
{ id : 'played' , label : 'Zuletzt gehört' , cmp : ( a , b ) => byDate ( 'last_played_at' ) ( a , b ) || byDate ( 'latest_episode_at' ) ( a , b ) } ,
{ id : 'added' , label : 'Zuletzt abonniert' , cmp : byDate ( 'added_at' ) } ,
] ;
const COUNTERS = [
{ id : 'new' , label : 'Neue Folgen' , hint : 'wie in „Neu“' , value : f => f . new _count } ,
{ id : 'unplayed' , label : 'Ungehörte Folgen' , value : f => f . unplayed _count } ,
{ id : 'downloaded' , label : 'Geladene Folgen' , app : true ,
value : f => Object . values ( downloads ) . filter ( d => d . state === 'done' && d . feedId === f . id ) . length } ,
{ id : 'none' , label : 'Keine Zahl' , value : ( ) => null } ,
] ;
let feedSort = 'latest' ;
let feedCounter = 'new' ;
try {
feedSort = localStorage . getItem ( 'nui_pod_feedsort' ) || feedSort ;
feedCounter = localStorage . getItem ( 'nui_pod_counter' ) || feedCounter ;
} catch ( e ) { }
function counter ( ) {
return COUNTERS . find ( c => c . id === feedCounter && ( ! c . app || DL ) ) || COUNTERS [ 0 ] ;
}
function feedCount ( f ) {
const c = counter ( ) ;
return c . id === 'none' ? COUNTERS [ 1 ] . value ( f ) : c . value ( f ) ;
}
function feedSortMenu ( ) {
const pick = ( key , value ) => {
if ( key === 'sort' ) feedSort = value ; else feedCounter = value ;
try { localStorage . setItem ( key === 'sort' ? 'nui_pod_feedsort' : 'nui_pod_counter' , value ) ; } catch ( e ) { }
render ( ) ;
} ;
openSheet ( 'Abos sortieren' , FEED _SORTS . map ( s => ( {
icon : s . id === feedSort ? '●' : '○' , label : s . label , hint : s . hint || null , active : s . id === feedSort ,
action : ( ) => pick ( 'sort' , s . id ) ,
} ) ) . concat ( [ { icon : '#' , label : 'Zahl am Podcast' , hint : counter ( ) . label , action : ( ) => openSheet ( 'Zahl am Podcast' ,
COUNTERS . filter ( c => ! c . app || DL ) . map ( c => ( {
icon : c . id === counter ( ) . id ? '●' : '○' , label : c . label , hint : c . hint || null , active : c . id === counter ( ) . id ,
action : ( ) => pick ( 'counter' , c . id ) ,
} ) ) ) } ] ) ) ;
}
function feedRow ( f ) {
const row = el ( 'div' , 'nui-pod-row' ) ;
row . append ( art ( f . artwork _url ) ) ;
const main = el ( 'div' , 'nui-pod-main' ) ;
main . append ( el ( 'div' , 'nui-pod-title' , plain ( f . title ) ) ) ;
const meta = [ ] ;
if ( f . author ) meta . push ( plain ( f . author ) ) ;
if ( feedSort === 'played' && f . last _played _at ) meta . push ( 'gehört ' + day ( f . last _played _at ) ) ;
else if ( f . latest _episode _at ) meta . push ( 'neueste ' + day ( f . latest _episode _at ) ) ;
main . append ( el ( 'div' , 'nui-pod-meta' , meta . join ( ' · ' ) ) ) ;
if ( f . last _error ) main . append ( el ( 'div' , 'nui-pod-feederr' , '⚠ lädt nicht: ' + f . last _error ) ) ;
row . append ( main ) ;
if ( f . new _episodes === 'ignore' ) row . append ( el ( 'span' , 'nui-pod-badge' , 'still' ) ) ;
else if ( f . auto _queue || f . new _episodes === 'queue' ) row . append ( el ( 'span' , 'nui-pod-badge' , 'auto' ) ) ;
const n = counter ( ) . value ( f ) ;
if ( n ) {
const c = el ( 'span' , 'nui-pod-count nui-pod-feedcount' , String ( n ) ) ;
c . title = ` ${ n } ${ counter ( ) . label . replace ( ' Folgen' , '' ) . toLowerCase ( ) } ` ;
row . append ( c ) ;
}
row . addEventListener ( 'click' , ( ) => openFeed ( f ) ) ;
return row ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function renderFeeds ( ) {
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
const sort = FEED _SORTS . find ( s => s . id === feedSort ) || FEED _SORTS [ 0 ] ;
const items = feeds ( ) . slice ( ) . sort ( ( a , b ) => sort . cmp ( a , b ) || byTitle ( a , b ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! items . length ) {
empty ( 'Noch keine Abos. Unter „Suche“ findest du Podcasts.' ) ;
return ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
const head = el ( 'div' , 'nui-pod-head' ) ;
head . append ( el ( 'div' , 'nui-pod-summary' , ` ${ items . length } ${ items . length === 1 ? 'Abo' : 'Abos' } · ${ sort . label . toLowerCase ( ) } ` ) ,
listTools ( [ button ( '⇅' , 'Sortieren und Zahl wählen' , feedSortMenu ) ] ) ) ;
root . append ( head ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const list = el ( 'div' , 'nui-pod-list' ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
for ( const f of items ) list . append ( feedRow ( f ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
root . append ( list ) ;
}
async function openFeed ( f ) {
feedView = { feed : f , episodes : null } ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
selected = null ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
render ( ) ;
scrollTop ( 0 ) ;
try {
const r = await fetch ( ` /podcasts/feeds/ ${ f . id } /episodes/ ` ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
const data = await r . json ( ) ;
if ( feedView && feedView . feed . id === f . id ) {
feedView = { feed : Object . assign ( { } , f , data . feed ) , episodes : data . episodes } ;
render ( ) ;
}
} catch ( e ) {
if ( feedView && feedView . feed . id === f . id ) {
feedView . episodes = [ ] ;
feedView . error = true ;
render ( ) ;
}
}
}
function renderFeed ( ) {
const f = feedView . feed ;
const head = el ( 'div' , 'nui-pod-feedhead' ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
head . append ( button ( '‹ ' , 'Zurück zu den Abos' , ( ) => { feedView = null ; selected = null ; render ( ) ; } ) , art ( f . artwork _url , 'big' ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const info = el ( 'div' , 'nui-pod-main' ) ;
info . append ( el ( 'div' , 'nui-pod-title' , f . title ) ) ;
if ( f . author ) info . append ( el ( 'div' , 'nui-pod-meta' , f . author ) ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
head . append ( info , button ( 'ⓘ' , 'Über diesen Podcast' , ( ) => openFeedInfo ( f ) , 'nui-pod-infobtn' ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
root . append ( head ) ;
const live = feeds ( ) . find ( x => x . id === f . id ) || f ;
if ( live . last _error ) {
root . append ( el ( 'div' , 'nui-pod-feederr nui-pod-feederr-box' ,
` ⚠ Der Feed ließ sich zuletzt nicht laden ( ${ live . last _error } ${ live . last _error _at ? ', ' + day ( live . last _error _at ) : '' } ). ` +
'Der Server versucht es weiter, bei wiederholten Fehlern seltener.' ) ) ;
}
if ( 'new_episodes' in live && N . podcastFeedSettings ) {
root . append ( newEpisodesRow ( live ) ) ;
if ( N . podcastFeedPrefs ) root . append ( feedSettings ( f ) ) ;
root . append ( unsubscribeBox ( f ) ) ;
return renderFeedEpisodes ( f ) ;
}
const auto = live . auto _queue ;
const toggle = el ( 'label' , 'nui-pod-switch' ) ;
const box = el ( 'input' ) ;
box . type = 'checkbox' ;
box . checked = ! ! auto ;
box . addEventListener ( 'change' , ( ) => N . podcastAutoQueue ( f . id , box . checked ) ) ;
const pos = ( POSITIONS . find ( p => p . value === settings ( ) . auto _queue _position ) || POSITIONS [ 1 ] ) . label . toLowerCase ( ) ;
toggle . append ( box , el ( 'span' , null , 'Neue Folgen automatisch einreihen' ) , el ( 'span' , 'nui-pod-meta' , pos ) ) ;
root . append ( toggle ) ;
if ( N . podcastFeedPrefs ) root . append ( feedSettings ( f ) ) ;
root . append ( unsubscribeBox ( f ) ) ;
renderFeedEpisodes ( f ) ;
}
// Where this podcast's new episodes go, and which ones count at all.
function newEpisodesRow ( live ) {
const row = el ( 'div' , 'nui-pod-feedprefs' ) ;
const chip = ( label , value , on , onClick ) => {
const b = el ( 'button' , 'nui-pod-chip' + ( on ? ' on' : '' ) ) ;
b . type = 'button' ;
b . append ( el ( 'span' , null , label ) , el ( 'strong' , null , value ) ) ;
b . addEventListener ( 'click' , onClick ) ;
return b ;
} ;
const save = changes => { N . podcastFeedSettings ( live . id , JSON . stringify ( changes ) ) ; } ;
const where = { inbox : 'in „Neu“' , queue : 'in die Warteschlange' , ignore : 'nirgends' } ;
const pos = ( POSITIONS . find ( p => p . value === settings ( ) . auto _queue _position ) || POSITIONS [ 1 ] ) . label . toLowerCase ( ) ;
row . append ( chip ( 'Neue Folgen' , where [ live . new _episodes ] || where . inbox , live . new _episodes !== 'inbox' , ( ) =>
openSheet ( ` Neue Folgen von „ ${ plain ( live . title ) } “ … ` , [
{ icon : live . new _episodes === 'inbox' ? '●' : '○' , label : 'In „Neu“' , hint : 'du entscheidest selbst' , action : ( ) => save ( { new _episodes : 'inbox' } ) } ,
{ icon : live . new _episodes === 'queue' ? '●' : '○' , label : 'Gleich in die Warteschlange' , hint : pos , action : ( ) => save ( { new _episodes : 'queue' } ) } ,
{ icon : live . new _episodes === 'ignore' ? '●' : '○' , label : 'Nirgends anzeigen' , hint : 'nur in der Folgenliste des Podcasts' , action : ( ) => save ( { new _episodes : 'ignore' } ) } ,
] ) ) ) ;
const words = [ live . include _words && ` nur „ ${ live . include _words } “ ` , live . exclude _words && ` ohne „ ${ live . exclude _words } “ ` ,
live . min _duration && ` ab ${ live . min _duration } min ` ] . filter ( Boolean ) . join ( ', ' ) ;
const ask = async ( key , title , now ) => {
const v = typeof customPrompt === 'function' ? await customPrompt ( title , now || '' ) : null ;
if ( v != null ) save ( { [ key ] : v . trim ( ) } ) ;
} ;
2026-10-02 11:46:37 +02:00
// AntennaPod's per-podcast auto-download: new episodes land on the phone by themselves.
if ( 'auto_download' in live && N . podcastDownloads ) {
row . append ( chip ( 'Auto-Download' , live . auto _download ? 'an' : 'aus' , ! ! live . auto _download , ( ) =>
openSheet ( ` Neue Folgen von „ ${ plain ( live . title ) } “ herunterladen? ` , [
{ icon : live . auto _download ? '●' : '○' , label : 'Ja, von selbst' , hint : 'im WLAN, innerhalb des Download-Limits' ,
action : ( ) => save ( { auto _download : true } ) } ,
{ icon : ! live . auto _download ? '●' : '○' , label : 'Nein' , hint : 'nur die Warteschlange wird geladen' , action : ( ) => save ( { auto _download : false } ) } ,
] ) ) ) ;
}
2026-10-02 16:36:00 +02:00
// AntennaPod's episode notifications: the phone tells when a new episode is in „Neu“.
if ( 'notify' in live ) {
row . append ( chip ( 'Benachrichtigen' , live . notify ? 'an' : 'aus' , ! ! live . notify , ( ) =>
openSheet ( ` Bei neuen Folgen von „ ${ plain ( live . title ) } “ benachrichtigen? ` , [
{ icon : live . notify ? '●' : '○' , label : 'Ja' , hint : 'eine Mitteilung auf dem Telefon, sobald eine neue Folge in „Neu“ ist' ,
action : ( ) => save ( { notify : true } ) } ,
{ icon : ! live . notify ? '●' : '○' , label : 'Nein' , action : ( ) => save ( { notify : false } ) } ,
] ) ) ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
row . append ( chip ( 'Filter' , words || 'aus' , ! ! words , ( ) => openSheet ( 'Welche neuen Folgen zählen?' , [
{ icon : '+' , label : 'Nur mit diesen Wörtern im Titel' , hint : live . include _words || 'alle' ,
action : ( ) => ask ( 'include_words' , 'Nur Folgen, deren Titel eines dieser Wörter enthält (mit Komma getrennt):' , live . include _words ) } ,
{ icon : '− ' , label : 'Ohne diese Wörter im Titel' , hint : live . exclude _words || 'keine' ,
action : ( ) => ask ( 'exclude_words' , 'Folgen mit einem dieser Wörter im Titel weglassen (mit Komma getrennt):' , live . exclude _words ) } ,
{ icon : '⏱' , label : 'Mindestlänge' , hint : live . min _duration ? ` ${ live . min _duration } Minuten ` : 'keine' ,
action : ( ) => openSheet ( 'Kürzere Folgen weglassen' , [ 0 , 5 , 10 , 15 , 20 , 30 , 45 , 60 ] . map ( m => ( {
icon : ( live . min _duration || 0 ) === m ? '●' : '○' , label : m ? ` unter ${ m } Minuten weglassen ` : 'Keine Mindestlänge' ,
action : ( ) => save ( { min _duration : m } ) ,
} ) ) ) } ,
] ) ) ) ;
return row ;
}
function renderFeedEpisodes ( f ) {
if ( feedView . episodes == null ) { empty ( 'Folgen werden geladen …' ) ; return ; }
if ( feedView . error ) { empty ( 'Die Folgen lassen sich gerade nicht laden. Das braucht eine Verbindung.' ) ; return ; }
for ( const o of feedView . episodes ) {
// Feed episodes don't name their podcast; queue entries need it.
o . feed _ _id = f . id ;
o . feed _ _title = f . title ;
o . feed _ _artwork _url = f . artwork _url ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
}
const prefs = feedListPrefs ( f . id ) ;
const filters = ( prefs . filters || [ ] ) . filter ( id => FILTERS . some ( x => x . id === id && ! x . all && ( ! x . app || DL ) ) ) ;
const sort = EP _SORTS . some ( s => s . id === prefs . sort && ! s . all ) ? prefs . sort : 'new' ;
const all = feedView . episodes ;
const items = sortEpisodes ( all . filter ( o => matchesAll ( o , filters ) ) , sort ) . map ( ep ) ;
if ( selected ) {
root . append ( episodeSelection ( items ) ) ;
} else {
const save = changes => { saveFeedListPrefs ( f . id , Object . assign ( { filters , sort } , changes ) ) ; render ( ) ; } ;
root . append ( filterRow ( filters , next => save ( { filters : next } ) , false ) ) ;
const head = el ( 'div' , 'nui-pod-head' ) ;
head . append ( el ( 'div' , 'nui-pod-summary' , ( items . length === all . length ? nEpisodes ( all . length )
: ` ${ items . length } von ${ nEpisodes ( all . length ) } ` ) + ' · ' + sortLabel ( sort ) . toLowerCase ( ) ) ,
listTools ( [
button ( '⇅' , 'Sortieren' , ( ) => sortMenuFor ( sort , false , id => save ( { sort : id } ) ) ) ,
items . length > 0 && button ( '☐' , 'Mehrere auswählen' , ( ) => startSelecting ( ) ) ,
] ) ) ;
root . append ( head ) ;
}
if ( ! items . length ) { empty ( all . length ? noneText ( filters ) : 'Dieser Podcast hat noch keine Folgen.' ) ; return ; }
const list = el ( 'div' , 'nui-pod-list' ) ;
for ( const e of items ) {
const o = e . raw ;
const row = episodeRow ( e , { showDate : true } ) ;
if ( o . favorite ) row . querySelector ( '.nui-pod-title' ) . prepend ( el ( 'span' , 'nui-pod-star' , '★ ' ) ) ;
if ( ! selected ) {
row . append (
inQueue ( e . id ) ? button ( '✓' , 'In der Warteschlange' , ( ) => toast ( 'Schon in der Warteschlange' ) , 'nui-pod-queued' )
: button ( '+' , 'Einreihen' , ( ) => { enqueue ( e ) ; o . in _queue = true ; } ) ,
button ( '⋯' , 'Mehr' , ( ) => episodeMenu ( e ) ) ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
list . append ( row ) ;
}
root . append ( list ) ;
}
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// A podcast's list: its filters and order, per podcast. On the server (episode_sort,
// episode_filter: the same on every device) where it knows them; else on this device.
const serverLists = live => ! ! live && 'episode_sort' in live && ! ! N . podcastFeedSettings ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
function feedListPrefs ( id ) {
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
const live = feeds ( ) . find ( x => x . id === id ) ;
if ( serverLists ( live ) ) {
return { sort : live . episode _sort || 'new' , filters : Array . isArray ( live . episode _filter ) ? live . episode _filter : [ ] } ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
try { return JSON . parse ( localStorage . getItem ( 'nui_pod_feedlists' ) || '{}' ) [ id ] || { } ; } catch ( e ) { return { } ; }
}
function saveFeedListPrefs ( id , prefs ) {
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
const live = feeds ( ) . find ( x => x . id === id ) ;
if ( serverLists ( live ) ) {
const sort = prefs . sort === 'new' ? '' : prefs . sort ;
// Shows at once; the host sends it on.
live . episode _sort = sort ;
live . episode _filter = prefs . filters . slice ( ) ;
N . podcastFeedSettings ( id , JSON . stringify ( { episode _sort : sort , episode _filter : prefs . filters } ) ) ;
return ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
try {
const all = JSON . parse ( localStorage . getItem ( 'nui_pod_feedlists' ) || '{}' ) ;
if ( ! prefs . filters . length && prefs . sort === 'new' ) delete all [ id ] ; else all [ id ] = prefs ;
localStorage . setItem ( 'nui_pod_feedlists' , JSON . stringify ( all ) ) ;
} catch ( e ) { }
}
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// Once the server keeps the lists: what this device remembered goes there (where the
// server has nothing set for that podcast yet), then the device forgets it.
function moveFeedListsToServer ( ) {
if ( ! N . podcastFeedSettings ) return ;
let all = null ;
try { all = JSON . parse ( localStorage . getItem ( 'nui_pod_feedlists' ) || 'null' ) ; } catch ( e ) { all = null ; }
if ( ! all ) return ;
const list = feeds ( ) ;
if ( ! list . length || ! list . some ( f => 'episode_sort' in f ) ) return ;
try { localStorage . removeItem ( 'nui_pod_feedlists' ) ; } catch ( e ) { return ; }
for ( const id of Object . keys ( all ) ) {
const live = list . find ( f => String ( f . id ) === id ) ;
const p = all [ id ] || { } ;
if ( ! serverLists ( live ) || live . episode _sort || ( live . episode _filter || [ ] ) . length ) continue ;
const filters = ( Array . isArray ( p . filters ) ? p . filters : [ ] ) . filter ( x => FILTERS . some ( y => y . id === x && ! y . all ) ) ;
const sort = EP _SORTS . some ( s => s . id === p . sort && ! s . all ) ? p . sort : 'new' ;
if ( filters . length || sort !== 'new' ) saveFeedListPrefs ( live . id , { filters , sort } ) ;
}
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
// ---- a podcast's info page: description, website, feed address, numbers ----------------
// In the shownotes' sheet (closeNotes closes it, also with the back button).
function openFeedInfo ( f ) {
const live = Object . assign ( { } , f , feedView && feedView . feed . id === f . id ? feedView . feed : { } ,
feeds ( ) . find ( x => x . id === f . id ) || { } ) ;
closeNotes ( ) ;
notesSheet = el ( 'div' ) ;
notesSheet . id = 'nui-pod-player' ;
notesSheet . className = 'nui-pod-notes-sheet nui-pod-info' ;
const panel = el ( 'div' , 'nui-pod-player-panel' ) ;
const top = el ( 'div' , 'nui-pod-player-top' ) ;
top . append ( button ( '⌄' , 'Schließen' , closeNotes ) ) ;
const head = el ( 'div' , 'nui-pod-feedhead' ) ;
const title = el ( 'div' , 'nui-pod-main' ) ;
title . append ( el ( 'div' , 'nui-pod-title' , plain ( live . title ) ) ) ;
if ( live . author ) title . append ( el ( 'div' , 'nui-pod-meta' , plain ( live . author ) ) ) ;
head . append ( art ( live . artwork _url , 'big' ) , title ) ;
panel . append ( top , head ) ;
// Feed descriptions may carry HTML: only its text.
const text = live . description ? ( new DOMParser ( ) . parseFromString ( live . description , 'text/html' ) . body . textContent || '' ) . trim ( ) : '' ;
if ( text ) panel . append ( el ( 'div' , 'nui-pod-info-desc' , text ) ) ;
else if ( feedView && feedView . feed . id === f . id && feedView . episodes == null ) {
panel . append ( el ( 'div' , 'nui-pod-meta' , 'Beschreibung wird geladen …' ) ) ;
}
const facts = el ( 'dl' , 'nui-pod-facts' ) ;
const fact = ( label , ... value ) => {
if ( ! value . length || value [ 0 ] == null || value [ 0 ] === '' ) return ;
const dd = el ( 'dd' ) ;
dd . append ( ... value . map ( v => ( typeof v === 'string' ? document . createTextNode ( v ) : v ) ) ) ;
facts . append ( el ( 'dt' , null , label ) , dd ) ;
} ;
const when = iso => ( iso ? day ( iso ) + ', ' + new Date ( iso ) . toLocaleTimeString ( 'de-DE' , { hour : '2-digit' , minute : '2-digit' } ) : null ) ;
if ( live . link && /^https?:\/\//i . test ( live . link ) ) {
const a = el ( 'a' , null , live . link . replace ( /^https?:\/\/(www\.)?/i , '' ) . replace ( /\/$/ , '' ) ) ;
a . href = live . link ;
a . target = '_blank' ;
a . rel = 'noopener' ;
fact ( 'Website' , a ) ;
}
if ( live . rss _url ) {
const row = el ( 'div' , 'nui-pod-info-actions' ) ;
const action = ( label , onClick ) => {
const b = el ( 'button' , 'btn' , label ) ;
b . type = 'button' ;
b . addEventListener ( 'click' , onClick ) ;
row . append ( b ) ;
} ;
action ( 'Kopieren' , ( ) => copyText ( live . rss _url ) ) ;
if ( navigator . share ) action ( 'Teilen' , ( ) => navigator . share ( { title : plain ( live . title ) , url : live . rss _url } ) . catch ( ( ) => { } ) ) ;
fact ( 'Feed' , el ( 'span' , 'nui-pod-info-url' , live . rss _url ) , row ) ;
}
const count = live . episode _count != null ? live . episode _count : feedView && feedView . feed . id === f . id && feedView . episodes
? feedView . episodes . length : null ;
if ( count != null ) {
fact ( 'Folgen' , [ String ( count ) , live . unplayed _count != null && ` ${ live . unplayed _count } ungehört ` ,
live . new _count && ` ${ live . new _count } neu ` ] . filter ( Boolean ) . join ( ' · ' ) ) ;
}
if ( live . latest _episode _at ) fact ( 'Neueste Folge' , day ( live . latest _episode _at ) ) ;
if ( live . last _played _at ) fact ( 'Zuletzt gehört' , day ( live . last _played _at ) ) ;
if ( live . added _at ) fact ( 'Abonniert' , new Date ( live . added _at ) . toLocaleDateString ( 'de-DE' , { day : 'numeric' , month : 'long' , year : 'numeric' } ) ) ;
fact ( 'Abgerufen' , when ( live . last _refreshed _at ) || 'noch nie' ) ;
if ( live . last _error ) {
fact ( 'Fehler' , el ( 'span' , 'nui-pod-feederr' , ` ⚠ ${ live . last _error } ${ live . last _error _at ? ' (' + day ( live . last _error _at ) + ')' : '' } ` +
( live . error _count > 1 ? ` · ${ live . error _count } × nacheinander` : '' ) ) ) ;
}
panel . append ( facts ) ;
notesSheet . append ( panel ) ;
notesSheet . addEventListener ( 'click' , ev => { if ( ev . target === notesSheet ) closeNotes ( ) ; } ) ;
document . body . append ( notesSheet ) ;
}
async function copyText ( text ) {
try {
await navigator . clipboard . writeText ( text ) ;
toast ( 'Kopiert' ) ;
return ;
} catch ( e ) { }
const t = el ( 'textarea' ) ;
t . value = text ;
t . style . cssText = 'position:fixed;top:0;left:0;opacity:0' ;
document . body . append ( t ) ;
t . select ( ) ;
let ok = false ;
try { ok = document . execCommand ( 'copy' ) ; } catch ( e ) { }
t . remove ( ) ;
toast ( ok ? 'Kopiert' : 'Kopieren ging nicht' ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function volumeLabel ( v ) {
if ( ! v || v === 1 ) return '' ;
return { 0.2 : 'viel leiser' , 0.5 : 'leiser' , 1.5 : '+5 dB' , 2 : '+10 dB' , 2.5 : '+15 dB' } [ v ] || '' ;
}
// This podcast's own playback: speed, intro and outro to skip.
function feedSettings ( f ) {
const live = feeds ( ) . find ( x => x . id === f . id ) || f ;
const own = 'skip_intro' in live
? { speed : live . playback _speed || 0 , intro : live . skip _intro || 0 , outro : live . skip _outro || 0 , volume : live . volume || 1 }
: ( devicePrefs ( ) . feeds || { } ) [ String ( f . id ) ] || { } ;
const row = el ( 'div' , 'nui-pod-feedprefs' ) ;
const chip = ( label , value , onClick ) => {
const b = el ( 'button' , 'nui-pod-chip' + ( value ? ' on' : '' ) ) ;
b . type = 'button' ;
b . append ( el ( 'span' , null , label ) , el ( 'strong' , null , value || 'aus' ) ) ;
b . addEventListener ( 'click' , onClick ) ;
return b ;
} ;
const set = ( key , v ) => { N . podcastFeedPrefs ( f . id , JSON . stringify ( { [ key ] : v } ) ) ; setTimeout ( render , 50 ) ; } ;
const seconds = ( key , title ) => openSheet ( title , [ 0 , 10 , 15 , 20 , 30 , 45 , 60 , 90 , 120 ] . map ( v => ( {
icon : ( own [ key ] || 0 ) === v ? '●' : '○' , label : v ? ` ${ v } Sekunden ` : 'Nichts überspringen' , action : ( ) => set ( key , v ) ,
} ) ) ) ;
row . append (
chip ( 'Tempo' , own . speed ? speedLabel ( own . speed ) : '' , ( ) => openSheet ( ` Geschwindigkeit für „ ${ plain ( f . title ) } “ ` ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
speedChoices ( own . speed ) . map ( v => ( { icon : Math . abs ( ( own . speed || 0 ) - v ) < 0.01 ? '●' : '○' , label : speedLabel ( v ) ,
action : ( ) => set ( 'speed' , v ) } ) )
. concat ( [ { icon : '↺' , label : 'Wie alle anderen Podcasts' , action : ( ) => set ( 'speed' , 0 ) } ] )
. concat ( presetsEditItem ( ) ) ) ) ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
chip ( 'Intro' , own . intro ? ` ${ own . intro } s ` : '' , ( ) => seconds ( 'intro' , 'Am Anfang jeder Folge überspringen' ) ) ,
chip ( 'Outro' , own . outro ? ` ${ own . outro } s ` : '' , ( ) => seconds ( 'outro' , 'Am Ende jeder Folge überspringen' ) ) ,
chip ( 'Lautstärke' , volumeLabel ( own . volume ) , ( ) => openSheet ( ` Lautstärke für „ ${ plain ( f . title ) } “ ` ,
[ [ 0.2 , 'Viel leiser' ] , [ 0.5 , 'Leiser' ] , [ 1 , 'Normal' ] , [ 1.5 , 'Lauter (+5 dB)' ] , [ 2 , 'Viel lauter (+10 dB)' ] , [ 2.5 , 'Sehr viel lauter (+15 dB)' ] ]
. map ( ( [ v , label ] ) => ( { icon : ( own . volume || 1 ) === v ? '●' : '○' , label , action : ( ) => set ( 'volume' , v ) } ) ) ) ) ,
) ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// Skip silence for this podcast (server), where this player can skip silence at all.
if ( 'skip_silence' in live && 'skip_silence' in devicePrefs ( ) && N . podcastFeedSettings ) {
const mine = live . skip _silence ;
const device = ! ! devicePrefs ( ) . skip _silence ;
const b = el ( 'button' , 'nui-pod-chip' + ( mine != null ? ' on' : '' ) ) ;
b . type = 'button' ;
b . append ( el ( 'span' , null , 'Stille' ) , el ( 'strong' , null , mine === true ? 'überspringen' : mine === false ? 'nie überspringen'
: 'wie eingestellt' ) ) ;
const save = v => { N . podcastFeedSettings ( f . id , JSON . stringify ( { skip _silence : v } ) ) ; setTimeout ( render , 50 ) ; } ;
b . addEventListener ( 'click' , ( ) => openSheet ( ` Stille in „ ${ plain ( f . title ) } “ … ` , [
{ icon : mine == null ? '●' : '○' , label : 'Wie eingestellt' , hint : device ? 'überspringen (alle Podcasts)' : 'nicht überspringen (alle Podcasts)' ,
action : ( ) => save ( null ) } ,
{ icon : mine === true ? '●' : '○' , label : 'Überspringen' , hint : 'Pausen werden ausgelassen' , action : ( ) => save ( true ) } ,
{ icon : mine === false ? '●' : '○' , label : 'Nie überspringen' , action : ( ) => save ( false ) } ,
] ) ) ;
row . append ( b ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
return row ;
}
// Unsubscribing (server: POST /podcasts/feeds/<id>/remove/). Asked once more
// right here; then the server forgets the podcast with its episodes, and the
// app deletes their downloads (not those of favourites).
function unsubscribeBox ( f ) {
const box = el ( 'div' , 'nui-pod-unsub' ) ;
if ( ! feedView . confirmRemove ) {
const b = el ( 'button' , 'nui-pod-unsub-btn' , 'Abo beenden' ) ;
b . type = 'button' ;
b . addEventListener ( 'click' , ( ) => {
if ( ! navigator . onLine ) { toast ( 'Abo beenden braucht eine Verbindung.' ) ; return ; }
feedView . confirmRemove = true ;
render ( ) ;
} ) ;
box . append ( b ) ;
return box ;
}
box . classList . add ( 'nui-pod-unsub-ask' ) ;
box . append ( el ( 'div' , 'nui-pod-title' , ` „ ${ plain ( f . title ) } “ nicht mehr abonnieren? ` ) ,
el ( 'div' , 'nui-pod-meta' , 'Die Folgen verschwinden aus Warteschlange und Listen, mit Fortschritt und Favoriten, ' +
'auch auf dem Server.' + ( DL ? ' Die Downloads dieses Podcasts werden gelöscht.' : '' ) ) ) ;
const actions = el ( 'div' , 'nui-pod-unsub-actions' ) ;
const yes = el ( 'button' , 'btn nui-pod-unsub-yes' , feedView . removing ? 'Wird beendet …' : 'Abo beenden' ) ;
yes . type = 'button' ;
yes . disabled = ! ! feedView . removing ;
yes . addEventListener ( 'click' , ( ) => unsubscribe ( f ) ) ;
const no = el ( 'button' , 'btn' , 'Abbrechen' ) ;
no . type = 'button' ;
no . disabled = ! ! feedView . removing ;
no . addEventListener ( 'click' , ( ) => { feedView . confirmRemove = false ; render ( ) ; } ) ;
actions . append ( yes , no ) ;
box . append ( actions ) ;
return box ;
}
async function unsubscribe ( f ) {
if ( ! navigator . onLine ) { toast ( 'Abo beenden braucht eine Verbindung.' ) ; return ; }
feedView . removing = true ;
render ( ) ;
try {
const r = await fetch ( ` /podcasts/feeds/ ${ f . id } /remove/ ` , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : '{}' ,
} ) ;
// 404: gone already (removed elsewhere) — the same result.
if ( ! r . ok && r . status !== 404 ) throw new Error ( r . status ) ;
} catch ( e ) {
if ( feedView && feedView . feed . id === f . id ) { feedView . removing = false ; render ( ) ; }
toast ( navigator . onLine ? 'Abo beenden hat nicht geklappt.' : 'Abo beenden braucht eine Verbindung.' ) ;
return ;
}
// The episodes the page knows of it: downloads from before the app kept their podcast match by id.
const ids = ( ( feedView && feedView . feed . id === f . id && feedView . episodes ) || [ ] ) . map ( o => o . id )
. concat ( queue ( ) . filter ( q => q . episode _ _feed _ _id === f . id ) . map ( q => q . episode _ _id ) )
. concat ( inbox ( ) . filter ( o => o . feed _ _id === f . id ) . map ( o => o . id ) ) ;
state . feeds = feeds ( ) . filter ( x => x . id !== f . id ) ;
if ( feedView && feedView . feed . id === f . id ) feedView = null ;
refreshing = true ;
render ( ) ;
toast ( ` „ ${ plain ( f . title ) } “ abbestellt ` ) ;
// The app deletes the downloads and fetches the state again.
if ( N . podcastFeedRemoved ) N . podcastFeedRemoved ( f . id , JSON . stringify ( ids ) ) ;
else N . podcastRefresh ( false ) ;
}
// ---- all episodes, filtered (server: GET /podcasts/episodes/) ----------------------
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
// Filters combine (all have to match); "Geladen" asks for the episodes downloaded here.
let epFilters = [ ] ;
let epSort = 'new' ;
try {
const kept = localStorage . getItem ( 'nui_pod_filters' ) ;
if ( kept != null ) epFilters = JSON . parse ( kept ) ;
else {
// Before filters combined: one of them.
const old = localStorage . getItem ( 'nui_pod_filter' ) ;
epFilters = old == null ? [ 'unplayed' ] : old === 'all' ? [ ] : [ old ] ;
if ( old === 'history' ) epSort = 'played' ;
}
epSort = localStorage . getItem ( 'nui_pod_epsort' ) || epSort ;
} catch ( e ) { }
if ( ! Array . isArray ( epFilters ) ) epFilters = [ ] ;
epFilters = epFilters . filter ( id => FILTERS . some ( f => f . id === id && ( ! f . app || ! ! N . podcastDownloads ) ) ) ;
if ( ! EP _SORTS . some ( s => s . id === epSort ) ) epSort = 'new' ;
let epList = null ; // {key, items, total, loading, error}
const epKey = ( ) => epFilters . slice ( ) . sort ( ) . join ( ',' ) + '|' + epSort ;
function setEpisodeList ( filters , sort ) {
// The history goes by the last listening; leaving it, by date again.
if ( sort == null ) {
sort = epSort ;
if ( filters . includes ( 'history' ) && ! epFilters . includes ( 'history' ) && sort === 'new' ) sort = 'played' ;
if ( ! filters . includes ( 'history' ) && epFilters . includes ( 'history' ) && sort === 'played' ) sort = 'new' ;
}
epFilters = filters ;
epSort = sort ;
try {
localStorage . setItem ( 'nui_pod_filters' , JSON . stringify ( epFilters ) ) ;
localStorage . setItem ( 'nui_pod_epsort' , epSort ) ;
} catch ( e ) { }
loadEpisodes ( false ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
async function loadEpisodes ( append , quiet ) {
const key = epKey ( ) ;
const offset = append && epList && epList . key === key ? epList . items . length : 0 ;
if ( ! append ) epList = { key , items : [ ] , total : 0 , loading : true } ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
else epList . loading = true ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( ! quiet ) render ( ) ;
const params = new URLSearchParams ( {
filter : epFilters . filter ( id => id !== 'downloaded' ) . join ( ',' ) , sort : epSort , limit : '100' , offset : String ( offset ) ,
} ) ;
if ( epFilters . includes ( 'downloaded' ) ) {
const ids = Object . keys ( downloads ) . filter ( id => downloads [ id ] . state === 'done' ) ;
if ( ! ids . length ) { epList = { key , items : [ ] , total : 0 } ; render ( ) ; return ; }
params . set ( 'ids' , ids . join ( ',' ) ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
try {
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
const r = await fetch ( ` /podcasts/episodes/? ${ params } ` ) ;
if ( r . status === 400 ) throw new Error ( 'old' ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! r . ok ) throw new Error ( r . status ) ;
const d = await r . json ( ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( ! epList || epList . key !== key ) return ;
epList = { key , items : ( append ? epList . items : [ ] ) . concat ( d . episodes || [ ] ) , total : d . total || 0 } ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} catch ( e ) {
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( ! epList || epList . key !== key ) return ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
epList . loading = false ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
epList . error = e . message === 'old' ? 'Der Server kennt diese Filter noch nicht.' : 'Die Folgenliste braucht eine Verbindung.' ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
render ( ) ;
}
function renderEpisodes ( ) {
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( ! selected ) root . append ( filterRow ( epFilters , next => setEpisodeList ( next ) , true ) ) ;
if ( ! epList || epList . key !== epKey ( ) ) { loadEpisodes ( false , true ) ; empty ( 'Wird geladen …' ) ; return ; }
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( epList . error && ! epList . items . length ) { empty ( epList . error ) ; return ; }
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
// What changed here since (marked, starred, queued) shows at once.
const items = epList . items . filter ( o => matchesAll ( o , epFilters ) ) . map ( ep ) ;
const total = Math . max ( items . length , epList . total - ( epList . items . length - items . length ) ) ;
if ( ! items . length ) {
empty ( epList . loading ? 'Wird geladen …' : noneText ( epFilters ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
return ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( selected ) {
root . append ( episodeSelection ( items ) ) ;
} else {
const head = el ( 'div' , 'nui-pod-head' ) ;
head . append ( el ( 'div' , 'nui-pod-summary' , ` ${ nEpisodes ( total ) } · ${ sortLabel ( epSort ) . toLowerCase ( ) } ` ) ,
listTools ( [
button ( '⇅' , 'Sortieren' , ( ) => sortMenuFor ( epSort , true , id => setEpisodeList ( epFilters , id ) ) ) ,
button ( '☐' , 'Mehrere auswählen' , ( ) => startSelecting ( ) ) ,
] ) ) ;
root . append ( head ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const list = el ( 'div' , 'nui-pod-list' ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
for ( const e of items ) {
const o = e . raw ;
const row = episodeRow ( e , { showFeed : true , showDate : epSort !== 'played' } ) ;
if ( epSort === 'played' && o . last _played _at ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
row . querySelector ( '.nui-pod-meta' ) . textContent = 'gehört ' + day ( o . last _played _at ) + ' · ' + ( e . feed || '' ) ;
}
if ( o . favorite ) row . querySelector ( '.nui-pod-title' ) . prepend ( el ( 'span' , 'nui-pod-star' , '★ ' ) ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( ! selected ) {
row . append (
inQueue ( e . id ) ? button ( '✓' , 'In der Warteschlange' , ( ) => toast ( 'Schon in der Warteschlange' ) , 'nui-pod-queued' )
: button ( '+' , 'Einreihen' , ( ) => enqueue ( e ) ) ,
button ( '⋯' , 'Mehr' , ( ) => episodeMenu ( e ) ) ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
list . append ( row ) ;
}
root . append ( list ) ;
if ( epList . items . length < epList . total ) {
const more = el ( 'button' , 'btn nui-pod-more' , epList . loading ? 'Wird geladen …' : 'Weitere laden' ) ;
more . type = 'button' ;
more . disabled = ! ! epList . loading ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
more . addEventListener ( 'click' , ( ) => loadEpisodes ( true ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
root . append ( more ) ;
}
}
// ---- favourites -------------------------------------------------------------------------
function favoriteItem ( e ) {
const o = e . raw || { } ;
const known = typeof o . favorite === 'boolean' ;
return { icon : o . favorite ? '★' : '☆' , label : known ? ( o . favorite ? 'Kein Favorit mehr' : 'Als Favorit markieren' ) : 'Favorit an/aus' ,
action : ( ) => toggleFavorite ( e , star => { if ( e . raw ) e . raw . favorite = star ; } ) } ;
}
async function toggleFavorite ( e , done ) {
const o = e . raw || { } ;
const body = typeof o . favorite === 'boolean' ? { favorite : ! o . favorite } : { } ;
try {
const r = await fetch ( ` /podcasts/episodes/ ${ e . id } /favorite/ ` , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) ,
} ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
const d = await r . json ( ) ;
toast ( d . favorite ? '★ Als Favorit markiert' : 'Kein Favorit mehr' ) ;
// Favourites keep their download (Downloads.kt).
if ( N . podcastFavoriteChanged ) N . podcastFavoriteChanged ( e . id , ! ! d . favorite ) ;
const cached = storedDetails ( e . id ) ;
if ( cached ) keepDetails ( e . id , Object . assign ( { } , cached , { favorite : d . favorite } ) ) ;
if ( done ) done ( d . favorite ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( epList ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const item = epList . items . find ( x => x . id === e . id ) ;
if ( item ) item . favorite = d . favorite ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
// The ★ in the lists (Folgen, one podcast, search) and their filters.
render ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} catch ( err ) {
toast ( navigator . onLine ? 'Favorit ließ sich nicht setzen' : 'Favoriten brauchen eine Verbindung' ) ;
}
}
// ---- listening statistics (server: GET /podcasts/stats/) -----------------------------
let statsSheet = null ;
function closeStats ( ) {
if ( ! statsSheet ) return false ;
statsSheet . remove ( ) ;
statsSheet = null ;
return true ;
}
function hours ( seconds ) {
const h = Math . floor ( seconds / 3600 ) ;
const m = Math . round ( ( seconds % 3600 ) / 60 ) ;
return h ? ` ${ h } h ${ String ( m ) . padStart ( 2 , '0' ) } min ` : ` ${ m } min ` ;
}
function openStats ( ) {
closeStats ( ) ;
statsSheet = el ( 'div' ) ;
statsSheet . id = 'nui-pod-player' ;
statsSheet . className = 'nui-pod-notes-sheet' ;
const panel = el ( 'div' , 'nui-pod-player-panel' ) ;
const top = el ( 'div' , 'nui-pod-player-top' ) ;
top . append ( button ( '⌄' , 'Schließen' , closeStats ) ) ;
const tabs = el ( 'div' , 'nui-pod-subtabs' ) ;
const body = el ( 'div' , 'nui-pod-stats' ) ;
panel . append ( top , el ( 'div' , 'nui-pod-player-title' , 'Hörstatistik' ) , tabs , body ) ;
statsSheet . append ( panel ) ;
statsSheet . addEventListener ( 'click' , ev => { if ( ev . target === statsSheet ) closeStats ( ) ; } ) ;
document . body . append ( statsSheet ) ;
const ranges = [ { days : 30 , label : '30 Tage' } , { days : 365 , label : 'Jahr' } , { days : 0 , label : 'Gesamt' } ] ;
const show = async range => {
tabs . querySelectorAll ( 'button' ) . forEach ( b => b . classList . toggle ( 'active' , b . dataset . days === String ( range . days ) ) ) ;
body . textContent = '' ;
body . append ( el ( 'div' , 'nui-pod-meta' , 'Wird geladen …' ) ) ;
let d = null ;
try {
const r = await fetch ( '/podcasts/stats/' + ( range . days ? ` ?days= ${ range . days } ` : '' ) ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
d = await r . json ( ) ;
} catch ( e ) {
body . textContent = '' ;
body . append ( el ( 'div' , 'nui-pod-meta' , 'Die Statistik braucht eine Verbindung.' ) ) ;
return ;
}
body . textContent = '' ;
const big = el ( 'div' , 'nui-pod-stat-total' ) ;
big . append ( el ( 'strong' , null , hours ( d . total _seconds || 0 ) ) , el ( 'span' , null ,
` ${ d . episodes _played || 0 } Folgen gehört · ${ d . episodes _started || 0 } angefangen ` ) ) ;
body . append ( big ) ;
const feedsList = d . feeds || [ ] ;
if ( ! feedsList . length ) { body . append ( el ( 'div' , 'nui-pod-meta' , 'In diesem Zeitraum nichts gehört.' ) ) ; return ; }
const max = Math . max ( ... feedsList . map ( f => f . seconds || 0 ) , 1 ) ;
for ( const f of feedsList ) {
const row = el ( 'div' , 'nui-pod-stat-row' ) ;
row . append ( art ( f . artwork _url ) ) ;
const main = el ( 'div' , 'nui-pod-main' ) ;
main . append ( el ( 'div' , 'nui-pod-title' , plain ( f . title ) ) ,
el ( 'div' , 'nui-pod-meta' , ` ${ hours ( f . seconds || 0 ) } · ${ f . episodes _played || 0 } Folgen ` ) ) ;
const bar = el ( 'div' , 'nui-pod-progress nui-pod-stat-bar' ) ;
const fill = el ( 'span' ) ;
fill . style . width = Math . max ( 2 , Math . round ( ( f . seconds || 0 ) / max * 100 ) ) + '%' ;
bar . append ( fill ) ;
main . append ( bar ) ;
row . append ( main ) ;
body . append ( row ) ;
}
} ;
for ( const r of ranges ) {
const b = el ( 'button' , 'nui-pod-subtab' , r . label ) ;
b . type = 'button' ;
b . dataset . days = String ( r . days ) ;
b . addEventListener ( 'click' , ( ) => show ( r ) ) ;
tabs . append ( b ) ;
}
show ( ranges [ 0 ] ) ;
}
// ---- OPML: take the subscriptions to another app, or bring them in -------------------
async function exportOpml ( ) {
try {
const r = await fetch ( '/podcasts/feeds/export.opml' ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
const blob = new Blob ( [ await r . text ( ) ] , { type : 'text/x-opml' } ) ;
const a = document . createElement ( 'a' ) ;
a . href = URL . createObjectURL ( blob ) ;
a . download = 'diora-podcasts.opml' ;
a . click ( ) ; // the app offers to share or save it (native.js)
setTimeout ( ( ) => URL . revokeObjectURL ( a . href ) , 60000 ) ;
} catch ( e ) {
toast ( navigator . onLine ? 'Export hat nicht geklappt' : 'Der Export braucht eine Verbindung' ) ;
}
}
function importOpml ( ) {
const input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . accept = '.opml,.xml,text/x-opml,text/xml,application/xml' ;
input . addEventListener ( 'change' , async ( ) => {
const file = input . files && input . files [ 0 ] ;
if ( ! file ) return ;
const form = new FormData ( ) ;
form . append ( 'file' , file ) ;
toast ( 'Abos werden importiert …' ) ;
try {
const r = await fetch ( '/podcasts/feeds/import/' , { method : 'POST' , body : form } ) ;
const d = await r . json ( ) ;
if ( ! r . ok || ! d . ok ) throw new Error ( d . error || r . status ) ;
toast ( ` ${ d . added } neue Abos ${ d . skipped ? ` , ${ d . skipped } schon vorhanden ` : '' } ` ) ;
refresh ( true ) ;
} catch ( e ) {
toast ( 'Diese Datei ließ sich nicht importieren' ) ;
}
} ) ;
input . click ( ) ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
// ---- search: own podcasts, their episodes (also in the shownotes), the directory ----
// searchResults: {q, feeds, eps: {items, total, loading, local}, dir: {items, loading, error}}.
// Without a connection the episodes come from what this device knows (queue,
// "Neu", lists seen), the podcasts from the kept state.
let searchDraft = null ;
function fold ( s ) {
return plain ( s || '' ) . toLocaleLowerCase ( 'de' ) . normalize ( 'NFD' ) . replace ( /[̀-ͯ]/g , '' ) ;
}
// All words of the query, anywhere in the text.
function matcher ( q ) {
const words = fold ( q ) . split ( /\s+/ ) . filter ( Boolean ) ;
return text => words . every ( w => text . includes ( w ) ) ;
}
function knownEpisodes ( ) {
const seen = new Map ( ) ;
const add = o => { if ( o && ! seen . has ( shape ( o ) . id ) ) seen . set ( shape ( o ) . id , o ) ; } ;
queue ( ) . forEach ( add ) ;
inbox ( ) . forEach ( add ) ;
if ( epList ) epList . items . forEach ( add ) ;
if ( feedView && feedView . episodes ) feedView . episodes . forEach ( add ) ;
return [ ... seen . values ( ) ] ;
}
function searchText ( o ) {
const e = shape ( o ) ;
const notes = o . description ? o . description . replace ( /<[^>]*>/g , ' ' ) : '' ;
return fold ( e . title + ' ' + notes ) ;
}
function searchNote ( text ) {
root . append ( el ( 'div' , 'nui-pod-sec-note' , text ) ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function renderSearch ( ) {
const form = el ( 'form' , 'nui-pod-search' ) ;
const input = el ( 'input' , 'search-input' ) ;
input . type = 'search' ;
input . id = 'nui-pod-q' ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
input . placeholder = 'Abos, Folgen, Verzeichnis – oder Feed-Adresse …' ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
input . setAttribute ( 'enterkeyhint' , 'search' ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
input . value = searchDraft != null ? searchDraft : searchResults ? searchResults . q : '' ;
input . addEventListener ( 'input' , ( ) => { searchDraft = input . value ; } ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const go = el ( 'button' , 'btn' , 'Suchen' ) ;
go . type = 'submit' ;
form . append ( input , go ) ;
form . addEventListener ( 'submit' , ev => {
ev . preventDefault ( ) ;
search ( input . value . trim ( ) ) ;
} ) ;
root . append ( form ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
const s = searchResults ;
if ( ! s ) {
empty ( 'Sucht in deinen Abos und ihren Folgen (auch in den Shownotes) und im Podcast-Verzeichnis von Apple. ' +
'Eine Feed-Adresse (https://…) abonnierst du direkt.' ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
return ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( s . feeds . length ) {
root . append ( el ( 'div' , 'nui-pod-sec' , ` Deine Podcasts ( ${ s . feeds . length } ) ` ) ) ;
const list = el ( 'div' , 'nui-pod-list' ) ;
for ( const f of s . feeds ) list . append ( feedRow ( f ) ) ;
root . append ( list ) ;
}
const eps = s . eps ;
root . append ( el ( 'div' , 'nui-pod-sec' , 'Folgen' + ( eps . items . length ? ` ( ${ eps . total } ) ` : '' ) ) ) ;
if ( eps . local ) searchNote ( 'Ohne Verbindung: nur Folgen, die dieses Gerät kennt.' ) ;
if ( ! eps . items . length ) {
searchNote ( eps . loading ? 'Wird gesucht …' : 'Keine Folgen gefunden.' ) ;
} else {
const list = el ( 'div' , 'nui-pod-list' ) ;
for ( const o of eps . items ) {
const e = ep ( o ) ;
const row = episodeRow ( e , { showFeed : true , showDate : true , noSelect : true } ) ;
if ( o . favorite ) row . querySelector ( '.nui-pod-title' ) . prepend ( el ( 'span' , 'nui-pod-star' , '★ ' ) ) ;
row . append (
inQueue ( e . id ) ? button ( '✓' , 'In der Warteschlange' , ( ) => toast ( 'Schon in der Warteschlange' ) , 'nui-pod-queued' )
: button ( '+' , 'Einreihen' , ( ) => enqueue ( e ) ) ,
button ( '⋯' , 'Mehr' , ( ) => episodeMenu ( e ) ) ) ;
list . append ( row ) ;
}
root . append ( list ) ;
if ( ! eps . local && eps . items . length < eps . total ) {
const more = el ( 'button' , 'btn nui-pod-more' , eps . loading ? 'Wird geladen …' : 'Weitere Folgen' ) ;
more . type = 'button' ;
more . disabled = ! ! eps . loading ;
more . addEventListener ( 'click' , ( ) => searchEpisodes ( s , true ) ) ;
root . append ( more ) ;
}
}
const dir = s . dir ;
root . append ( el ( 'div' , 'nui-pod-sec' , 'Im Podcast-Verzeichnis' ) ) ;
if ( dir . loading ) { searchNote ( 'Wird gesucht …' ) ; return ; }
if ( dir . error ) { searchNote ( dir . error ) ; return ; }
if ( ! dir . items . length ) { searchNote ( 'Nichts gefunden.' ) ; return ; }
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const subscribed = new Set ( feeds ( ) . map ( f => f . rss _url ) ) ;
const list = el ( 'div' , 'nui-pod-list' ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
for ( const r of dir . items ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const row = el ( 'div' , 'nui-pod-row' ) ;
row . append ( art ( r . artwork _url ) ) ;
const main = el ( 'div' , 'nui-pod-main' ) ;
main . append ( el ( 'div' , 'nui-pod-title' , r . title ) , el ( 'div' , 'nui-pod-meta' , [ r . author , r . genre ] . filter ( Boolean ) . join ( ' · ' ) ) ) ;
row . append ( main ) ;
if ( ! r . rss _url ) row . append ( el ( 'span' , 'nui-pod-meta' , 'kein Feed' ) ) ;
else if ( subscribed . has ( r . rss _url ) ) row . append ( el ( 'span' , 'nui-pod-badge' , 'abonniert' ) ) ;
else row . append ( button ( '+' , 'Abonnieren' , ( ) => subscribe ( r . rss _url , r . title ) ) ) ;
list . append ( row ) ;
}
root . append ( list ) ;
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
function search ( q ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! q ) return ;
2026-10-02 16:29:15 +02:00
if ( /^https?:\/\//i . test ( q ) ) { if ( N . podcastPreview ) openPreview ( q ) ; else subscribe ( q , q ) ; return ; }
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
searchDraft = null ;
const test = matcher ( q ) ;
const s = searchResults = {
q ,
feeds : feeds ( ) . filter ( f => test ( fold ( f . title ) + ' ' + fold ( f . author ) ) ) ,
eps : { items : [ ] , total : 0 , loading : true } ,
dir : { items : [ ] , loading : true } ,
} ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
render ( ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
searchEpisodes ( s , false ) ;
searchDirectory ( s ) ;
}
async function searchEpisodes ( s , more ) {
const offset = more ? s . eps . items . length : 0 ;
s . eps . loading = true ;
if ( more ) render ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
try {
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
const r = await fetch ( ` /podcasts/episodes/?q= ${ encodeURIComponent ( s . q ) } ¬es=1&limit=30&offset= ${ offset } ` ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
const d = await r . json ( ) ;
s . eps = { items : ( more ? s . eps . items : [ ] ) . concat ( d . episodes || [ ] ) , total : d . total || 0 } ;
} catch ( e ) {
if ( more ) {
s . eps . loading = false ;
toast ( 'Weitere Folgen brauchen eine Verbindung.' ) ;
} else {
const test = matcher ( s . q ) ;
const items = knownEpisodes ( ) . filter ( o => test ( searchText ( o ) ) ) ;
s . eps = { items , total : items . length , local : true } ;
}
}
if ( searchResults === s ) render ( ) ;
}
async function searchDirectory ( s ) {
try {
const r = await fetch ( '/podcasts/search/?q=' + encodeURIComponent ( s . q ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const data = await r . json ( ) ;
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
s . dir = { items : data . results || [ ] , error : data . error ? 'Das Verzeichnis ist gerade nicht erreichbar.' : null } ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} catch ( e ) {
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
s . dir = { items : [ ] , error : 'Das Verzeichnis braucht eine Verbindung.' } ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
Podcast-Listen: Zähler pro Abo, kombinierbare Folgen-Filter, Suche in Shownotes, mehrere Favoriten; Oberfläche aus diora-android, sw v62
- feeds/ und state/: pro Abo episode_count, unplayed_count, new_count (wie
„Neu“), last_played_at und link – zwei Abfragen, egal wie viele Abos.
- feeds/<id>/episodes/: favorite pro Folge; description, link, rss_url und
last_refreshed_at des Podcasts für die Infoseite.
- episodes/: filter mit Komma kombinierbar (alle müssen passen), neuer Filter
queued, ids= (die auf einem Gerät geladenen), sort= (new, old, long, short,
title, played), notes=1 sucht q auch in den Shownotes (Titeltreffer zuerst).
- POST episodes/favorite/: mehrere Folgen auf einmal (ent)favorisieren.
- Tests dafür; Podcast-Oberfläche aus diora-android übernommen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:23:00 +02:00
if ( searchResults === s ) render ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
async function subscribe ( rssUrl , title ) {
toast ( 'Wird abonniert …' ) ;
try {
const r = await fetch ( '/podcasts/feeds/add/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { rss _url : rssUrl , title } ) ,
} ) ;
const data = await r . json ( ) ;
if ( ! r . ok || ! data . ok ) throw new Error ( data . error || r . status ) ;
toast ( ` „ ${ data . title || title } “ abonniert ` ) ;
refresh ( false ) ;
2026-10-02 16:29:15 +02:00
return true ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} catch ( e ) {
toast ( navigator . onLine ? 'Dieser Feed lässt sich nicht abonnieren.' : 'Abonnieren braucht eine Verbindung.' ) ;
2026-10-02 16:29:15 +02:00
return false ;
}
}
// A feed from another app (shared link, pcast:// …) or a pasted address:
// the app fetches it (N.podcastPreview, FeedPreview.kt) and the page shows
// title, cover, description and the newest episodes before subscribing.
function openPreview ( url ) {
if ( ! N . podcastPreview ) { subscribe ( url , url ) ; return ; }
closePlayer ( ) ;
feedView = null ;
selected = null ;
preview = { id : ++ previewSeq , url } ;
if ( typeof showTab === 'function' ) showTab ( 'podcasts' ) ;
render ( ) ;
scrollTop ( 0 ) ;
N . podcastPreview ( preview . id , url ) ;
}
function closePreview ( ) {
if ( ! preview ) return false ;
preview = null ;
render ( ) ;
return true ;
}
function renderPreview ( ) {
const p = preview ;
const d = p . data || { } ;
const head = el ( 'div' , 'nui-pod-feedhead' ) ;
head . append ( button ( '‹ ' , 'Zurück' , closePreview ) , art ( d . artwork , 'big' ) ) ;
const info = el ( 'div' , 'nui-pod-main' ) ;
info . append ( el ( 'div' , 'nui-pod-title' , plain ( d . title ) || 'Podcast' ) ,
el ( 'div' , 'nui-pod-meta' , plain ( d . author ) || p . url ) ) ;
head . append ( info ) ;
root . append ( head ) ;
if ( p . choices ) {
root . append ( el ( 'div' , 'nui-pod-empty' , 'Diese Seite verlinkt mehrere Feeds. Welcher ist es?' ) ) ;
const list = el ( 'div' , 'nui-pod-list' ) ;
for ( const c of p . choices ) {
const row = el ( 'div' , 'nui-pod-row' ) ;
const main = el ( 'div' , 'nui-pod-main' ) ;
main . append ( el ( 'div' , 'nui-pod-title' , plain ( c . title ) || c . url ) , el ( 'div' , 'nui-pod-meta' , c . url ) ) ;
row . append ( main ) ;
row . addEventListener ( 'click' , ( ) => openPreview ( c . url ) ) ;
list . append ( row ) ;
}
root . append ( list ) ;
return ;
}
if ( p . error ) {
// The server may still manage (it fetches the feed itself).
const box = el ( 'div' , 'nui-pod-offer' , p . error ) ;
const actions = el ( 'div' , 'nui-pod-offer-row' ) ;
const anyway = el ( 'button' , 'btn' , 'Trotzdem abonnieren' ) ;
anyway . type = 'button' ;
anyway . addEventListener ( 'click' , ( ) => subscribeFromPreview ( p . url , p . url , anyway ) ) ;
actions . append ( anyway ) ;
box . append ( actions ) ;
root . append ( box ) ;
return ;
}
if ( ! p . data ) { empty ( 'Feed wird geladen …' ) ; return ; }
const known = feeds ( ) . find ( f => f . rss _url === d . url || f . rss _url === p . url ) ;
const actions = el ( 'div' , 'nui-pod-offer-row' ) ;
const go = el ( 'button' , 'btn' , known ? 'Abonniert – öffnen' : 'Abonnieren' ) ;
go . type = 'button' ;
go . addEventListener ( 'click' , ( ) => {
if ( known ) { preview = null ; openFeed ( known ) ; return ; }
subscribeFromPreview ( d . url , d . title , go ) ;
} ) ;
actions . append ( go ) ;
root . append ( actions ) ;
if ( d . description ) {
const desc = el ( 'div' , 'nui-pod-offer' , plain ( d . description ) ) ;
desc . style . whiteSpace = 'pre-line' ;
root . append ( desc ) ;
}
const eps = d . episodes || [ ] ;
if ( eps . length ) {
const list = el ( 'div' , 'nui-pod-list' ) ;
for ( const e of eps ) {
const row = el ( 'div' , 'nui-pod-row' ) ;
row . append ( art ( e . artwork || d . artwork ) ) ;
const main = el ( 'div' , 'nui-pod-main' ) ;
main . append ( el ( 'div' , 'nui-pod-title' , plain ( e . title ) ) ,
el ( 'div' , 'nui-pod-meta' , [ e . duration && minutes ( e . duration ) , day ( e . date ) ] . filter ( Boolean ) . join ( ' · ' ) ) ) ;
row . append ( main ) ;
list . append ( row ) ;
}
root . append ( list ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
}
2026-10-02 16:29:15 +02:00
async function subscribeFromPreview ( url , title , btn ) {
btn . disabled = true ;
const ok = await subscribe ( url , title ) ;
btn . disabled = false ;
if ( ! ok ) return ;
preview = null ;
view = 'feeds' ;
try { localStorage . setItem ( 'nui_pod_view' , view ) ; } catch ( e ) { }
render ( ) ;
scrollTop ( 0 ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// ---- drag to reorder (touch) -----------------------------------------------------------
function dragSort ( handle , row , list ) {
handle . addEventListener ( 'pointerdown' , ev => {
ev . preventDefault ( ) ;
ev . stopPropagation ( ) ;
handle . setPointerCapture ( ev . pointerId ) ;
const rows = [ ... list . children ] ;
const from = rows . indexOf ( row ) ;
const rects = rows . map ( r => r . getBoundingClientRect ( ) ) ;
const startY = ev . clientY ;
let to = from ;
row . classList . add ( 'nui-pod-dragging' ) ;
const move = e => {
const dy = e . clientY - startY ;
row . style . transform = ` translateY( ${ dy } px) ` ;
const mid = rects [ from ] . top + rects [ from ] . height / 2 + dy ;
to = rects . findIndex ( r => mid < r . top + r . height / 2 ) ;
if ( to < 0 ) to = rects . length - 1 ;
rows . forEach ( ( r , i ) => {
if ( r === row ) return ;
let shift = 0 ;
if ( from < to && i > from && i <= to ) shift = - rects [ from ] . height ;
if ( from > to && i >= to && i < from ) shift = rects [ from ] . height ;
r . style . transform = shift ? ` translateY( ${ shift } px) ` : '' ;
} ) ;
} ;
const end = ( ) => {
handle . removeEventListener ( 'pointermove' , move ) ;
handle . removeEventListener ( 'pointerup' , end ) ;
handle . removeEventListener ( 'pointercancel' , end ) ;
rows . forEach ( r => { r . style . transform = '' ; } ) ;
row . classList . remove ( 'nui-pod-dragging' ) ;
if ( to !== from ) {
// Show the new order at once; the app's state follows right after.
list . insertBefore ( row , to > from ? rows [ to ] . nextSibling : rows [ to ] ) ;
N . podcastMove ( parseInt ( row . dataset . id , 10 ) , to ) ;
}
} ;
handle . addEventListener ( 'pointermove' , move ) ;
handle . addEventListener ( 'pointerup' , end ) ;
handle . addEventListener ( 'pointercancel' , end ) ;
} ) ;
}
// ---- mini player and full player -----------------------------------------------------------
let mini = null ;
function buildMini ( ) {
const dock = $id ( 'nui-dock' ) ;
if ( ! dock || $id ( 'nui-pod-mini' ) ) return ;
mini = el ( 'div' ) ;
mini . id = 'nui-pod-mini' ;
mini . hidden = true ;
mini . innerHTML = '<div class="nui-pod-mini-bar"><span></span></div>' ;
const text = el ( 'button' , 'nui-mini-text' ) ;
text . type = 'button' ;
text . append ( el ( 'span' , 'nui-mini-station' ) , el ( 'span' , 'nui-mini-track' ) ) ;
2026-10-02 12:44:49 +02:00
text . addEventListener ( 'click' , ( ) => ( miniKind === 'radio' ? openRadioPlayer ( ) : openPlayer ( ) ) ) ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
const devices = button ( '⇄' , 'Wiedergabe auf …' , deviceMenu , 'nui-mini-devices' ) ;
const here = button ( '⇩' , 'Hier abspielen' , takeOver , 'nui-mini-here' ) ;
devices . hidden = here . hidden = true ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
mini . append ( art ( '' , 'mini' ) , text ,
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
button ( '↺' , '15 s zurück' , ( ) => ( miniKind === 'remote'
? remoteCmd ( 'skip' , - ( devicePrefs ( ) . skip _back || 15 ) ) : N . podcastSkip ( false ) ) , 'nui-pod-skip' ) ,
2026-10-02 12:44:49 +02:00
button ( '☆' , 'Sender speichern' , ( ) => window . _ _dioraRadioSave && window . _ _dioraRadioSave ( ) , 'nui-mini-save' ) ,
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
devices , here ,
button ( '▶' , 'Abspielen' , ( ) => ( miniKind === 'remote' ? remoteToggle ( )
: miniKind === 'radio' ? window . _ _dioraRadioToggle && window . _ _dioraRadioToggle ( ) : N . podcastToggle ( ) ) , 'nui-mini-toggle' ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
dock . prepend ( mini ) ;
2026-10-02 12:44:49 +02:00
// One player for podcasts and radio: the radio's own bar steps aside.
document . documentElement . classList . add ( 'nui-one-player' ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// A "Podcasts" button in the bottom bar, between books and radio.
const tabbar = $id ( 'nui-tabbar' ) ;
if ( tabbar && ! tabbar . querySelector ( '[data-tab="podcasts"]' ) ) {
const b = el ( 'button' ) ;
b . type = 'button' ;
b . dataset . tab = 'podcasts' ;
b . append ( el ( 'span' , 'nui-tab-icon' , '◎' ) , document . createTextNode ( 'Podcasts' ) ) ;
b . addEventListener ( 'click' , ( ) => { showTab ( 'podcasts' ) ; scrollTop ( 0 ) ; } ) ;
tabbar . insertBefore ( b , tabbar . querySelector ( '[data-tab="radio"]' ) ) ;
}
}
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
// What the bar shows: 'podcast', 'radio' (a station is chosen), 'remote'
// (another device is the active one) or ''.
2026-10-02 12:44:49 +02:00
let miniKind = '' ;
function radioNow ( ) {
return typeof window . _ _dioraRadioNow === 'function' ? window . _ _dioraRadioNow ( ) : null ;
}
function setArt ( box , src ) {
const img = box . querySelector ( 'img' ) ;
src = ( src || '' ) . replace ( /^http:/ , 'https:' ) ;
if ( ! img && src ) {
const i = el ( 'img' ) ;
i . alt = '' ;
i . src = src ;
i . addEventListener ( 'error' , ( ) => i . remove ( ) ) ;
box . append ( i ) ;
} else if ( img && img . getAttribute ( 'src' ) !== src ) {
if ( src ) img . src = src ; else img . remove ( ) ;
}
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function renderMini ( ) {
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
const r = remote ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const isPod = player . kind === 'podcast' ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
document . documentElement . classList . toggle ( 'nui-pod-playing' , isPod || ! ! r ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! mini ) return ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
const radio = isPod || r ? null : radioNow ( ) ;
miniKind = r ? 'remote' : isPod ? 'podcast' : radio ? 'radio' : '' ;
2026-10-02 12:44:49 +02:00
mini . hidden = ! miniKind ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
mini . classList . toggle ( 'nui-mini-radio' , miniKind === 'radio' || ( ! ! r && r . kind === 'radio' ) ) ;
mini . classList . toggle ( 'nui-mini-remote' , ! ! r ) ;
2026-10-02 12:44:49 +02:00
document . documentElement . classList . toggle ( 'nui-pod-mini-open' , ! ! miniKind ) ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
mini . querySelector ( '.nui-pod-skip' ) . hidden = ! ( isPod || r ) || ( ! ! r && r . kind !== 'podcast' ) ;
2026-10-02 12:44:49 +02:00
mini . querySelector ( '.nui-mini-save' ) . hidden = miniKind !== 'radio' || radio . saved ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
mini . querySelector ( '.nui-mini-devices' ) . hidden = ! r || ! N . playerTransfer ;
mini . querySelector ( '.nui-mini-here' ) . hidden = ! r || ! N . playerTakeOver ;
if ( r ) {
const onRadio = r . kind === 'radio' ;
mini . querySelector ( '.nui-mini-station' ) . textContent = ( onRadio ? r . station && r . station . name : r . title ) || '' ;
mini . querySelector ( '.nui-mini-track' ) . textContent = remoteLine ( r ) ;
const t = mini . querySelector ( '.nui-mini-toggle' ) ;
t . textContent = r . playing ? ( onRadio ? '■' : '❚❚' ) : '▶' ;
t . title = r . playing ? ( onRadio ? 'Stopp' : 'Pause' ) + ' auf ' + remoteName ( r ) : 'Abspielen auf ' + remoteName ( r ) ;
setArt ( mini . querySelector ( '.nui-pod-art' ) , onRadio ? r . station && r . station . logo : r . artwork ) ;
mini . querySelector ( '.nui-pod-mini-bar span' ) . style . width = onRadio ? ( r . playing ? '100%' : '0' )
: r . dur ? Math . min ( 100 , r . pos / r . dur * 100 ) + '%' : '0' ;
return ;
}
2026-10-02 12:44:49 +02:00
if ( miniKind === 'radio' ) {
mini . querySelector ( '.nui-mini-station' ) . textContent = radio . name ;
mini . querySelector ( '.nui-mini-track' ) . textContent = radio . buffering ? 'Verbinde …'
: radio . playing ? ( radio . track || '● Live' ) : 'Angehalten' ;
const t = mini . querySelector ( '.nui-mini-toggle' ) ;
t . textContent = radio . playing ? '■' : '▶' ;
t . title = radio . playing ? 'Stopp' : 'Abspielen' ;
setArt ( mini . querySelector ( '.nui-pod-art' ) , radio . logo ) ;
mini . querySelector ( '.nui-pod-mini-bar span' ) . style . width = radio . playing ? '100%' : '0' ;
return ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! isPod ) return ;
mini . querySelector ( '.nui-mini-station' ) . textContent = player . title || '' ;
// The back button names the set skip length (checked now and then, it rarely changes).
if ( ! mini . skipChecked || Date . now ( ) - mini . skipChecked > 5000 ) {
mini . skipChecked = Date . now ( ) ;
const sk = mini . querySelector ( '.nui-pod-skip' ) ;
sk . title = ` ${ devicePrefs ( ) . skip _back || 15 } s zurück ` ;
sk . setAttribute ( 'aria-label' , sk . title ) ;
}
mini . querySelector ( '.nui-mini-track' ) . textContent = player . buffering && player . playing ? 'Lädt …'
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
: ( player . feed || '' ) + ( player . dur ? ' · ' + clock ( shown ( ( player . dur - player . pos ) / 1000 , 0 , player . speed || 1 ) ) + ' übrig' : '' ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const t = mini . querySelector ( '.nui-mini-toggle' ) ;
t . textContent = player . playing ? '❚❚' : '▶' ;
t . title = player . playing ? 'Pause' : 'Abspielen' ;
2026-10-02 12:44:49 +02:00
setArt ( mini . querySelector ( '.nui-pod-art' ) , player . artwork ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
mini . querySelector ( '.nui-pod-mini-bar span' ) . style . width = player . dur ? Math . min ( 100 , player . pos / player . dur * 100 ) + '%' : '0' ;
}
let sheet = null ;
let sheetEpisode = 0 ;
let seeking = false ;
// ---- chapters and shownotes (server: /podcasts/episodes/<id>/) ------------------------
// Fetched per episode when wanted and kept for offline use (the last 40).
const details = new Map ( ) ;
function storedDetails ( id ) {
if ( details . has ( id ) ) return details . get ( id ) ;
try {
const d = JSON . parse ( localStorage . getItem ( 'nui_pod_ep_' + id ) || 'null' ) ;
if ( d ) details . set ( id , d ) ;
return d ;
} catch ( e ) {
return null ;
}
}
function keepDetails ( id , d ) {
details . set ( id , d ) ;
try {
const ids = JSON . parse ( localStorage . getItem ( 'nui_pod_ep_ids' ) || '[]' ) . filter ( x => x !== id ) ;
ids . push ( id ) ;
while ( ids . length > 40 ) localStorage . removeItem ( 'nui_pod_ep_' + ids . shift ( ) ) ;
localStorage . setItem ( 'nui_pod_ep_' + id , JSON . stringify ( d ) ) ;
localStorage . setItem ( 'nui_pod_ep_ids' , JSON . stringify ( ids ) ) ;
} catch ( e ) { }
}
async function loadDetails ( id ) {
try {
const r = await fetch ( ` /podcasts/episodes/ ${ id } / ` ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
const d = await r . json ( ) ;
keepDetails ( id , d ) ;
return d ;
} catch ( e ) {
return storedDetails ( id ) ;
}
}
// Timestamps in shownotes ("12:34", "1:02:03") become links that jump there.
const STAMP = /\b(?:(\d{1,2}):)?(\d{1,2}):(\d{2})\b/g ;
function linkStamps ( root ) {
const walker = document . createTreeWalker ( root , NodeFilter . SHOW _TEXT ) ;
const nodes = [ ] ;
while ( walker . nextNode ( ) ) {
const n = walker . currentNode ;
if ( ! n . parentElement . closest ( 'a' ) && STAMP . test ( n . nodeValue ) ) nodes . push ( n ) ;
STAMP . lastIndex = 0 ;
}
for ( const n of nodes ) {
const frag = document . createDocumentFragment ( ) ;
let last = 0 ;
n . nodeValue . replace ( STAMP , ( m , h , mi , se , at ) => {
const secs = ( parseInt ( h || '0' , 10 ) * 3600 ) + parseInt ( mi , 10 ) * 60 + parseInt ( se , 10 ) ;
if ( parseInt ( se , 10 ) > 59 ) return m ;
frag . append ( document . createTextNode ( n . nodeValue . slice ( last , at ) ) ) ;
const a = el ( 'a' , 'nui-pod-stamp' , m ) ;
a . href = '#' ;
a . dataset . t = String ( secs ) ;
frag . append ( a ) ;
last = at + m . length ;
return m ;
} ) ;
frag . append ( document . createTextNode ( n . nodeValue . slice ( last ) ) ) ;
n . replaceWith ( frag ) ;
}
}
// Chapters and shownotes into a box; with `live`, taps jump in the playing episode.
async function showDetails ( box , id , live ) {
box . textContent = '' ;
box . dataset . episode = String ( id ) ;
let d = storedDetails ( id ) ;
if ( ! d ) box . append ( el ( 'div' , 'nui-pod-meta' , 'Shownotes werden geladen …' ) ) ;
const fresh = await loadDetails ( id ) ;
if ( box . dataset . episode !== String ( id ) ) return ;
d = fresh || d ;
box . textContent = '' ;
if ( ! d ) {
box . append ( el ( 'div' , 'nui-pod-meta' , 'Shownotes brauchen beim ersten Mal eine Verbindung.' ) ) ;
return ;
}
if ( live && sheet ) {
const fav = sheet . querySelector ( '.nui-pod-fav' ) ;
if ( fav ) { fav . textContent = d . favorite ? '★' : '☆' ; fav . classList . toggle ( 'on' , ! ! d . favorite ) ; }
}
const chapters = d . chapters || [ ] ;
if ( live && chapters . length > 1 ) {
// Previous / next chapter, like AntennaPod's chapter buttons.
const nav = el ( 'div' , 'nui-pod-chapnav' ) ;
const jump = dir => {
const t = ( player . pos || 0 ) / 1000 ;
let i = chapters . length - 1 ;
while ( i > 0 && chapters [ i ] . start > t + 0.5 ) i -- ;
// "Back" within the first seconds of a chapter goes to the one before.
const target = dir > 0 ? i + 1 : ( t - chapters [ i ] . start < 3 ? i - 1 : i ) ;
if ( target < 0 || target >= chapters . length ) return ;
N . podcastSeek ( Math . round ( chapters [ target ] . start * 1000 ) ) ;
} ;
nav . append ( button ( '⏮' , 'Voriges Kapitel' , ( ) => jump ( - 1 ) ) , el ( 'span' , 'nui-pod-chapnow' ) , button ( '⏭' , 'Nächstes Kapitel' , ( ) => jump ( 1 ) ) ) ;
box . append ( nav ) ;
}
const tabs = el ( 'div' , 'nui-pod-subtabs' ) ;
const body = el ( 'div' , 'nui-pod-notesbody' ) ;
const show = which => {
tabs . querySelectorAll ( 'button' ) . forEach ( b => b . classList . toggle ( 'active' , b . dataset . which === which ) ) ;
body . textContent = '' ;
if ( which === 'chapters' ) {
const ol = el ( 'ol' , 'nui-pod-chapters' ) ;
chapters . forEach ( ( c , i ) => {
const li = el ( 'li' , 'nui-pod-chapter' ) ;
li . dataset . start = String ( c . start || 0 ) ;
li . dataset . index = String ( i ) ;
li . append ( el ( 'span' , 'nui-pod-chapter-t' , clock ( c . start || 0 ) ) , el ( 'span' , 'nui-pod-chapter-title' , plain ( c . title || ` Kapitel ${ i + 1 } ` ) ) ) ;
if ( live ) li . addEventListener ( 'click' , ( ) => N . podcastSeek ( Math . round ( ( c . start || 0 ) * 1000 ) ) ) ;
ol . append ( li ) ;
} ) ;
body . append ( ol ) ;
markChapter ( box , ( player . pos || 0 ) / 1000 ) ;
} else if ( which === 'transcript' ) {
showTranscript ( body , id , live ) ;
} else {
const notes = el ( 'div' , 'nui-pod-notes' ) ;
notes . innerHTML = d . description || '<p>Keine Shownotes.</p>' ;
notes . querySelectorAll ( 'a[href]' ) . forEach ( a => a . setAttribute ( 'target' , '_blank' ) ) ;
if ( live ) {
linkStamps ( notes ) ;
notes . addEventListener ( 'click' , ev => {
const a = ev . target . closest ( 'a.nui-pod-stamp' ) ;
if ( ! a ) return ;
ev . preventDefault ( ) ;
N . podcastSeek ( parseInt ( a . dataset . t , 10 ) * 1000 ) ;
} ) ;
}
body . append ( notes ) ;
}
} ;
const tab = ( which , label ) => {
const b = el ( 'button' , 'nui-pod-subtab' , label ) ;
b . type = 'button' ;
b . dataset . which = which ;
b . addEventListener ( 'click' , ( ) => show ( which ) ) ;
tabs . append ( b ) ;
} ;
if ( chapters . length ) tab ( 'chapters' , ` Kapitel ( ${ chapters . length } ) ` ) ;
tab ( 'notes' , 'Shownotes' ) ;
if ( d . has _transcript ) tab ( 'transcript' , 'Transkript' ) ;
box . append ( tabs , body ) ;
show ( chapters . length ? 'chapters' : 'notes' ) ;
}
function markChapter ( scope , seconds ) {
const items = scope . querySelectorAll ( '.nui-pod-chapter' ) ;
let current = - 1 ;
items . forEach ( ( li , i ) => { if ( parseFloat ( li . dataset . start ) <= seconds + 0.5 ) current = i ; } ) ;
items . forEach ( ( li , i ) => li . classList . toggle ( 'current' , i === current ) ) ;
const now = scope . querySelector ( '.nui-pod-chapnow' ) ;
if ( now ) {
const d = storedDetails ( parseInt ( ( scope . querySelector ( '.nui-pod-notesbox' ) || scope ) . dataset . episode , 10 ) ) ;
const ch = d && d . chapters ? d . chapters . filter ( c => c . start <= seconds + 0.5 ) . pop ( ) : null ;
now . textContent = ch ? plain ( ch . title || '' ) : '' ;
}
markTranscript ( scope , seconds ) ;
}
// ---- transcript: follows the playback, a tap jumps there --------------------------------
const transcripts = new Map ( ) ;
let transcriptTouched = 0 ; // the user scrolled the transcript: don't pull it along for a while
async function showTranscript ( body , id , live ) {
body . append ( el ( 'div' , 'nui-pod-meta' , 'Transkript wird geladen …' ) ) ;
let t = transcripts . get ( id ) ;
if ( ! t ) {
try {
const r = await fetch ( ` /podcasts/episodes/ ${ id } /transcript/ ` ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
t = await r . json ( ) ;
transcripts . set ( id , t ) ;
} catch ( e ) {
body . textContent = '' ;
body . append ( el ( 'div' , 'nui-pod-meta' , navigator . onLine ? 'Für diese Folge gibt es kein lesbares Transkript.' : 'Das Transkript braucht eine Verbindung.' ) ) ;
return ;
}
}
body . textContent = '' ;
const list = el ( 'div' , 'nui-pod-transcript' ) ;
for ( const seg of t . segments || [ ] ) {
const p = el ( 'p' , 'nui-pod-seg' ) ;
p . dataset . start = String ( seg . start || 0 ) ;
if ( seg . speaker ) p . append ( el ( 'strong' , 'nui-pod-speaker' , seg . speaker + ': ' ) ) ;
p . append ( document . createTextNode ( seg . text || '' ) ) ;
if ( live ) p . addEventListener ( 'click' , ( ) => N . podcastSeek ( Math . round ( ( seg . start || 0 ) * 1000 ) ) ) ;
list . append ( p ) ;
}
list . addEventListener ( 'touchstart' , ( ) => { transcriptTouched = Date . now ( ) ; } , { passive : true } ) ;
body . append ( list ) ;
markTranscript ( body , ( player . pos || 0 ) / 1000 ) ;
}
function markTranscript ( scope , seconds ) {
const segs = scope . querySelectorAll ( '.nui-pod-seg' ) ;
if ( ! segs . length ) return ;
let current = null ;
segs . forEach ( p => { if ( parseFloat ( p . dataset . start ) <= seconds + 0.3 ) current = p ; } ) ;
segs . forEach ( p => p . classList . toggle ( 'current' , p === current ) ) ;
if ( current && current . dataset . shown !== '1' && Date . now ( ) - transcriptTouched > 8000 ) {
segs . forEach ( p => { p . dataset . shown = '' ; } ) ;
current . dataset . shown = '1' ;
current . scrollIntoView ( { block : 'center' , behavior : 'smooth' } ) ;
}
}
// Shownotes of an episode that isn't playing: its own sheet.
let notesSheet = null ;
function openNotes ( e ) {
closeNotes ( ) ;
notesSheet = el ( 'div' ) ;
notesSheet . id = 'nui-pod-player' ;
notesSheet . className = 'nui-pod-notes-sheet' ;
const panel = el ( 'div' , 'nui-pod-player-panel' ) ;
const top = el ( 'div' , 'nui-pod-player-top' ) ;
top . append ( button ( '⌄' , 'Schließen' , closeNotes ) ) ;
panel . append ( top , el ( 'div' , 'nui-pod-player-title' , e . title ) , el ( 'div' , 'nui-pod-player-feed' , e . feed || '' ) ) ;
const play1 = el ( 'button' , 'btn nui-pod-notes-play' , '▶ Abspielen' ) ;
play1 . type = 'button' ;
play1 . addEventListener ( 'click' , ( ) => { closeNotes ( ) ; play ( e ) ; } ) ;
panel . append ( play1 ) ;
const box = el ( 'div' , 'nui-pod-notesbox' ) ;
panel . append ( box ) ;
notesSheet . append ( panel ) ;
notesSheet . addEventListener ( 'click' , ev => { if ( ev . target === notesSheet ) closeNotes ( ) ; } ) ;
document . body . append ( notesSheet ) ;
showDetails ( box , e . id , false ) ;
}
function closeNotes ( ) {
if ( ! notesSheet ) return false ;
notesSheet . remove ( ) ;
notesSheet = null ;
return true ;
}
2026-10-02 12:44:49 +02:00
// The full player: one for both. Which one it shows now.
let sheetKind = '' ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function openPlayer ( ) {
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
if ( remote ( ) ) { openRemotePlayer ( ) ; return ; }
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( player . kind !== 'podcast' ) return ;
closePlayer ( ) ;
2026-10-02 12:44:49 +02:00
sheetKind = 'podcast' ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
sheet = el ( 'div' ) ;
sheet . id = 'nui-pod-player' ;
sheet . innerHTML = '<div class="nui-pod-player-panel" role="dialog" aria-label="Player"></div>' ;
const panel = sheet . firstChild ;
const top = el ( 'div' , 'nui-pod-player-top' ) ;
top . append ( button ( '⌄' , 'Player schließen' , closePlayer ) ) ;
panel . append ( top , art ( player . artwork , 'player' ) ) ;
panel . append ( el ( 'div' , 'nui-pod-player-title' ) , el ( 'div' , 'nui-pod-player-feed' ) ) ;
const range = el ( 'input' , 'nui-pod-seek' ) ;
range . type = 'range' ;
range . min = 0 ;
range . step = 1 ;
range . id = 'nui-pod-seek' ;
range . addEventListener ( 'input' , ( ) => {
seeking = true ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
sheet . querySelector ( '.nui-pod-time-pos' ) . textContent = clock ( shown ( range . value , 0 , player . speed || 1 ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} ) ;
range . addEventListener ( 'change' , ( ) => {
N . podcastSeek ( range . value * 1000 ) ;
seeking = false ;
} ) ;
const times = el ( 'div' , 'nui-pod-times' ) ;
times . append ( el ( 'span' , 'nui-pod-time-pos' ) , el ( 'span' , 'nui-pod-time-left' ) ) ;
panel . append ( range , times ) ;
const prefs = devicePrefs ( ) ;
const back = prefs . skip _back || 15 ;
const fwd = prefs . skip _forward || 30 ;
const controls = el ( 'div' , 'nui-pod-controls' ) ;
controls . append (
button ( String ( back ) , ` ${ back } Sekunden zurück ` , ( ) => N . podcastSkip ( false ) , 'nui-pod-back' ) ,
button ( '▶' , 'Abspielen' , ( ) => N . podcastToggle ( ) , 'nui-pod-play' ) ,
button ( String ( fwd ) , ` ${ fwd } Sekunden vor ` , ( ) => N . podcastSkip ( true ) , 'nui-pod-fwd' ) ,
) ;
panel . append ( controls ) ;
const extra = el ( 'div' , 'nui-pod-extra' ) ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
extra . append ( ... [
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
button ( '1× ' , 'Geschwindigkeit' , speedMenu , 'nui-pod-speed' ) ,
button ( '☾' , 'Sleep-Timer' , sleepMenu , 'nui-pod-sleep' ) ,
button ( '☆' , 'Favorit' , ( ) => toggleFavorite ( { id : player . episodeId } , star => {
const b = sheet && sheet . querySelector ( '.nui-pod-fav' ) ;
if ( b ) { b . textContent = star ? '★' : '☆' ; b . classList . toggle ( 'on' , star ) ; }
} ) , 'nui-pod-fav' ) ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
button ( '⏭' , keepSkipped ( ) ? 'Nächste Folge (diese bleibt in der Warteschlange)' : 'Folge abschließen, nächste spielen' ,
finishAndNext , 'nui-pod-next' ) ,
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
N . playerTransfer && button ( '⇄' , 'Wiedergabe auf …' , deviceMenu , 'nui-pod-devices' ) ,
button ( '≡' , 'Warteschlange' , showQueue ) ,
] . filter ( Boolean ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
panel . append ( extra ) ;
const notes = el ( 'div' , 'nui-pod-notesbox' ) ;
panel . append ( notes ) ;
sheet . addEventListener ( 'click' , e => { if ( e . target === sheet ) closePlayer ( ) ; } ) ;
document . body . append ( sheet ) ;
sheetEpisode = player . episodeId ;
showDetails ( notes , player . episodeId , true ) ;
renderPlayer ( ) ;
}
function closePlayer ( ) {
if ( ! sheet ) return false ;
sheet . remove ( ) ;
sheet = null ;
return true ;
}
2026-10-02 12:44:49 +02:00
// The radio in the same player: the station instead of the episode, "Live"
// instead of the seek bar, save / stop / sleep timer as the buttons.
function openRadioPlayer ( ) {
const r = radioNow ( ) ;
if ( ! r ) return ;
closePlayer ( ) ;
sheetKind = 'radio' ;
sheet = el ( 'div' ) ;
sheet . id = 'nui-pod-player' ;
sheet . innerHTML = '<div class="nui-pod-player-panel" role="dialog" aria-label="Radio"></div>' ;
const panel = sheet . firstChild ;
const top = el ( 'div' , 'nui-pod-player-top' ) ;
top . append ( button ( '⌄' , 'Player schließen' , closePlayer ) ) ;
const cover = art ( r . logo , 'player' ) ;
cover . classList . add ( 'nui-radio-art' ) ;
panel . append ( top , cover , el ( 'div' , 'nui-pod-player-title' ) , el ( 'div' , 'nui-pod-player-feed' ) ,
el ( 'div' , 'nui-radio-live' ) ) ;
const controls = el ( 'div' , 'nui-pod-controls' ) ;
controls . append (
button ( '☆' , 'Sender speichern' , ( ) => window . _ _dioraRadioSave && window . _ _dioraRadioSave ( ) , 'nui-pod-fav nui-radio-save' ) ,
button ( '▶' , 'Abspielen' , ( ) => window . _ _dioraRadioToggle && window . _ _dioraRadioToggle ( ) , 'nui-pod-play' ) ,
button ( '☾' , 'Sleep-Timer' , ( ) => sleepMenu ( true ) , 'nui-pod-sleep' ) ,
) ;
const extra = el ( 'div' , 'nui-pod-extra' ) ;
const stations = el ( 'button' , 'btn nui-radio-stations' , 'Sender wechseln' ) ;
stations . type = 'button' ;
stations . addEventListener ( 'click' , ( ) => { closePlayer ( ) ; showTab ( 'radio' ) ; } ) ;
extra . append ( stations ) ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
if ( N . playerTransfer ) extra . append ( devicesButton ( ) ) ;
2026-10-02 12:44:49 +02:00
panel . append ( controls , extra ) ;
sheet . addEventListener ( 'click' , e => { if ( e . target === sheet ) closePlayer ( ) ; } ) ;
document . body . append ( sheet ) ;
renderRadioPlayer ( ) ;
}
function renderRadioPlayer ( ) {
const r = radioNow ( ) ;
if ( ! r ) { closePlayer ( ) ; return ; }
sheet . querySelector ( '.nui-pod-player-title' ) . textContent = r . name ;
sheet . querySelector ( '.nui-pod-player-feed' ) . textContent = r . track || '' ;
const live = sheet . querySelector ( '.nui-radio-live' ) ;
live . textContent = r . buffering ? 'Verbinde …' : r . playing ? '● Live' : 'Angehalten' ;
live . classList . toggle ( 'on' , r . playing && ! r . buffering ) ;
const p = sheet . querySelector ( '.nui-pod-play' ) ;
p . textContent = r . playing ? '■' : '▶' ;
p . title = r . playing ? 'Stopp' : 'Abspielen' ;
const s = sheet . querySelector ( '.nui-radio-save' ) ;
s . textContent = r . saved ? '★' : '☆' ;
s . classList . toggle ( 'on' , r . saved ) ;
s . disabled = r . saved ;
s . title = r . saved ? 'Gespeichert' : 'Sender speichern' ;
const sl = sheet . querySelector ( '.nui-pod-sleep' ) ;
sl . textContent = sleepLabel ( ) || '☾' ;
sl . classList . toggle ( 'on' , ! ! sleepLabel ( ) ) ;
setArt ( sheet . querySelector ( '.nui-pod-art-player' ) , r . logo ) ;
}
// native-radio.js: the station, its track or its state changed.
window . _ _dioraRadioChanged = function ( ) {
renderMini ( ) ;
if ( sheet && sheetKind === 'radio' ) renderRadioPlayer ( ) ;
} ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
window . _ _dioraOpenPlayer = kind => ( kind === 'radio' && ! remote ( ) ? openRadioPlayer ( ) : openPlayer ( ) ) ;
2026-10-02 12:44:49 +02:00
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function renderPlayer ( ) {
if ( ! sheet ) return ;
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
const r = remote ( ) ;
if ( sheetKind === 'remote' ) {
// Back here (taken over) or the other device went away: this device's player.
if ( ! r ) {
closePlayer ( ) ;
if ( player . kind === 'podcast' ) openPlayer ( ) ; else if ( radioNow ( ) ) openRadioPlayer ( ) ;
} else if ( sheet . dataset . remote !== remoteKey ( r ) ) {
openRemotePlayer ( ) ;
} else {
renderRemotePlayer ( ) ;
}
return ;
}
if ( r ) { openRemotePlayer ( ) ; return ; }
2026-10-02 12:44:49 +02:00
if ( sheetKind === 'radio' ) {
if ( player . kind === 'podcast' ) closePlayer ( ) ; else renderRadioPlayer ( ) ;
return ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( player . kind !== 'podcast' ) { closePlayer ( ) ; return ; }
if ( sheetEpisode !== player . episodeId ) {
// The next episode started while the player was open.
sheetEpisode = player . episodeId ;
showDetails ( sheet . querySelector ( '.nui-pod-notesbox' ) , player . episodeId , true ) ;
}
markChapter ( sheet , ( player . pos || 0 ) / 1000 ) ;
sheet . querySelector ( '.nui-pod-player-title' ) . textContent = player . title || '' ;
sheet . querySelector ( '.nui-pod-player-feed' ) . textContent = player . feed || '' ;
const range = sheet . querySelector ( '.nui-pod-seek' ) ;
const dur = Math . round ( ( player . dur || 0 ) / 1000 ) ;
const pos = Math . round ( ( player . pos || 0 ) / 1000 ) ;
range . max = dur || 1 ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// At the speed, when so set: the place and what's left as long as they take.
const at = s => shown ( s , 0 , player . speed || 1 ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! seeking ) {
range . value = pos ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
sheet . querySelector ( '.nui-pod-time-pos' ) . textContent = clock ( at ( pos ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
sheet . querySelector ( '.nui-pod-time-left' ) . textContent = dur ? '− ' + clock ( at ( dur - pos ) ) : '' ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const p = sheet . querySelector ( '.nui-pod-play' ) ;
p . textContent = player . playing ? '❚❚' : '▶' ;
p . title = player . playing ? 'Pause' : 'Abspielen' ;
const sp = sheet . querySelector ( '.nui-pod-speed' ) ;
sp . textContent = speedLabel ( player . speed || 1 ) + ( player . podcastSpeed ? '•' : '' ) ;
sp . title = player . podcastSpeed ? 'Geschwindigkeit (eigene für diesen Podcast)' : 'Geschwindigkeit' ;
const sl = sheet . querySelector ( '.nui-pod-sleep' ) ;
sl . textContent = sleepLabel ( ) || '☾' ;
sl . classList . toggle ( 'on' , ! ! sleepLabel ( ) ) ;
const img = sheet . querySelector ( '.nui-pod-art-player img' ) ;
const src = ( player . artwork || '' ) . replace ( /^http:/ , 'https:' ) ;
if ( img && src && img . getAttribute ( 'src' ) !== src ) img . src = src ;
}
function speedLabel ( s ) {
return String ( Math . round ( s * 100 ) / 100 ) . replace ( '.' , ',' ) + '× ' ;
}
// Speed: for all podcasts, or (AntennaPod-like) remembered for this podcast.
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// The presets, and the speed now if it isn't one of them (a podcast's own 3,5× …).
function speedChoices ( now ) {
const list = presets ( ) . slice ( ) ;
if ( now > 0 && ! list . some ( s => Math . abs ( s - now ) < 0.01 ) ) list . push ( now ) ;
return list . sort ( ( a , b ) => a - b ) ;
}
function presetsEditItem ( ) {
return 'speed_presets' in settings ( ) ? [ { icon : '✎' , label : 'Voreinstellungen bearbeiten' , hint : 'hinzufügen, entfernen' ,
action : ( ) => presetsEditor ( ) } ] : [ ] ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function speedMenu ( ) {
const own = ! ! player . podcastSpeed ;
const pick = forPodcast => openSheet ( forPodcast ? ` Geschwindigkeit für „ ${ plain ( player . feed ) } “ ` : 'Geschwindigkeit für alle Podcasts' ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
speedChoices ( own === forPodcast ? player . speed || 1 : 0 ) . map ( s => ( {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
icon : Math . abs ( ( player . speed || 1 ) - s ) < 0.01 && own === forPodcast ? '●' : '○' ,
label : speedLabel ( s ) ,
action : ( ) => ( forPodcast ? N . podcastSpeedForPodcast ( s ) : N . podcastSpeed ( s ) ) ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
} ) ) . concat ( forPodcast && own ? [ { icon : '↺' , label : 'Wie alle anderen Podcasts' , action : ( ) => N . podcastSpeedForPodcast ( 0 ) } ] : [ ] )
. concat ( presetsEditItem ( ) ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! N . podcastSpeedForPodcast || ! player . feedId ) { pick ( false ) ; return ; }
openSheet ( 'Geschwindigkeit' , [
{ icon : '◎' , label : 'Für alle Podcasts' , hint : own ? null : speedLabel ( player . speed || 1 ) , action : ( ) => pick ( false ) } ,
{ icon : '◉' , label : ` Nur für „ ${ plain ( player . feed ) } “ ` , hint : own ? speedLabel ( player . speed || 1 ) : 'noch keine eigene' ,
action : ( ) => pick ( true ) } ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
] . concat ( presetsEditItem ( ) ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
// ---- sleep timer (runs in the app, also with the screen off) ----
function sleepLabel ( ) {
const t = player . sleep || { } ;
if ( t . episodes > 0 ) return t . episodes === 1 ? '☾ Folgenende' : ` ☾ ${ t . episodes } Folgen ` ;
if ( t . leftMs > 0 ) return ` ☾ ${ Math . max ( 1 , Math . ceil ( t . leftMs / 60000 ) ) } min ` ;
return '' ;
}
// Minutes count only while playing; shaking the phone starts them over.
2026-10-02 12:44:49 +02:00
function sleepMenu ( radio ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( ! N . podcastSleep ) return ;
const t = player . sleep || { } ;
const on = ! ! sleepLabel ( ) ;
const items = [ ] ;
if ( on ) {
items . push ( { icon : '✕' , label : 'Ausschalten' , hint : sleepLabel ( ) . replace ( '☾ ' , 'läuft: ' ) , action : ( ) => N . podcastSleep ( 0 ) } ) ;
if ( t . leftMs > 0 && N . podcastSleepExtend ) {
items . push ( { icon : '+' , label : '5 Minuten länger' , action : ( ) => N . podcastSleepExtend ( 5 ) } ) ;
items . push ( { icon : '+' , label : '10 Minuten länger' , action : ( ) => N . podcastSleepExtend ( 10 ) } ) ;
}
}
for ( const m of [ 5 , 10 , 15 , 30 , 45 , 60 , 90 ] ) {
items . push ( { icon : '☾' , label : ` ${ m } Minuten ` , hint : m === t . last && ! on ? 'zuletzt' : null , action : ( ) => N . podcastSleep ( m ) } ) ;
}
2026-10-02 12:44:49 +02:00
if ( radio !== true ) {
items . push ( { icon : '⏹' , label : 'Am Ende der Folge' , action : ( ) => N . podcastSleep ( - 1 ) } ) ;
items . push ( { icon : '⏹' , label : 'Nach 2 Folgen' , action : ( ) => N . podcastSleep ( - 2 ) } ) ;
items . push ( { icon : '⏹' , label : 'Nach 3 Folgen' , action : ( ) => N . podcastSleep ( - 3 ) } ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( N . podcastSleepAuto ) {
items . push ( { icon : t . auto ? '☑' : '☐' , label : 'Nachts von selbst (22– 6 Uhr)' ,
hint : t . auto ? ` an: ${ t . last || 15 } Minuten, wenn eine Folge startet ` : 'aus' ,
action : ( ) => { N . podcastSleepAuto ( ! t . auto ) ; t . auto = ! t . auto ; } } ) ;
}
openSheet ( 'Sleep-Timer' , items ) ;
}
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// ⏭: the next episode plays. The host decides about this one ("keep skipped"): kept
// unplayed in the queue, or played. With nothing after it, it only counts as played
// when skipped episodes aren't kept.
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
function finishAndNext ( ) {
const id = currentId ( ) ;
if ( ! id ) return ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
const ids = queue ( ) . map ( q => q . episode _ _id ) ;
const at = ids . indexOf ( id ) ;
const last = at < 0 ? ! ids . length : at === ids . length - 1 ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
N . podcastNext ( ) ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
if ( last && ! keepSkipped ( ) ) N . podcastMarkPlayed ( id , true ) ;
else if ( last ) toast ( 'Das ist die letzte Folge der Warteschlange' ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
Player-Sync: ein Player für alle Geräte (wie Spotify Connect)
Server: PlayerState und PlayerDevice (Migration 0012), /podcasts/player/
(Stand), report/ (das Gerät, das zu spielen anfängt, wird das aktive und
bleibt es auch pausiert, solange es online ist), command/ (Fernbedienung,
transfer auf ein anderes Gerät) und stream/ (Server-Sent Events, zugleich
die Anwesenheit der Geräte). seq steigt in einem UPDATE und nie zurück.
Browser: podcasts-web.js meldet Abspielen, Pause, Springen, Folgen- und
Senderwechsel (alle 10 s beim Spielen), hört den Stream, hört auf, wenn ein
anderes Gerät startet, führt Befehle aus, zeigt das andere Gerät als remote
und übernimmt, was ein verschwundenes Gerät zuletzt hatte. Eine pausierte
Folge folgt einem neueren Stand von anderswo; im Vordergrund wird nach
15 s neu geholt. Podcast-Seite neu kopiert, sw v58.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:18:38 +02:00
function showQueue ( ) {
closePlayer ( ) ;
view = 'queue' ;
feedView = null ;
showTab ( 'podcasts' ) ;
render ( ) ;
}
// ---- another device plays (player sync, like Spotify Connect) ------------------------------
// The host (Bridge.kt / podcasts-web.js) shares one player among the user's
// devices. While another device is the active one (online, playing or
// paused), player.remote is what it plays: the bar and the player show it,
// their buttons control it (N.playerRemote), "Hier abspielen" plays it here
// from its place, and the device sheet moves it to any device that is online.
function remote ( ) {
return player . remote && N . playerRemote ? player . remote : null ;
}
function remoteName ( r ) {
return r . device || 'einem anderen Gerät' ;
}
function remoteKey ( r ) {
return r . kind + ':' + ( r . kind === 'radio' ? ( r . station && r . station . url ) || '' : r . episodeId ) ;
}
function remoteLine ( r ) {
return ( r . playing ? '▸ läuft auf ' : '‖ pausiert auf ' ) + remoteName ( r ) ;
}
function remoteCmd ( action , value ) {
if ( N . playerRemote ) N . playerRemote ( action , Math . round ( value || 0 ) ) ;
}
function remoteToggle ( ) {
const r = remote ( ) ;
if ( r ) remoteCmd ( r . playing ? 'pause' : 'play' ) ;
}
function takeOver ( ) {
if ( N . playerTakeOver ) N . playerTakeOver ( ) ;
}
// Which device plays: the ones online, this one first; choosing one moves the playback there.
function deviceMenu ( ) {
if ( ! N . playerTransfer ) return ;
const all = ( player . devices || [ ] ) . filter ( d => d . online || d . me ) ;
const devices = all . filter ( d => d . me ) . concat ( all . filter ( d => ! d . me ) ) ;
const r = remote ( ) ;
const radio = r ? null : radioNow ( ) ;
const activePlays = r ? r . playing : player . kind === 'podcast' ? ! ! player . playing : ! ! ( radio && radio . playing ) ;
const items = devices . map ( d => ( {
icon : d . active ? '◉' : '○' ,
label : d . me ? 'Dieses Gerät' : d . name || 'Unbekanntes Gerät' ,
hint : d . active ? ( activePlays ? 'spielt gerade' : 'pausiert' ) : d . me ? null : 'hierhin verschieben' ,
active : ! ! d . active ,
action : ( ) => N . playerTransfer ( d . id ) ,
} ) ) ;
if ( devices . length < 2 ) {
items . push ( { custom : el ( 'div' , 'nui-pod-meta nui-pod-devnote' ,
'Keine anderen Geräte online. Sobald diora auf einem anderen Gerät offen ist, erscheint es hier.' ) } ) ;
}
openSheet ( 'Wiedergabe auf …' , items ) ;
}
function devicesButton ( ) {
const b = el ( 'button' , 'btn nui-pod-devbtn' , '⇄ Wiedergabe auf …' ) ;
b . type = 'button' ;
b . addEventListener ( 'click' , deviceMenu ) ;
return b ;
}
// The full player for what another device plays: the same layout, remote buttons.
function openRemotePlayer ( ) {
const r = remote ( ) ;
if ( ! r ) return ;
closePlayer ( ) ;
sheetKind = 'remote' ;
const onRadio = r . kind === 'radio' ;
sheet = el ( 'div' ) ;
sheet . id = 'nui-pod-player' ;
sheet . className = 'nui-pod-remote' ;
sheet . dataset . remote = remoteKey ( r ) ;
sheet . innerHTML = '<div class="nui-pod-player-panel" role="dialog" aria-label="Player"></div>' ;
const panel = sheet . firstChild ;
const top = el ( 'div' , 'nui-pod-player-top' ) ;
top . append ( button ( '⌄' , 'Player schließen' , closePlayer ) ) ;
const cover = art ( onRadio ? r . station && r . station . logo : r . artwork , 'player' ) ;
if ( onRadio ) cover . classList . add ( 'nui-radio-art' ) ;
panel . append ( top , cover , el ( 'div' , 'nui-pod-player-title' ) , el ( 'div' , 'nui-pod-player-feed nui-pod-remote-line' ) ) ;
const controls = el ( 'div' , 'nui-pod-controls' ) ;
if ( onRadio ) {
controls . append ( button ( '▶' , 'Abspielen' , remoteToggle , 'nui-pod-play' ) ) ;
} else {
panel . append ( el ( 'div' , 'nui-pod-player-feed nui-pod-remote-feed' ) ) ;
const range = el ( 'input' , 'nui-pod-seek' ) ;
range . type = 'range' ;
range . min = 0 ;
range . step = 1 ;
range . addEventListener ( 'input' , ( ) => {
seeking = true ;
sheet . querySelector ( '.nui-pod-time-pos' ) . textContent = clock ( range . value ) ;
} ) ;
range . addEventListener ( 'change' , ( ) => {
remoteCmd ( 'seek' , range . value * 1000 ) ;
seeking = false ;
} ) ;
const times = el ( 'div' , 'nui-pod-times' ) ;
times . append ( el ( 'span' , 'nui-pod-time-pos' ) , el ( 'span' , 'nui-pod-time-left' ) ) ;
panel . append ( range , times ) ;
const prefs = devicePrefs ( ) ;
const back = prefs . skip _back || 15 ;
const fwd = prefs . skip _forward || 30 ;
controls . append (
button ( String ( back ) , ` ${ back } Sekunden zurück ` , ( ) => remoteCmd ( 'skip' , - back ) , 'nui-pod-back' ) ,
button ( '▶' , 'Abspielen' , remoteToggle , 'nui-pod-play' ) ,
button ( String ( fwd ) , ` ${ fwd } Sekunden vor ` , ( ) => remoteCmd ( 'skip' , fwd ) , 'nui-pod-fwd' ) ,
) ;
}
panel . append ( controls ) ;
if ( N . playerTakeOver ) {
const here = el ( 'button' , 'btn nui-pod-here' , '⇩ Hier abspielen' ) ;
here . type = 'button' ;
here . addEventListener ( 'click' , takeOver ) ;
panel . append ( here ) ;
}
const extra = el ( 'div' , 'nui-pod-extra' ) ;
if ( ! onRadio ) extra . append ( button ( '⏭' , 'Nächste Folge' , ( ) => remoteCmd ( 'next' ) , 'nui-pod-next' ) ) ;
if ( N . playerTransfer ) extra . append ( onRadio ? devicesButton ( ) : button ( '⇄' , 'Wiedergabe auf …' , deviceMenu , 'nui-pod-devices' ) ) ;
if ( ! onRadio ) extra . append ( button ( '≡' , 'Warteschlange' , showQueue ) ) ;
panel . append ( extra ) ;
if ( ! onRadio && r . episodeId ) {
const notes = el ( 'div' , 'nui-pod-notesbox' ) ;
panel . append ( notes ) ;
showDetails ( notes , r . episodeId , false ) ;
}
sheet . addEventListener ( 'click' , e => { if ( e . target === sheet ) closePlayer ( ) ; } ) ;
document . body . append ( sheet ) ;
renderRemotePlayer ( ) ;
}
function renderRemotePlayer ( ) {
const r = remote ( ) ;
if ( ! r || ! sheet ) return ;
const onRadio = r . kind === 'radio' ;
sheet . querySelector ( '.nui-pod-player-title' ) . textContent = ( onRadio ? r . station && r . station . name : r . title ) || '' ;
sheet . querySelector ( '.nui-pod-remote-line' ) . textContent = remoteLine ( r ) ;
const p = sheet . querySelector ( '.nui-pod-play' ) ;
p . textContent = r . playing ? ( onRadio ? '■' : '❚❚' ) : '▶' ;
p . title = ( r . playing ? ( onRadio ? 'Stopp' : 'Pause' ) : 'Abspielen' ) + ' auf ' + remoteName ( r ) ;
if ( onRadio ) return ;
sheet . querySelector ( '.nui-pod-remote-feed' ) . textContent = r . feed || '' ;
const range = sheet . querySelector ( '.nui-pod-seek' ) ;
const dur = Math . round ( ( r . dur || 0 ) / 1000 ) ;
const pos = Math . round ( ( r . pos || 0 ) / 1000 ) ;
range . max = dur || 1 ;
if ( ! seeking ) {
range . value = pos ;
sheet . querySelector ( '.nui-pod-time-pos' ) . textContent = clock ( pos ) ;
}
sheet . querySelector ( '.nui-pod-time-left' ) . textContent = dur ? '− ' + clock ( dur - pos ) : '' ;
markChapter ( sheet , pos ) ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// ---- downloads (Downloads.kt) ------------------------------------------------------------
// Episodes kept on the phone to play without a connection. Each episode row
// gets a mark (↓ fetch it, a ring while it loads, ✓ when it's here), the ⋯
// menus and the settings sheet get their entries. The app downloads the
// first episodes of the queue by itself and tells us via __dioraDownloads.
const DL = ! ! N . podcastDownloads ;
let downloads = DL ? parse ( N . podcastDownloads ( ) , { } ) : { } ;
let dlSettings = DL ? parse ( N . podcastDownloadSettings ( ) , { } ) : { } ;
2026-10-02 12:44:49 +02:00
// All at a glance (Downloads.infoJson): paused, how many run and wait, done of all in this round.
let dlInfo = DL && N . podcastDownloadsInfo ? parse ( N . podcastDownloadsInfo ( ) , { } ) : { } ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// How many of the queue's first episodes load by themselves; 0 = all of them.
const DL _COUNTS = [ 1 , 2 , 3 , 5 , 10 , 0 ] ;
const dlCountLabel = n => ( n === 0 ? 'Alle' : n === 1 ? '1 Folge' : n + ' Folgen' ) ;
2026-10-02 12:44:49 +02:00
const DL _PARALLEL = [ 1 , 2 , 3 , 4 ] ;
// Why a download waits (DownloadPlan.waitReason).
const DL _REASONS = {
paused : 'pausiert' , wifi : 'wartet auf WLAN' , offline : 'wartet auf eine Verbindung' , queue : 'in der Reihe' ,
retry : 'neuer Versuch' ,
} ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// The limit and the clean-up the AntennaPod way (Downloads.kt, DownloadPlan.toClean).
const DL _LIMITS = [ 5 , 10 , 25 , 50 , 100 , 500 , 0 ] ;
const DL _CLEANUPS = [
2026-10-02 11:46:37 +02:00
{ value : 'queue' , label : 'Wenn Platz gebraucht wird' ,
hint : 'beim Limit gehen Folgen, die nicht in der Warteschlange stehen: gehörte zuerst' } ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
{ value : 'never' , label : 'Nie' , hint : 'beim Limit lädt die App nichts mehr von selbst' } ,
] ;
const dlLimitLabel = n => ( n ? n + ' Folgen' : 'Unbegrenzt' ) ;
function size ( bytes ) {
if ( bytes >= 1073741824 ) return ( bytes / 1073741824 ) . toFixed ( 1 ) . replace ( '.' , ',' ) + ' GB' ;
return Math . max ( 1 , Math . round ( ( bytes || 0 ) / 1048576 ) ) + ' MB' ;
}
function dlStart ( e ) {
N . podcastDownload ( JSON . stringify ( e . raw ) ) ;
2026-10-02 12:44:49 +02:00
window . _ _dioraDownloads ( Object . assign ( { } , downloads ,
{ [ e . id ] : { state : 'queued' , progress : 0 , bytes : 0 , total : 0 , title : e . title , reason : 'queue' } } ) ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
toast ( 'Wird heruntergeladen' ) ;
}
function dlDelete ( e , running ) {
N . podcastDeleteDownload ( e . id ) ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// "Löschen nimmt aus der Warteschlange" (account): the app takes it out too.
const out = ! running && settings ( ) . delete _dequeue !== false && 'delete_dequeue' in settings ( ) && inQueue ( e . id ) ;
toast ( running ? 'Download abgebrochen' : out ? 'Download gelöscht, aus der Warteschlange genommen' : 'Download gelöscht' ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
function dlAppend ( row , e ) {
if ( ! DL || ! ( e . raw . episode _ _audio _url || e . raw . audio _url ) ) return ;
const d = downloads [ e . id ] ;
const state = d ? d . state : null ;
let b ;
if ( state === 'done' ) {
b = button ( '✓' , 'Heruntergeladen' , ( ) => openSheet ( e . title , dlMenuItems ( e ) ) , 'nui-pod-dl nui-pod-dl-done' ) ;
} else if ( state === 'queued' || state === 'downloading' ) {
b = button ( '' , state === 'queued' ? 'Wartet auf den Download' : 'Wird heruntergeladen' ,
( ) => openSheet ( e . title , dlMenuItems ( e ) ) , 'nui-pod-dl nui-pod-dl-ring' ) ;
b . append ( el ( 'span' ) ) ;
} else {
b = button ( '↓' , state === 'failed' ? 'Download hat nicht geklappt — nochmal' : 'Herunterladen' , ( ) => dlStart ( e ) ,
'nui-pod-dl' + ( state === 'failed' ? ' nui-pod-dl-failed' : '' ) ) ;
}
b . dataset . dl = e . id ;
dlRing ( b ) ;
row . append ( b ) ;
}
function dlRing ( b ) {
const d = downloads [ b . dataset . dl ] ;
if ( ! d || ( d . state !== 'queued' && d . state !== 'downloading' ) ) return ;
b . style . setProperty ( '--p' , ( d . progress || 0 ) + '%' ) ;
2026-10-02 12:44:49 +02:00
b . firstChild . textContent = d . state === 'queued' ? ( d . reason === 'paused' ? '‖' : '…' ) : d . progress + '' ;
}
// "wartet auf WLAN", "neuer Versuch um 14:05" …
function dlReason ( d ) {
if ( d . reason === 'retry' && d . retryAt ) {
return 'neuer Versuch um ' + new Date ( d . retryAt ) . toLocaleTimeString ( 'de-DE' , { hour : '2-digit' , minute : '2-digit' } ) ;
}
return DL _REASONS [ d . reason ] || 'wartet' ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
function dlMenuItems ( e ) {
if ( ! DL || ! ( e . raw . episode _ _audio _url || e . raw . audio _url ) ) return [ ] ;
const d = downloads [ e . id ] ;
if ( ! d || d . state === 'failed' ) {
return [ { icon : '↓' , label : 'Herunterladen' , hint : d ? 'hat eben nicht geklappt (' + ( d . error || '?' ) + ')' : 'zum Hören ohne Verbindung' ,
action : ( ) => dlStart ( e ) } ] ;
}
if ( d . state === 'done' ) return [ { icon : '✕' , label : 'Download löschen' , hint : size ( d . bytes ) , action : ( ) => dlDelete ( e ) } ] ;
2026-10-02 12:44:49 +02:00
return [
d . state === 'queued' && d . order > 0 && N . podcastDownloadFirst &&
{ icon : '⇈' , label : 'Zuerst laden' , hint : 'vor die anderen wartenden' , action : ( ) => N . podcastDownloadFirst ( e . id ) } ,
{ icon : '✕' , label : 'Download abbrechen' , hint : d . state === 'queued' ? dlReason ( d ) : d . progress + ' %' ,
action : ( ) => dlDelete ( e , true ) } ,
] ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
}
function dlSet ( changes ) {
Object . assign ( dlSettings , changes ) ;
N . podcastSetDownloadSettings ( JSON . stringify ( changes ) ) ;
settingsMenu ( ) ;
}
function dlSettingsItems ( ) {
if ( ! DL ) return [ ] ;
const s = dlSettings = parse ( N . podcastDownloadSettings ( ) , dlSettings ) ;
const done = Object . values ( downloads ) . filter ( d => d . state === 'done' ) ;
const bytes = done . reduce ( ( n , d ) => n + ( d . bytes || 0 ) , 0 ) ;
return [
{ icon : s . auto ? '☑' : '☐' , label : 'Automatisch herunterladen' ,
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
hint : s . auto ? ( s . count === 0 ? 'die ganze Warteschlange' : ` die ersten ${ s . count } Folgen der Warteschlange ` ) : 'aus' ,
action : ( ) => dlSet ( { auto : ! s . auto } ) } ,
s . auto && { icon : '#' , label : 'Wie viele Folgen' , hint : dlCountLabel ( s . count ) , action : ( ) => openSheet ( 'Automatisch herunterladen' ,
DL _COUNTS . map ( n => ( { icon : s . count === n ? '●' : '○' , label : dlCountLabel ( n ) , hint : n === 0 ? 'die ganze Warteschlange' : null ,
active : s . count === n ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
action : ( ) => dlSet ( { count : n } ) } ) ) ) } ,
s . auto && { icon : s . mobile ? '☑' : '☐' , label : 'Auch über mobile Daten' , hint : s . mobile ? 'nicht nur im WLAN' : 'sonst nur im WLAN' ,
action : ( ) => dlSet ( { mobile : ! s . mobile } ) } ,
2026-10-02 12:44:49 +02:00
s . parallel != null && { icon : '⇉' , label : 'Gleichzeitig laden' , hint : s . parallel === 1 ? '1 Folge' : s . parallel + ' Folgen' ,
action : ( ) => openSheet ( 'Gleichzeitig laden' , DL _PARALLEL . map ( n => ( {
icon : s . parallel === n ? '●' : '○' , label : n === 1 ? '1 Folge' : n + ' Folgen' , active : s . parallel === n ,
hint : n === 3 ? 'Standard' : null , action : ( ) => dlSet ( { parallel : n } ) } ) ) ) } ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
{ icon : s . deletePlayed ? '☑' : '☐' , label : 'Gehörte Folgen löschen' ,
hint : 'sobald die Folge zu Ende ist – nicht bei Favoriten' ,
action : ( ) => dlSet ( { deletePlayed : ! s . deletePlayed } ) } ,
s . limit != null && { icon : '▦' , label : 'Höchstens behalten' ,
hint : dlLimitLabel ( s . limit ) + ` · jetzt ${ Object . values ( downloads ) . filter ( d => d . state !== 'failed' ) . length } ` ,
action : ( ) => openSheet ( 'Höchstens so viele Folgen auf dem Telefon' , DL _LIMITS . map ( n => ( {
icon : s . limit === n ? '●' : '○' , label : dlLimitLabel ( n ) , active : s . limit === n ,
hint : n === 25 ? 'Standard' : null , action : ( ) => dlSet ( { limit : n } ) } ) ) ) } ,
2026-10-02 11:46:37 +02:00
'enqueue' in s && { icon : s . enqueue ? '☑' : '☐' , label : 'Heruntergeladene Folgen einreihen' ,
hint : s . enqueue ? 'an: was geladen wird, kommt in die Warteschlange' : 'aus' ,
action : ( ) => dlSet ( { enqueue : ! s . enqueue } ) } ,
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
s . cleanup && { icon : '♻' , label : 'Aufräumen' , hint : ( DL _CLEANUPS . find ( c => c . value === s . cleanup ) || DL _CLEANUPS [ 0 ] ) . label ,
action : ( ) => openSheet ( 'Downloads aufräumen' , DL _CLEANUPS . map ( c => ( {
icon : s . cleanup === c . value ? '●' : '○' , label : c . label , hint : c . hint , active : s . cleanup === c . value ,
action : ( ) => dlSet ( { cleanup : c . value } ) } ) ) ) } ,
done . length > 0 && { icon : '✕' , label : ` Downloads löschen ( ${ size ( bytes ) } ) ` , danger : true ,
hint : done . length === 1 ? '1 Folge' : done . length + ' Folgen' ,
action : ( ) => openSheet ( ` Alle ${ done . length } Downloads löschen? ` , [
{ icon : '✕' , label : 'Löschen' , hint : size ( bytes ) , danger : true ,
action : ( ) => { N . podcastDeleteAllDownloads ( ) ; toast ( 'Downloads gelöscht' ) ; } } ,
{ icon : '‹ ' , label : 'Abbrechen' , action : ( ) => { } } ,
] ) } ,
] ;
}
2026-10-02 12:44:49 +02:00
window . _ _dioraDownloads = function ( map , info ) {
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const before = downloads ;
downloads = ( typeof map === 'string' ? parse ( map , downloads ) : map ) || { } ;
2026-10-02 12:44:49 +02:00
if ( info ) dlInfo = typeof info === 'string' ? parse ( info , dlInfo ) : info ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
const keys = Object . keys ( downloads ) ;
const same = keys . length === Object . keys ( before ) . length &&
2026-10-02 12:44:49 +02:00
keys . every ( id => before [ id ] && before [ id ] . state === downloads [ id ] . state && before [ id ] . reason === downloads [ id ] . reason ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// Only the numbers moved: turn the rings, no new list (keeps a drag or a scroll going).
2026-10-02 12:44:49 +02:00
if ( same ) {
document . querySelectorAll ( '#nui-pod [data-dl]' ) . forEach ( dlRing ) ;
dlIndicatorText ( ) ;
} else {
render ( ) ;
}
renderDownloads ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} ;
2026-10-02 12:44:49 +02:00
function dlCounts ( ) {
const c = { running : 0 , waiting : 0 , failed : 0 , done : 0 , bytes : 0 } ;
for ( const d of Object . values ( downloads ) ) {
if ( d . state === 'downloading' ) c . running ++ ;
else if ( d . state === 'queued' ) c . waiting ++ ;
else if ( d . state === 'failed' ) c . failed ++ ;
else if ( d . state === 'done' ) { c . done ++ ; c . bytes += d . bytes || 0 ; }
}
return c ;
}
function dlSummary ( ) {
const c = dlCounts ( ) ;
const parts = [ ] ;
if ( c . running ) parts . push ( c . running + ( c . running === 1 ? ' läuft' : ' laufen' ) ) ;
if ( c . waiting ) parts . push ( c . waiting + ( c . waiting === 1 ? ' wartet' : ' warten' ) + ( dlInfo . paused ? ' (pausiert)' : '' ) ) ;
if ( c . failed ) parts . push ( c . failed + ' fehlgeschlagen' ) ;
if ( c . done ) parts . push ( c . done + ' fertig' ) ;
return parts . join ( ' · ' ) || 'keine' ;
}
// The top bar's sign while downloads run or wait: "↓ 3/68" (the one loading of all in this round,
// like the notification), "‖ 68" while paused. Opens the overview.
function dlIndicator ( ) {
if ( ! DL || ! N . podcastDownloadsInfo ) return null ;
const b = button ( '' , 'Downloads' , openDownloads , 'nui-pod-dlind' ) ;
b . id = 'nui-pod-dlind' ;
dlIndicatorText ( b ) ;
return b ;
}
function dlIndicatorText ( b ) {
b = b || $id ( 'nui-pod-dlind' ) ;
if ( ! b ) return ;
const c = dlCounts ( ) ;
const busy = c . running + c . waiting ;
b . hidden = ! busy ;
if ( ! busy ) return ;
const total = Math . max ( dlInfo . total || 0 , busy ) ;
const done = Math . min ( dlInfo . done || 0 , total ) ;
b . textContent = dlInfo . paused ? '‖ ' + busy : c . running ? ` ↓ ${ Math . min ( done + 1 , total ) } / ${ total } ` : '↓ ' + busy ;
b . title = 'Downloads: ' + dlSummary ( ) ;
b . setAttribute ( 'aria-label' , b . title ) ;
}
// ---- the downloads overview: what runs, waits, failed, is done ---------------------------
let dlSheet = null ;
let dlBody = null ;
function closeDownloads ( ) {
if ( ! dlSheet ) return false ;
dlSheet . remove ( ) ;
dlSheet = dlBody = null ;
return true ;
}
function openDownloads ( ) {
if ( ! DL ) return ;
closeDownloads ( ) ;
dlSheet = el ( 'div' ) ;
dlSheet . id = 'nui-pod-player' ;
dlSheet . className = 'nui-pod-notes-sheet nui-pod-dls' ;
const panel = el ( 'div' , 'nui-pod-player-panel' ) ;
const top = el ( 'div' , 'nui-pod-player-top' ) ;
top . append ( button ( '⌄' , 'Schließen' , closeDownloads ) ) ;
dlBody = el ( 'div' , 'nui-pod-dls-body' ) ;
panel . append ( top , el ( 'div' , 'nui-pod-player-title' , 'Downloads' ) , dlBody ) ;
dlSheet . append ( panel ) ;
dlSheet . addEventListener ( 'click' , ev => { if ( ev . target === dlSheet ) closeDownloads ( ) ; } ) ;
document . body . append ( dlSheet ) ;
renderDownloads ( true ) ;
}
function dlTitle ( d ) {
return plain ( d . title ) || 'Folge ' + d . id ;
}
// The line under the title: how far, why it waits, what went wrong.
function dlStatus ( d ) {
const of = d . total ? ` ${ size ( d . bytes ) } von ${ size ( d . total ) } ` : size ( d . bytes ) ;
if ( d . state === 'downloading' ) return ( d . total ? d . progress + ' % · ' : '' ) + of ;
if ( d . state === 'failed' ) return d . error || 'hat nicht geklappt' ;
const parts = [ dlReason ( d ) ] ;
if ( d . bytes ) parts . push ( of + ' geladen' ) ;
if ( d . reason === 'retry' && d . error ) parts . push ( d . error ) ;
return parts . join ( ' · ' ) ;
}
function dlActions ( d ) {
const items = [ ] ;
if ( d . state === 'queued' && d . order > 0 && N . podcastDownloadFirst ) {
items . push ( { icon : '⇈' , label : 'Zuerst laden' , hint : 'vor die anderen wartenden' , action : ( ) => N . podcastDownloadFirst ( d . id ) } ) ;
}
if ( ( d . state === 'failed' || ( d . state === 'queued' && d . error ) ) && N . podcastRetryDownload ) {
items . push ( { icon : '↻' , label : 'Erneut versuchen' , hint : d . error || null , action : ( ) => N . podcastRetryDownload ( d . id ) } ) ;
}
items . push ( d . state === 'failed'
? { icon : '✕' , label : 'Löschen' , danger : true , action : ( ) => N . podcastDeleteDownload ( d . id ) }
: { icon : '✕' , label : 'Abbrechen' , hint : d . bytes ? 'was schon geladen ist, geht mit' : null , danger : true ,
action : ( ) => N . podcastDeleteDownload ( d . id ) } ) ;
return items ;
}
function dlRow ( d ) {
const row = el ( 'div' , 'nui-pod-dlrow' + ( d . state === 'failed' ? ' nui-pod-dlrow-failed' : '' ) ) ;
row . dataset . dlrow = d . id ;
const main = el ( 'div' , 'nui-pod-main' ) ;
main . append ( el ( 'div' , 'nui-pod-title' , dlTitle ( d ) ) ) ;
const f = d . feedId && feeds ( ) . find ( x => x . id === d . feedId ) ;
const meta = el ( 'div' , 'nui-pod-meta nui-pod-dlstatus' ) ;
main . append ( meta ) ;
if ( f ) main . append ( el ( 'div' , 'nui-pod-meta' , plain ( f . title ) ) ) ;
if ( d . state === 'downloading' || ( d . state === 'queued' && d . bytes ) ) {
const bar = el ( 'div' , 'nui-pod-progress nui-pod-stat-bar' ) ;
bar . append ( el ( 'span' ) ) ;
main . append ( bar ) ;
}
row . append ( main ) ;
// The likely tap right there; all of them in the row's sheet.
if ( d . state === 'queued' && d . order > 0 && N . podcastDownloadFirst ) {
row . append ( button ( '⇈' , 'Zuerst laden' , ( ) => N . podcastDownloadFirst ( d . id ) ) ) ;
}
if ( d . state === 'failed' && N . podcastRetryDownload ) {
row . append ( button ( '↻' , 'Erneut versuchen' , ( ) => N . podcastRetryDownload ( d . id ) ) ) ;
}
row . append ( button ( '✕' , d . state === 'failed' ? 'Löschen' : 'Abbrechen' , ( ) => N . podcastDeleteDownload ( d . id ) ) ) ;
row . addEventListener ( 'click' , ( ) => openSheet ( dlTitle ( d ) , dlActions ( d ) ) ) ;
dlRowNumbers ( row , d ) ;
return row ;
}
function dlRowNumbers ( row , d ) {
row . querySelector ( '.nui-pod-dlstatus' ) . textContent = dlStatus ( d ) ;
const fill = row . querySelector ( '.nui-pod-progress > span' ) ;
if ( fill ) fill . style . width = ( d . total ? Math . round ( d . bytes / d . total * 100 ) : 0 ) + '%' ;
}
function renderDownloads ( force ) {
if ( ! dlBody ) return ;
const all = Object . keys ( downloads ) . map ( id => Object . assign ( { id : + id } , downloads [ id ] ) ) ;
const running = all . filter ( d => d . state === 'downloading' ) ;
const waiting = all . filter ( d => d . state === 'queued' ) . sort ( ( a , b ) => ( a . order || 0 ) - ( b . order || 0 ) ) ;
const failed = all . filter ( d => d . state === 'failed' ) ;
const done = all . filter ( d => d . state === 'done' ) ;
// The same rows as before: only the numbers move (keeps a tap or the scroll going).
const sig = [ running , waiting , failed ] . map ( l => l . map ( d => ` ${ d . id } : ${ d . reason || '' } : ${ d . error || '' } ` ) . join ( ',' ) )
. join ( '|' ) + ` | ${ done . length } | ${ ! ! dlInfo . paused } ` ;
if ( ! force && sig === dlBody . dataset . sig ) {
for ( const d of running . concat ( waiting ) ) {
const row = dlBody . querySelector ( ` [data-dlrow=" ${ d . id } "] ` ) ;
if ( row ) dlRowNumbers ( row , d ) ;
}
return ;
}
dlBody . dataset . sig = sig ;
const panel = dlBody . parentElement ;
const scroll = panel . scrollTop ;
dlBody . textContent = '' ;
const head = el ( 'div' , 'nui-pod-dls-head' ) ;
head . append ( el ( 'div' , 'nui-pod-summary' , dlSummary ( ) ) ) ;
const tools = el ( 'div' , 'nui-pod-offer-row' ) ;
if ( N . podcastPauseDownloads && ( running . length || waiting . length || dlInfo . paused ) ) {
const paused = ! ! dlInfo . paused ;
tools . append ( button ( paused ? '▶ Fortsetzen' : '‖ Alle pausieren' , paused ? 'Downloads fortsetzen' : 'Alle Downloads pausieren' ,
( ) => { dlInfo . paused = ! paused ; N . podcastPauseDownloads ( ! paused ) ; renderDownloads ( true ) ; } , 'btn' ) ) ;
}
if ( failed . length > 1 && N . podcastRetryDownload ) {
tools . append ( button ( '↻ Alle erneut' , 'Alle fehlgeschlagenen erneut versuchen' , ( ) => N . podcastRetryDownload ( 0 ) , 'btn' ) ) ;
}
if ( tools . childNodes . length ) head . append ( tools ) ;
dlBody . append ( head ) ;
const section = ( title , list ) => {
if ( ! list . length ) return ;
dlBody . append ( el ( 'div' , 'nui-pod-dls-sec' , ` ${ title } ( ${ list . length } ) ` ) ) ;
const box = el ( 'div' , 'nui-pod-list' ) ;
for ( const d of list ) box . append ( dlRow ( d ) ) ;
dlBody . append ( box ) ;
} ;
section ( 'Läuft' , running ) ;
section ( 'Wartet' , waiting ) ;
section ( 'Fehlgeschlagen' , failed ) ;
if ( done . length ) {
dlBody . append ( el ( 'div' , 'nui-pod-dls-sec' , 'Fertig' ) ) ;
dlBody . append ( el ( 'div' , 'nui-pod-meta nui-pod-dls-done' ,
` ${ done . length } ${ done . length === 1 ? 'Folge' : 'Folgen' } auf dem Telefon · ${ size ( done . reduce ( ( n , d ) => n + ( d . bytes || 0 ) , 0 ) ) } ` ) ) ;
}
if ( ! all . length ) dlBody . append ( el ( 'div' , 'nui-pod-empty' , 'Keine Downloads. Beim ↓ einer Folge lädst du sie aufs Telefon.' ) ) ;
panel . scrollTop = scroll ;
}
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
// ---- hooks from the app ---------------------------------------------------------------
2026-10-02 16:29:15 +02:00
// A podcast address from another app is waiting (MainActivity): show its preview.
window . _ _dioraSubscribe = function ( ) {
const url = N . podcastTakeSubscribe ? N . podcastTakeSubscribe ( ) : '' ;
if ( url ) openPreview ( url ) ;
} ;
window . _ _dioraPreview = function ( id , d ) {
if ( ! preview || preview . id !== id ) return ;
if ( d && d . ok ) preview = { id , url : d . url , data : d } ;
else if ( d && d . choices && d . choices . length ) preview = { id , url : preview . url , choices : d . choices } ;
else preview = { id , url : preview . url , error : ( d && d . error ) || 'Dieser Feed ließ sich nicht laden.' } ;
render ( ) ;
} ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
window . _ _dioraPodcasts = function ( json ) {
state = typeof json === 'string' ? parse ( json , state ) : json ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
moveFeedListsToServer ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
render ( ) ;
} ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
// The app won't stream this episode over mobile data without a yes ("Nachfragen", RadioService.mayStream).
window . _ _dioraAskStream = function ( id , title ) {
if ( ! N . podcastAllowStream ) return ;
openSheet ( 'Über mobile Daten streamen?' , [
{ icon : '▶' , label : 'Ja, diese Folge streamen' , hint : plain ( title ) || null , action : ( ) => N . podcastAllowStream ( id ) } ,
{ icon : '✕' , label : 'Nein' , hint : 'im WLAN oder heruntergeladen hören' , action : ( ) => { } } ,
] ) ;
} ;
2026-10-02 16:36:00 +02:00
// A new-episode notification was tapped: that podcast's page (gone meanwhile: „Neu“).
window . _ _dioraOpenFeed = function ( feedId ) {
if ( typeof showTab !== 'function' ) return ;
closeStats ( ) ;
closeDownloads ( ) ;
closeNotes ( ) ;
closePlayer ( ) ;
selected = null ;
showTab ( 'podcasts' ) ;
const f = feeds ( ) . find ( x => x . id === feedId ) ;
if ( f ) {
openFeed ( f ) ;
} else {
feedView = null ;
view = 'inbox' ;
render ( ) ;
scrollTop ( 0 ) ;
}
} ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
window . _ _dioraPodcastsRefreshed = function ( ok ) {
refreshing = false ;
if ( ! ok ) toast ( navigator . onLine ? 'Abrufen hat nicht geklappt' : 'Offline — es gilt der letzte Stand' ) ;
render ( ) ;
} ;
window . _ _dioraPlayer = function ( p ) {
const before = player ;
player = p || { kind : null } ;
renderMini ( ) ;
renderPlayer ( ) ;
// The current episode changed: the queue view marks it.
if ( before . episodeId !== player . episodeId || before . kind !== player . kind ) render ( ) ;
} ;
// The app loads this in <head>; the browser may load it after the page is parsed.
function start ( fn ) {
if ( document . readyState === 'loading' ) document . addEventListener ( 'DOMContentLoaded' , fn ) ;
else fn ( ) ;
}
start ( ( ) => {
if ( typeof showTab !== 'function' ) return ;
// The web's podcast tab loads its own lists; the app has them already.
window . loadPodcastTab = function ( ) { } ;
window . refreshAllFeeds = async function ( ) { } ;
build ( ) ;
buildMini ( ) ;
player = parse ( N . playerState ( ) , { kind : null } ) ;
Podcast-Routine aus AntennaPod: Abspiel-Einstellungen fürs Konto, Listen und Stille pro Podcast; Oberfläche aus diora-android 304abf0, sw v65
- PodcastSettings (Migration 0014): skip_back / skip_forward (5–60 s),
smart_played, speed_presets (0,5–3,5×), keep_skipped, delete_dequeue,
mobile_downloads, mobile_streaming (allow/ask/never), times_by_speed;
die ersten vier sind anfangs leer (null) und werden mit POST
settings/ {"initial": {...}} nur dort gesetzt, wo noch nichts steht
- GET/POST /podcasts/settings/ und state.settings mit allen Feldern,
geprüft, nichts ändert sich bei einem ungültigen Wert
- PodcastFeed: episode_sort, episode_filter, skip_silence in
feeds/<id>/settings/ und den Feeds des States
- Tempo pro Podcast bis 3,5× (Server und AntennaPod-Import)
- AntennaPod-Import: Sortierung, Filter und Stille pro Podcast, soweit
diora sie zeigen kann; nicht darstellbare werden gezählt
- Browser-Host: Sprungweiten, "gehört" und keep_skipped aus dem Konto,
Tempo bis 3,5×, neue Einstellungen werden gespeichert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:46:55 +02:00
moveFeedListsToServer ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
render ( ) ;
renderMini ( ) ;
const back = window . _ _dioraBack ;
window . _ _dioraBack = function ( ) {
if ( closeStats ( ) ) return true ;
2026-10-02 12:44:49 +02:00
if ( closeDownloads ( ) ) return true ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( closeNotes ( ) ) return true ;
if ( closePlayer ( ) ) return true ;
if ( typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts' && stopSelecting ( ) ) return true ;
if ( back && back ( ) ) return true ;
if ( typeof _currentTopTab !== 'undefined' && _currentTopTab === 'podcasts' ) {
2026-10-02 16:29:15 +02:00
if ( closePreview ( ) ) return true ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
if ( feedView ) { feedView = null ; render ( ) ; return true ; }
showTab ( 'books' ) ;
return true ;
}
return false ;
} ;
// Pull the latest state once per start; the server has done the fetching.
N . podcastRefresh ( false ) ;
2026-10-02 16:29:15 +02:00
window . _ _dioraSubscribe ( ) ;
Podcasts im Browser: die Podcast-Seite der App statt des alten Podcast-Tabs
Der Podcast-Tab der Web-App zeigt jetzt dieselbe Oberfläche wie die
Android-App: native-podcast.js aus diora-android, unverändert kopiert nach
static/podcasts/ (tools/sync_podcast_ui.py, mit Quell-Commit im Kopf),
dazu die Podcast-Teile von native.css.
static/js/podcasts-web.js spielt im Browser die Rolle der App
(window.DioraPodcastHost statt DioraNative):
- Zustand aus /podcasts/state/, in localStorage für einen schnellen Start;
Änderungen sofort lokal und der Reihe nach an den Server (Outbox, übersteht
Neuladen und Offline), wie Podcasts.kt, inkl. „immer sortiert halten“
- eigener Player (nicht das Radio-Audio-Element), Warteschlange = Playlist,
wie RadioService.EpisodeTracker: Stelle fortsetzen, Intro/Outro, Fortschritt
alle 10 s und bei Pause, „gehört“ kurz vor Schluss (auch bei „nächste“),
automatisch weiter, nach Pause etwas zurück (0,8/3/10/20 s), Tempo und
Lautstärke (bis 1) pro Podcast, Sleep-Timer nur während des Abspielens mit
Ausblenden
- Sperrbildschirm und Medientasten über navigator.mediaSession
- Abspiel-Einstellungen dieses Geräts in localStorage (wie PlaybackPrefs)
- keine Downloads (die Oberfläche blendet sie aus)
- Radio und Podcast nie gleichzeitig: eine Folge stoppt das Radio, ein Sender
legt die Folge weg
- Bottom-Sheet und Mini-Player-Dock über der Radio-Leiste; auf breiten
Bildschirmen eine mittige Spalte (760 px)
In der App und der Desktop-App (window.DioraNative vorhanden) tut
podcasts-web.js nichts.
Entfernt: der alte Podcast-Tab (Markup, ~1000 Zeilen in app.js, CSS), die
Podcast-Leiste im Player, INITIAL_PODCAST_FEEDS und das Herunterladen von
Folgen in den Browser-Cache (der Service Worker räumt diora-podcast-v1 ab).
Service-Worker-Cache auf v52, neue Dateien im App-Shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:26:32 +02:00
} ) ;
} ) ( ) ;