diora-web/static/js/app/53-library.js

974 lines
55 KiB
JavaScript
Raw Normal View History

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)
series: null, // the series shown (its key), or 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
};
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)}));
}
// ---- series (docs/library.md): set per book in its encrypted meta (series, seriesIndex),
// read from the EPUB on upload, else guessed from the title ("Der Zauberberg. Erster Band").
// series '' means: none, on purpose.
const _LIB_ORD = ['erst', 'zweit', 'dritt', 'viert', 'fünft', 'sechst', 'siebt', 'acht', 'neunt', 'zehnt', 'elft', 'zwölft'];
const _LIB_ORD_EN = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth'];
const _LIB_ROMAN = {i: 1, ii: 2, iii: 3, iv: 4, v: 5, vi: 6, vii: 7, viii: 8, ix: 9, x: 10};
function _libVolumeNumber(text) {
const t = String(text || '').toLowerCase();
const d = /(\d+(?:[.,]\d+)?)/.exec(t);
if (d) return parseFloat(d[1].replace(',', '.'));
for (const w of t.split(/[^a-zäöüß]+/)) {
if (_LIB_ROMAN[w]) return _LIB_ROMAN[w];
for (let i = 0; i < _LIB_ORD.length; i++) if (w.indexOf(_LIB_ORD[i]) === 0) return i + 1;
for (let i = 0; i < _LIB_ORD_EN.length; i++) if (w === _LIB_ORD_EN[i]) return i + 1;
}
return null;
}
function _libSeriesGuess(title) {
const t = String(title || '');
const m = /^(.+?)\s+[–—-]\s+(.+)$/.exec(t) || /^(.+?):\s+(.+)$/.exec(t) || /^(.+?)[.,]\s+(.+)$/.exec(t);
if (!m || !/\b(band|bd\.|teil|buch|folge|staffel|vol\.?|volume|part|book)\b/i.test(m[2]) || /bände|bundle/i.test(m[2])) return null;
return {name: m[1].trim(), index: _libVolumeNumber(m[2]), auto: true};
}
// A book's series: {name, index, auto} or null.
function _libSeries(b) {
if (b.series === '') return null;
if (b.series) {
const ix = typeof b.seriesIndex === 'number' ? b.seriesIndex : (_libSeriesGuess(b.title) || {}).index;
return {name: b.series, index: ix == null ? null : ix, auto: false};
}
return _libSeriesGuess(b.title);
}
const _libSeriesKey = s => s.name.toLowerCase().replace(/\s+/g, ' ').trim();
function _libVolumeSort(a, b) {
const sa = _libSeries(a), sb = _libSeries(b);
const ia = sa && sa.index != null ? sa.index : 1e6, ib = sb && sb.index != null ? sb.index : 1e6;
return ia - ib || a.title.localeCompare(b.title, 'de', {numeric: true});
}
// Every series in the library: key -> {name, books (in order)}.
function _libAllSeries(books) {
const map = new Map();
books.forEach(b => {
const s = _libSeries(b);
if (!s) return;
const k = _libSeriesKey(s);
if (!map.has(k)) map.set(k, {key: k, name: s.name, books: []});
map.get(k).books.push(b);
});
map.forEach(g => g.books.sort(_libVolumeSort));
return map;
}
// ---- editions: one work as PDF and EPUB (or two translations …), shown once ----------------------
// Books with the same `work` key in their meta are editions of one work. The list shows one of
// them: the one read last, else an EPUB, else the first added; the others are behind "Ausgaben".
const _libWork = b => (b && typeof b.work === 'string' && b.work) || '';
function _libEditionGroups(books) {
const map = new Map();
books.forEach(b => {
const w = _libWork(b);
if (!w) return;
if (!map.has(w)) map.set(w, []);
map.get(w).push(b);
});
map.forEach((list, w) => { if (list.length < 2) map.delete(w); });
return map;
}
function _libEditionLead(list) {
return list.slice().sort((a, b) => (_bookRecency(b) - _bookRecency(a)) ||
((a.type === 'epub' ? 0 : 1) - (b.type === 'epub' ? 0 : 1)) || a.id - b.id)[0];
}
// The books with every work once; _lib.editions: the shown book's id -> all its editions.
function _libCollapse(books) {
const groups = _libEditionGroups(books);
_lib.editions = new Map();
if (!groups.size) return books;
const hide = new Set();
groups.forEach(list => {
const lead = _libEditionLead(list);
_lib.editions.set(lead.id, list);
list.forEach(b => { if (b !== lead) hide.add(b.id); });
});
return books.filter(b => !hide.has(b.id));
}
function _libEditionsOf(b) {
const w = _libWork(b);
return w ? (_lastBookListData || []).filter(x => _libWork(x) === w) : [b];
}
const _libFormat = b => (b.type || 'epub').toUpperCase();
function _libFormats(list) {
return Array.from(new Set(list.map(_libFormat))).join(' · ');
}
async function _libLinkEditions(ids) {
const books = ids.map(id => _lastBookListData.find(b => b.id === id)).filter(Boolean);
if (books.length < 2) return;
const key = books.map(_libWork).filter(Boolean)[0] ||
'w' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
try {
for (const b of books) if (_libWork(b) !== key) await _updateBookMeta(b.id, {work: key});
renderBookList(_lastBookListData);
} catch (e) { customAlert('Das ließ sich nicht speichern: ' + e.message); }
}
async function _libUnlinkEdition(b) {
const rest = _libEditionsOf(b).filter(x => x !== b);
try {
await _updateBookMeta(b.id, {work: ''});
// One left on its own is no longer an edition of anything.
if (rest.length === 1) await _updateBookMeta(rest[0].id, {work: ''});
renderBookList(_lastBookListData);
} catch (e) { customAlert('Das ließ sich nicht speichern: ' + e.message); }
}
// "Ausgaben": the editions of a book, each to open or to set apart; or, for a book with none,
// another book to join it with (the ones with a similar title first).
function _libEditionsSheet(b) {
const eds = _libEditionsOf(b);
if (eds.length > 1) {
const rows = eds.slice().sort((x, y) => x.id - y.id).map(x => `<div class="lib-ed-row">
<button type="button" class="lib-sheet-item" data-open="${x.id}"><b>${_libEsc(_libFormat(x))}</b> · ${_libEsc(_libState(x))}
<span class="lib-meta">${_libEsc(x.title)}${x.last_read ? ' · ' + _libEsc(_libAgo(x.last_read)) : ''}</span></button>
<button type="button" class="lib-btn" data-unlink="${x.id}">Lösen</button></div>`).join('');
const wrap = _libSheetBox(`Ausgaben von „${b.title}“`, `<div class="dfo-list">${rows}</div>
<div class="dfo-actions"><button type="button" class="dfo-btn" data-add="1">Weitere Ausgabe verbinden …</button></div>`);
wrap.addEventListener('click', e => {
const o = e.target.closest('[data-open]'), u = e.target.closest('[data-unlink]'), a = e.target.closest('[data-add]');
if (o) { wrap.remove(); openBook(Number(o.dataset.open)); }
else if (u) { wrap.remove(); _libUnlinkEdition(eds.find(x => x.id === Number(u.dataset.unlink))); }
else if (a) { wrap.remove(); _libPickEdition(b); }
});
return;
}
_libPickEdition(b);
}
function _libPickEdition(b) {
const mine = new Set(_libEditionsOf(b).map(x => x.id));
const words = t => _libNorm(t).split(/[^a-z0-9äöüß]+/).filter(w => w.length > 3);
const own = new Set(words(b.title).concat(words(b.author)));
const score = x => words(x.title + ' ' + x.author).filter(w => own.has(w)).length;
const others = _lastBookListData.filter(x => !mine.has(x.id) && x.keyOk !== false)
.map(x => [score(x), x]).sort((p, q) => q[0] - p[0] || p[1].title.localeCompare(q[1].title, 'de')).map(p => p[1]);
const item = x => `<button type="button" class="lib-sheet-item" data-pick="${x.id}"><b>${_libEsc(_libFormat(x))}</b> · ${_libEsc(x.title)}
<span class="lib-meta">${_libEsc(x.author || '')}</span></button>`;
const wrap = _libSheetBox(`Als Ausgabe von „${b.title}“`, `<input class="dfo-input lib-ed-q" type="search" placeholder="Titel, Autor …">
<div class="dfo-list lib-ed-list">${others.slice(0, 30).map(item).join('')}</div>`);
const q = wrap.querySelector('.lib-ed-q');
q.addEventListener('input', () => {
const t = _libNorm(q.value).trim();
wrap.querySelector('.lib-ed-list').innerHTML = others.filter(x => !t || _libNorm(x.title + ' ' + x.author).indexOf(t) >= 0)
.slice(0, 30).map(item).join('');
});
wrap.addEventListener('click', e => {
const p = e.target.closest('[data-pick]');
if (!p) return;
wrap.remove();
_libLinkEditions([b.id, Number(p.dataset.pick)]);
});
}
window.bookEditions = id => { const b = (_lastBookListData || []).find(x => x.id === Number(id)); if (b) _libEditionsSheet(b); };
window.bookEditionsOf = id => { const b = (_lastBookListData || []).find(x => x.id === Number(id)); return b ? _libEditionsOf(b) : []; };
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
// ---- 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) {
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, label) {
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 sel = _lib.selecting && _lib.selecting.has(b.id);
const eds = _lib.editions && _lib.editions.get(b.id);
const by = (label ? label + (b.author ? ' · ' : '') : '') + (b.author || '') + // "Band 2 · Autor" in a series
(eds && _libLayout() === 'list' ? ' · ' + _libFormats(eds) : '');
const edBadge = eds ? `<span class="lib-ed" title="${eds.length} Ausgaben">${_libEsc(_libFormats(eds))}</span>` : '';
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 pct = _libPct(b);
// An issue with its articles (54-issues.js): read of them instead of a percentage.
const iss = typeof _issueSummary === 'function' && _isMagazine(b) ? _issueSummary(b) : null;
if (iss && _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)}</span>
<span class="lib-meta">${_libEsc(_issueCounts(iss))}</span>${_issueSegments(iss, -1, 'iss-seg-thin')}</span>
<button type="button" class="lib-more" data-lib-more="${b.id}" aria-label="Optionen">⋮</button></div>`;
}
if (iss) {
return `<div class="lib-card${sel ? ' lib-sel' : ''}" data-lib-book="${b.id}">
<span class="lib-coverbox">${_libCover(b, 'lib-cover')}
<button type="button" class="lib-more" data-lib-more="${b.id}" aria-label="Optionen">⋮</button></span>
${_issueSegments(iss, -1, 'iss-seg-thin')}
<span class="lib-title">${_libEsc(b.title)}</span>
<span class="lib-meta">${iss.read}/${iss.total} gelesen</span></div>`;
}
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 (_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)}</span>
<span class="lib-meta">${_libEsc(by)}</span></span>
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
<span class="lib-state${b.is_read ? ' lib-done' : ''}">${_libEsc(_libState(b))}</span>
<button type="button" class="lib-more" data-lib-more="${b.id}" aria-label="Optionen">⋮</button></div>`;
}
return `<div class="lib-card${sel ? ' lib-sel' : ''}" data-lib-book="${b.id}">
<span class="lib-coverbox">${_libCover(b, 'lib-cover')}${edBadge}
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 ? '<span class="lib-badge">✓</span>' : ''}
<button type="button" class="lib-more" data-lib-more="${b.id}" aria-label="Optionen">⋮</button></span>
<span class="lib-bar"><span style="width:${pct}%"></span></span>
<span class="lib-title">${_libEsc(b.title)}</span>
<span class="lib-meta">${_libEsc(by)}</span></div>`;
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
}
// ---- "Zuletzt gelesen": the book read last, large, and the ones read before it in a row --------
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
function _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;
}
// The others read lately (finished ones too), newest first, without the large one.
function _libRecentBooks(books, going) {
if (!going) return [];
return _recentlyReadBooks(books).filter(x => x !== going && x.keyOk !== false).slice(0, 6);
}
// "vor 5 Minuten", "gestern", "vor 3 Tagen", "am 4. Okt."
function _libAgo(iso) {
const t = Date.parse(iso || '');
if (!t) return '';
const min = Math.round((Date.now() - t) / 60000);
if (min < 2) return 'gerade eben';
if (min < 60) return `vor ${min} Minuten`;
const h = Math.round(min / 60);
const day = d => new Date(d).toDateString();
if (day(t) === day(Date.now())) return h === 1 ? 'vor einer Stunde' : `vor ${h} Stunden`;
if (day(t) === day(Date.now() - 86400000)) return 'gestern';
const days = Math.round((Date.now() - t) / 86400000);
if (days < 7) return `vor ${days} Tagen`;
return 'am ' + new Date(t).toLocaleDateString('de-DE', {day: 'numeric', month: 'short'});
}
function _libContinue(b, recent) {
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 (!b) return '';
const iss = typeof _issueSummary === 'function' && _isMagazine(b) ? _issueSummary(b) : null;
const nx = iss && iss.next >= 0 ? iss.arts[iss.next] : 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
const pages = b.pages ? ` · Seite ${Math.max(1, Math.round(b.pages * (b.scroll_fraction || 0)))} von ${b.pages}` : '';
const ago = _libAgo(b.last_read);
const row = (recent || []).map(r => `<div class="lib-recent-item" data-lib-book="${r.id}">
<span class="lib-coverbox">${_libCover(r, 'lib-cover')}${r.is_read ? '<span class="lib-badge">✓</span>' : ''}</span>
${(() => { const ri = typeof _issueSummary === 'function' && _isMagazine(r) ? _issueSummary(r) : null;
return ri ? _issueSegments(ri, -1, 'iss-seg-thin') : `<span class="lib-bar"><span style="width:${_libPct(r)}%"></span></span>`; })()}
<span class="lib-title">${_libEsc(r.title)}</span></div>`).join('');
return `<section class="lib-recent" aria-label="Zuletzt gelesen">
<div class="lib-continue" data-lib-book="${b.id}">${_libCover(b, 'lib-hero-cover')}
<span class="lib-text"><span class="lib-kicker">${iss ? 'Im Heft weiterlesen' : 'Weiterlesen'}${ago ? ' · ' + ago : ''}</span>
<span class="lib-title lib-hero-title">${_libEsc(b.title)}</span>
<span class="lib-meta">${nx ? _libEsc('Artikel ' + (iss.next + 1) + ' · ' + nx.t) : _libEsc(b.author || '')}</span>
${iss ? `<span class="lib-hero-progress">${_issueSegments(iss, -1)}<span class="lib-meta">${_libEsc(_issueCounts(iss))}</span></span>`
: `<span class="lib-hero-progress"><span class="lib-bar"><span style="width:${_libPct(b)}%"></span></span>
<span class="lib-meta">${_libPct(b)} %${pages}</span></span>`}
<span class="lib-play" aria-hidden="true">▶ Weiterlesen</span></span></div>
${row ? `<div class="lib-recent-head">Zuletzt gelesen</div><div class="lib-recent-row">${row}</div>` : ''}
</section>`;
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
}
function _libVolumeLabel(b) {
const s = _libSeries(b);
return s && s.index != null ? 'Band ' + String(s.index).replace('.', ',') : '';
}
function _libStackCard(g) {
const next = g.books.filter(b => !b.is_read)[0] || g.books[0];
const read = g.books.filter(b => b.is_read).length;
const going = g.books.filter(_libStarted)[0];
const meta = g.books.length + ' Bände' + (going ? ' · bei ' + (_libVolumeLabel(going) || _libEsc(going.title))
: read === g.books.length ? ' · gelesen' : read ? ' · ' + read + ' gelesen' : '');
const pct = Math.round(100 * g.books.reduce((n, b) => n + _libPct(b), 0) / (100 * g.books.length) * 100) / 100;
if (_libLayout() === 'list') {
return `<div class="lib-row lib-stackrow" data-lib-series="${_libEsc(g.key)}">${_libCover(next, 'lib-thumb')}
<span class="lib-text"><span class="lib-title">${_libEsc(g.name)}</span><span class="lib-meta">${meta}</span></span>
<span class="lib-state">›</span></div>`;
}
return `<div class="lib-card lib-stack" data-lib-series="${_libEsc(g.key)}">
<span class="lib-coverbox lib-stackbox">${_libCover(next, 'lib-cover')}</span>
<span class="lib-bar"><span style="width:${pct}%"></span></span>
<span class="lib-title">${_libEsc(g.name)}</span>
<span class="lib-meta">${meta}</span></div>`;
}
// The books shown as cards, a series of two or more as one stack (in its last-read place).
function _libStacked(list, pool) {
const groups = _libAllSeries(pool); // the volumes in what is shown (a folder, all)
const out = [];
const placed = new Set();
list.forEach(b => {
const s = _libSeries(b);
const g = s ? groups.get(_libSeriesKey(s)) : null;
if (!g || g.books.length < 2) { out.push(_libCard(b)); return; }
if (placed.has(g.key)) return;
placed.add(g.key);
out.push(_libStackCard(g));
});
return out.join('');
}
function _libSeriesView(books) {
const g = _libAllSeries(books).get(_lib.series);
if (!g) { _lib.series = null; return ''; }
const cards = g.books.map(b => _libCard(b, _libVolumeLabel(b))).join('');
return `<div class="lib-place"><button type="button" class="lib-btn" data-lib="series-up">‹ Bücher</button>
<strong class="lib-place-name">${_libEsc(g.name)}</strong>
<button type="button" class="lib-icon" data-lib="series-manage" aria-label="Serie verwalten">⋯</button></div>
<div class="${_libLayout() === 'list' ? 'lib-list' : 'lib-grid'}">${cards}</div>`;
}
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
// 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 = _libCollapse(_bookSectionFilter(allBooks));
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 magazines = window.DioraNav && _bookSection === 'magazines';
const shown = _libShown(books);
// An issue opened (54-issues.js): its contents instead of the list, without the list's search and chips.
const issueOpen = magazines && _lib.issue != null && typeof _issueView === 'function' && books.some(b => b.id === _lib.issue);
listEl.classList.toggle('iss-open', !!issueOpen);
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 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('')}</select>
<button type="button" class="lib-icon" data-lib="layout" aria-label="${_libLayout() === 'grid' ? 'Als Liste' : 'Als Raster'}">${_libLayout() === 'grid' ? '☰' : '⊞'}</button>
<button type="button" class="lib-icon lib-add" data-lib="add" aria-label="Hinzufügen">+</button></div>
<div class="lib-chips"></div>`;
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 …</button>
<button type="button" class="lib-btn" data-lib="sel-editions"${_lib.selecting.size > 1 ? '' : ' disabled'}>Als Ausgaben verbinden</button>
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
<button type="button" class="lib-btn" data-lib="sel-done">Fertig</button></div>`;
}
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 if (issueOpen) {
h += _issueView(books.find(b => b.id === _lib.issue));
} else if (_lib.series && !magazines) {
h += _libSeriesView(books);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
} else {
const going = _libContinueBook(books);
const pool = shown.slice();
h += _libContinue(going, _libRecentBooks(books, going));
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 (going) shown.splice(shown.indexOf(going), 1); // once, on top
// Series as stacks, unless searching, filtering by state or choosing several.
const stack = !magazines && !_lib.q && _lib.status === 'all' && !_lib.selecting;
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 (!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'}">${stack ? _libStacked(shown, pool) : shown.map(_libCard).join('')}</div>`;
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 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);
// Issues still without their articles' index: built now and then, in the background.
if (magazines && typeof _issueIndexLater === 'function') _issueIndexLater(books);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
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(() => {}),
});
}
// ---- series: assign, manage ------------------------------------------------------------------------
function _libSheetBox(title, body) {
const wrap = document.createElement('div');
wrap.className = 'dfo-scrim';
wrap.innerHTML = `<div class="dfo-sheet" role="dialog"><div class="dfo-head"><strong>${_libEsc(title)}</strong>
<button type="button" class="dfo-x" data-x="1" aria-label="Schließen">✕</button></div>${body}</div>`;
wrap.addEventListener('click', e => { if (e.target === wrap || e.target.closest('[data-x]')) wrap.remove(); });
document.body.appendChild(wrap);
return wrap;
}
// A book into a series (an existing one, or a new name) with its number, or out of any.
function _libSeriesSheet(b) {
const now = _libSeries(b);
const names = Array.from(_libAllSeries(_lastBookListData).values()).map(g => g.name).sort((x, y) => x.localeCompare(y, 'de'));
const radios = names.map(n => `<label class="dfo-check"><input type="radio" name="sr" value="${_libEsc(n)}"${now && now.name === n ? ' checked' : ''}> ${_libEsc(n)}</label>`).join('');
const wrap = _libSheetBox(`Serie für „${b.title}“`, `<form class="lib-series-form">
<div class="dfo-list">${radios || '<p class="dfo-note">Noch keine Serien.</p>'}
<label class="dfo-check"><input type="radio" name="sr" value="__new"${now ? '' : ' checked'}> Neue Serie:
<input class="dfo-input" name="newname" maxlength="120" placeholder="Name der Serie" value="${now ? '' : _libEsc((_libSeriesGuess(b.title) || {}).name || '')}"></label></div>
<label class="lib-series-ix">Band <input class="dfo-input" name="ix" inputmode="decimal" size="5" value="${now && now.index != null ? String(now.index).replace('.', ',') : ''}"></label>
<div class="dfo-actions"><button type="button" class="dfo-btn" data-none="1">Keine Serie</button>
<button type="submit" class="dfo-btn dfo-primary">Übernehmen</button></div></form>`);
const form = wrap.querySelector('form');
form.newname.addEventListener('focus', () => { form.querySelector('input[value="__new"]').checked = true; });
const save = async (series, ix) => {
wrap.remove();
try {
await _updateBookMeta(b.id, {series, seriesIndex: ix});
renderBookList(_lastBookListData);
} catch (e) { customAlert('Das ließ sich nicht speichern: ' + e.message); }
};
wrap.querySelector('[data-none]').addEventListener('click', () => save('', null));
form.addEventListener('submit', e => {
e.preventDefault();
const pick = (form.querySelector('input[name="sr"]:checked') || {}).value;
const name = pick === '__new' ? form.newname.value.trim().replace(/\s+/g, ' ') : pick;
if (!name) { form.newname.focus(); return; }
const n = parseFloat(String(form.ix.value).replace(',', '.'));
save(name.slice(0, 120), isFinite(n) ? n : null);
});
}
window.assignBookSeries = id => {
const b = _lastBookListData.find(x => x.id === Number(id));
if (b) _libSeriesSheet(b);
};
// The series shown: rename (every volume), order (numbers 1 … n), dissolve.
function _libSeriesManage() {
const g = _libAllSeries(_lastBookListData).get(_lib.series);
if (!g) return;
let order = g.books.slice();
const draw = () => order.map((b, i) => `<div class="dfo-mrow"><span class="dfo-mname">${i + 1}. ${_libEsc(b.title)}</span>
<button type="button" class="dfo-icon" data-up="${i}"${i ? '' : ' disabled'} aria-label="Nach oben">↑</button>
<button type="button" class="dfo-icon" data-down="${i}"${i < order.length - 1 ? '' : ' disabled'} aria-label="Nach unten">↓</button></div>`).join('');
const wrap = _libSheetBox(`Serie „${g.name}“`, `<div class="dfo-list lib-series-order">${draw()}</div>
<div class="dfo-actions"><button type="button" class="dfo-btn" data-a="rename">Umbenennen …</button>
<button type="button" class="dfo-btn" data-a="dissolve">Auflösen</button>
<button type="button" class="dfo-btn dfo-primary" data-a="save">Reihenfolge übernehmen</button></div>`);
const list = wrap.querySelector('.lib-series-order');
const write = async fn => {
wrap.remove();
try {
for (let i = 0; i < order.length; i++) await _updateBookMeta(order[i].id, fn(order[i], i));
} catch (e) { customAlert('Das ließ sich nicht speichern: ' + e.message); }
renderBookList(_lastBookListData);
};
wrap.addEventListener('click', async e => {
const t = e.target.closest('button');
if (!t) return;
if (t.dataset.up != null || t.dataset.down != null) {
const i = Number(t.dataset.up != null ? t.dataset.up : t.dataset.down), j = t.dataset.up != null ? i - 1 : i + 1;
const x = order[i]; order[i] = order[j]; order[j] = x;
list.innerHTML = draw();
} else if (t.dataset.a === 'save') {
write((b, i) => ({series: g.name, seriesIndex: i + 1}));
} else if (t.dataset.a === 'rename') {
const n = await customPrompt('Neuer Name der Serie:', g.name);
const name = n == null ? '' : String(n).trim().replace(/\s+/g, ' ').slice(0, 120);
if (!name || name === g.name) return;
_lib.series = name.toLowerCase();
write(b => ({series: name, seriesIndex: (_libSeries(b) || {}).index == null ? null : _libSeries(b).index}));
} else if (t.dataset.a === 'dissolve') {
if (!(await customConfirm(`Serie „${g.name}“ auflösen? Die Bücher bleiben.`))) return;
_lib.series = null;
write(() => ({series: '', seriesIndex: 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
// ---- 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)],
[b.cover === 'generated' ? 'Eigenes Cover verwenden' : 'Generiertes Cover verwenden', () => toggleBookCover(b.id)],
['Serie …', () => _libSeriesSheet(b)],
[_libEditionsOf(b).length > 1 ? `Ausgaben (${_libEditionsOf(b).length}) …` : 'Als Ausgabe verbinden …', () => _libEditionsSheet(b)],
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">✕</button></div>
${items.map((it, i) => `<button type="button" class="lib-sheet-item${it[2] ? ' lib-danger' : ''}" data-i="${i}">${it[0]}</button>`).join('')}</div>`;
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]');
const stack = t.closest('[data-lib-series]');
if (!more && !act && !card && !stack) 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
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 || []));
else if (a === 'sel-editions') {
const ids = Array.from(_lib.selecting || []);
_lib.selecting = null;
_libLinkEditions(ids);
}
else if (a === 'series-up') { _lib.series = null; renderBookList(_lastBookListData); }
else if (a === 'series-manage') _libSeriesManage();
return;
}
if (stack && !card) {
_lib.series = stack.dataset.libSeries;
renderBookList(_lastBookListData);
window.scrollTo(0, 0);
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
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;
}
if (b && _isMagazine(b) && typeof _issueOpen === 'function') {
if (card.classList.contains('lib-continue') && _issueSummary(b)) _issueContinue(id);
else _issueOpen(id);
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
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.issue != null) { _lib.issue = null; renderBookList(_lastBookListData); return true; }
if (shown && _lib.series) { _lib.series = null; renderBookList(_lastBookListData); return true; }
Bibliothek neu: Ordner als Filter-Chips überall, Cover-Raster, Cover verschlüsselt und generiert (noch nicht veröffentlicht) - Ordner wie Tags (docs/library.md): folders.js zeichnet die Chip-Reihe, das Blatt „In Ordner …“ (Häkchen, Neuer Ordner) und „Ordner verwalten“ (umbenennen = zusammenlegen, löschen, Reihenfolge). E-Books: `folders` im verschlüsselten Meta, Reihenfolge in verschlüsselten Bibliotheks-Einstellungen (/books/prefs/). Podcasts/Hörbücher: PodcastFeed.folders (Migration 0021, übernimmt `folder`), /podcasts/folders/ (+rename, +delete). Blogs: Reihenfolge (/feeds/folders/order/). - Bibliothek (53-library.js): Kopf mit Suche, Sortierung, Raster/Liste und +; Chips Angefangen · Ungelesen · Gelesen · Ordner; eine „Weiterlesen“-Karte; Mehrfachauswahl (lange drücken). E-Ink und diora Reader beginnen als Liste. - Cover Ende-zu-Ende verschlüsselt (covers.js, /books/<id>/cover/): aus dem E-Book (EPUB-Coverbild, PDF Seite 1), 300 px, mit dem Nutzerschlüssel verschlüsselt, einmal hochgeladen; sparsam im Leerlauf, nie beim Lesen, nie auf E-Ink. - Bücher ohne Cover bekommen eins (gencover.js): Stichwörter aus Titel, Autor, Regal und Ordnern wählen ein Gestaltungskonzept (Konstruktivismus, Geometrie, Psyche, Wellen, Karte, Kosmos, Ornament, Botanik, Berge, Zellen, Raster, Schatten, Typo, sonst Bauhaus); ein Hash aus Titel und Autor Palette, Aufbau und Details – einzigartig, überall gleich, nichts gespeichert. - Hörbücher: Ordner-Chips statt Kacheln, „In Ordner …“ mit mehreren Ordnern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:44:23 +02:00
if (shown && (_lib.q || _lib.searching)) { _lib.q = ''; _lib.searching = false; renderBookList(_lastBookListData); return true; }
return back ? back() : false;
};
if (window.DioraCovers) {
DioraCovers.onReady(id => {
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); });
// 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">✕</button></div>
<button type="button" class="lib-sheet-item" data-a="file">E-Book-Datei (.epub, .pdf) …</button>
${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</button></div>`;
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-kicker { color: var(--accent, #e63946); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }',
// "Zuletzt gelesen": the last book large (cover, title, progress, a button), the others in a row.
'#book-list .lib-recent { margin: 0 0 22px; }',
'#book-list .lib-continue { display: flex; align-items: stretch; gap: 16px; padding: 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 12px; background: var(--card, #0b0b0b); cursor: pointer; }',
'#book-list .lib-hero-cover { flex: none; width: 104px; height: 156px; border-radius: 5px; object-fit: cover; box-shadow: 0 2px 8px rgba(0,0,0,.35); }',
'#book-list .lib-hero-cover.lib-nocover { padding: 8px; }',
'#book-list .lib-continue .lib-text { justify-content: center; gap: 4px; }',
'#book-list .lib-continue .lib-hero-title { font-size: 18px; font-weight: 700; line-height: 1.25; -webkit-line-clamp: 3; }',
'#book-list .lib-hero-progress { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }',
'#book-list .lib-hero-progress .lib-bar { height: 5px; margin-top: 0; }',
'#book-list .lib-play { align-self: flex-start; margin-top: 10px; padding: 9px 18px; border-radius: 20px; background: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 14px; font-weight: 600; white-space: nowrap; }',
'@media (min-width: 721px) { #book-list .lib-hero-cover { width: 136px; height: 204px; } #book-list .lib-continue .lib-hero-title { font-size: 22px; } #book-list .lib-continue { padding: 18px; gap: 22px; } }',
'#book-list .lib-recent-head { margin: 16px 0 8px; font-size: 13px; color: var(--muted, #9a9a9a); }',
'#book-list .lib-recent-row { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }',
'#book-list .lib-recent-row::-webkit-scrollbar { display: none; }',
'#book-list .lib-recent-item { flex: none; width: 92px; display: flex; flex-direction: column; gap: 3px; cursor: pointer; }',
'@media (min-width: 721px) { #book-list .lib-recent-item { width: 112px; } }',
'#book-list .lib-recent-item .lib-title { font-size: 12px; }',
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
'#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-ed { position: absolute; left: 4px; top: 4px; padding: 1px 6px; border-radius: 4px; background: rgba(0,0,0,.62); color: #fff; font-size: 10px; letter-spacing: .03em; }',
'body.eink #book-list .lib-ed { background: #fff; color: #000; border: 1px solid #000; }',
'.lib-ed-row { display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--border, #2a2a2a); }',
'.lib-ed-row .lib-sheet-item { border-bottom: none; flex: 1; }',
'.lib-sheet-item .lib-meta { display: block; font-size: 12px; color: var(--muted, #9a9a9a); }',
'.lib-ed-q { width: 100%; box-sizing: border-box; margin: 0 0 8px; }',
'.lib-ed-list { max-height: 55vh; overflow-y: auto; }',
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
'#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; }',
'body.eink #book-list .lib-continue { border: 2px solid #000; background: #fff; }',
'body.eink #book-list .lib-hero-cover { box-shadow: none; border: 1px solid #000; }',
// Progress on e-ink: a light track, the part read in black (the border colour is black there).
'body.eink #book-list .lib-bar { background: #ccc; }',
'body.eink #book-list .lib-bar > span { background: #000; }',
'#tab-books.lib-drop { outline: 2px dashed var(--accent, #e63946); outline-offset: -6px; }',
'#book-list .lib-stackbox::before, #book-list .lib-stackbox::after { content: ""; position: absolute; left: 7%; right: 7%; height: 100%; border-radius: 4px; border: 1px solid var(--border-strong, #444); background: var(--panel, #161616); }',
'#book-list .lib-stackbox::before { top: -9px; left: 14%; right: 14%; }',
'#book-list .lib-stackbox::after { top: -5px; }',
'#book-list .lib-stackbox .lib-cover { position: relative; z-index: 1; }',
'#book-list .lib-stack { margin-top: 9px; }',
'#book-list .lib-place { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; }',
'#book-list .lib-place-name { flex: 1; min-width: 0; font-size: 16px; }',
'.lib-series-ix { display: flex; align-items: center; gap: 8px; margin: 10px 0 0; }',
'.lib-series-form .dfo-check .dfo-input { margin-left: 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);
})();