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
'use strict' ;
// ---------------------------------------------------------------------------
// The library (docs/library.md): Bücher and Zeitschriften as one page — a header with
// search, sort, grid/list and +, one row of chips (Angefangen · Ungelesen · Gelesen,
// then the folders, docs/library.md: tags, any number per book), one "Weiterlesen"
// card, then the books as covers (grid) or rows (list; e-ink and diora Reader start
// with it). Folders live in each book's encrypted meta (`folders`), their order in the
// encrypted library prefs (/books/prefs/). Covers come from covers.js.
// Takes over renderBookList; loading, decrypting and the book actions stay in
// 52-book-list.js.
// ---------------------------------------------------------------------------
const _LIB _EINK = ( ) => document . documentElement . classList . contains ( 'nui-reader-app' ) ||
( document . body && document . body . classList . contains ( 'eink' ) ) ;
const _lib = {
q : '' ,
searching : false ,
sort : ( ( ) => { try { return localStorage . getItem ( 'diora_lib_sort' ) || 'recent' ; } catch ( e ) { return 'recent' ; } } ) ( ) ,
layout : ( ( ) => { try { return localStorage . getItem ( 'diora_lib_layout' ) || '' ; } catch ( e ) { return '' ; } } ) ( ) ,
status : 'all' , // all | started | unread | read
folder : null , // null: all, '': without a folder, else a name
selecting : null , // a Set of book ids while choosing several
order : null , // the folders' order (decrypted prefs)
} ;
window . _ _dioraLibraryV2 = true ;
document . documentElement . classList . add ( 'lib-v2' ) ;
const _libLayout = ( ) => _lib . layout || ( _LIB _EINK ( ) ? 'list' : 'grid' ) ;
const _libFolders = b => ( typeof _foldersOfMeta === 'function' ? _foldersOfMeta ( b ) : ( b . folder ? [ b . folder ] : [ ] ) ) ;
const _libNorm = s => String ( s || '' ) . normalize ( 'NFD' ) . replace ( /[̀-ͯ]/g , '' ) . toLowerCase ( ) ;
const _libStarted = b => ! b . is _read && ( b . scroll _fraction || 0 ) > 0 ;
const _libUnread = b => ! b . is _read && ! ( b . scroll _fraction > 0 ) ;
// ---- the folders' order: encrypted prefs ------------------------------------------------
async function _libLoadPrefs ( ) {
try { _lib . order = JSON . parse ( localStorage . getItem ( 'diora_lib_order' ) || 'null' ) ; } catch ( e ) { }
try {
const r = await fetch ( '/books/prefs/' , { cache : 'no-store' } ) ;
const d = r . ok ? await r . json ( ) : null ;
if ( d && d . prefs _ct ) {
const plain = await decryptBytes ( await getOrCreateEncKey ( ) , d . prefs _iv , d . prefs _ct ) ;
const prefs = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
_lib . order = Array . isArray ( prefs . folder _order ) ? prefs . folder _order : [ ] ;
try { localStorage . setItem ( 'diora_lib_order' , JSON . stringify ( _lib . order ) ) ; } catch ( e ) { }
if ( _lastBookListData && _lastBookListData . length ) renderBookList ( _lastBookListData ) ;
}
} catch ( e ) { /* offline: the order kept here */ }
}
async function _libSaveOrder ( order ) {
_lib . order = order ;
try { localStorage . setItem ( 'diora_lib_order' , JSON . stringify ( order ) ) ; } catch ( e ) { }
const enc = await encryptBytes ( await getOrCreateEncKey ( ) , new TextEncoder ( ) . encode ( JSON . stringify ( { folder _order : order } ) ) ) ;
await fetch ( '/books/prefs/' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { prefs _ct : enc . ciphertext , prefs _iv : enc . iv } ) } ) ;
}
// Every folder of the books shown (this section), in the user's order.
function _libFolderList ( books ) {
const counts = new Map ( ) ;
books . forEach ( b => _libFolders ( b ) . forEach ( f => counts . set ( f , ( counts . get ( f ) || 0 ) + 1 ) ) ) ;
const order = _lib . order || [ ] ;
const rank = n => { const i = order . indexOf ( n ) ; return i < 0 ? 1e6 : i ; } ;
return Array . from ( counts . keys ( ) ) . sort ( ( a , b ) => rank ( a ) - rank ( b ) || a . localeCompare ( b , 'de' ) )
. map ( n => ( { name : n , count : counts . get ( n ) } ) ) ;
}
// ---- which books, in which order ---------------------------------------------------------------
function _libShown ( books ) {
const q = _libNorm ( _lib . q ) . trim ( ) ;
let list = books . filter ( b =>
( _lib . status === 'all' || ( _lib . status === 'started' ? _libStarted ( b ) : _lib . status === 'unread' ? _libUnread ( b ) : b . is _read ) ) &&
( _lib . folder == null || ( _lib . folder === '' ? ! _libFolders ( b ) . length : _libFolders ( b ) . indexOf ( _lib . folder ) >= 0 ) ) &&
( ! q || _libNorm ( b . title + ' ' + b . author ) . indexOf ( q ) >= 0 ) ) ;
const by = {
recent : ( a , b ) => ( _bookRecency ( b ) - _bookRecency ( a ) ) || String ( b . uploaded _at || '' ) . localeCompare ( String ( a . uploaded _at || '' ) ) ,
title : ( a , b ) => a . title . localeCompare ( b . title , 'de' , { numeric : true } ) ,
author : ( a , b ) => ( a . author || '~' ) . localeCompare ( b . author || '~' , 'de' ) || a . title . localeCompare ( b . title , 'de' , { numeric : true } ) ,
added : ( a , b ) => String ( b . uploaded _at || '' ) . localeCompare ( String ( a . uploaded _at || '' ) ) ,
} ;
return list . slice ( ) . sort ( by [ _lib . sort ] || by . recent ) ;
}
// ---- drawing -------------------------------------------------------------------------------------
function _libEsc ( s ) {
return typeof escapeHtml === 'function' ? escapeHtml ( s ) : String ( s == null ? '' : s ) ;
}
function _libHue ( s ) {
let h = 0 ;
for ( let i = 0 ; i < s . length ; i ++ ) h = ( h * 31 + s . charCodeAt ( i ) ) % 360 ;
return h ;
}
function _libCover ( b , cls ) {
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 url = window . DioraCovers && b . cover !== 'generated' ? DioraCovers . url ( b ) : null ;
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht)
- Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das
Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“
(umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im
verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen
(/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021,
übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge
(/feeds/folders/order/).
- Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +;
Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte;
Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste.
- Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem
E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel
verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie
auf E-Ink.
- Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor,
Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie,
Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster,
Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau
und Details – einzigartig, überall gleich, nichts gespeichert.
- Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
if ( url ) return ` <img class=" ${ cls } " src=" ${ url } " alt="" data-cover-id=" ${ b . id } "> ` ;
// None (yet): one drawn from the book's title, author, category and folders (gencover.js).
if ( window . DioraGenCover ) {
return ` <img class=" ${ cls } " src=" ${ DioraGenCover . url ( b , { gray : _LIB _EINK ( ) } )}" alt="" data-cover-id=" ${ b . id } "> ` ;
}
const initials = ( b . title || '?' ) . trim ( ) . slice ( 0 , 1 ) . toUpperCase ( ) ;
return ` <span class=" ${ cls } lib-nocover" data-cover-id=" ${ b . id } " style="--h: ${ _libHue ( b . title || '' ) } "><span> ${ _libEsc ( b . title || '' ) } </span><b> ${ _libEsc ( initials ) } </b></span> ` ;
}
function _libPct ( b ) {
return b . is _read ? 100 : Math . round ( ( b . scroll _fraction || 0 ) * 100 ) ;
}
function _libState ( b ) {
if ( b . keyOk === false ) return '⚠ falscher Schlüssel' ;
if ( b . is _read ) return '✓ gelesen' ;
const p = _libPct ( b ) ;
return p > 0 ? p + ' %' : 'neu' ;
}
function _libCard ( b ) {
const sel = _lib . selecting && _lib . selecting . has ( b . id ) ;
const pct = _libPct ( b ) ;
if ( _libLayout ( ) === 'list' ) {
return ` <div class="lib-row ${ sel ? ' lib-sel' : '' } " data-lib-book=" ${ b . id } ">
$ { _libCover ( b , 'lib-thumb' ) }
< span class = "lib-text" > < span class = "lib-title" > $ { _libEsc ( b . title ) } < / s p a n >
< span class = "lib-meta" > $ { _libEsc ( b . author || '' ) } < / s p a n > < / s p a n >
< span class = "lib-state${b.is_read ? ' lib-done' : ''}" > $ { _libEsc ( _libState ( b ) ) } < / s p a n >
< button type = "button" class = "lib-more" data - lib - more = "${b.id}" aria - label = "Optionen" > ⋮ < / b u t t o n > < / d i v > ` ;
}
return ` <div class="lib-card ${ sel ? ' lib-sel' : '' } " data-lib-book=" ${ b . id } ">
< span class = "lib-coverbox" > $ { _libCover ( b , 'lib-cover' ) }
$ { b . is _read ? '<span class="lib-badge">✓</span>' : '' }
< button type = "button" class = "lib-more" data - lib - more = "${b.id}" aria - label = "Optionen" > ⋮ < / b u t t o n > < / s p a n >
< span class = "lib-bar" > < span style = "width:${pct}%" > < / s p a n > < / s p a n >
< span class = "lib-title" > $ { _libEsc ( b . title ) } < / s p a n >
< span class = "lib-meta" > $ { _libEsc ( b . author || '' ) } < / s p a n > < / d i v > ` ;
}
function _libContinueBook ( books ) {
if ( _lib . status !== 'all' || _lib . folder != null || _lib . q || _lib . selecting ) return null ;
return _recentlyReadBooks ( books ) . filter ( x => ! x . is _read && x . keyOk !== false ) [ 0 ] || null ;
}
function _libContinue ( b ) {
if ( ! b ) return '' ;
const pages = b . pages ? ` · Seite ${ Math . max ( 1 , Math . round ( b . pages * ( b . scroll _fraction || 0 ) ) ) } von ${ b . pages } ` : '' ;
return ` <div class="lib-continue" data-lib-book=" ${ b . id } "> ${ _libCover ( b , 'lib-thumb lib-thumb-l' ) }
< span class = "lib-text" > < span class = "lib-kicker" > Weiterlesen < / s p a n >
< span class = "lib-title" > $ { _libEsc ( b . title ) } < / s p a n >
< span class = "lib-meta" > $ { _libEsc ( b . author || '' ) } < / s p a n >
< span class = "lib-bar" > < span style = "width:${_libPct(b)}%" > < / s p a n > < / s p a n >
< span class = "lib-meta" > $ { _libPct ( b ) } % $ { pages } < / s p a n > < / s p a n >
< span class = "lib-play" aria - hidden = "true" > ▶ < / s p a n > < / d i v > ` ;
}
// Zeitschriften: the issues under their periodical (automatic, not a folder).
function _libMagazines ( list ) {
const groups = new Map ( ) ;
list . forEach ( b => {
const k = typeof _periodicalOf === 'function' ? _periodicalOf ( b ) : '' ;
if ( ! groups . has ( k ) ) groups . set ( k , [ ] ) ;
groups . get ( k ) . push ( b ) ;
} ) ;
const names = Array . from ( groups . keys ( ) ) . sort ( ( a , b ) => a . localeCompare ( b , 'de' ) ) ;
return names . map ( n => {
const issues = groups . get ( n ) . sort ( ( a , b ) => b . title . localeCompare ( a . title , 'de' , { numeric : true } ) ) ;
return ` <h3 class="lib-group"> ${ _libEsc ( n ) } <span class="lib-count"> ${ issues . length } </span></h3> ` +
` <div class=" ${ _libLayout ( ) === 'list' ? 'lib-list' : 'lib-grid' } "> ${ issues . map ( _libCard ) . join ( '' ) } </div> ` ;
} ) . join ( '' ) ;
}
function _libRender ( allBooks ) {
const listEl = $ ( 'book-list' ) ;
if ( ! listEl ) return ;
_lastBookListData = allBooks ;
const books = _bookSectionFilter ( allBooks ) ;
const magazines = window . DioraNav && _bookSection === 'magazines' ;
const shown = _libShown ( books ) ;
const narrow = window . matchMedia && matchMedia ( '(max-width: 720px)' ) . matches ;
const sorts = [ [ 'recent' , 'Zuletzt' ] , [ 'title' , 'Titel' ] , [ 'author' , 'Autor' ] , [ 'added' , 'Neu hinzugefügt' ] ] ;
let h = ` <div class="lib-head"><h2 class="lib-h"> ${ magazines ? 'Zeitschriften' : 'Bücher' } </h2>
$ { ! narrow || _lib . searching || _lib . q ? ` <input class="lib-q" type="search" placeholder="Titel, Autor …" value=" ${ _libEsc ( _lib . q ) } "> ` :
'<button type="button" class="lib-icon" data-lib="search" aria-label="Suchen">⌕</button>' }
< select class = "lib-sort" aria - label = "Sortieren" > $ { sorts . map ( s => ` <option value=" ${ s [ 0 ] } " ${ _lib . sort === s [ 0 ] ? ' selected' : '' } > ${ s [ 1 ] } </option> ` ) . join ( '' ) } < / s e l e c t >
< button type = "button" class = "lib-icon" data - lib = "layout" aria - label = "${_libLayout() === 'grid' ? 'Als Liste' : 'Als Raster'}" > $ { _libLayout ( ) === 'grid' ? '☰' : '⊞' } < / b u t t o n >
< button type = "button" class = "lib-icon lib-add" data - lib = "add" aria - label = "Hinzufügen" > + < / b u t t o n > < / d i v >
< div class = "lib-chips" > < / d i v > ` ;
if ( _lib . selecting ) {
h += ` <div class="lib-selbar"><span> ${ _lib . selecting . size } ausgewählt</span>
< button type = "button" class = "lib-btn" data - lib = "sel-folders" $ { _lib . selecting . size ? '' : ' disabled' } > In Ordner … < / b u t t o n >
< button type = "button" class = "lib-btn" data - lib = "sel-done" > Fertig < / b u t t o n > < / d i v > ` ;
}
if ( ! allBooks . length ) {
h += '<p class="lib-empty">Noch keine Bücher. Mit + eine .epub- oder .pdf-Datei hinzufügen.</p>' ;
} else if ( ! books . length ) {
h += ` <p class="lib-empty"> ${ magazines ? 'Noch keine Zeitschriften. Ein Heft unter Bücher über ⋮ → „Als Zeitschrift führen“ hierher holen.' : 'Alle Bücher stehen bei den Zeitschriften.' } </p> ` ;
} else {
const going = _libContinueBook ( books ) ;
h += _libContinue ( going ) ;
if ( going ) shown . splice ( shown . indexOf ( going ) , 1 ) ; // once, on top
if ( ! shown . length && ! going ) h += '<p class="lib-empty">Nichts gefunden.</p>' ;
else if ( magazines && _lib . sort === 'recent' && ! _lib . q ) h += _libMagazines ( shown ) ;
else h += ` <div class=" ${ _libLayout ( ) === 'list' ? 'lib-list' : 'lib-grid' } "> ${ shown . map ( _libCard ) . join ( '' ) } </div> ` ;
}
const q = listEl . querySelector ( '.lib-q' ) ;
const focused = q && document . activeElement === q ;
listEl . innerHTML = h ;
const status = document . getElementById ( 'book-upload-status' ) ;
if ( status && status . textContent . trim ( ) ) listEl . querySelector ( '.lib-head' ) . after ( status ) ;
_libChips ( books ) ;
if ( focused || ( _lib . searching && ! _lib . q ) ) {
const n = listEl . querySelector ( '.lib-q' ) ;
if ( n ) { n . focus ( ) ; n . setSelectionRange ( n . value . length , n . value . length ) ; }
}
}
function _libChips ( books ) {
const el = document . querySelector ( '#book-list .lib-chips' ) ;
if ( ! el || ! window . DioraFolders ) return ;
const folders = _libFolderList ( books ) ;
const none = books . filter ( b => ! _libFolders ( b ) . length ) . length ;
DioraFolders . chips ( el , {
lead : [
{ id : 'started' , label : 'Angefangen' , count : books . filter ( _libStarted ) . length , on : _lib . status === 'started' } ,
{ id : 'unread' , label : 'Ungelesen' , count : books . filter ( _libUnread ) . length , on : _lib . status === 'unread' } ,
{ id : 'read' , label : 'Gelesen' , count : books . filter ( b => b . is _read ) . length , on : _lib . status === 'read' } ,
] ,
folders , active : _lib . folder , none : folders . length ? none : 0 ,
onPick ( p ) {
if ( p && p . lead ) _lib . status = _lib . status === p . lead ? 'all' : p . lead ;
else _lib . folder = p ;
renderBookList ( _lastBookListData ) ;
} ,
onManage : folders . length ? _libManage : null ,
} ) ;
}
window . renderBookList = _libRender ;
// ---- folders ---------------------------------------------------------------------------------------
// Into folders: one book (its folders as they are), or several (the folders they share).
function _libAssign ( ids ) {
const books = ids . map ( id => _lastBookListData . find ( b => b . id === id ) ) . filter ( Boolean ) ;
if ( ! books . length || ! window . DioraFolders ) return ;
const common = _libFolders ( books [ 0 ] ) . filter ( f => books . every ( b => _libFolders ( b ) . indexOf ( f ) >= 0 ) ) ;
DioraFolders . assign ( {
title : books . length === 1 ? ` Ordner für „ ${ books [ 0 ] . title } “ ` : ` Ordner für ${ books . length } Bücher ` ,
folders : _libFolderList ( _lastBookListData ) . map ( f => f . name ) ,
selected : common ,
async onDone ( picked ) {
try {
for ( const b of books ) {
const kept = _libFolders ( b ) . filter ( f => common . indexOf ( f ) < 0 || picked . indexOf ( f ) >= 0 ) ;
const next = kept . concat ( picked . filter ( f => kept . indexOf ( f ) < 0 ) ) ;
if ( next . join ( '\u0000' ) !== _libFolders ( b ) . join ( '\u0000' ) ) await _updateBookMeta ( b . id , { folders : next } ) ;
}
} catch ( e ) {
customAlert ( 'Das ließ sich nicht speichern: ' + e . message ) ;
}
_lib . selecting = null ;
renderBookList ( _lastBookListData ) ;
} ,
} ) ;
}
window . assignBookFolder = id => _libAssign ( [ Number ( id ) ] ) ;
function _libManage ( ) {
if ( ! window . DioraFolders ) return ;
const rewrite = async fn => {
for ( const b of _lastBookListData ) {
const was = _libFolders ( b ) ;
const next = fn ( was ) . filter ( ( f , i , all ) => f && all . indexOf ( f ) === i ) ;
if ( next . join ( '\u0000' ) !== was . join ( '\u0000' ) ) await _updateBookMeta ( b . id , { folders : next } ) ;
}
renderBookList ( _lastBookListData ) ;
} ;
DioraFolders . manage ( {
title : 'Ordner der Bücher' ,
folders : _libFolderList ( _lastBookListData ) . map ( f => f . name ) ,
onRename : ( old , n ) => {
if ( _lib . folder === old ) _lib . folder = n ;
return rewrite ( fs => fs . map ( f => ( f === old ? n : f ) ) ) . then ( ( ) =>
_libSaveOrder ( ( _lib . order || [ ] ) . map ( f => ( f === old ? n : f ) ) . filter ( ( f , i , a ) => a . indexOf ( f ) === i ) ) ) ;
} ,
onDelete : name => {
if ( _lib . folder === name ) _lib . folder = null ;
return rewrite ( fs => fs . filter ( f => f !== name ) ) . then ( ( ) => _libSaveOrder ( ( _lib . order || [ ] ) . filter ( f => f !== name ) ) ) ;
} ,
onOrder : order => _libSaveOrder ( order ) . then ( ( ) => renderBookList ( _lastBookListData ) ) . catch ( ( ) => { } ) ,
} ) ;
}
// ---- a book's actions (the apps may bring their own sheet: __dioraBookSheet) -------------------------
function _libSheet ( b ) {
if ( window . _ _dioraBookSheet && window . _ _dioraBookSheet ( b ) !== false ) return ;
const broken = typeof _brokenBooks !== 'undefined' && _brokenBooks . has ( b . id ) ;
const items = [
b . keyOk !== false && [ 'Lesen' , ( ) => openBook ( b . id ) ] ,
[ 'In Ordner …' , ( ) => _libAssign ( [ b . id ] ) ] ,
[ _isMagazine ( b ) ? 'Als Buch führen' : 'Als Zeitschrift führen' , ( ) => toggleBookMagazine ( b . id ) ] ,
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
[ b . cover === 'generated' ? 'Eigenes Cover verwenden' : 'Generiertes Cover verwenden' , ( ) => toggleBookCover ( b . id ) ] ,
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
[ b . is _read ? 'Als ungelesen markieren' : 'Als gelesen markieren' , ( ) => toggleBookRead ( b . id , ! ! b . is _read ) ] ,
b . has _highlights && [ 'Markierungen exportieren' , ( ) => downloadBookAnnotationsFromList ( b . id , null ) ] ,
[ 'Regal-Kategorie abrufen' , ( ) => lookupBookMetadata ( b . id ) ] ,
[ 'Mehrere auswählen' , ( ) => { _lib . selecting = new Set ( [ b . id ] ) ; renderBookList ( _lastBookListData ) ; } ] ,
broken && [ 'Reparieren' , ( ) => repairBook ( b . id ) ] ,
[ 'Löschen' , ( ) => deleteBook ( b . id ) , true ] ,
] . filter ( Boolean ) ;
const wrap = document . createElement ( 'div' ) ;
wrap . className = 'dfo-scrim' ;
wrap . innerHTML = ` <div class="dfo-sheet" role="dialog"><div class="dfo-head"><strong> ${ _libEsc ( b . title ) } </strong>
< button type = "button" class = "dfo-x" data - x = "1" aria - label = "Schließen" > ✕ < / b u t t o n > < / d i v >
$ { items . map ( ( it , i ) => ` <button type="button" class="lib-sheet-item ${ it [ 2 ] ? ' lib-danger' : '' } " data-i=" ${ i } "> ${ it [ 0 ] } </button> ` ) . join ( '' ) } < / d i v > ` ;
const close = ( ) => wrap . remove ( ) ;
wrap . addEventListener ( 'click' , e => {
const t = e . target . closest ( '[data-i]' ) ;
if ( e . target === wrap || e . target . closest ( '[data-x]' ) ) { close ( ) ; return ; }
if ( t ) { close ( ) ; items [ Number ( t . dataset . i ) ] [ 1 ] ( ) ; }
} ) ;
document . body . appendChild ( wrap ) ;
}
// ---- events ------------------------------------------------------------------------------------------
( function ( ) {
const listEl = $ ( 'book-list' ) ;
if ( ! listEl ) return ;
let pressTimer = 0 ;
let pressed = null ;
// First on the list: the apps' own card clicks (native-ui.js) don't run for these cards.
listEl . addEventListener ( 'click' , e => {
const t = e . target ;
const more = t . closest ( '[data-lib-more]' ) ;
const act = t . closest ( '[data-lib]' ) ;
const card = t . closest ( '[data-lib-book]' ) ;
if ( ! more && ! act && ! card ) return ;
e . stopImmediatePropagation ( ) ;
if ( pressed ) { pressed = null ; return ; } // the long press already chose
if ( more ) {
const b = _lastBookListData . find ( x => x . id === Number ( more . dataset . libMore ) ) ;
if ( b ) _libSheet ( b ) ;
return ;
}
if ( act ) {
const a = act . dataset . lib ;
if ( a === 'search' ) { _lib . searching = true ; renderBookList ( _lastBookListData ) ; }
else if ( a === 'layout' ) {
_lib . layout = _libLayout ( ) === 'grid' ? 'list' : 'grid' ;
try { localStorage . setItem ( 'diora_lib_layout' , _lib . layout ) ; } catch ( err ) { }
renderBookList ( _lastBookListData ) ;
} else if ( a === 'add' ) _libAddMenu ( ) ;
else if ( a === 'sel-done' ) { _lib . selecting = null ; renderBookList ( _lastBookListData ) ; }
else if ( a === 'sel-folders' ) _libAssign ( Array . from ( _lib . selecting || [ ] ) ) ;
return ;
}
const id = Number ( card . dataset . libBook ) ;
if ( _lib . selecting ) {
if ( _lib . selecting . has ( id ) ) _lib . selecting . delete ( id ) ; else _lib . selecting . add ( id ) ;
renderBookList ( _lastBookListData ) ;
return ;
}
const b = _lastBookListData . find ( x => x . id === id ) ;
if ( b && b . keyOk === false ) {
customAlert ( 'Dieses Buch lässt sich mit deinem Schlüssel nicht entschlüsseln. Über ⋮ → „Schlüssel einfügen …“ den richtigen Schlüssel eintragen.' ) ;
return ;
}
openBook ( id ) ;
} , true ) ;
// A long press on a book: choosing several.
listEl . addEventListener ( 'pointerdown' , e => {
const card = e . target . closest ( '[data-lib-book]' ) ;
if ( ! card || e . target . closest ( 'button' ) ) return ;
clearTimeout ( pressTimer ) ;
pressTimer = setTimeout ( ( ) => {
const id = Number ( card . dataset . libBook ) ;
_lib . selecting = _lib . selecting || new Set ( ) ;
_lib . selecting . add ( id ) ;
pressed = id ;
renderBookList ( _lastBookListData ) ;
} , 550 ) ;
} ) ;
[ 'pointerup' , 'pointercancel' , 'pointerleave' ] . forEach ( n => listEl . addEventListener ( n , ( ) => clearTimeout ( pressTimer ) ) ) ;
listEl . addEventListener ( 'input' , e => {
if ( ! e . target . classList . contains ( 'lib-q' ) ) return ;
_lib . q = e . target . value ;
renderBookList ( _lastBookListData ) ;
} ) ;
listEl . addEventListener ( 'change' , e => {
if ( ! e . target . classList . contains ( 'lib-sort' ) ) return ;
_lib . sort = e . target . value ;
try { localStorage . setItem ( 'diora_lib_sort' , _lib . sort ) ; } catch ( err ) { }
renderBookList ( _lastBookListData ) ;
} ) ;
// Back (Android): out of choosing, out of a search.
const back = window . _ _dioraBack ;
window . _ _dioraBack = function ( ) {
const shown = $ ( 'tab-books' ) && $ ( 'tab-books' ) . style . display !== 'none' ;
if ( shown && _lib . selecting ) { _lib . selecting = null ; renderBookList ( _lastBookListData ) ; return true ; }
if ( shown && ( _lib . q || _lib . searching ) ) { _lib . q = '' ; _lib . searching = false ; renderBookList ( _lastBookListData ) ; return true ; }
return back ? back ( ) : false ;
} ;
if ( window . DioraCovers ) {
DioraCovers . onReady ( id => {
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 b = ( _lastBookListData || [ ] ) . find ( x => x . id === id ) ;
if ( b && b . cover === 'generated' ) return ;
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 url = DioraCovers . url ( { id } ) ;
document . querySelectorAll ( ` #book-list [data-cover-id=" ${ id } "] ` ) . forEach ( el => {
if ( el . tagName === 'IMG' ) { el . src = url ; return ; }
const img = document . createElement ( 'img' ) ;
img . className = el . className . replace ( 'lib-nocover' , '' ) . trim ( ) ;
img . alt = '' ;
img . dataset . coverId = id ;
img . src = url ;
el . replaceWith ( img ) ;
} ) ;
} ) ;
}
window . addEventListener ( 'resize' , ( ) => { if ( _lastBookListData && _lastBookListData . length ) renderBookList ( _lastBookListData ) ; } ) ;
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
// Files dropped anywhere on the library (the old drop zone is hidden): one or several.
const tabEl = $ ( 'tab-books' ) ;
if ( tabEl ) {
tabEl . addEventListener ( 'dragover' , e => {
if ( e . dataTransfer && Array . from ( e . dataTransfer . types || [ ] ) . indexOf ( 'Files' ) >= 0 ) { e . preventDefault ( ) ; tabEl . classList . add ( 'lib-drop' ) ; }
} ) ;
tabEl . addEventListener ( 'dragleave' , e => { if ( e . target === tabEl ) tabEl . classList . remove ( 'lib-drop' ) ; } ) ;
tabEl . addEventListener ( 'drop' , e => {
tabEl . classList . remove ( 'lib-drop' ) ;
const files = Array . from ( ( e . dataTransfer && e . dataTransfer . files ) || [ ] ) ;
if ( ! files . length ) return ;
e . preventDefault ( ) ;
if ( files . length === 1 ) uploadEbook ( files [ 0 ] ) ;
else _importMany ( files . map ( f => ( { name : f . name , size : f . size , buf : ( ) => f . arrayBuffer ( ) } ) ) ) ;
} ) ;
}
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
_libLoadPrefs ( ) ;
} ) ( ) ;
function _libAddMenu ( ) {
const wrap = document . createElement ( 'div' ) ;
wrap . className = 'dfo-scrim' ;
const cloud = ! ( window . DioraNative && DioraNative . isReaderApp && DioraNative . isReaderApp ( ) ) ;
wrap . innerHTML = ` <div class="dfo-sheet" role="dialog"><div class="dfo-head"><strong>Hinzufügen</strong>
< button type = "button" class = "dfo-x" data - x = "1" aria - label = "Schließen" > ✕ < / b u t t o n > < / d i v >
< button type = "button" class = "lib-sheet-item" data - a = "file" > E - Book - Datei ( . epub , . pdf ) … < / b u t t o n >
$ { cloud ? '<button type="button" class="lib-sheet-item" data-a="cloud">Aus der Cloud (WebDAV, Nextcloud) …</button>' : '' }
< button type = "button" class = "lib-sheet-item" data - a = "select" > Mehrere Bücher auswählen < / b u t t o n > < / d i v > ` ;
wrap . addEventListener ( 'click' , e => {
const a = e . target . closest ( '[data-a]' ) ;
if ( e . target === wrap || e . target . closest ( '[data-x]' ) ) { wrap . remove ( ) ; return ; }
if ( ! a ) return ;
wrap . remove ( ) ;
if ( a . dataset . a === 'file' ) { const f = $ ( 'book-file-input' ) ; if ( f ) f . click ( ) ; }
else if ( a . dataset . a === 'cloud' ) toggleCloudImport ( ) ;
else { _lib . selecting = new Set ( ) ; renderBookList ( _lastBookListData ) ; }
} ) ;
document . body . appendChild ( wrap ) ;
}
// ---- styles ---------------------------------------------------------------------------------------------
( function ( ) {
const style = document . createElement ( 'style' ) ;
style . textContent = [
'html.lib-v2 #tab-books .book-drop-zone, html.lib-v2 #tab-books .book-list-filter,' ,
'html.lib-v2 #book-upload-area > button.btn, html.lib-v2 #book-upload-area > .nui-add-row { display: none !important; }' ,
'#book-list .lib-head { display: flex; align-items: center; gap: 6px; margin: 4px 0 8px; }' ,
'#book-list .lib-h { margin: 0 auto 0 0; font-size: 18px; }' ,
'#book-list .lib-q { flex: 0 1 260px; min-width: 0; padding: 8px 10px; border-radius: 6px; border: 1px solid var(--border, #2a2a2a); background: var(--bg, #000); color: inherit; font: inherit; }' ,
'#book-list .lib-sort { padding: 7px 6px; border-radius: 6px; border: 1px solid var(--border, #2a2a2a); background: var(--bg, #000); color: inherit; font: inherit; font-size: 13px; max-width: 140px; }' ,
'#book-list .lib-icon { min-width: 38px; min-height: 38px; border-radius: 19px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 17px; cursor: pointer; }' ,
'#book-list .lib-add { background: var(--accent, #e63946); border-color: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 20px; }' ,
'#book-list .lib-chips { margin: 0 0 10px; }' ,
'#book-list .lib-selbar { display: flex; align-items: center; gap: 8px; padding: 8px 10px; margin: 0 0 10px; border-radius: 8px; background: var(--raised, #222); }' ,
'#book-list .lib-selbar span { flex: 1; }' ,
'#book-list .lib-btn, .lib-sheet-item { min-height: 36px; padding: 0 14px; border-radius: 18px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 14px; cursor: pointer; }' ,
'.lib-sheet-item { display: block; width: 100%; text-align: left; border: none; border-radius: 0; border-bottom: 1px solid var(--border, #2a2a2a); min-height: 46px; }' ,
'.lib-danger { color: var(--accent, #e63946); }' ,
'#book-list .lib-continue { display: flex; align-items: center; gap: 12px; padding: 10px; margin: 0 0 16px; border: 1px solid var(--border, #2a2a2a); border-radius: 10px; background: var(--card, #0b0b0b); cursor: pointer; }' ,
'#book-list .lib-kicker { color: var(--accent, #e63946); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }' ,
'#book-list .lib-play { flex: none; width: 46px; height: 46px; border-radius: 50%; background: var(--accent, #e63946); color: var(--on-accent, #fff); display: flex; align-items: center; justify-content: center; font-size: 17px; }' ,
'#book-list .lib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 18px 14px; }' ,
'@media (max-width: 480px) { #book-list .lib-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 10px; } }' ,
'#book-list .lib-card { display: flex; flex-direction: column; gap: 3px; min-width: 0; cursor: pointer; }' ,
'#book-list .lib-coverbox { position: relative; display: block; }' ,
'#book-list .lib-cover { display: block; width: 100%; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 4px; background: var(--panel, #161616); box-shadow: 0 1px 3px rgba(0,0,0,.35); }' ,
'#book-list .lib-nocover { display: flex; flex-direction: column; justify-content: space-between; padding: 8px; box-sizing: border-box; overflow: hidden; background: hsl(var(--h), 28%, 24%); color: hsl(var(--h), 40%, 88%); }' ,
'#book-list .lib-nocover span { font-size: 12px; line-height: 1.25; display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; }' ,
'#book-list .lib-nocover b { font-size: 26px; opacity: .5; align-self: flex-end; }' ,
'#book-list .lib-thumb { width: 34px; height: 51px; flex: none; border-radius: 3px; object-fit: cover; }' ,
'#book-list .lib-thumb.lib-nocover { padding: 2px; }' ,
'#book-list .lib-thumb.lib-nocover span { display: none; }' ,
'#book-list .lib-thumb.lib-nocover b { font-size: 15px; align-self: center; margin: auto; }' ,
'#book-list .lib-thumb-l { width: 52px; height: 78px; }' ,
'#book-list .lib-badge { position: absolute; left: 6px; bottom: 6px; padding: 1px 6px; border-radius: 9px; background: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 11px; }' ,
'#book-list .lib-card .lib-more { position: absolute; right: 2px; top: 2px; width: 30px; height: 30px; border-radius: 15px; border: none; background: rgba(0,0,0,.45); color: #fff; font-size: 16px; cursor: pointer; opacity: 0; }' ,
'#book-list .lib-card:hover .lib-more, #book-list .lib-card:focus-within .lib-more { opacity: 1; }' ,
'@media (hover: none) { #book-list .lib-card .lib-more { opacity: .85; } }' ,
'#book-list .lib-bar { display: block; height: 3px; border-radius: 2px; background: var(--border, #2a2a2a); overflow: hidden; margin-top: 4px; }' ,
'#book-list .lib-bar > span { display: block; height: 100%; background: var(--accent, #e63946); }' ,
'#book-list .lib-title { font-size: 13px; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }' ,
'#book-list .lib-meta { font-size: 12px; color: var(--muted, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }' ,
'#book-list .lib-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }' ,
'#book-list .lib-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border, #2a2a2a); cursor: pointer; }' ,
'#book-list .lib-row .lib-more { background: none; border: none; color: inherit; font-size: 18px; min-width: 36px; min-height: 36px; cursor: pointer; }' ,
'#book-list .lib-state { font-size: 12px; color: var(--muted, #9a9a9a); white-space: nowrap; }' ,
'#book-list .lib-done { color: var(--accent, #e63946); }' ,
'#book-list .lib-sel .lib-cover, #book-list .lib-sel .lib-thumb { outline: 3px solid var(--accent, #e63946); outline-offset: 2px; }' ,
'#book-list .lib-row.lib-sel { background: var(--hover, rgba(255,255,255,.05)); }' ,
'#book-list .lib-group { margin: 18px 0 8px; font-size: 13px; font-weight: 400; color: var(--muted, #9a9a9a); }' ,
'#book-list .lib-count { opacity: .7; }' ,
'#book-list .lib-empty { color: var(--muted, #9a9a9a); font-size: 14px; }' ,
'body.eink #book-list .lib-add, body.eink #book-list .lib-play { background: #000; color: #fff; }' ,
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
'#tab-books.lib-drop { outline: 2px dashed var(--accent, #e63946); outline-offset: -6px; }' ,
'#book-upload-status { display: block; margin: 0 0 10px; color: var(--muted, #9a9a9a); font-size: 13px; }' ,
'.cloud-chosen { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 6px 0; }' ,
'.cloud-chosen:empty { display: none; }' ,
'.cloud-entry .cloud-check { width: 18px; height: 18px; margin: 0 8px 0 2px; flex: none; }' ,
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
'body.eink #book-list .lib-cover { box-shadow: none; border: 1px solid #000; }' ,
] . join ( '\n' ) ;
document . head . appendChild ( style ) ;
} ) ( ) ;