Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
/ *
Navigation: Lesen und Hören (Phase 1, noch nicht veröffentlicht)
Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit
Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts,
Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen
(DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je
Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter
ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben.
- Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books.
- Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“).
- Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann.
- Ein Link auf einen Abschnitt (#news …) schlägt die Startseite.
Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls
daraus; bis dahin bleibt das hier unveröffentlicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +02:00
* Audiobooks , the section "Hörbücher" of Hören ( docs / audiobooks . md , docs / navigation . md ) :
* podcast subscriptions marked as a Hörbuch or Hörspiel are listened to like a book —
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
* "Weiterhören" , one progress per book , the chapters in order , bookmarks with a note ,
* the sleep timer to the end of the chapter . The server knows the books
* ( / p o d c a s t s / a u d i o b o o k s / ) , t h e p o d c a s t h o s t p l a y s t h e m ( p o d c a s t P l a y B o o k : t h e c h a p t e r s i n
* order , outside the queue ) . Runs in the web page , the desktop app and the Android app
* ( not in diora Reader , which has no podcasts ) . Chrome - 61 - safe : no ? . / ? ? .
* /
( function ( ) {
'use strict' ;
Navigation: Lesen und Hören (Phase 1, noch nicht veröffentlicht)
Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit
Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts,
Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen
(DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je
Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter
ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben.
- Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books.
- Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“).
- Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann.
- Ein Link auf einen Abschnitt (#news …) schlägt die Startseite.
Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls
daraus; bis dahin bleibt das hier unveröffentlicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +02:00
const tab = document . getElementById ( 'tab-audiobooks' ) ;
if ( ! tab ) return ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
if ( typeof IS _AUTHENTICATED !== 'undefined' && ! IS _AUTHENTICATED ) return ;
// The podcast host: the app's own on Android, podcasts-web.js elsewhere.
function host ( ) {
const n = window . DioraNative ;
if ( n && typeof n . podcastPlayBook === 'function' ) return n ;
const h = window . DioraPodcastHost ;
return h && typeof h . podcastPlayBook === 'function' ? h : null ;
}
const st = {
books : null , // /podcasts/audiobooks/ books
open : null , // feed id of the book shown
detail : null , // /podcasts/audiobooks/<id>/
picking : false , // choosing a podcast to make an audiobook
pickQuery : '' ,
now : null , // playerState() of the last poll
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
folder : null , // the folder chip chosen (null: all, '': without a folder)
folderOrder : [ ] , // the podcasts' folder order (/podcasts/folders/)
2026-10-16 15:45:14 +02:00
series : null , // the series shown (its key), inside the folder
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
} ;
const shelf = document . createElement ( 'div' ) ;
shelf . id = 'audiobook-shelf' ;
Navigation: Lesen und Hören (Phase 1, noch nicht veröffentlicht)
Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit
Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts,
Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen
(DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je
Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter
ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben.
- Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books.
- Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“).
- Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann.
- Ein Link auf einen Abschnitt (#news …) schlägt die Startseite.
Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls
daraus; bis dahin bleibt das hier unveröffentlicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +02:00
tab . appendChild ( shelf ) ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
// ---- helpers ------------------------------------------------------------------------
function esc ( s ) {
return String ( s == null ? '' : s ) . replace ( /[&<>"']/g , c =>
( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' } [ c ] ) ) ;
}
function hours ( seconds ) {
const m = Math . round ( Math . max ( 0 , seconds ) / 60 ) ;
if ( m < 60 ) return m + ' min' ;
const h = Math . floor ( m / 60 ) ;
return h + ' h' + ( m % 60 ? ' ' + ( m % 60 ) + ' min' : '' ) ;
}
function clock ( ms ) {
const s = Math . max ( 0 , Math . floor ( ms / 1000 ) ) ;
const h = Math . floor ( s / 3600 ) ;
const mm = String ( Math . floor ( s / 60 ) % 60 ) ;
const ss = String ( s % 60 ) . padStart ( 2 , '0' ) ;
return h ? h + ':' + mm . padStart ( 2 , '0' ) + ':' + ss : mm + ':' + ss ;
}
function progressText ( b ) {
const parts = [ ] ;
if ( b . total _seconds > 0 ) parts . push ( hours ( b . heard _seconds ) + ' von ' + hours ( b . total _seconds ) ) ;
if ( ! b . next ) parts . push ( 'fertig gehört' ) ;
else parts . push ( 'Kapitel ' + ( b . next . index + 1 ) + '/' + b . chapters ) ;
return parts . join ( ' · ' ) ;
}
function pct ( b ) {
if ( b . total _seconds > 0 ) return Math . min ( 100 , Math . round ( 100 * b . heard _seconds / b . total _seconds ) ) ;
return b . chapters ? Math . round ( 100 * b . done / b . chapters ) : 0 ;
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
function cover ( url , title , cls , author ) {
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
const art = String ( url || '' ) . replace ( /^http:/ , 'https:' ) ;
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
if ( ! art && window . DioraGenCover ) { // none: one drawn from the title (gencover.js)
return '<img class="' + cls + '" src="' + DioraGenCover . url ( { title : title || '' , author : author || '' } ) + '" alt="">' ;
}
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
return art
? '<img class="' + cls + '" src="' + esc ( art ) + '" alt="" loading="lazy">'
: '<span class="' + cls + ' ab-nocover">' + esc ( ( title || '?' ) . trim ( ) . charAt ( 0 ) . toUpperCase ( ) ) + '</span>' ;
}
function toast ( text ) {
let t = document . getElementById ( 'ab-toast' ) ;
if ( ! t ) {
t = document . createElement ( 'div' ) ;
t . id = 'ab-toast' ;
document . body . appendChild ( t ) ;
}
t . textContent = text ;
t . classList . add ( 'ab-show' ) ;
clearTimeout ( toast . timer ) ;
toast . timer = setTimeout ( ( ) => t . classList . remove ( 'ab-show' ) , 2600 ) ;
}
function cached ( name , value ) {
try {
if ( value === undefined ) return JSON . parse ( localStorage . getItem ( 'diora_ab_' + name ) || 'null' ) ;
localStorage . setItem ( 'diora_ab_' + name , JSON . stringify ( value ) ) ;
} catch ( e ) { }
return null ;
}
async function api ( path , body ) {
const opts = { credentials : 'same-origin' , cache : 'no-store' } ;
if ( body !== undefined ) {
opts . method = 'POST' ;
opts . headers = { 'Content-Type' : 'application/json' } ;
opts . body = JSON . stringify ( body ) ;
}
const r = await fetch ( path , opts ) ;
const d = await r . json ( ) . catch ( ( ) => null ) ;
if ( ! r . ok || ! d ) throw new Error ( ( d && d . error ) || ( 'HTTP ' + r . status ) ) ;
return d ;
}
function podcastState ( ) {
const h = host ( ) ;
if ( ! h ) return null ;
try { return JSON . parse ( h . podcastState ( ) ) ; } catch ( e ) { return null ; }
}
function playerState ( ) {
const h = host ( ) ;
if ( ! h || typeof h . playerState !== 'function' ) return null ;
try { return JSON . parse ( h . playerState ( ) ) ; } catch ( e ) { return null ; }
}
// The chapter of the open book playing (or paused) now, else null.
function nowInBook ( ) {
const p = st . now ;
const d = st . detail ;
if ( ! p || ! d || p . kind !== 'podcast' ) return null ;
for ( let i = 0 ; i < d . chapters . length ; i ++ ) {
if ( d . chapters [ i ] . episode _ _id === p . episodeId ) return { index : i , pos : p . pos || 0 , dur : p . dur || 0 , playing : ! ! p . playing } ;
}
return null ;
}
// ---- loading ------------------------------------------------------------------------
async function loadList ( ) {
if ( ! host ( ) ) { shelf . innerHTML = '' ; return ; }
if ( ! st . books ) { st . books = cached ( 'books' ) ; renderShelf ( ) ; }
try {
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const got = await Promise . all ( [ api ( '/podcasts/audiobooks/' ) , loadFolderOrder ( ) ] ) ;
st . books = got [ 0 ] . books || [ ] ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
cached ( 'books' , st . books ) ;
} catch ( e ) {
if ( ! st . books ) st . books = [ ] ;
}
renderShelf ( ) ;
}
async function loadDetail ( feedId ) {
if ( ! st . detail || st . detail . book . feed _id !== feedId ) st . detail = cached ( 'book_' + feedId ) ;
renderView ( ) ;
try {
const d = await api ( '/podcasts/audiobooks/' + feedId + '/' ) ;
if ( st . open !== feedId ) return d ;
st . detail = d ;
cached ( 'book_' + feedId , d ) ;
} catch ( e ) {
if ( ! st . detail ) toast ( 'Das Hörbuch braucht eine Verbindung.' ) ;
}
renderView ( ) ;
return st . detail ;
}
// ---- the shelf in "Bücher" ------------------------------------------------------------
function renderShelf ( ) {
const h = host ( ) ;
const books = st . books || [ ] ;
const state = h ? podcastState ( ) : null ;
const pods = state && state . feeds ? state . feeds . filter ( f => f . kind !== 'audiobook' ) : [ ] ;
Navigation: Lesen und Hören (Phase 1, noch nicht veröffentlicht)
Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit
Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts,
Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen
(DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je
Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter
ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben.
- Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books.
- Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“).
- Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann.
- Ein Link auf einen Abschnitt (#news …) schlägt die Startseite.
Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls
daraus; bis dahin bleibt das hier unveröffentlicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +02:00
if ( ! h ) { shelf . innerHTML = '' ; return ; }
if ( ! books . length && ! pods . length ) {
shelf . innerHTML = '<p class="ab-empty">Hörbücher und Hörspiele sind hier Podcasts, die wie ein Buch gehört ' +
'werden: Kapitel in Reihenfolge, Lesezeichen. Erst einen Podcast abonnieren (Podcasts → Suche), dann hier markieren.</p>' ;
return ;
}
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
let html = '' ;
const go = books . filter ( b => b . next && b . last _played _at ) [ 0 ] ;
if ( go ) {
html += '<div class="ab-continue" data-ab-open="' + go . feed _id + '">' +
cover ( go . artwork _url , go . title , 'ab-cover ab-cover-s' ) +
'<div class="ab-card-text"><div class="ab-label">Weiterhören</div>' +
'<div class="ab-title">' + esc ( go . title ) + '</div>' +
'<div class="ab-meta">' + esc ( progressText ( go ) ) + '</div>' +
'<div class="ab-bar"><span style="width:' + pct ( go ) + '%"></span></div></div>' +
'<button type="button" class="ab-play" data-ab-go="' + go . feed _id + '" aria-label="Weiterhören">▶</button></div>' ;
}
html += '<div class="ab-head"><h3 class="ab-h">Hörbücher</h3>' +
( pods . length ? '<button type="button" class="ab-link" data-ab="pick">+ Hörbuch</button>' : '' ) + '</div>' ;
if ( books . length ) {
2026-10-16 15:45:14 +02:00
html += gridHtml ( books ) ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
} else {
html += '<p class="ab-empty">Podcasts, die eigentlich Hörbücher oder Hörspiele sind, lassen sich hier ' +
'wie ein Buch hören: in Kapitelreihenfolge, mit Lesezeichen. „+ Hörbuch“ wählt einen aus.</p>' ;
}
if ( st . picking ) html += pickHtml ( pods ) ;
shelf . innerHTML = html ;
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
drawChips ( books ) ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
const input = shelf . querySelector ( '.ab-pick-q' ) ;
if ( input && document . activeElement !== input && st . pickFocus ) { input . focus ( ) ; st . pickFocus = false ; }
}
2026-10-16 15:45:14 +02:00
// ---- organising: folders (set per book) and series (found in the titles) ----------------
// "Das Kapital – Erster Band", "Harry Potter: Teil 3", "Die drei ??? – Folge 12":
// the series' name and the volume's number, or null.
const VOLUME _WORD = /\b(band|bd\.|teil|buch|folge|staffel|episode|vol\.?|volume|part|book)\b/i ;
const ORDINALS = [ 'erst' , 'zweit' , 'dritt' , 'viert' , 'fünft' , 'sechst' , 'siebt' , 'acht' , 'neunt' , 'zehnt' ,
'elft' , 'zwölft' ] ;
const ORDINALS _EN = [ 'first' , 'second' , 'third' , 'fourth' , 'fifth' , 'sixth' , 'seventh' , 'eighth' , 'ninth' , 'tenth' ] ;
const ROMAN = { i : 1 , ii : 2 , iii : 3 , iv : 4 , v : 5 , vi : 6 , vii : 7 , viii : 8 , ix : 9 , x : 10 , xi : 11 , xii : 12 } ;
function volumeNumber ( text ) {
const t = text . toLowerCase ( ) ;
const digits = /(\d+)/ . exec ( t ) ;
if ( digits ) return parseInt ( digits [ 1 ] , 10 ) ;
for ( const word of t . split ( /[^a-zäöüß]+/ ) ) {
if ( ROMAN [ word ] ) return ROMAN [ word ] ;
for ( let i = 0 ; i < ORDINALS . length ; i ++ ) if ( word . indexOf ( ORDINALS [ i ] ) === 0 ) return i + 1 ;
for ( let i = 0 ; i < ORDINALS _EN . length ; i ++ ) if ( word === ORDINALS _EN [ i ] ) return i + 1 ;
}
return 999 ;
}
function seriesOf ( b ) {
const m = /^(.+?)\s+[–—-]\s+(.+)$/ . exec ( b . title ) || /^(.+?):\s+(.+)$/ . exec ( b . title ) ||
/^(.+?)\.\s+(.+)$/ . exec ( b . title ) ; // "Der Zauberberg. Erster Band"
if ( ! m || ! ( VOLUME _WORD . test ( m [ 2 ] ) || /^\s*\d+\s*$/ . test ( m [ 2 ] ) ) ) return null ;
const name = m [ 1 ] . trim ( ) ;
return { key : ( name + '|' + ( b . author || '' ) ) . toLowerCase ( ) , name , number : volumeNumber ( m [ 2 ] ) , volume : m [ 2 ] . trim ( ) } ;
}
// The books of one level (all, or one folder) as cards: series of two or more volumes
// become one stack; folders come first (at the top level).
function entries ( books ) {
const groups = new Map ( ) ;
const single = [ ] ;
books . forEach ( b => {
const sr = seriesOf ( b ) ;
if ( ! sr ) { single . push ( { kind : 'book' , b , sort : b . title } ) ; return ; }
if ( ! groups . has ( sr . key ) ) groups . set ( sr . key , { kind : 'series' , key : sr . key , name : sr . name , books : [ ] , sort : sr . name } ) ;
groups . get ( sr . key ) . books . push ( Object . assign ( { } , b , { _volume : sr . volume , _number : sr . number } ) ) ;
} ) ;
const out = single ;
groups . forEach ( g => {
if ( g . books . length < 2 ) { out . push ( { kind : 'book' , b : g . books [ 0 ] , sort : g . books [ 0 ] . title } ) ; return ; }
g . books . sort ( ( x , y ) => x . _number - y . _number || x . title . localeCompare ( y . title ) ) ;
out . push ( g ) ;
} ) ;
// The last listened first (as the list comes), then the rest by name.
const recent = e => {
const list = e . kind === 'series' ? e . books : [ e . b ] ;
return list . map ( x => x . last _played _at || '' ) . sort ( ) . pop ( ) || '' ;
} ;
return out . sort ( ( x , y ) => ( recent ( y ) > recent ( x ) ? 1 : recent ( y ) < recent ( x ) ? - 1 : 0 ) ||
x . sort . localeCompare ( y . sort , 'de' ) ) ;
}
function bookCard ( b , label ) {
return '<button type="button" class="ab-card" data-ab-open="' + b . feed _id + '">' +
cover ( b . artwork _url , b . title , 'ab-cover' ) +
'<span class="ab-card-text"><span class="ab-title">' + esc ( label || b . title ) + '</span>' +
( b . author && ! label ? '<span class="ab-meta">' + esc ( b . author ) + '</span>' : '' ) +
'<span class="ab-meta">' + esc ( progressText ( b ) ) + '</span>' +
'<span class="ab-bar"><span style="width:' + pct ( b ) + '%"></span></span></span></button>' ;
}
function seriesCard ( g ) {
const total = g . books . reduce ( ( n , b ) => n + ( b . total _seconds || 0 ) , 0 ) ;
const heard = g . books . reduce ( ( n , b ) => n + ( b . heard _seconds || 0 ) , 0 ) ;
const going = g . books . filter ( b => b . next && b . last _played _at ) [ 0 ] ;
const done = g . books . filter ( b => ! b . next ) . length ;
const meta = g . books . length + ' Bände' + ( going ? ' · bei ' + going . _volume
: done === g . books . length ? ' · fertig gehört' : done ? ' · ' + done + ' gehört' : '' ) ;
return '<button type="button" class="ab-card ab-stack" data-ab-series="' + esc ( g . key ) + '">' +
'<span class="ab-stack-covers">' + cover ( g . books [ 0 ] . artwork _url , g . name , 'ab-cover' ) + '</span>' +
'<span class="ab-card-text"><span class="ab-title">' + esc ( g . name ) + '</span>' +
( g . books [ 0 ] . author ? '<span class="ab-meta">' + esc ( g . books [ 0 ] . author ) + '</span>' : '' ) +
'<span class="ab-meta">' + esc ( meta ) + '</span>' +
'<span class="ab-bar"><span style="width:' + ( total ? Math . round ( 100 * heard / total ) : 0 ) + '%"></span></span></span></button>' ;
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const foldersOf = b => ( b . folders && b . folders . length ? b . folders : ( b . folder ? [ b . folder ] : [ ] ) ) ;
// The audiobooks' folders as chips (docs/library.md), in the podcasts' folder order.
function folderChips ( books ) {
const counts = new Map ( ) ;
let none = 0 ;
books . forEach ( b => {
const fs = foldersOf ( b ) ;
if ( ! fs . length ) none ++ ;
fs . forEach ( f => counts . set ( f , ( counts . get ( f ) || 0 ) + 1 ) ) ;
} ) ;
const rank = n => { const i = st . folderOrder . indexOf ( n ) ; return i < 0 ? 1e6 : i ; } ;
const names = Array . from ( counts . keys ( ) ) . sort ( ( a , b ) => rank ( a ) - rank ( b ) || a . localeCompare ( b , 'de' ) ) ;
if ( st . folder && ! counts . has ( st . folder ) ) st . folder = null ;
return { folders : names . map ( n => ( { name : n , count : counts . get ( n ) } ) ) , none : names . length ? none : 0 } ;
}
2026-10-16 15:45:14 +02:00
function gridHtml ( books ) {
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const here = books . filter ( b => st . folder == null ? true
: st . folder === '' ? ! foldersOf ( b ) . length : foldersOf ( b ) . indexOf ( st . folder ) >= 0 ) ;
2026-10-16 15:45:14 +02:00
const list = entries ( here ) ;
if ( st . series != null ) {
const g = list . filter ( e => e . kind === 'series' && e . key === st . series ) [ 0 ] ;
if ( g ) {
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
return '<div class="ab-place"><button type="button" class="ab-back" data-ab="up">‹ Hörbücher</button>' +
'<strong>' + esc ( g . name ) + '</strong></div>' +
2026-10-16 15:45:14 +02:00
'<div class="ab-grid">' + g . books . map ( b => bookCard ( b , b . _volume ) ) . join ( '' ) + '</div>' ;
}
st . series = null ;
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
return '<div class="ab-chips"></div><div class="ab-grid">' +
list . map ( e => e . kind === 'series' ? seriesCard ( e ) : bookCard ( e . b ) ) . join ( '' ) + '</div>' ;
}
function drawChips ( books ) {
const el = shelf . querySelector ( '.ab-chips' ) ;
if ( ! el || ! window . DioraFolders ) return ;
const c = folderChips ( books ) ;
if ( ! c . folders . length ) { el . innerHTML = '' ; return ; }
window . DioraFolders . chips ( el , {
folders : c . folders , active : st . folder , none : c . none ,
onPick ( name ) { st . folder = name ; renderShelf ( ) ; } ,
onManage : manageFolders ,
} ) ;
2026-10-16 15:45:14 +02:00
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
async function loadFolderOrder ( ) {
try { st . folderOrder = ( await api ( '/podcasts/folders/' ) ) . order || [ ] ; } catch ( e ) { }
}
// All folder names of the podcasts and audiobooks, in their order.
async function allFolderNames ( ) {
2026-10-16 15:45:14 +02:00
try {
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
const d = await api ( '/podcasts/folders/' ) ;
st . folderOrder = d . order || [ ] ;
return st . folderOrder ;
2026-10-16 15:45:14 +02:00
} catch ( e ) {
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
return Array . from ( new Set ( [ ] . concat . apply ( [ ] , ( st . books || [ ] ) . map ( foldersOf ) ) ) ) ;
2026-10-16 15:45:14 +02:00
}
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
}
function manageFolders ( ) {
if ( ! window . DioraFolders ) return ;
allFolderNames ( ) . then ( names => window . DioraFolders . manage ( {
title : 'Ordner der Podcasts und Hörbücher' ,
folders : names ,
onRename : ( old , n ) => api ( '/podcasts/folders/rename/' , { old , new : n } ) . then ( loadList ) . catch ( ( ) => toast ( 'Das braucht eine Verbindung.' ) ) ,
onDelete : name => api ( '/podcasts/folders/delete/' , { name } ) . then ( loadList ) . catch ( ( ) => toast ( 'Das braucht eine Verbindung.' ) ) ,
onOrder : order => api ( '/podcasts/folders/' , { order } ) . then ( d => { st . folderOrder = d . order || order ; renderShelf ( ) ; } ) . catch ( ( ) => { } ) ,
} ) ) ;
}
// Into folders (any number): the server keeps them with the subscription.
async function assignFolder ( feedId ) {
if ( ! window . DioraFolders ) return ;
const b = ( st . books || [ ] ) . filter ( x => x . feed _id === feedId ) [ 0 ] ;
const names = await allFolderNames ( ) ;
window . DioraFolders . assign ( {
title : 'Ordner für „' + ( b ? b . title : 'Hörbuch' ) + '“' ,
folders : names ,
selected : b ? foldersOf ( b ) : [ ] ,
async onDone ( picked ) {
try {
await api ( '/podcasts/feeds/' + feedId + '/settings/' , { folders : picked } ) ;
toast ( picked . length ? 'In ' + picked . map ( n => '„' + n + '“' ) . join ( ', ' ) : 'Ohne Ordner' ) ;
} catch ( e ) {
toast ( 'Das braucht eine Verbindung.' ) ;
}
loadList ( ) ;
if ( st . open === feedId ) loadDetail ( feedId ) ;
} ,
} ) ;
2026-10-16 15:45:14 +02:00
}
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
function pickHtml ( pods ) {
const q = st . pickQuery . trim ( ) . toLowerCase ( ) ;
const found = pods . filter ( f => ! q || ( f . title + ' ' + ( f . author || '' ) ) . toLowerCase ( ) . indexOf ( q ) >= 0 )
. sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) . slice ( 0 , 60 ) ;
return '<div class="ab-pick"><div class="ab-pick-head"><strong>Welcher Podcast ist ein Hörbuch?</strong>' +
'<button type="button" class="ab-icon" data-ab="pick-close" aria-label="Schließen">✕</button></div>' +
'<input type="search" class="ab-pick-q" placeholder="Abo suchen …" value="' + esc ( st . pickQuery ) + '">' +
'<div class="ab-pick-list">' + found . map ( f =>
'<button type="button" class="ab-row" data-ab-mark="' + f . id + '">' + cover ( f . artwork _url , f . title , 'ab-cover ab-cover-xs' ) +
'<span class="ab-card-text"><span class="ab-title">' + esc ( f . title ) + '</span>' +
( f . author ? '<span class="ab-meta">' + esc ( f . author ) + '</span>' : '' ) + '</span></button>' ) . join ( '' ) +
( found . length ? '' : '<p class="ab-empty">Kein Abo gefunden.</p>' ) + '</div></div>' ;
}
function setKind ( feedId , kind ) {
const h = host ( ) ;
if ( ! h ) return ;
h . podcastFeedSettings ( feedId , JSON . stringify ( { kind } ) ) ;
// The host sends it on its own; the books are asked again once the server has it.
setTimeout ( loadList , 900 ) ;
setTimeout ( loadList , 3500 ) ;
}
// ---- the book --------------------------------------------------------------------------
let view = null ;
function openBook ( feedId ) {
st . open = feedId ;
if ( ! view ) {
view = document . createElement ( 'div' ) ;
view . id = 'ab-view' ;
view . addEventListener ( 'click' , onViewClick ) ;
document . body . appendChild ( view ) ;
}
view . style . display = '' ;
document . documentElement . classList . add ( 'ab-open' ) ;
view . scrollTop = 0 ;
poll ( ) ;
loadDetail ( feedId ) ;
}
function closeBook ( ) {
st . open = null ;
if ( view ) view . style . display = 'none' ;
document . documentElement . classList . remove ( 'ab-open' ) ;
loadList ( ) ;
}
function renderView ( ) {
if ( ! view || st . open == null ) return ;
const d = st . detail && st . detail . book . feed _id === st . open ? st . detail : null ;
const fromList = ( st . books || [ ] ) . filter ( b => b . feed _id === st . open ) [ 0 ] ;
const b = d ? d . book : fromList ;
if ( ! b ) { view . innerHTML = '<div class="ab-top"><button type="button" class="ab-back" data-ab="close">‹ Bücher</button></div><p class="ab-empty">Wird geladen …</p>' ; return ; }
const now = nowInBook ( ) ;
const marks = d ? d . bookmarks : [ ] ;
2026-10-17 10:16:39 +02:00
const dlMap = d ? downloads ( ) : null ;
const dl = dlMap ? bookDownloads ( d , dlMap ) : null ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
const titleOf = id => {
const ch = d ? d . chapters . filter ( c => c . episode _ _id === id ) [ 0 ] : null ;
return ch ? 'Kapitel ' + ( ch . index + 1 ) : '' ;
} ;
view . innerHTML =
'<div class="ab-top"><button type="button" class="ab-back" data-ab="close">‹ Bücher</button>' +
'<button type="button" class="ab-icon" data-ab="menu" aria-label="Optionen">⋮</button></div>' +
'<div class="ab-menu" hidden>' +
'<button type="button" data-ab="unplayed">Alles als ungehört markieren</button>' +
'<button type="button" data-ab="played">Alles als gehört markieren</button>' +
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'<button type="button" data-ab="folder">In Ordner …</button>' +
2026-10-17 10:16:39 +02:00
( dl && dl . held ? '<button type="button" data-ab="dl-remove">Vom Gerät löschen (' + dl . held + ' Kapitel)</button>' : '' ) +
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
'<button type="button" data-ab="podcast">Wieder als Podcast führen</button></div>' +
'<div class="ab-hero">' + cover ( b . artwork _url , b . title , 'ab-cover ab-cover-l' ) +
'<div class="ab-hero-text"><h2 class="ab-book-title">' + esc ( b . title ) + '</h2>' +
( b . author ? '<div class="ab-meta">' + esc ( b . author ) + '</div>' : '' ) +
'<div class="ab-meta ab-progress">' + esc ( progressText ( b ) ) + '</div>' +
'<div class="ab-bar"><span style="width:' + pct ( b ) + '%"></span></div></div></div>' +
'<div class="ab-actions">' +
'<button type="button" class="ab-btn ab-primary" data-ab="go">' + goLabel ( b , now ) + '</button>' +
'<button type="button" class="ab-btn" data-ab="sleep"' + ( now ? '' : ' disabled' ) + '>☾ Kapitelende</button>' +
2026-10-17 10:16:39 +02:00
'<button type="button" class="ab-btn" data-ab="mark"' + ( now ? '' : ' disabled' ) + '>+ Lesezeichen</button>' +
( dl ? downloadButton ( dl ) : '' ) + '</div>' +
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
( marks . length ? '<h3 class="ab-h">Lesezeichen</h3><div class="ab-marks">' + marks . map ( m =>
'<div class="ab-mark"><button type="button" class="ab-row" data-ab-bm="' + m . id + '">' +
'<span class="ab-card-text"><span class="ab-meta">' + esc ( titleOf ( m . episode _id ) ) + ' · ' + clock ( m . position _ms ) + '</span>' +
'<span class="ab-title">' + ( m . note ? esc ( m . note ) : '<span class="ab-meta">ohne Notiz</span>' ) + '</span></span></button>' +
'<button type="button" class="ab-icon" data-ab-bm-edit="' + m . id + '" aria-label="Notiz ändern">✎</button>' +
'<button type="button" class="ab-icon" data-ab-bm-del="' + m . id + '" aria-label="Löschen">✕</button></div>' ) . join ( '' ) + '</div>' : '' ) +
'<h3 class="ab-h">Kapitel</h3>' +
2026-10-17 10:16:39 +02:00
( d ? '<div class="ab-chapters">' + d . chapters . map ( c => chapterHtml ( c , now , dlMap ) ) . join ( '' ) + '</div>'
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
: '<p class="ab-empty">Wird geladen …</p>' ) ;
}
function goLabel ( b , now ) {
if ( now && now . playing ) return '❚❚ Pause' ;
if ( ! b . next ) return '↺ Von vorn' ;
return '▶ ' + ( b . done || b . heard _seconds > 0 ? 'Weiterhören' : 'Anfangen' ) ;
}
2026-10-17 10:16:39 +02:00
// ---- downloads (the Android app: chapters on the device, for listening without a connection) ----
function dlHost ( ) {
const h = host ( ) ;
return h && typeof h . podcastDownload === 'function' && typeof h . podcastDownloads === 'function' ? h : null ;
}
// The host's downloads: {episodeId: {state: queued|downloading|done|failed, progress …}}, or null.
function downloads ( ) {
const h = dlHost ( ) ;
if ( ! h ) return null ;
try { return JSON . parse ( h . podcastDownloads ( ) ) || { } ; } catch ( e ) { return { } ; }
}
function bookDownloads ( d , map ) {
const n = { total : d . chapters . length , done : 0 , busy : 0 , failed : 0 , held : 0 } ;
d . chapters . forEach ( c => {
const e = map [ c . episode _ _id ] ;
if ( ! e ) return ;
n . held ++ ;
if ( e . state === 'done' ) n . done ++ ;
else if ( e . state === 'failed' ) n . failed ++ ;
else n . busy ++ ;
} ) ;
return n ;
}
function downloadButton ( n ) {
if ( n . done === n . total ) return '<button type="button" class="ab-btn ab-dl-on" data-ab="dl-info">' + DL _DONE + ' Auf dem Gerät</button>' ;
if ( n . busy ) return '<button type="button" class="ab-btn" data-ab="dl-info">↓ Lädt … ' + n . done + '/' + n . total + '</button>' ;
return '<button type="button" class="ab-btn" data-ab="dl-all">↓ Herunterladen' + ( n . done ? ' (noch ' + ( n . total - n . done ) + ')' : '' ) + '</button>' ;
}
// Arrow into a tray; on the device: the same, white in a full circle (not a ✓: that's "heard").
const DL _ICON = '<svg class="ab-dl-ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 10.5l5 5 5-5M5 19.5h14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="square"/></svg>' ;
const DL _DONE = '<svg class="ab-dl-ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor"/><path d="M12 6v8M8.5 10.5l3.5 3.5 3.5-3.5M7.5 17.5h9" fill="none" stroke="var(--bg, #fff)" stroke-width="2" stroke-linecap="square"/></svg>' ;
function chapterDownload ( c , map ) {
const e = map [ c . episode _ _id ] ;
let sign = DL _ICON ;
let label = 'Kapitel herunterladen' ;
let cls = '' ;
if ( e && e . state === 'done' ) { sign = DL _DONE ; label = 'Auf dem Gerät – tippen: löschen' ; cls = ' ab-dl-on' ; }
else if ( e && e . state === 'failed' ) { sign = '!' ; label = 'Fehlgeschlagen – tippen: erneut versuchen' ; }
else if ( e && e . state === 'downloading' ) { sign = Math . round ( ( e . progress || 0 ) * 100 ) + '%' ; label = 'Lädt' ; cls = ' ab-dl-busy' ; }
else if ( e ) { sign = '…' ; label = 'Wartet auf den Download' ; cls = ' ab-dl-busy' ; }
return '<button type="button" class="ab-icon ab-dl' + cls + '" data-ab-dl="' + c . index + '" aria-label="' + label + '" title="' + label + '">' + sign + '</button>' ;
}
// All chapters not yet on the device, from the one that comes next on (then the ones before).
function downloadAll ( ) {
const h = dlHost ( ) ;
const d = st . detail ;
if ( ! h || ! d ) return ;
const map = downloads ( ) || { } ;
const from = d . book . next ? d . book . next . index : 0 ;
const order = d . chapters . slice ( from ) . concat ( d . chapters . slice ( 0 , from ) ) ;
let n = 0 ;
order . forEach ( c => {
const e = map [ c . episode _ _id ] ;
if ( e && e . state !== 'failed' ) return ;
if ( e && typeof h . podcastRetryDownload === 'function' ) h . podcastRetryDownload ( c . episode _ _id ) ;
else h . podcastDownload ( JSON . stringify ( c ) ) ;
n ++ ;
} ) ;
toast ( n ? n + ( n === 1 ? ' Kapitel wird' : ' Kapitel werden' ) + ' heruntergeladen' : 'Schon alles auf dem Gerät' ) ;
setTimeout ( renderView , 300 ) ;
}
async function removeDownloads ( ) {
const h = dlHost ( ) ;
const d = st . detail ;
if ( ! h || ! d || typeof h . podcastDeleteDownload !== 'function' ) return ;
const ok = typeof customConfirm === 'function'
? await customConfirm ( 'Die Kapitel von „' + d . book . title + '“ vom Gerät löschen? Stellen und Lesezeichen bleiben; abspielen geht dann nur mit Verbindung.' , { danger : true } )
: true ;
if ( ! ok ) return ;
const map = downloads ( ) || { } ;
d . chapters . forEach ( c => { if ( map [ c . episode _ _id ] ) h . podcastDeleteDownload ( c . episode _ _id ) ; } ) ;
toast ( 'Vom Gerät gelöscht' ) ;
setTimeout ( renderView , 300 ) ;
}
async function chapterDownloadTap ( index ) {
const h = dlHost ( ) ;
const d = st . detail ;
const c = d && d . chapters [ index ] ;
if ( ! h || ! c ) return ;
const e = ( downloads ( ) || { } ) [ c . episode _ _id ] ;
if ( ! e ) h . podcastDownload ( JSON . stringify ( c ) ) ;
else if ( e . state === 'failed' && typeof h . podcastRetryDownload === 'function' ) h . podcastRetryDownload ( c . episode _ _id ) ;
else if ( e . state === 'done' && typeof h . podcastDeleteDownload === 'function' ) {
const ok = typeof customConfirm === 'function' ? await customConfirm ( 'Kapitel ' + ( index + 1 ) + ' vom Gerät löschen?' ) : true ;
if ( ok ) h . podcastDeleteDownload ( c . episode _ _id ) ;
}
setTimeout ( renderView , 300 ) ;
}
function chapterHtml ( c , now , dlMap ) {
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
const playing = now && now . index === c . index ;
const dur = c . episode _ _duration _seconds || 0 ;
let meta ;
if ( c . played ) meta = '✓ gehört' ;
else if ( playing && now . dur ) meta = clock ( now . pos ) + ' / ' + clock ( now . dur ) ;
else if ( c . position _seconds > 0 && dur ) meta = 'noch ' + hours ( dur - c . position _seconds ) ;
else meta = dur ? hours ( dur ) : '' ;
2026-10-16 14:08:44 +02:00
return '<div class="ab-chrow"><button type="button" class="ab-row ab-chapter' + ( c . played ? ' ab-done' : '' ) + ( playing ? ' ab-now' : '' ) +
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
'" data-ab-ch="' + c . index + '"><span class="ab-num">' + ( playing ? '♪' : ( c . index + 1 ) ) + '</span>' +
'<span class="ab-card-text"><span class="ab-title">' + esc ( c . episode _ _title ) + '</span>' +
2026-10-16 14:08:44 +02:00
'<span class="ab-meta">' + esc ( meta ) + '</span></span></button>' +
'<button type="button" class="ab-icon ab-tick' + ( c . played ? ' ab-ticked' : '' ) + '" data-ab-played="' + c . index + '" ' +
'aria-label="' + ( c . played ? 'Als nicht gehört markieren' : 'Als gehört markieren' ) + '" ' +
2026-10-17 10:16:39 +02:00
'title="' + ( c . played ? 'Als nicht gehört markieren' : 'Als gehört markieren' ) + '">' + ( c . played ? '✓' : '○' ) + '</button>' +
( dlMap ? chapterDownload ( c , dlMap ) : '' ) + '</div>' ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
}
// Play the open book's chapter `index` (from its own place, or from `ms`).
function play ( index , ms ) {
const h = host ( ) ;
const d = st . detail ;
if ( ! h || ! d || ! d . chapters [ index ] ) return ;
const ch = d . chapters [ index ] ;
const body = { feed _id : d . book . feed _id , chapters : d . chapters , index } ;
2026-10-16 14:08:44 +02:00
const dur = ch . episode _ _duration _seconds || 0 ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
if ( ms != null ) body . position _ms = ms ;
2026-10-16 14:08:44 +02:00
else if ( ch . played || ( dur && ch . position _seconds >= dur - 30 ) ) body . position _ms = 0 ; // heard already: from its start
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
h . podcastPlayBook ( JSON . stringify ( body ) ) ;
setTimeout ( poll , 400 ) ;
}
async function goOn ( feedId ) {
if ( st . open !== feedId || ! st . detail || st . detail . book . feed _id !== feedId ) {
st . open = feedId ;
await loadDetail ( feedId ) ;
if ( ! view || view . style . display === 'none' ) st . open = null ;
}
const d = st . detail ;
if ( ! d || d . book . feed _id !== feedId ) return ;
const b = d . book ;
play ( b . next ? b . next . index : 0 , b . next ? undefined : 0 ) ;
}
function togglePlay ( ) {
const now = nowInBook ( ) ;
const h = host ( ) ;
if ( now && now . playing && h ) {
if ( typeof h . podcastToggle === 'function' ) h . podcastToggle ( ) ;
else if ( typeof h . podcastPause === 'function' ) h . podcastPause ( ) ;
setTimeout ( poll , 300 ) ;
return ;
}
if ( now && h ) { play ( now . index , now . pos ) ; return ; }
goOn ( st . open ) ;
}
// The sleep timer to the end of the chapter: the episode's own chapter mark if it has
// some (a long file), else the end of the episode.
async function sleepAtChapterEnd ( ) {
const h = host ( ) ;
const now = nowInBook ( ) ;
if ( ! h || ! now ) return ;
const id = st . detail . chapters [ now . index ] . episode _ _id ;
let marks = [ ] ;
try { marks = ( await api ( '/podcasts/episodes/' + id + '/' ) ) . chapters || [ ] ; } catch ( e ) { }
const pos = ( playerState ( ) || { } ) . pos || now . pos ;
const next = marks . map ( m => m . start * 1000 ) . filter ( s => s > pos + 1000 ) . sort ( ( a , b ) => a - b ) [ 0 ] ;
if ( next ) {
const speed = ( st . now && st . now . speed ) || 1 ;
h . podcastSleep ( ( next - pos ) / speed / 60000 ) ;
toast ( 'Schlummer: in ' + clock ( ( next - pos ) / speed ) + ', am Kapitelende' ) ;
} else {
h . podcastSleep ( - 1 ) ;
toast ( 'Schlummer: am Ende dieses Kapitels' ) ;
}
}
async function addBookmark ( ) {
const now = nowInBook ( ) ;
if ( ! now ) return ;
const pos = ( playerState ( ) || { } ) . pos || now . pos ;
const ch = st . detail . chapters [ now . index ] ;
const note = typeof customPrompt === 'function'
? await customPrompt ( 'Lesezeichen bei Kapitel ' + ( now . index + 1 ) + ', ' + clock ( pos ) + ' – Notiz (optional):' , '' )
: window . prompt ( 'Notiz (optional):' , '' ) ;
if ( note === null || note === undefined ) return ;
try {
await api ( '/podcasts/bookmarks/' , { episode _id : ch . episode _ _id , position _ms : Math . round ( pos ) , note : String ( note ) } ) ;
toast ( 'Lesezeichen gesetzt' ) ;
} catch ( e ) {
toast ( 'Lesezeichen brauchen eine Verbindung.' ) ;
}
loadDetail ( st . open ) ;
}
async function editBookmark ( id ) {
const m = st . detail . bookmarks . filter ( x => x . id === id ) [ 0 ] ;
if ( ! m ) return ;
const note = typeof customPrompt === 'function' ? await customPrompt ( 'Notiz:' , m . note ) : window . prompt ( 'Notiz:' , m . note ) ;
if ( note === null || note === undefined ) return ;
try { await api ( '/podcasts/bookmarks/' + id + '/' , { note : String ( note ) } ) ; } catch ( e ) { toast ( 'Braucht eine Verbindung.' ) ; }
loadDetail ( st . open ) ;
}
async function deleteBookmark ( id ) {
const ok = typeof customConfirm === 'function' ? await customConfirm ( 'Lesezeichen löschen?' ) : window . confirm ( 'Lesezeichen löschen?' ) ;
if ( ! ok ) return ;
try { await api ( '/podcasts/bookmarks/' + id + '/delete/' , { } ) ; } catch ( e ) { toast ( 'Braucht eine Verbindung.' ) ; }
loadDetail ( st . open ) ;
}
function playBookmark ( id ) {
const m = st . detail . bookmarks . filter ( x => x . id === id ) [ 0 ] ;
if ( ! m ) return ;
const ch = st . detail . chapters . filter ( c => c . episode _ _id === m . episode _id ) [ 0 ] ;
if ( ch ) play ( ch . index , m . position _ms ) ;
}
2026-10-16 14:08:44 +02:00
// One chapter heard or not: at once here, the host tells the server.
function markChapter ( index ) {
const h = host ( ) ;
const d = st . detail ;
const ch = d && d . chapters [ index ] ;
if ( ! h || ! ch ) return ;
ch . played = ! ch . played ;
const dur = ch . episode _ _duration _seconds || 0 ;
if ( ! ch . played && dur && ch . position _seconds >= dur - 30 ) ch . position _seconds = 0 ; // from the start again
h . podcastMarkPlayedAll ( JSON . stringify ( [ ch . episode _ _id ] ) , ch . played ) ;
renderView ( ) ;
setTimeout ( ( ) => loadDetail ( st . open ) , 1500 ) ;
}
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
function markAll ( played ) {
const h = host ( ) ;
const d = st . detail ;
if ( ! h || ! d ) return ;
h . podcastMarkPlayedAll ( JSON . stringify ( d . chapters . map ( c => c . episode _ _id ) ) , ! ! played ) ;
setTimeout ( ( ) => loadDetail ( st . open ) , 900 ) ;
}
async function onViewClick ( e ) {
const t = e . target . closest ( 'button' ) ;
const menu = view . querySelector ( '.ab-menu' ) ;
if ( menu && ( ! t || t . dataset . ab !== 'menu' ) ) menu . hidden = true ;
if ( ! t ) return ;
const act = t . dataset . ab ;
if ( act === 'close' ) closeBook ( ) ;
else if ( act === 'menu' ) { if ( menu ) menu . hidden = ! menu . hidden ; }
else if ( act === 'go' ) togglePlay ( ) ;
else if ( act === 'sleep' ) sleepAtChapterEnd ( ) ;
else if ( act === 'mark' ) addBookmark ( ) ;
else if ( act === 'unplayed' ) markAll ( false ) ;
else if ( act === 'played' ) markAll ( true ) ;
2026-10-16 15:45:14 +02:00
else if ( act === 'folder' ) assignFolder ( st . open ) ;
2026-10-17 10:16:39 +02:00
else if ( act === 'dl-all' ) downloadAll ( ) ;
else if ( act === 'dl-remove' ) removeDownloads ( ) ;
else if ( act === 'dl-info' ) {
const n = bookDownloads ( st . detail , downloads ( ) || { } ) ;
toast ( n . done === n . total ? 'Alle ' + n . total + ' Kapitel sind auf dem Gerät (löschen: ⋮ oben)' : n . done + ' von ' + n . total + ' Kapiteln sind da, der Rest lädt' ) ;
}
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
else if ( act === 'podcast' ) {
const ok = typeof customConfirm === 'function'
? await customConfirm ( '„' + st . detail . book . title + '“ wieder als Podcast führen? Es verlässt die Bücher; Stellen und Lesezeichen bleiben.' )
: true ;
if ( ! ok ) return ;
setKind ( st . open , 'podcast' ) ;
closeBook ( ) ;
}
2026-10-16 14:08:44 +02:00
else if ( t . dataset . abPlayed != null ) markChapter ( Number ( t . dataset . abPlayed ) ) ;
2026-10-17 10:16:39 +02:00
else if ( t . dataset . abDl != null ) chapterDownloadTap ( Number ( t . dataset . abDl ) ) ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
else if ( t . dataset . abCh != null ) play ( Number ( t . dataset . abCh ) ) ;
else if ( t . dataset . abBm != null ) playBookmark ( Number ( t . dataset . abBm ) ) ;
else if ( t . dataset . abBmEdit != null ) editBookmark ( Number ( t . dataset . abBmEdit ) ) ;
else if ( t . dataset . abBmDel != null ) deleteBookmark ( Number ( t . dataset . abBmDel ) ) ;
}
shelf . addEventListener ( 'click' , e => {
const t = e . target . closest ( 'button, .ab-continue' ) ;
if ( ! t ) return ;
if ( t . dataset . abGo != null ) { e . stopPropagation ( ) ; goOn ( Number ( t . dataset . abGo ) ) ; return ; }
if ( t . dataset . abOpen != null ) { openBook ( Number ( t . dataset . abOpen ) ) ; return ; }
2026-10-16 15:45:14 +02:00
if ( t . dataset . abSeries != null ) { st . series = t . dataset . abSeries ; renderShelf ( ) ; return ; }
if ( t . dataset . ab === 'up' ) {
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
st . series = null ;
2026-10-16 15:45:14 +02:00
renderShelf ( ) ;
return ;
}
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
if ( t . dataset . abMark != null ) {
const id = Number ( t . dataset . abMark ) ;
st . picking = false ;
st . pickQuery = '' ;
// At once on the shelf, before the server has it: a card that fills in when it answers.
const f = ( ( podcastState ( ) || { } ) . feeds || [ ] ) . filter ( x => x . id === id ) [ 0 ] ;
if ( f ) {
st . books = [ { feed _id : id , title : f . title , author : f . author || '' , artwork _url : f . artwork _url || '' ,
chapters : f . episode _count || 0 , done : 0 , total _seconds : 0 , heard _seconds : 0 , next : null , last _played _at : null } ]
. concat ( ( st . books || [ ] ) . filter ( b => b . feed _id !== id ) ) ;
}
setKind ( id , 'audiobook' ) ;
renderShelf ( ) ;
toast ( '„' + ( f ? f . title : 'Podcast' ) + '“ ist jetzt ein Hörbuch' ) ;
return ;
}
if ( t . dataset . ab === 'pick' ) { st . picking = ! st . picking ; st . pickFocus = st . picking ; renderShelf ( ) ; return ; }
if ( t . dataset . ab === 'pick-close' ) { st . picking = false ; renderShelf ( ) ; }
} ) ;
shelf . addEventListener ( 'input' , e => {
if ( ! e . target . classList . contains ( 'ab-pick-q' ) ) return ;
st . pickQuery = e . target . value ;
const box = shelf . querySelector ( '.ab-pick-list' ) ;
const pods = ( ( podcastState ( ) || { } ) . feeds || [ ] ) . filter ( f => f . kind !== 'audiobook' ) ;
const tmp = document . createElement ( 'div' ) ;
tmp . innerHTML = pickHtml ( pods ) ;
const fresh = tmp . querySelector ( '.ab-pick-list' ) ;
if ( box && fresh ) box . innerHTML = fresh . innerHTML ;
} ) ;
2026-10-16 14:42:20 +02:00
// Home's "Weiterhören" card (home.js) plays the book from where it was left.
window . _ _dioraAudiobookGo = feedId => goOn ( feedId ) ;
Abos & Quellen: alle Abos an einem Ort (Navigation Phase 2)
Ein neuer Abschnitt (docs/sources.md, static/js/sources.js) für Podcasts,
YouTube-Kanäle, Hörbücher, Nachrichten und Blogs:
- ein Feld zum Hinzufügen: Adresse (Feed, Seite, YouTube) oder Name eines
Podcasts; die Art ist wählbar (Podcast, Hörbuch, Nachrichten, Blog);
/feeds/discover/ erkennt dafür Podcasts (Folgen mit Ton oder Video);
- eine Liste aller Abos mit Filtern (auch „Probleme“) und Suche;
- je Abo: Inhalt öffnen, Einstellungen (die vorhandenen Editoren), Art ändern,
abbestellen; OPML-Import und -Export.
In der Seitenleiste unten, auf dem Telefon im App-Menü; ⚙ in den Feeds führt
hierher. Abschnitte, die beim Laden schon gezeigt werden, füllen sich jetzt
auch (nav.js zeigt sie nach dem Laden aller Skripte noch einmal).
Podcast-UI aus diora-android 50c6f9c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:23:12 +02:00
// Abos & Quellen (sources.js): open a book.
window . _ _dioraAudiobookOpen = feedId => openBook ( Number ( feedId ) ) ;
2026-10-16 14:42:20 +02:00
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
// ---- following the player ------------------------------------------------------------
let pollTimer = 0 ;
let lastEpisode = null ;
function poll ( ) {
clearTimeout ( pollTimer ) ;
const p = playerState ( ) ;
st . now = p ;
const ep = p && p . kind === 'podcast' ? p . episodeId : null ;
if ( st . open != null && view && view . style . display !== 'none' ) {
if ( ep !== lastEpisode && lastEpisode != null ) loadDetail ( st . open ) ; // a chapter ended: progress moved
else renderView ( ) ;
pollTimer = setTimeout ( poll , 1000 ) ;
} else if ( ep !== lastEpisode && lastEpisode != null && tab . style . display !== 'none' ) {
loadList ( ) ;
}
lastEpisode = ep ;
if ( st . open == null ) pollTimer = setTimeout ( poll , 5000 ) ;
}
// ---- when to look ------------------------------------------------------------------------
const showTab = window . showTab ;
if ( typeof showTab === 'function' ) {
window . showTab = function ( name ) {
const r = showTab . apply ( this , arguments ) ;
Navigation: Lesen und Hören (Phase 1, noch nicht veröffentlicht)
Die Bereiche nach dem, was man tut (docs/navigation.md): Home; Lesen mit
Bücher, Zeitschriften, Nachrichten, Blogs; Hören mit Hörbücher, Podcasts,
Radio, Ambient. static/js/nav.js ist die eine Definition für alle Plattformen
(DioraNav: Abschnitte, Gruppen, go, Hash, zuletzt gewählter Abschnitt je
Gruppe). Die Webseite zeichnet ihre Leiste daraus: breit alle Abschnitte unter
ihrer Gruppe, schmal Home · Lesen · Hören mit einer Abschnittszeile oben.
- Hörbücher haben ein eigenes Panel (#tab-audiobooks), nicht mehr über den E-Books.
- Zeitschriften: die Bücher im Ordner „Zeitschriften“ (oder „Magazine“).
- Nachrichten/Blogs wählt die Navigation; der eigene Umschalter verschwindet dann.
- Ein Link auf einen Abschnitt (#news …) schlägt die Startseite.
Die Apps zeichnen ihre Leiste (native-radio.js, diora-android) bald ebenfalls
daraus; bis dahin bleibt das hier unveröffentlicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +02:00
if ( name === 'audiobooks' ) loadList ( ) ;
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
else if ( st . open != null ) closeBook ( ) ;
return r ;
} ;
}
const back = window . _ _dioraBack ;
window . _ _dioraBack = function ( ) {
if ( st . open != null ) {
const menu = view && view . querySelector ( '.ab-menu' ) ;
if ( menu && ! menu . hidden ) { menu . hidden = true ; return true ; }
closeBook ( ) ;
return true ;
}
if ( st . picking && tab . style . display !== 'none' ) { st . picking = false ; renderShelf ( ) ; return true ; }
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
if ( st . series != null && tab . style . display !== 'none' ) {
st . series = null ;
2026-10-16 15:45:14 +02:00
renderShelf ( ) ;
return true ;
}
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
return back ? back ( ) : false ;
} ;
document . addEventListener ( 'keydown' , e => {
if ( e . key === 'Escape' && st . open != null ) closeBook ( ) ;
} ) ;
document . addEventListener ( 'visibilitychange' , ( ) => {
if ( ! document . hidden && tab . style . display !== 'none' ) loadList ( ) ;
} ) ;
// The podcast host fills its state a moment after the page; the shelf needs its abos.
setTimeout ( loadList , 300 ) ;
setTimeout ( poll , 1500 ) ;
// ---- styles ----------------------------------------------------------------------------
const style = document . createElement ( 'style' ) ;
style . textContent = [
'#audiobook-shelf { margin: 0 0 14px; }' ,
'#audiobook-shelf:empty { display: none; }' ,
'.ab-h { margin: 18px 0 8px; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted, #9a9a9a); }' ,
'.ab-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }' ,
'.ab-link { background: none; border: none; color: var(--accent, #e63946); font: inherit; font-size: 14px; padding: 8px 0; cursor: pointer; }' ,
'.ab-empty { color: var(--muted, #9a9a9a); font-size: 13px; line-height: 1.45; margin: 6px 0; }' ,
'.ab-meta { display: block; color: var(--muted, #9a9a9a); font-size: 12px; line-height: 1.35; }' ,
'.ab-title { display: block; font-size: 14px; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; }' ,
'.ab-label { color: var(--accent, #e63946); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }' ,
'.ab-bar { display: block; height: 3px; border-radius: 2px; background: var(--border, #2a2a2a); margin-top: 6px; overflow: hidden; }' ,
'.ab-bar > span { display: block; height: 100%; background: var(--accent, #e63946); }' ,
'.ab-cover { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 6px; background: var(--panel, #161616); display: block; }' ,
'.ab-nocover { display: flex; align-items: center; justify-content: center; font-size: 28px; color: var(--muted, #9a9a9a); }' ,
'.ab-cover-s { width: 56px; height: 56px; flex: none; }' ,
'.ab-cover-xs { width: 40px; height: 40px; flex: none; font-size: 16px; }' ,
'.ab-cover-l { width: 120px; height: 120px; flex: none; }' ,
'.ab-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; text-align: left; }' ,
'.ab-continue { display: flex; align-items: center; gap: 12px; padding: 10px; margin: 4px 0 6px; border: 1px solid var(--border, #2a2a2a); border-radius: 8px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); cursor: pointer; }' ,
'.ab-play { flex: none; width: 48px; height: 48px; border-radius: 50%; border: none; background: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 18px; cursor: pointer; }' ,
'.ab-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 14px; }' ,
'.ab-card { display: flex; flex-direction: column; gap: 6px; padding: 0; background: none; border: none; color: inherit; font: inherit; cursor: pointer; text-align: left; min-width: 0; }' ,
'.ab-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; cursor: pointer; text-align: left; }' ,
'.ab-icon { background: none; border: none; color: inherit; font: inherit; font-size: 18px; min-width: 40px; min-height: 40px; cursor: pointer; }' ,
'.ab-pick { margin-top: 10px; border: 1px solid var(--border, #2a2a2a); border-radius: 8px; padding: 10px 12px; background: var(--card, #0b0b0b); }' ,
'.ab-pick-head { display: flex; align-items: center; justify-content: space-between; }' ,
'.ab-pick-q { width: 100%; box-sizing: border-box; margin: 6px 0; padding: 9px 10px; border-radius: 6px; border: 1px solid var(--border, #2a2a2a); background: var(--bg, #000); color: inherit; font: inherit; }' ,
'.ab-pick-list { max-height: 50vh; overflow-y: auto; }' ,
// Above the tab bar (150), below the page's dialogs (990): a note is asked for on top of it.
'#ab-view { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 900; overflow-y: auto; background: var(--bg, #000); color: var(--fg, #eee); padding: 8px 16px 120px; box-sizing: border-box; }' ,
'#ab-view > * { max-width: 760px; margin-left: auto; margin-right: auto; }' ,
'html.ab-open { overflow: hidden; }' ,
'.ab-top { display: flex; align-items: center; justify-content: space-between; position: relative; }' ,
'.ab-back { background: none; border: none; color: inherit; font: inherit; font-size: 15px; padding: 10px 0; cursor: pointer; }' ,
'.ab-menu { position: absolute; right: 16px; z-index: 2; display: flex; flex-direction: column; min-width: 240px; background: var(--raised, #222); border: 1px solid var(--border, #2a2a2a); border-radius: 8px; box-shadow: var(--shadow, 0 6px 24px rgba(0,0,0,.4)); }' ,
'#ab-view .ab-menu { margin: 0; left: auto; }' ,
'.ab-menu[hidden] { display: none; }' ,
'.ab-menu button { background: none; border: none; color: inherit; font: inherit; text-align: left; padding: 12px 14px; cursor: pointer; }' ,
'.ab-menu button:hover, .ab-row:hover { background: var(--hover, rgba(255,255,255,0.05)); }' ,
'.ab-hero { display: flex; gap: 16px; align-items: center; margin: 6px auto 14px; }' ,
'.ab-hero-text { flex: 1; min-width: 0; }' ,
'.ab-book-title { margin: 0 0 4px; font-size: 19px; line-height: 1.3; }' ,
'.ab-progress { margin-top: 6px; }' ,
'.ab-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 6px; }' ,
'.ab-btn { min-height: 42px; padding: 0 14px; border-radius: 21px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 14px; cursor: pointer; }' ,
'.ab-btn[disabled] { opacity: 0.4; cursor: default; }' ,
'.ab-primary { background: var(--accent, #e63946); border-color: var(--accent, #e63946); color: var(--on-accent, #fff); min-width: 150px; }' ,
'.ab-num { flex: none; width: 28px; text-align: right; color: var(--muted, #9a9a9a); font-size: 13px; }' ,
'.ab-chapter.ab-done .ab-title { color: var(--muted, #9a9a9a); }' ,
'.ab-chapter.ab-now .ab-title, .ab-chapter.ab-now .ab-num { color: var(--accent, #e63946); }' ,
2026-10-16 14:08:44 +02:00
'.ab-mark, .ab-chrow { display: flex; align-items: center; }' ,
2026-10-16 15:45:14 +02:00
'.ab-place { display: flex; align-items: center; gap: 12px; margin: 0 0 10px; }' ,
'.ab-place .ab-back { padding: 6px 0; }' ,
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
'.ab-chips { margin: 0 0 10px; }' ,
2026-10-16 15:45:14 +02:00
'.ab-stack-covers { position: relative; display: block; }' ,
'.ab-stack-covers::before, .ab-stack-covers::after { content: ""; position: absolute; left: 6%; right: 6%; height: 100%; border-radius: 6px; border: 1px solid var(--border-strong, #444); background: var(--panel, #161616); }' ,
'.ab-stack-covers::before { top: -8px; left: 12%; right: 12%; }' ,
'.ab-stack-covers::after { top: -4px; }' ,
'.ab-stack-covers .ab-cover { position: relative; z-index: 1; }' ,
2026-10-16 14:08:44 +02:00
'.ab-chrow .ab-row { flex: 1; min-width: 0; }' ,
'.ab-chrow { border-bottom: 1px solid var(--border, #2a2a2a); }' ,
'.ab-chrow .ab-row { border-bottom: none; }' ,
'.ab-tick { color: var(--muted, #9a9a9a); font-size: 17px; }' ,
'.ab-tick.ab-ticked { color: var(--accent, #e63946); }' ,
2026-10-17 10:16:39 +02:00
'.ab-dl { color: var(--muted, #9a9a9a); font-size: 15px; min-width: 44px; }' ,
'.ab-dl-ic { width: 20px; height: 20px; vertical-align: -4px; }' ,
'.ab-dl.ab-dl-on { color: var(--fg, inherit); }' ,
'.ab-dl.ab-dl-busy { font-size: 12px; }' ,
'.ab-btn.ab-dl-on { border-color: var(--fg, currentColor); }' ,
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.
Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00
'.ab-mark .ab-row { flex: 1; min-width: 0; }' ,
'#ab-toast { position: fixed; left: 50%; bottom: calc(var(--nui-dock-h, 0px) + 90px); transform: translateX(-50%); z-index: 2500; background: var(--raised, #222); color: var(--fg, #eee); padding: 10px 16px; border-radius: 8px; font-size: 14px; opacity: 0; pointer-events: none; transition: opacity .2s; max-width: calc(100vw - 32px); }' ,
'#ab-toast.ab-show { opacity: 1; }' ,
'@media (max-width: 480px) { .ab-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; } .ab-cover-l { width: 96px; height: 96px; } }' ,
] . join ( '\n' ) ;
document . head . appendChild ( style ) ;
} ) ( ) ;