Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
'use strict' ;
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
// A book's folders: the list, or the single folder of older metas.
function _foldersOfMeta ( meta ) {
if ( Array . isArray ( meta . folders ) ) return meta . folders . filter ( f => typeof f === 'string' && f . trim ( ) ) ;
return meta . folder ? [ meta . folder ] : [ ] ;
}
window . _foldersOfMeta = _foldersOfMeta ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
async function loadBookList ( ) {
if ( ! IS _AUTHENTICATED ) return ;
const listEl = $ ( 'book-list' ) ;
if ( ! listEl ) return ;
listEl . innerHTML = '<p class="muted">Bücher werden geladen …</p>' ;
try {
listEl . innerHTML = '<p class="muted">Bücherliste wird vom Server geholt …</p>' ;
_flushProgressOutbox ( ) ; // own positions the server doesn't have yet
const res = await fetch ( '/books/' , { cache : 'no-store' } ) ;
_noteServerNowHeader ( res ) ;
if ( ! res . ok ) {
listEl . innerHTML = ` <p class="muted">Serverfehler ${ res . status } beim Laden der Bücher.</p> ` ;
return ;
}
const books = await res . json ( ) ;
_evictBookCache ( books ) ; // fire-and-forget
if ( ! Array . isArray ( books ) ) {
listEl . innerHTML = ` <p class="muted">Unerwartete Antwort vom Server.</p> ` ;
return ;
}
if ( ! books . length ) {
2026-10-16 20:56:19 +02:00
// Drawn like any list: the page's header (with +) is how a first book gets in.
if ( window . _ _dioraLibraryV2 ) { renderBookList ( [ ] ) ; return ; }
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
listEl . innerHTML = '<p class="muted">Noch keine Bücher. Zieh oben eine .epub- oder .pdf-Datei hinein.</p>' ;
return ;
}
listEl . innerHTML = ` <p class="muted"> ${ books . length === 1 ? '1 Buch' : books . length + ' Bücher' } auf dem Server. Entschlüssele …</p> ` ;
let key ;
try {
key = await getOrCreateEncKey ( ) ;
} catch ( e ) {
listEl . innerHTML = ` <p class="muted">Verschlüsselung nicht verfügbar: ${ e . message } . Die Seite muss über HTTPS laufen.</p> ` ;
return ;
}
const decrypted = [ ] ;
for ( const b of books ) {
try {
const metaBuf = await decryptBytes ( key , b . meta _iv , b . meta _ct ) ;
const meta = JSON . parse ( new TextDecoder ( ) . decode ( metaBuf ) ) ;
2026-10-19 14:27:14 +02:00
bookMetaCache [ b . id ] = { title : meta . title || '?' , author : meta . author || '' , type : meta . type || 'epub' , filename : meta . filename || '' , folder : meta . folder || '' , folders : _foldersOfMeta ( meta ) , isbn : meta . isbn || '' , shelfTag : meta . shelfTag || '' , pages : meta . pages || 0 , magazine : meta . magazine , cover : meta . cover , series : meta . series , seriesIndex : meta . seriesIndex , work : meta . work , issue : meta . issue , issueRead : meta . issueRead , issueAt : meta . issueAt } ;
2026-10-19 18:03:49 +02:00
decrypted . push ( { id : b . id , title : meta . title || '?' , author : meta . author || '' , type : meta . type || 'epub' , folder : meta . folder || '' , folders : _foldersOfMeta ( meta ) , isbn : meta . isbn || '' , shelfTag : meta . shelfTag || '' , pages : meta . pages || 0 , magazine : meta . magazine , cover : meta . cover , series : meta . series , seriesIndex : meta . seriesIndex , work : meta . work , issue : meta . issue , issueRead : meta . issueRead , issueAt : meta . issueAt , meta _iv : b . meta _iv , has _cover : ! ! b . has _cover , scroll _fraction : b . scroll _fraction , position _anchor : b . position _anchor || '' , read _at : b . read _at == null ? null : b . read _at , device : b . device || '' , furthest _fraction : b . furthest _fraction || 0 , furthest _anchor : b . furthest _anchor || '' , uploaded _at : b . uploaded _at , last _read : b . last _read || null , keyOk : true , is _read : ! ! b . is _read , has _highlights : ! ! b . has _highlights } ) ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
} catch ( e ) {
bookMetaCache [ b . id ] = { title : ` Buch # ${ b . id } ` , author : '' , type : 'epub' , filename : '' , folder : '' , isbn : '' , shelfTag : '' } ;
decrypted . push ( { id : b . id , title : ` Buch # ${ b . id } ` , author : '' , type : 'epub' , folder : '' , isbn : '' , shelfTag : '' , scroll _fraction : b . scroll _fraction , position _anchor : b . position _anchor || '' , read _at : b . read _at == null ? null : b . read _at , device : b . device || '' , furthest _fraction : b . furthest _fraction || 0 , furthest _anchor : b . furthest _anchor || '' , uploaded _at : b . uploaded _at , last _read : b . last _read || null , keyOk : false , is _read : ! ! b . is _read , has _highlights : ! ! b . has _highlights } ) ;
}
}
// An own position the server doesn't have yet (sent above) shows if it's the newer one.
for ( let i = 0 ; i < decrypted . length ; i ++ ) decrypted [ i ] = _withPendingProgress ( decrypted [ i ] ) ;
decrypted . sort ( ( a , b ) => ( _bookRecency ( b ) - _bookRecency ( a ) ) || b . uploaded _at . localeCompare ( a . uploaded _at ) ) ;
_saveBookMeta ( decrypted ) ; // persist for offline
renderBookList ( decrypted ) ;
} catch ( e ) {
// Network error — try offline fallback
const cachedBooks = ( await _loadCachedBookMeta ( ) ) . map ( _withPendingProgress ) ;
if ( cachedBooks . length ) {
cachedBooks . sort ( ( a , b ) => ( _bookRecency ( b ) - _bookRecency ( a ) ) || ( b . uploaded _at || '' ) . localeCompare ( a . uploaded _at || '' ) ) ;
2026-10-19 14:27:14 +02:00
for ( const b of cachedBooks ) bookMetaCache [ b . id ] = { title : b . title , author : b . author , type : b . type || 'epub' , filename : b . filename || '' , folder : b . folder || '' , folders : _foldersOfMeta ( b ) , isbn : b . isbn || '' , shelfTag : b . shelfTag || '' , pages : b . pages || 0 , magazine : b . magazine , cover : b . cover , series : b . series , seriesIndex : b . seriesIndex , work : b . work , issue : b . issue , issueRead : b . issueRead , issueAt : b . issueAt } ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
const uploadArea = $ ( 'book-upload-area' ) ;
if ( uploadArea ) uploadArea . style . display = 'none' ;
renderBookList ( cachedBooks ) ;
const notice = document . createElement ( 'p' ) ;
notice . className = 'muted offline-notice' ;
notice . textContent = 'Offline — gespeicherte Bücher. Fortschritt wird beim nächsten Öffnen synchronisiert.' ;
listEl . prepend ( notice ) ;
} else {
if ( listEl ) listEl . innerHTML = ` <p class="muted">Fehler beim Laden der Bücher: ${ e . message } </p> ` ;
}
}
}
const _brokenBooks = new Set ( ) ;
function markBookBroken ( bookId ) {
_brokenBooks . add ( bookId ) ;
const item = document . querySelector ( ` .book-item[data-book-id=" ${ bookId } "] ` ) ;
if ( ! item ) return ;
if ( ! item . querySelector ( '.book-broken-btn' ) ) {
const btn = document . createElement ( 'button' ) ;
btn . className = 'btn btn-sm btn-danger book-broken-btn' ;
btn . title = 'Buch konnte nicht geöffnet werden — Datei erneut hochladen' ;
btn . textContent = '!' ;
btn . onclick = ( ) => repairBook ( bookId ) ;
item . querySelector ( '.book-item-actions' ) . prepend ( btn ) ;
}
}
function _extractIsbnFromOpf ( opfDoc ) {
const identifiers = opfDoc . querySelectorAll ( 'metadata > identifier, metadata > *|identifier' ) ;
for ( const el of identifiers ) {
const digits = ( el . textContent || '' ) . replace ( /[^0-9Xx]/g , '' ) ;
if ( digits . length === 10 || digits . length === 13 ) return digits ;
}
return '' ;
}
function repairBook ( bookId ) {
const input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . accept = '.epub,.pdf' ;
input . onchange = async ( ) => {
const file = input . files [ 0 ] ;
if ( ! file ) return ;
const item = document . querySelector ( ` .book-item[data-book-id=" ${ bookId } "] ` ) ;
const btn = item ? . querySelector ( '.book-broken-btn' ) ;
if ( btn ) btn . textContent = '…' ;
try {
const key = await getOrCreateEncKey ( ) ;
const buf = await file . arrayBuffer ( ) ;
const isPdf = /\.pdf$/i . test ( file . name ) ;
const type = isPdf ? 'pdf' : 'epub' ;
let title = file . name . replace ( /\.(epub|pdf)$/i , '' ) , author = '' , isbn = '' ;
try {
if ( isPdf ) {
const pdfDoc = await pdfjsLib . getDocument ( { data : new Uint8Array ( buf . slice ( 0 ) ) } ) . promise ;
const meta = await pdfDoc . getMetadata ( ) ;
title = meta . info ? . Title ? . trim ( ) || title ;
author = meta . info ? . Author ? . trim ( ) || '' ;
} else {
const zip = await JSZip . loadAsync ( buf . slice ( 0 ) ) ;
const containerXml = await zip . file ( 'META-INF/container.xml' ) . async ( 'text' ) ;
const opfPath = new DOMParser ( )
. parseFromString ( containerXml , 'application/xml' )
. querySelector ( 'rootfile' ) ? . getAttribute ( 'full-path' ) ;
if ( opfPath ) {
const opfDoc = new DOMParser ( ) . parseFromString (
await zip . file ( opfPath ) . async ( 'text' ) , 'application/xml' ) ;
title = opfDoc . querySelector ( 'metadata > title, metadata > *|title' ) ? . textContent ? . trim ( ) || title ;
author = opfDoc . querySelector ( 'metadata > creator, metadata > *|creator' ) ? . textContent ? . trim ( ) || '' ;
isbn = _extractIsbnFromOpf ( opfDoc ) ;
}
}
} catch ( e ) { /* keep filename as title */ }
// Preserve folder/shelfTag assigned before this repair — replacing the book's
// data shouldn't silently wipe organization the user already set up.
const prevMeta = bookMetaCache [ bookId ] || { } ;
const metaJson = new TextEncoder ( ) . encode ( JSON . stringify ( {
title , author , filename : file . name , type ,
isbn : isbn || prevMeta . isbn || '' ,
folder : prevMeta . folder || '' ,
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
folders : prevMeta . folders || _foldersOfMeta ( prevMeta ) ,
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
shelfTag : prevMeta . shelfTag || '' ,
2026-10-16 15:45:14 +02:00
magazine : prevMeta . magazine ,
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
cover : prevMeta . cover ,
Serien für Bücher: erkannt, als Stapel, selbst zuweisen und verwalten
- Beim Hochladen liest diora die Serie aus dem E-Book (calibre:series und
series_index, EPUB 3 belongs-to-collection mit group-position); vorhandene
Bücher werden am Titel erkannt („Der Zauberberg. Erster Band“, „…: Teil 3“).
- In der Bibliothek ist eine Serie ab zwei Bänden ein Stapel („2 Bände · bei
Band 1“), darin die Bände in Reihenfolge; beim Suchen und Filtern einzeln.
- ⋮ → „Serie …“: vorhandene oder neue Serie, Band-Nummer, oder „Keine Serie“.
In der Serie: umbenennen, Reihenfolge, auflösen. Alles im verschlüsselten
Meta (series, seriesIndex; '' = bewusst keine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:47:18 +02:00
series : prevMeta . series ,
seriesIndex : prevMeta . seriesIndex ,
2026-10-19 14:27:14 +02:00
work : prevMeta . work ,
Zeitschriften: das Heft und seine Artikel
Das Heft bleibt das Dokument, der Artikel die Einheit, in der man sich
bewegt (docs/library.md):
- Artikel-Index auf dem Gerät aus dem Inhaltsverzeichnis des EPUB bzw. den
Lesezeichen des PDF (Rubriken, Anreißer, Lesezeit), verschlüsselt im Meta.
- Stand pro Artikel (gelesen / übersprungen, wo man war), verschlüsselt im
Meta, offline gepuffert.
- Bibliothek: Segmentbalken pro Heft, „7 von 23 gelesen“; ein Tipp öffnet den
Heft-Inhalt mit Rubriken, Stand je Artikel, „Im Heft weiterlesen“, „Heft
abschließen“.
- Reader: Kopfzeile „Artikel 8 von 23“, am Ende jedes Artikels eine Zäsur
(gelesen, Weiter im Heft, Zum Inhalt, überspringen); bei PDF eine Leiste
beim Weiterblättern. Durchgelesene Artikel zählen als gelesen, Sprünge nicht.
sw.js: diora-v127.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:14:28 +02:00
// A new file: its articles are read anew; what was read of them stays.
issueRead : prevMeta . issueRead ,
issueAt : prevMeta . issueAt ,
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
} ) ) ;
const [ metaEnc , dataEnc ] = await Promise . all ( [
encryptBytes ( key , metaJson ) ,
encryptBytes ( key , buf ) ,
] ) ;
const res = await fetch ( ` /books/ ${ bookId } /replace-data/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( {
data _ct : dataEnc . ciphertext , data _iv : dataEnc . iv ,
meta _ct : metaEnc . ciphertext , meta _iv : metaEnc . iv ,
} ) ,
} ) ;
if ( ! ( await res . json ( ) ) . ok ) throw new Error ( 'Serverfehler' ) ;
await _evictCachedBook ( bookId ) ;
_brokenBooks . delete ( bookId ) ;
if ( btn ) btn . remove ( ) ;
openBook ( bookId ) ;
} catch ( e ) {
if ( btn ) btn . textContent = '!' ;
await customAlert ( 'Reparatur fehlgeschlagen: ' + e . message ) ;
}
} ;
input . click ( ) ;
}
let _lastBookListData = [ ] ;
const _bookShowReadKey = 'diora_book_show_read' ;
let _bookShowRead = localStorage . getItem ( _bookShowReadKey ) === '1' ;
function _onBookShowReadToggle ( checked ) {
_bookShowRead = checked ;
localStorage . setItem ( _bookShowReadKey , checked ? '1' : '0' ) ;
renderBookList ( _lastBookListData ) ;
}
async function toggleBookRead ( bookId , currentlyRead ) {
const verb = currentlyRead ? 'als ungelesen markieren' : 'als gelesen markieren' ;
if ( ! await customConfirm ( ` Dieses Buch ${ verb } ? ` ) ) return ;
try {
const res = await fetch ( ` /books/ ${ bookId } /read/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { is _read : ! currentlyRead } ) ,
} ) ;
const data = await res . json ( ) ;
if ( ! data . ok ) throw new Error ( 'Serverfehler' ) ;
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( book ) book . is _read = data . is _read ;
renderBookList ( _lastBookListData ) ;
} catch ( e ) {
await customAlert ( 'Konnte Lesestatus nicht ändern: ' + e . message ) ;
}
}
// Folder assigned to a book lives inside its encrypted meta blob (like title/author), so the
// server never sees plaintext folder names — no schema change needed, and it rides along for
// free in /api/sync/. One level only: folders don't nest.
let _currentBookFolder = null ; // null = root view (folder tiles + unfiled books)
2026-10-16 15:45:14 +02:00
// Bücher and Zeitschriften are two sections of Lesen (static/js/nav.js), one list: a
// magazine is an issue marked as one (⋮ → Als Zeitschrift, kept in the encrypted meta),
// or, unmarked, a book in a folder called "Zeitschriften" (or "Magazine").
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
let _bookSection = 'books' ;
const _MAGAZINE _FOLDER = /^(zeitschriften|magazine|magazines)$/i ;
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 _isMagazine = b => b . magazine === true ||
( b . magazine !== false && _foldersOfMeta ( b ) . some ( f => _MAGAZINE _FOLDER . test ( f . trim ( ) ) ) ) ;
2026-10-16 15:45:14 +02:00
window . _isMagazine = _isMagazine ;
// The periodical an issue belongs to: its title up to the issue's number ("Jacobin #24",
// "New Left Review 157", "LuXemburg 1/2026"), else its author ("Vol 9 – Issue 2– 3" by Catalyst).
function _periodicalOf ( b ) {
const m = /^(.*?)\s*(?:#|\bno\.|\bnr\.|\bheft\b|\bausgabe\b|\bissue\b|\bvol\b|\d)/i . exec ( b . title || '' ) ;
const name = ( m ? m [ 1 ] : '' ) . replace ( /[\s– —:,-]+$/ , '' ) . trim ( ) ;
return name . length >= 3 ? name : ( b . author || '' ) . trim ( ) || ( b . title || '' ) . trim ( ) ;
}
async function toggleBookMagazine ( bookId ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return ;
try {
await _updateBookMeta ( bookId , { magazine : ! _isMagazine ( book ) } ) ;
renderBookList ( _lastBookListData ) ;
} catch ( e ) {
customAlert ( 'Das ließ sich nicht speichern: ' + e . message ) ;
}
}
window . toggleBookMagazine = toggleBookMagazine ;
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
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
// The generated cover instead of the book's own (or back): a choice per book.
async function toggleBookCover ( bookId ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return ;
try {
await _updateBookMeta ( bookId , { cover : book . cover === 'generated' ? '' : 'generated' } ) ;
renderBookList ( _lastBookListData ) ;
} catch ( e ) {
customAlert ( 'Das ließ sich nicht speichern: ' + e . message ) ;
}
}
window . toggleBookCover = toggleBookCover ;
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
// The books of the section shown (all of them where there are no sections).
function _bookSectionFilter ( books ) {
if ( ! window . DioraNav ) return books ;
return books . filter ( b => ( _bookSection === 'magazines' ) === _isMagazine ( b ) ) ;
}
window . _bookSectionFilter = _bookSectionFilter ;
window . _ _dioraBooksView = function ( view ) {
const next = view === 'magazines' ? 'magazines' : 'books' ;
if ( next === _bookSection ) return ;
_bookSection = next ;
_currentBookFolder = null ;
if ( _lastBookListData ) renderBookList ( _lastBookListData ) ;
} ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
function _recentlyReadBooks ( books ) {
return books
. filter ( b => b . last _read )
. sort ( ( a , b ) => b . last _read . localeCompare ( a . last _read ) )
. slice ( 0 , 7 ) ;
}
function _renderBookItemHtml ( b ) {
const pct = Math . round ( ( b . scroll _fraction || 0 ) * 100 ) ;
const keyWarning = b . keyOk === false ? '<span title="Falscher Schlüssel – importiere den richtigen Schlüssel, um dieses Buch zu öffnen" style="color:var(--accent,#e63946);margin-left:4px;">⚠ falscher Schlüssel</span>' : '' ;
const broken = _brokenBooks . has ( b . id ) ;
return ` <div class="book-item" data-book-id=" ${ b . id } ">
< div class = "book-item-info" >
< strong class = "book-title" > $ { escapeHtml ( b . title ) } $ { keyWarning } $ { b . is _read ? ' <span class="muted book-read-badge">✓ gelesen</span>' : '' } < / s t r o n g >
< span class = "muted book-author" > $ { escapeHtml ( b . author ) } < / s p a n >
< span class = "book-item-meta-row" >
$ { pct > 0 ? ` <span class="muted book-progress"> ${ pct } % gelesen ${ b . pages ? ` (≈ ${ Math . max ( 1 , Math . round ( b . pages * pct / 100 ) ) } von ${ b . pages } Seiten) ` : '' } </span> ` : ( b . pages ? ` <span class="muted book-progress">ca. ${ b . pages } Seiten</span> ` : '' ) }
$ { b . shelfTag ? ` <span class="book-shelf-badge" title="Regal-Kategorie"> ${ escapeHtml ( b . shelfTag ) } </span> ` : '' }
< / s p a n >
< / d i v >
< div class = "book-item-actions" >
< button class = "btn btn-sm" onclick = "openBook(${b.id})" $ { b . keyOk === false ? ' disabled title="Erst den richtigen Schlüssel importieren"' : '' } > Öffnen < / b u t t o n >
< div class = "book-item-menu" >
< button class = "btn btn-sm book-menu-toggle" title = "Weitere Optionen" onclick = "toggleBookMenu(this)" > ⋮ < / b u t t o n >
< div class = "book-item-menu-list" >
$ { broken ? ` <button class="book-menu-item book-menu-item--danger book-broken-btn" onclick="repairBook( ${ b . id } )">🔧 Reparieren (Datei erneut hochladen)</button> ` : '' }
$ { b . has _highlights ? ` <button class="book-menu-item" onclick="downloadBookAnnotationsFromList( ${ b . id } , this)">⭳ Markierungen & Notizen herunterladen</button> ` : '' }
< button class = "book-menu-item" onclick = "assignBookFolder(${b.id})" > 📁 Ordner zuweisen < / b u t t o n >
2026-10-16 15:45:14 +02:00
< button class = "book-menu-item" onclick = "toggleBookMagazine(${b.id})" > $ { _isMagazine ( b ) ? '▤ Als Buch führen' : '📰 Als Zeitschrift führen' } < / b u t t o n >
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
< button class = "book-menu-item" onclick = "lookupBookMetadata(${b.id})" > 🏷 ️ Metadaten abrufen ( Regal ) < / b u t t o n >
< button class = "book-menu-item" onclick = "toggleBookRead(${b.id}, ${!!b.is_read})" > $ { b . is _read ? '↺ Als ungelesen markieren' : '✓ Als gelesen markieren' } < / b u t t o n >
< button class = "book-menu-item book-menu-item--danger" onclick = "deleteBook(${b.id})" > 🗑 Löschen < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
< / d i v > ` ;
}
function toggleBookMenu ( toggleBtn ) {
// Looked up relative to the clicked button (not by id) because the same book — and
// thus the same rendered menu — can appear twice at once: once in the "Zuletzt
// gelesen" section and again further down in the folder/unfiled list.
const menu = toggleBtn . closest ( '.book-item-menu' ) ? . querySelector ( '.book-item-menu-list' ) ;
if ( ! menu ) return ;
const willOpen = ! menu . classList . contains ( 'open' ) ;
document . querySelectorAll ( '.book-item-menu-list.open' ) . forEach ( m => m . classList . remove ( 'open' ) ) ;
if ( willOpen ) menu . classList . add ( 'open' ) ;
}
// Close any open book menu on an outside click, or right after an item inside it is
// clicked (the toggle button manages its own open/close state above, so it's excluded).
document . addEventListener ( 'click' , ( e ) => {
if ( e . target . closest ( '.book-menu-toggle' ) ) return ;
document . querySelectorAll ( '.book-item-menu-list.open' ) . forEach ( m => m . classList . remove ( 'open' ) ) ;
} ) ;
function _openBookFolder ( name ) {
_currentBookFolder = name ;
renderBookList ( _lastBookListData ) ;
}
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
function renderBookList ( allBooks ) {
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
const listEl = $ ( 'book-list' ) ;
if ( ! listEl ) return ;
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
_lastBookListData = allBooks ;
const books = _bookSectionFilter ( allBooks ) ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
if ( ! books . length ) {
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
listEl . innerHTML = window . DioraNav && allBooks . length
? ( _bookSection === 'magazines'
2026-10-16 15:45:14 +02:00
? '<p class="muted">Noch keine Zeitschriften. Ein Heft unter Bücher über ⋮ → „Als Zeitschrift führen“ hierher holen.</p>'
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
: '<p class="muted">Alle Bücher liegen bei den Zeitschriften.</p>' )
: '' ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
return ;
}
let html = '' ;
// Always-visible "recently read" section, independent of folder navigation and the
// read/unread filter below — it's a shortcut back into whatever was open last. Same
// vertical list-item look as the rest of the list (not a card shelf).
const recent = _recentlyReadBooks ( books ) ;
if ( recent . length ) {
html += '<div class="book-recent-section">'
+ '<h3 class="book-recent-title">Zuletzt gelesen</h3>'
+ recent . map ( _renderBookItemHtml ) . join ( '' )
+ '</div>' ;
}
const visible = _bookShowRead ? books : books . filter ( b => ! b . is _read ) ;
if ( ! visible . length ) {
html += books . length
? '<p class="muted">Keine ungelesenen Bücher. „Gelesene Bücher anzeigen“ aktivieren, um alle zu sehen.</p>'
: '' ;
listEl . innerHTML = html ;
return ;
}
const grouped = new Map ( ) ;
2026-10-16 15:45:14 +02:00
const magazines = _bookSection === 'magazines' && ! ! window . DioraNav ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
for ( const b of visible ) {
2026-10-16 15:45:14 +02:00
const key = magazines ? _periodicalOf ( b ) : ( b . folder || '' ) ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
if ( ! grouped . has ( key ) ) grouped . set ( key , [ ] ) ;
grouped . get ( key ) . push ( b ) ;
}
2026-10-16 15:45:14 +02:00
if ( magazines ) {
// A periodical with one issue is no tile; issues newest first.
const loose = grouped . get ( '' ) || [ ] ;
for ( const [ k , list ] of [ ... grouped ] ) {
if ( k !== '' && list . length < 2 ) { loose . push ( ... list ) ; grouped . delete ( k ) ; }
}
grouped . set ( '' , loose ) ;
const issue = ( a , b ) => b . title . localeCompare ( a . title , 'de' , { numeric : true } ) ;
for ( const list of grouped . values ( ) ) list . sort ( issue ) ;
}
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
// Folder emptied out (last book moved/deleted, or read-filter hid it) — fall back to root.
if ( _currentBookFolder !== null && ! grouped . has ( _currentBookFolder ) ) {
_currentBookFolder = null ;
}
if ( _currentBookFolder === null ) {
const folderNames = [ ... grouped . keys ( ) ] . filter ( k => k !== '' ) . sort ( ( a , b ) => a . localeCompare ( b , 'de' ) ) ;
if ( folderNames . length ) {
html += '<div class="book-folder-grid">'
+ folderNames . map ( name => ` <button class="book-folder-tile" data-folder=" ${ escapeHtml ( name ) } ">
2026-10-16 15:45:14 +02:00
< span class = "book-folder-tile-icon" > $ { magazines ? '📰' : '📁' } < / s p a n >
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
< span class = "book-folder-tile-name" > $ { escapeHtml ( name ) } < / s p a n >
< span class = "book-folder-tile-count" > $ { grouped . get ( name ) . length } < / s p a n >
< / b u t t o n > ` ) . j o i n ( ' ' )
+ '</div>' ;
}
html += ( grouped . get ( '' ) || [ ] ) . map ( _renderBookItemHtml ) . join ( '' ) ;
} else {
html += ` <div class="book-folder-header">
< button class = "btn btn-sm" onclick = "_openBookFolder(null)" > ← Alle Ordner < / b u t t o n >
< strong > $ { escapeHtml ( _currentBookFolder ) } < / s t r o n g >
< / d i v > ` ;
html += grouped . get ( _currentBookFolder ) . map ( _renderBookItemHtml ) . join ( '' ) ;
}
listEl . innerHTML = html ;
listEl . querySelectorAll ( '.book-folder-tile' ) . forEach ( tile => {
tile . addEventListener ( 'click' , ( ) => _openBookFolder ( tile . dataset . folder ) ) ;
} ) ;
}
// Shared by assignBookFolder/lookupBookMetadata: re-encrypts the meta blob with the given
// field overrides applied on top of whatever is already cached, and pushes it to the server.
async function _updateBookMeta ( bookId , overrides ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return false ;
const cached = bookMetaCache [ bookId ] || { } ;
const merged = {
title : cached . title || book . title ,
author : cached . author || book . author ,
filename : cached . filename || '' ,
type : cached . type || book . type || 'epub' ,
isbn : cached . isbn || book . isbn || '' ,
folder : cached . folder || book . folder || '' ,
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
folders : cached . folders || book . folders || _foldersOfMeta ( book ) ,
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
shelfTag : cached . shelfTag || book . shelfTag || '' ,
pages : cached . pages || book . pages || 0 ,
2026-10-16 15:45:14 +02:00
magazine : cached . magazine !== undefined ? cached . magazine : book . magazine ,
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
cover : cached . cover !== undefined ? cached . cover : book . cover ,
Serien für Bücher: erkannt, als Stapel, selbst zuweisen und verwalten
- Beim Hochladen liest diora die Serie aus dem E-Book (calibre:series und
series_index, EPUB 3 belongs-to-collection mit group-position); vorhandene
Bücher werden am Titel erkannt („Der Zauberberg. Erster Band“, „…: Teil 3“).
- In der Bibliothek ist eine Serie ab zwei Bänden ein Stapel („2 Bände · bei
Band 1“), darin die Bände in Reihenfolge; beim Suchen und Filtern einzeln.
- ⋮ → „Serie …“: vorhandene oder neue Serie, Band-Nummer, oder „Keine Serie“.
In der Serie: umbenennen, Reihenfolge, auflösen. Alles im verschlüsselten
Meta (series, seriesIndex; '' = bewusst keine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:47:18 +02:00
series : cached . series !== undefined ? cached . series : book . series ,
seriesIndex : cached . seriesIndex !== undefined ? cached . seriesIndex : book . seriesIndex ,
2026-10-19 14:27:14 +02:00
// Editions of one work (53-library.js): the same key on each.
work : cached . work !== undefined ? cached . work : book . work ,
Zeitschriften: das Heft und seine Artikel
Das Heft bleibt das Dokument, der Artikel die Einheit, in der man sich
bewegt (docs/library.md):
- Artikel-Index auf dem Gerät aus dem Inhaltsverzeichnis des EPUB bzw. den
Lesezeichen des PDF (Rubriken, Anreißer, Lesezeit), verschlüsselt im Meta.
- Stand pro Artikel (gelesen / übersprungen, wo man war), verschlüsselt im
Meta, offline gepuffert.
- Bibliothek: Segmentbalken pro Heft, „7 von 23 gelesen“; ein Tipp öffnet den
Heft-Inhalt mit Rubriken, Stand je Artikel, „Im Heft weiterlesen“, „Heft
abschließen“.
- Reader: Kopfzeile „Artikel 8 von 23“, am Ende jedes Artikels eine Zäsur
(gelesen, Weiter im Heft, Zum Inhalt, überspringen); bei PDF eine Leiste
beim Weiterblättern. Durchgelesene Artikel zählen als gelesen, Sprünge nicht.
sw.js: diora-v127.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:14:28 +02:00
// An issue's articles and what was read of them (54-issues.js).
issue : cached . issue !== undefined ? cached . issue : book . issue ,
issueRead : cached . issueRead !== undefined ? cached . issueRead : book . issueRead ,
issueAt : cached . issueAt !== undefined ? cached . issueAt : book . issueAt ,
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
... overrides ,
} ;
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
// The list decides; `folder` follows it (the first), for older clients.
if ( overrides . folders ) merged . folder = overrides . folders [ 0 ] || '' ;
else if ( 'folder' in overrides ) merged . folders = overrides . folder ? [ overrides . folder ] : [ ] ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
const key = await getOrCreateEncKey ( ) ;
2026-10-19 18:03:49 +02:00
// base_iv: the meta this change starts from. Changed elsewhere since (the server says 409
// and sends the current one): the change goes onto that one instead, once.
let toWrite = merged ;
let base = book . meta _iv ;
let data ;
for ( let attempt = 0 ; attempt < 2 ; attempt ++ ) {
const metaEnc = await encryptBytes ( key , new TextEncoder ( ) . encode ( JSON . stringify ( toWrite ) ) ) ;
const res = await fetch ( ` /books/ ${ bookId } /meta/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { meta _ct : metaEnc . ciphertext , meta _iv : metaEnc . iv , ... ( base ? { base _iv : base } : { } ) } ) ,
} ) ;
data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( res . status === 409 && data . meta _ct && attempt === 0 ) {
const now = JSON . parse ( new TextDecoder ( ) . decode ( await decryptBytes ( key , data . meta _iv , data . meta _ct ) ) ) ;
toWrite = { ... now , ... overrides } ;
if ( overrides . folders ) toWrite . folder = overrides . folders [ 0 ] || '' ;
else if ( 'folder' in overrides ) toWrite . folders = overrides . folder ? [ overrides . folder ] : [ ] ;
base = data . meta _iv ;
continue ;
}
if ( data . ok ) { book . meta _iv = metaEnc . iv ; break ; }
throw new Error ( res . status === 409 ? 'inzwischen anderswo geändert' : 'Serverfehler' ) ;
}
// What the server has now, all of it (from the rebase too: a title set elsewhere shows).
Object . assign ( book , toWrite , { folder : toWrite . folder || '' , folders : _foldersOfMeta ( toWrite ) } ) ;
bookMetaCache [ bookId ] = toWrite ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
_saveBookMeta ( _lastBookListData ) ;
return true ;
}
async function assignBookFolder ( bookId ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return ;
const folders = [ ... new Set ( _lastBookListData . map ( b => b . folder ) . filter ( Boolean ) ) ] . sort ( ( a , b ) => a . localeCompare ( b , 'de' ) ) ;
const hint = folders . length ? ` Vorhandene Ordner: ${ folders . join ( ', ' ) } . ` : '' ;
const result = await customPrompt ( ` Ordner für „ ${ book . title } “ (leer = kein Ordner). ${ hint } ` , book . folder || '' ) ;
if ( result === null ) return ;
const folder = result . trim ( ) ;
if ( folder === ( book . folder || '' ) ) return ;
try {
await _updateBookMeta ( bookId , { folder } ) ;
if ( folder ) _currentBookFolder = folder ;
renderBookList ( _lastBookListData ) ;
} catch ( e ) {
await customAlert ( 'Konnte Ordner nicht ändern: ' + e . message ) ;
}
}
// Fallback for books uploaded before ISBN extraction existed (or repaired/re-encrypted
// without it): pulls the already-accessible encrypted book bytes, decrypts and unzips just
// far enough to read the OPF identifiers — same source openBook() itself parses — without
// doing a full reader render. EPUB only; PDFs never carried a reliable ISBN via pdf.js.
async function _extractIsbnFromBookFile ( bookId ) {
try {
const key = await getOrCreateEncKey ( ) ;
let data _ct , data _iv ;
const cached = await _getCachedBook ( bookId ) ;
if ( cached ) {
( { data _ct , data _iv } = cached ) ;
} else {
const res = await fetch ( ` /books/ ${ bookId } /data/ ` ) ;
( { data _ct , data _iv } = await res . json ( ) ) ;
}
const plain = await decryptBytes ( key , data _iv , data _ct ) ;
const zip = await JSZip . loadAsync ( plain ) ;
const containerXml = await zip . file ( 'META-INF/container.xml' ) . async ( 'text' ) ;
const opfPath = new DOMParser ( )
. parseFromString ( containerXml , 'application/xml' )
. querySelector ( 'rootfile' ) ? . getAttribute ( 'full-path' ) ;
if ( ! opfPath ) return '' ;
const opfDoc = new DOMParser ( ) . parseFromString ( await zip . file ( opfPath ) . async ( 'text' ) , 'application/xml' ) ;
return _extractIsbnFromOpf ( opfDoc ) ;
} catch ( e ) {
return '' ;
}
}
async function _fetchShelfLookup ( isbn , title , author ) {
const params = new URLSearchParams ( ) ;
if ( isbn ) params . set ( 'isbn' , isbn ) ;
if ( title ) params . set ( 'title' , title ) ;
if ( author ) params . set ( 'author' , author ) ;
const res = await fetch ( ` /books/metadata-lookup/? ${ params . toString ( ) } ` ) ;
return res . json ( ) ;
}
// Manually triggered (never automatic) — looks up which library shelf/DDC category this
// book falls under via the server-side DNB→Open Library proxy, purely as an organizational
// hint (shown as a badge). See books/views.py:lookup_book_metadata for why this one call is
// allowed to briefly touch the server with a plaintext ISBN/title/author.
//
// Fallback chain when there's no ISBN (older upload, or a book that never had one — e.g.
// every PDF, since pdf.js doesn't expose one): (1) extract ISBN from the file itself, for
// EPUBs, (2) server-side title/author text search (less precise — could match the wrong
// edition/translation), (3) if that also comes up empty, ask the user to type in an ISBN
// by hand (from the book's cover/an online store) and retry once.
async function lookupBookMetadata ( bookId ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return ;
const cached = bookMetaCache [ bookId ] || { } ;
let isbn = cached . isbn || book . isbn || '' ;
const isPdf = ( cached . type || book . type ) === 'pdf' ;
const title = cached . title || book . title || '' ;
const author = cached . author || book . author || '' ;
if ( ! isbn && ! isPdf ) {
// Older upload without a stored ISBN — extract it from the book file itself (may take
// a moment for large books, since it downloads/decrypts the full file if not cached).
isbn = await _extractIsbnFromBookFile ( bookId ) ;
}
try {
let data = await _fetchShelfLookup ( isbn , title , author ) ;
if ( ! data . label ) {
const manual = await customPrompt (
'Keine Regal-Kategorie per ISBN/Titel/Autor gefunden. ISBN manuell eingeben (z. B. vom Buchrücken oder Online-Shop), oder leer lassen zum Abbrechen:' , ''
) ;
const manualIsbn = ( manual || '' ) . replace ( /[^0-9Xx]/g , '' ) ;
if ( manualIsbn ) {
isbn = manualIsbn ;
data = await _fetchShelfLookup ( isbn , title , author ) ;
}
}
// Persist whatever we ended up with either way, so a repeat lookup never needs to
// re-extract/re-search from scratch.
const overrides = { } ;
if ( isbn ) overrides . isbn = isbn ;
if ( data . label ) overrides . shelfTag = data . label ;
if ( Object . keys ( overrides ) . length ) {
await _updateBookMeta ( bookId , overrides ) ;
renderBookList ( _lastBookListData ) ;
}
if ( ! data . label ) {
await customAlert ( 'Keine Regal-Kategorie gefunden (weder DNB noch Open Library, per ISBN oder Titel/Autor).' ) ;
}
} catch ( e ) {
await customAlert ( 'Metadaten-Abruf fehlgeschlagen: ' + e . message ) ;
}
}
function bookFileSelected ( input ) {
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
const files = Array . from ( input . files || [ ] ) ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
input . value = '' ;
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
if ( files . length === 1 ) uploadEbook ( files [ 0 ] ) ;
else if ( files . length ) _importMany ( files . map ( f => ( { name : f . name , size : f . size , buf : ( ) => f . arrayBuffer ( ) } ) ) ) ;
}
// Several books, one after the other (a picked set of files, a cloud folder): each is
// read, encrypted and sent like a single one; a book already in the library (same title
// and author) is left out. statusEl shows how far it is and, at the end, what came of it.
async function _importMany ( items , statusEl ) {
statusEl = statusEl || $ ( 'book-upload-status' ) ;
2026-10-16 21:52:31 +02:00
const done = { ok : 0 , dup : 0 , failed : [ ] , big : [ ] } ;
const retry = [ ] ;
const wait = ms => new Promise ( r => setTimeout ( r , ms ) ) ;
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
for ( let i = 0 ; i < items . length ; i ++ ) {
const it = items [ i ] ;
2026-10-16 21:52:31 +02:00
if ( it . size && it . size > DIORA _CONFIG . ebookMaxBytes ) { done . big . push ( it . name ) ; continue ; }
// A failure is tried again, a little later each time: the server may be restarting
// (an update) or the connection gone for a moment — not a reason to lose the rest.
let r = false ;
for ( let attempt = 0 ; attempt < 4 && r === false ; attempt ++ ) {
if ( attempt ) {
if ( statusEl ) statusEl . textContent = ` Buch ${ i + 1 } von ${ items . length } : „ ${ it . name } “ – neuer Versuch … ` ;
await wait ( [ 0 , 3000 , 8000 , 20000 ] [ attempt ] ) ;
}
if ( statusEl && ! attempt ) statusEl . textContent = ` Buch ${ i + 1 } von ${ items . length } : „ ${ it . name } “ … ` ;
try {
r = await _importEbookBuffer ( await it . buf ( ) , it . name , null , { reload : false , skipKnown : true } ) ;
} catch ( e ) {
r = false ;
}
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
}
2026-10-16 21:52:31 +02:00
if ( r === 'dup' ) done . dup ++ ;
else if ( r ) done . ok ++ ;
else { done . failed . push ( it . name ) ; retry . push ( it ) ; }
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
}
2026-10-16 21:52:31 +02:00
const mb = Math . round ( DIORA _CONFIG . ebookMaxBytes / 1024 / 1024 ) ;
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
const parts = [ ` ✓ ${ done . ok } ${ done . ok === 1 ? 'Buch' : 'Bücher' } hinzugefügt ` ] ;
if ( done . dup ) parts . push ( ` ${ done . dup } schon da ` ) ;
2026-10-16 21:52:31 +02:00
if ( done . big . length ) parts . push ( ` zu groß (über ${ mb } MB): ${ done . big . join ( ', ' ) } ` ) ;
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
if ( done . failed . length ) parts . push ( ` nicht geklappt: ${ done . failed . join ( ', ' ) } ` ) ;
2026-10-16 21:52:31 +02:00
if ( statusEl ) {
statusEl . textContent = parts . join ( ' · ' ) ;
if ( retry . length ) {
const again = document . createElement ( 'button' ) ;
again . type = 'button' ;
again . className = 'btn btn-sm' ;
again . style . marginLeft = '8px' ;
again . textContent = ` ${ retry . length } erneut versuchen ` ;
again . onclick = ( ) => _importMany ( retry , statusEl ) ;
statusEl . appendChild ( again ) ;
}
}
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
if ( done . ok ) loadBookList ( ) ;
return done ;
}
Serien für Bücher: erkannt, als Stapel, selbst zuweisen und verwalten
- Beim Hochladen liest diora die Serie aus dem E-Book (calibre:series und
series_index, EPUB 3 belongs-to-collection mit group-position); vorhandene
Bücher werden am Titel erkannt („Der Zauberberg. Erster Band“, „…: Teil 3“).
- In der Bibliothek ist eine Serie ab zwei Bänden ein Stapel („2 Bände · bei
Band 1“), darin die Bände in Reihenfolge; beim Suchen und Filtern einzeln.
- ⋮ → „Serie …“: vorhandene oder neue Serie, Band-Nummer, oder „Keine Serie“.
In der Serie: umbenennen, Reihenfolge, auflösen. Alles im verschlüsselten
Meta (series, seriesIndex; '' = bewusst keine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:47:18 +02:00
// The series an EPUB names itself: calibre's <meta name="calibre:series" content> with
// calibre:series_index, or EPUB 3's belongs-to-collection with group-position. Or null.
function _seriesFromOpf ( opf ) {
const metas = Array . from ( opf . getElementsByTagName ( 'meta' ) ) ;
const named = n => metas . filter ( m => m . getAttribute ( 'name' ) === n ) [ 0 ] ;
const cs = named ( 'calibre:series' ) ;
if ( cs && cs . getAttribute ( 'content' ) ) {
const ix = named ( 'calibre:series_index' ) ;
const n = ix ? parseFloat ( ix . getAttribute ( 'content' ) ) : NaN ;
return { name : cs . getAttribute ( 'content' ) . trim ( ) , index : isFinite ( n ) ? n : null } ;
}
const coll = metas . filter ( m => m . getAttribute ( 'property' ) === 'belongs-to-collection' ) [ 0 ] ;
if ( coll && coll . textContent . trim ( ) ) {
const id = coll . getAttribute ( 'id' ) ;
const pos = id ? metas . filter ( m => m . getAttribute ( 'refines' ) === '#' + id && m . getAttribute ( 'property' ) === 'group-position' ) [ 0 ] : null ;
const n = pos ? parseFloat ( pos . textContent ) : NaN ;
return { name : coll . textContent . trim ( ) , index : isFinite ( n ) ? n : null } ;
}
return null ;
}
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
// Is a book with this title and author in the library already?
function _knownBook ( title , author ) {
const n = s => String ( s || '' ) . normalize ( 'NFD' ) . replace ( /[\u0300-\u036f]/g , '' ) . toLowerCase ( ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
return ( _lastBookListData || [ ] ) . some ( b => n ( b . title ) === n ( title ) && n ( b . author ) === n ( author ) ) ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
}
function initBookDropZone ( ) {
// Prevent Firefox from opening dragged files when dropped outside the zone
document . addEventListener ( 'dragover' , e => e . preventDefault ( ) ) ;
document . addEventListener ( 'drop' , e => {
const zone = $ ( 'book-drop-zone' ) ;
if ( ! zone || ! zone . contains ( e . target ) ) e . preventDefault ( ) ;
} ) ;
const zone = $ ( 'book-drop-zone' ) ;
if ( ! zone ) return ;
zone . addEventListener ( 'dragover' , e => {
e . preventDefault ( ) ;
zone . classList . add ( 'drag-over' ) ;
} ) ;
zone . addEventListener ( 'dragleave' , ( ) => zone . classList . remove ( 'drag-over' ) ) ;
zone . addEventListener ( 'drop' , e => {
e . preventDefault ( ) ;
zone . classList . remove ( 'drag-over' ) ;
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
const files = Array . from ( e . dataTransfer . files || [ ] ) ;
if ( files . length === 1 ) uploadEbook ( files [ 0 ] ) ;
else if ( files . length ) _importMany ( files . map ( f => ( { name : f . name , size : f . size , buf : ( ) => f . arrayBuffer ( ) } ) ) ) ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
} ) ;
}
async function deriveAndStoreKey ( ) {
const pwInput = document . getElementById ( 'enc-key-password' ) ;
const statusEl = $ ( 'enc-key-status' ) ;
const pw = pwInput ? pwInput . value : '' ;
if ( ! pw ) { if ( statusEl ) statusEl . textContent = 'Bitte gib dein Passwort ein.' ; return ; }
if ( statusEl ) statusEl . textContent = 'Schlüssel wird abgeleitet …' ;
try {
const enc = new TextEncoder ( ) ;
const username = document . querySelector ( 'meta[name="username"]' ) ? . content || '' ;
const mat = await crypto . subtle . importKey ( 'raw' , enc . encode ( pw ) , 'PBKDF2' , false , [ 'deriveKey' ] ) ;
const key = await crypto . subtle . deriveKey (
{ name : 'PBKDF2' , salt : enc . encode ( 'diora:' + username ) , iterations : 200000 , hash : 'SHA-256' } ,
mat , { name : 'AES-GCM' , length : 256 } , true , [ 'encrypt' , 'decrypt' ]
) ;
const raw = await crypto . subtle . exportKey ( 'raw' , key ) ;
const storageKey = ` diora_enc_key_ ${ window . USER _ID || 0 } ` ;
localStorage . setItem ( storageKey , bytesToBase64 ( new Uint8Array ( raw ) ) ) ;
_encKey = null ; // reset cached key
if ( statusEl ) statusEl . textContent = '✓ Entsperrt' ;
const prompt = $ ( 'enc-key-prompt' ) ;
const uploadArea = $ ( 'book-upload-area' ) ;
if ( prompt ) prompt . style . display = 'none' ;
if ( uploadArea ) uploadArea . style . display = '' ;
loadBookList ( ) ;
} catch ( err ) {
if ( statusEl ) statusEl . textContent = 'Fehler: ' + err . message ;
}
}
async function uploadEbook ( file ) {
const statusEl = $ ( 'book-upload-status' ) ;
if ( file . size > DIORA _CONFIG . ebookMaxBytes ) {
if ( statusEl ) statusEl . textContent = ` Datei zu groß (höchstens ${ DIORA _CONFIG . ebookMaxBytes / 1024 / 1024 } MB). ` ;
return false ;
}
return _importEbookBuffer ( await file . arrayBuffer ( ) , file . name , statusEl ) ;
}
// Shared tail of every import path (local file, cloud): parse metadata, encrypt
// with the user's key and POST ciphertext. Plaintext never leaves the browser.
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
async function _importEbookBuffer ( buf , filename , statusEl , opts ) {
opts = opts || { } ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
const isPdf = /\.pdf$/i . test ( filename ) ;
const isEpub = /\.epub$/i . test ( filename ) ;
if ( ! isPdf && ! isEpub ) {
if ( statusEl ) statusEl . textContent = 'Nur .epub- und .pdf-Dateien werden unterstützt.' ;
return false ;
}
if ( buf . byteLength > DIORA _CONFIG . ebookMaxBytes ) {
if ( statusEl ) statusEl . textContent = ` Datei zu groß (höchstens ${ DIORA _CONFIG . ebookMaxBytes / 1024 / 1024 } MB). ` ;
return false ;
}
if ( statusEl ) statusEl . textContent = 'Verschlüssele …' ;
try {
let title = filename . replace ( /\.(epub|pdf)$/i , '' ) ;
let author = '' ;
let isbn = '' ;
Serien für Bücher: erkannt, als Stapel, selbst zuweisen und verwalten
- Beim Hochladen liest diora die Serie aus dem E-Book (calibre:series und
series_index, EPUB 3 belongs-to-collection mit group-position); vorhandene
Bücher werden am Titel erkannt („Der Zauberberg. Erster Band“, „…: Teil 3“).
- In der Bibliothek ist eine Serie ab zwei Bänden ein Stapel („2 Bände · bei
Band 1“), darin die Bände in Reihenfolge; beim Suchen und Filtern einzeln.
- ⋮ → „Serie …“: vorhandene oder neue Serie, Band-Nummer, oder „Keine Serie“.
In der Serie: umbenennen, Reihenfolge, auflösen. Alles im verschlüsselten
Meta (series, seriesIndex; '' = bewusst keine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:47:18 +02:00
let series , seriesIndex ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
const type = isPdf ? 'pdf' : 'epub' ;
if ( isPdf ) {
try {
const pdfDoc = await pdfjsLib . getDocument ( { data : new Uint8Array ( buf . slice ( 0 ) ) } ) . promise ;
const meta = await pdfDoc . getMetadata ( ) ;
title = meta . info ? . Title ? . trim ( ) || title ;
author = meta . info ? . Author ? . trim ( ) || '' ;
} catch ( e ) { /* use filename as title */ }
} else {
try {
const zip = await JSZip . loadAsync ( buf . slice ( 0 ) ) ;
const containerXml = await zip . file ( 'META-INF/container.xml' ) . async ( 'text' ) ;
const containerDoc = new DOMParser ( ) . parseFromString ( containerXml , 'application/xml' ) ;
const opfPath = containerDoc . querySelector ( 'rootfile' ) ? . getAttribute ( 'full-path' ) ;
if ( opfPath ) {
const opfText = await zip . file ( opfPath ) . async ( 'text' ) ;
const opfDoc = new DOMParser ( ) . parseFromString ( opfText , 'application/xml' ) ;
title = opfDoc . querySelector ( 'metadata > title, metadata > *|title' ) ? . textContent ? . trim ( ) || title ;
author = opfDoc . querySelector ( 'metadata > creator, metadata > *|creator' ) ? . textContent ? . trim ( ) || '' ;
isbn = _extractIsbnFromOpf ( opfDoc ) ;
Serien für Bücher: erkannt, als Stapel, selbst zuweisen und verwalten
- Beim Hochladen liest diora die Serie aus dem E-Book (calibre:series und
series_index, EPUB 3 belongs-to-collection mit group-position); vorhandene
Bücher werden am Titel erkannt („Der Zauberberg. Erster Band“, „…: Teil 3“).
- In der Bibliothek ist eine Serie ab zwei Bänden ein Stapel („2 Bände · bei
Band 1“), darin die Bände in Reihenfolge; beim Suchen und Filtern einzeln.
- ⋮ → „Serie …“: vorhandene oder neue Serie, Band-Nummer, oder „Keine Serie“.
In der Serie: umbenennen, Reihenfolge, auflösen. Alles im verschlüsselten
Meta (series, seriesIndex; '' = bewusst keine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:47:18 +02:00
const sr = _seriesFromOpf ( opfDoc ) ;
if ( sr ) { series = sr . name ; seriesIndex = sr . index ; }
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
}
} catch ( e ) { /* use filename as title */ }
}
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
if ( opts . skipKnown && _knownBook ( title , author ) ) return 'dup' ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
const key = await getOrCreateEncKey ( ) ;
Serien für Bücher: erkannt, als Stapel, selbst zuweisen und verwalten
- Beim Hochladen liest diora die Serie aus dem E-Book (calibre:series und
series_index, EPUB 3 belongs-to-collection mit group-position); vorhandene
Bücher werden am Titel erkannt („Der Zauberberg. Erster Band“, „…: Teil 3“).
- In der Bibliothek ist eine Serie ab zwei Bänden ein Stapel („2 Bände · bei
Band 1“), darin die Bände in Reihenfolge; beim Suchen und Filtern einzeln.
- ⋮ → „Serie …“: vorhandene oder neue Serie, Band-Nummer, oder „Keine Serie“.
In der Serie: umbenennen, Reihenfolge, auflösen. Alles im verschlüsselten
Meta (series, seriesIndex; '' = bewusst keine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:47:18 +02:00
const meta = { title , author , filename , type , isbn , folder : '' , shelfTag : '' } ;
2026-10-25 12:28:41 +01:00
try { meta . pages = await bookPagesOfFile ( buf . slice ( 0 ) , type ) || undefined ; } catch ( e ) { /* the reader fills it in */ }
Serien für Bücher: erkannt, als Stapel, selbst zuweisen und verwalten
- Beim Hochladen liest diora die Serie aus dem E-Book (calibre:series und
series_index, EPUB 3 belongs-to-collection mit group-position); vorhandene
Bücher werden am Titel erkannt („Der Zauberberg. Erster Band“, „…: Teil 3“).
- In der Bibliothek ist eine Serie ab zwei Bänden ein Stapel („2 Bände · bei
Band 1“), darin die Bände in Reihenfolge; beim Suchen und Filtern einzeln.
- ⋮ → „Serie …“: vorhandene oder neue Serie, Band-Nummer, oder „Keine Serie“.
In der Serie: umbenennen, Reihenfolge, auflösen. Alles im verschlüsselten
Meta (series, seriesIndex; '' = bewusst keine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:47:18 +02:00
if ( series ) { meta . series = series ; if ( seriesIndex != null ) meta . seriesIndex = seriesIndex ; }
const metaJson = new TextEncoder ( ) . encode ( JSON . stringify ( meta ) ) ;
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
const [ metaEnc , dataEnc ] = await Promise . all ( [
encryptBytes ( key , metaJson ) ,
encryptBytes ( key , buf ) ,
] ) ;
if ( statusEl ) statusEl . textContent = 'Lade hoch …' ;
const res = await fetch ( '/books/upload/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( {
meta _ct : metaEnc . ciphertext ,
meta _iv : metaEnc . iv ,
data _ct : dataEnc . ciphertext ,
data _iv : dataEnc . iv ,
} ) ,
} ) ;
const data = await res . json ( ) ;
if ( data . ok ) {
if ( statusEl ) statusEl . textContent = ` ✓ „ ${ title } “ hochgeladen ` ;
Neue Navigations-Icons (Stil B), mehrere Bücher auf einmal, generiertes Cover pro Buch wählbar
- Icons: eigene SVGs, kräftig und geometrisch (2,4 px, eckige Enden), in
nav.js für alle Leisten (DioraNav.icon, sections()/groups() .svg); der
aktive Bereich gefüllt.
- Hochladen: mehrere Dateien auf einmal (Auswahl oder auf die Bibliothek
ziehen) und aus der Cloud per Häkchen („Alle auswählen“, „n importieren“);
nacheinander mit Fortschritt, ein schon vorhandenes Buch (Titel und Autor)
wird übersprungen.
- Pro Buch: „Generiertes Cover verwenden“ statt des eigenen (im verschlüsselten
Meta, cover: 'generated').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:10 +02:00
if ( opts . reload !== false ) loadBookList ( ) ;
else _lastBookListData . push ( { title , author } ) ; // known for the next of a set
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
return true ;
}
if ( statusEl ) statusEl . textContent = 'Fehler: ' + ( data . error ? _germanError ( data . error ) : 'Hochladen fehlgeschlagen' ) ;
return false ;
} catch ( e ) {
if ( statusEl ) statusEl . textContent = 'Hochladen fehlgeschlagen: ' + e . message ;
return false ;
}
}