'use strict'; // --------------------------------------------------------------------------- // The library (docs/library.md): Bücher and Zeitschriften as one page — a header with // search, sort, grid/list and +, one row of chips (Angefangen · Ungelesen · Gelesen, // then the folders, docs/library.md: tags, any number per book), one "Weiterlesen" // card, then the books as covers (grid) or rows (list; e-ink and diora Reader start // with it). Folders live in each book's encrypted meta (`folders`), their order in the // encrypted library prefs (/books/prefs/). Covers come from covers.js. // Takes over renderBookList; loading, decrypting and the book actions stay in // 52-book-list.js. // --------------------------------------------------------------------------- const _LIB_EINK = () => document.documentElement.classList.contains('nui-reader-app') || (document.body && document.body.classList.contains('eink')); const _lib = { q: '', searching: false, sort: (() => { try { return localStorage.getItem('diora_lib_sort') || 'recent'; } catch (e) { return 'recent'; } })(), layout: (() => { try { return localStorage.getItem('diora_lib_layout') || ''; } catch (e) { return ''; } })(), status: 'all', // all | started | unread | read folder: null, // null: all, '': without a folder, else a name selecting: null, // a Set of book ids while choosing several order: null, // the folders' order (decrypted prefs) }; window.__dioraLibraryV2 = true; document.documentElement.classList.add('lib-v2'); const _libLayout = () => _lib.layout || (_LIB_EINK() ? 'list' : 'grid'); const _libFolders = b => (typeof _foldersOfMeta === 'function' ? _foldersOfMeta(b) : (b.folder ? [b.folder] : [])); const _libNorm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase(); const _libStarted = b => !b.is_read && (b.scroll_fraction || 0) > 0; const _libUnread = b => !b.is_read && !(b.scroll_fraction > 0); // ---- the folders' order: encrypted prefs ------------------------------------------------ async function _libLoadPrefs() { try { _lib.order = JSON.parse(localStorage.getItem('diora_lib_order') || 'null'); } catch (e) {} try { const r = await fetch('/books/prefs/', {cache: 'no-store'}); const d = r.ok ? await r.json() : null; if (d && d.prefs_ct) { const plain = await decryptBytes(await getOrCreateEncKey(), d.prefs_iv, d.prefs_ct); const prefs = JSON.parse(new TextDecoder().decode(plain)); _lib.order = Array.isArray(prefs.folder_order) ? prefs.folder_order : []; try { localStorage.setItem('diora_lib_order', JSON.stringify(_lib.order)); } catch (e) {} if (_lastBookListData && _lastBookListData.length) renderBookList(_lastBookListData); } } catch (e) { /* offline: the order kept here */ } } async function _libSaveOrder(order) { _lib.order = order; try { localStorage.setItem('diora_lib_order', JSON.stringify(order)); } catch (e) {} const enc = await encryptBytes(await getOrCreateEncKey(), new TextEncoder().encode(JSON.stringify({folder_order: order}))); await fetch('/books/prefs/', {method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, body: JSON.stringify({prefs_ct: enc.ciphertext, prefs_iv: enc.iv})}); } // Every folder of the books shown (this section), in the user's order. function _libFolderList(books) { const counts = new Map(); books.forEach(b => _libFolders(b).forEach(f => counts.set(f, (counts.get(f) || 0) + 1))); const order = _lib.order || []; const rank = n => { const i = order.indexOf(n); return i < 0 ? 1e6 : i; }; return Array.from(counts.keys()).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b, 'de')) .map(n => ({name: n, count: counts.get(n)})); } // ---- which books, in which order --------------------------------------------------------------- function _libShown(books) { const q = _libNorm(_lib.q).trim(); let list = books.filter(b => (_lib.status === 'all' || (_lib.status === 'started' ? _libStarted(b) : _lib.status === 'unread' ? _libUnread(b) : b.is_read)) && (_lib.folder == null || (_lib.folder === '' ? !_libFolders(b).length : _libFolders(b).indexOf(_lib.folder) >= 0)) && (!q || _libNorm(b.title + ' ' + b.author).indexOf(q) >= 0)); const by = { recent: (a, b) => (_bookRecency(b) - _bookRecency(a)) || String(b.uploaded_at || '').localeCompare(String(a.uploaded_at || '')), title: (a, b) => a.title.localeCompare(b.title, 'de', {numeric: true}), author: (a, b) => (a.author || '~').localeCompare(b.author || '~', 'de') || a.title.localeCompare(b.title, 'de', {numeric: true}), added: (a, b) => String(b.uploaded_at || '').localeCompare(String(a.uploaded_at || '')), }; return list.slice().sort(by[_lib.sort] || by.recent); } // ---- drawing ------------------------------------------------------------------------------------- function _libEsc(s) { return typeof escapeHtml === 'function' ? escapeHtml(s) : String(s == null ? '' : s); } function _libHue(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360; return h; } function _libCover(b, cls) { const url = window.DioraCovers && b.cover !== 'generated' ? DioraCovers.url(b) : null; if (url) return ``; // None (yet): one drawn from the book's title, author, category and folders (gencover.js). if (window.DioraGenCover) { return ``; } const initials = (b.title || '?').trim().slice(0, 1).toUpperCase(); return `${_libEsc(b.title || '')}${_libEsc(initials)}`; } function _libPct(b) { return b.is_read ? 100 : Math.round((b.scroll_fraction || 0) * 100); } function _libState(b) { if (b.keyOk === false) return '⚠ falscher Schlüssel'; if (b.is_read) return '✓ gelesen'; const p = _libPct(b); return p > 0 ? p + ' %' : 'neu'; } function _libCard(b) { const sel = _lib.selecting && _lib.selecting.has(b.id); const pct = _libPct(b); if (_libLayout() === 'list') { return `
${_libCover(b, 'lib-thumb')} ${_libEsc(b.title)} ${_libEsc(b.author || '')} ${_libEsc(_libState(b))}
`; } return `
${_libCover(b, 'lib-cover')} ${b.is_read ? '✓' : ''} ${_libEsc(b.title)} ${_libEsc(b.author || '')}
`; } function _libContinueBook(books) { if (_lib.status !== 'all' || _lib.folder != null || _lib.q || _lib.selecting) return null; return _recentlyReadBooks(books).filter(x => !x.is_read && x.keyOk !== false)[0] || null; } function _libContinue(b) { if (!b) return ''; const pages = b.pages ? ` · Seite ${Math.max(1, Math.round(b.pages * (b.scroll_fraction || 0)))} von ${b.pages}` : ''; return `
${_libCover(b, 'lib-thumb lib-thumb-l')} Weiterlesen ${_libEsc(b.title)} ${_libEsc(b.author || '')} ${_libPct(b)} %${pages}
`; } // 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 `

${_libEsc(n)} ${issues.length}

` + `
${issues.map(_libCard).join('')}
`; }).join(''); } function _libRender(allBooks) { const listEl = $('book-list'); if (!listEl) return; _lastBookListData = allBooks; const books = _bookSectionFilter(allBooks); const magazines = window.DioraNav && _bookSection === 'magazines'; const shown = _libShown(books); const narrow = window.matchMedia && matchMedia('(max-width: 720px)').matches; const sorts = [['recent', 'Zuletzt'], ['title', 'Titel'], ['author', 'Autor'], ['added', 'Neu hinzugefügt']]; let h = `

${magazines ? 'Zeitschriften' : 'Bücher'}

${!narrow || _lib.searching || _lib.q ? `` : ''}
`; if (_lib.selecting) { h += `
${_lib.selecting.size} ausgewählt
`; } if (!allBooks.length) { h += '

Noch keine Bücher. Mit + eine .epub- oder .pdf-Datei hinzufügen.

'; } else if (!books.length) { h += `

${magazines ? 'Noch keine Zeitschriften. Ein Heft unter Bücher über ⋮ → „Als Zeitschrift führen“ hierher holen.' : 'Alle Bücher stehen bei den Zeitschriften.'}

`; } else { const going = _libContinueBook(books); h += _libContinue(going); if (going) shown.splice(shown.indexOf(going), 1); // once, on top if (!shown.length && !going) h += '

Nichts gefunden.

'; else if (magazines && _lib.sort === 'recent' && !_lib.q) h += _libMagazines(shown); else h += `
${shown.map(_libCard).join('')}
`; } const q = listEl.querySelector('.lib-q'); const focused = q && document.activeElement === q; listEl.innerHTML = h; const status = document.getElementById('book-upload-status'); if (status && status.textContent.trim()) listEl.querySelector('.lib-head').after(status); _libChips(books); if (focused || (_lib.searching && !_lib.q)) { const n = listEl.querySelector('.lib-q'); if (n) { n.focus(); n.setSelectionRange(n.value.length, n.value.length); } } } function _libChips(books) { const el = document.querySelector('#book-list .lib-chips'); if (!el || !window.DioraFolders) return; const folders = _libFolderList(books); const none = books.filter(b => !_libFolders(b).length).length; DioraFolders.chips(el, { lead: [ {id: 'started', label: 'Angefangen', count: books.filter(_libStarted).length, on: _lib.status === 'started'}, {id: 'unread', label: 'Ungelesen', count: books.filter(_libUnread).length, on: _lib.status === 'unread'}, {id: 'read', label: 'Gelesen', count: books.filter(b => b.is_read).length, on: _lib.status === 'read'}, ], folders, active: _lib.folder, none: folders.length ? none : 0, onPick(p) { if (p && p.lead) _lib.status = _lib.status === p.lead ? 'all' : p.lead; else _lib.folder = p; renderBookList(_lastBookListData); }, onManage: folders.length ? _libManage : null, }); } window.renderBookList = _libRender; // ---- folders --------------------------------------------------------------------------------------- // Into folders: one book (its folders as they are), or several (the folders they share). function _libAssign(ids) { const books = ids.map(id => _lastBookListData.find(b => b.id === id)).filter(Boolean); if (!books.length || !window.DioraFolders) return; const common = _libFolders(books[0]).filter(f => books.every(b => _libFolders(b).indexOf(f) >= 0)); DioraFolders.assign({ title: books.length === 1 ? `Ordner für „${books[0].title}“` : `Ordner für ${books.length} Bücher`, folders: _libFolderList(_lastBookListData).map(f => f.name), selected: common, async onDone(picked) { try { for (const b of books) { const kept = _libFolders(b).filter(f => common.indexOf(f) < 0 || picked.indexOf(f) >= 0); const next = kept.concat(picked.filter(f => kept.indexOf(f) < 0)); if (next.join('\u0000') !== _libFolders(b).join('\u0000')) await _updateBookMeta(b.id, {folders: next}); } } catch (e) { customAlert('Das ließ sich nicht speichern: ' + e.message); } _lib.selecting = null; renderBookList(_lastBookListData); }, }); } window.assignBookFolder = id => _libAssign([Number(id)]); function _libManage() { if (!window.DioraFolders) return; const rewrite = async fn => { for (const b of _lastBookListData) { const was = _libFolders(b); const next = fn(was).filter((f, i, all) => f && all.indexOf(f) === i); if (next.join('\u0000') !== was.join('\u0000')) await _updateBookMeta(b.id, {folders: next}); } renderBookList(_lastBookListData); }; DioraFolders.manage({ title: 'Ordner der Bücher', folders: _libFolderList(_lastBookListData).map(f => f.name), onRename: (old, n) => { if (_lib.folder === old) _lib.folder = n; return rewrite(fs => fs.map(f => (f === old ? n : f))).then(() => _libSaveOrder((_lib.order || []).map(f => (f === old ? n : f)).filter((f, i, a) => a.indexOf(f) === i))); }, onDelete: name => { if (_lib.folder === name) _lib.folder = null; return rewrite(fs => fs.filter(f => f !== name)).then(() => _libSaveOrder((_lib.order || []).filter(f => f !== name))); }, onOrder: order => _libSaveOrder(order).then(() => renderBookList(_lastBookListData)).catch(() => {}), }); } // ---- a book's actions (the apps may bring their own sheet: __dioraBookSheet) ------------------------- function _libSheet(b) { if (window.__dioraBookSheet && window.__dioraBookSheet(b) !== false) return; const broken = typeof _brokenBooks !== 'undefined' && _brokenBooks.has(b.id); const items = [ b.keyOk !== false && ['Lesen', () => openBook(b.id)], ['In Ordner …', () => _libAssign([b.id])], [_isMagazine(b) ? 'Als Buch führen' : 'Als Zeitschrift führen', () => toggleBookMagazine(b.id)], [b.cover === 'generated' ? 'Eigenes Cover verwenden' : 'Generiertes Cover verwenden', () => toggleBookCover(b.id)], [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 = ``; const close = () => wrap.remove(); wrap.addEventListener('click', e => { const t = e.target.closest('[data-i]'); if (e.target === wrap || e.target.closest('[data-x]')) { close(); return; } if (t) { close(); items[Number(t.dataset.i)][1](); } }); document.body.appendChild(wrap); } // ---- events ------------------------------------------------------------------------------------------ (function () { const listEl = $('book-list'); if (!listEl) return; let pressTimer = 0; let pressed = null; // First on the list: the apps' own card clicks (native-ui.js) don't run for these cards. listEl.addEventListener('click', e => { const t = e.target; const more = t.closest('[data-lib-more]'); const act = t.closest('[data-lib]'); const card = t.closest('[data-lib-book]'); if (!more && !act && !card) return; e.stopImmediatePropagation(); if (pressed) { pressed = null; return; } // the long press already chose if (more) { const b = _lastBookListData.find(x => x.id === Number(more.dataset.libMore)); if (b) _libSheet(b); return; } if (act) { const a = act.dataset.lib; if (a === 'search') { _lib.searching = true; renderBookList(_lastBookListData); } else if (a === 'layout') { _lib.layout = _libLayout() === 'grid' ? 'list' : 'grid'; try { localStorage.setItem('diora_lib_layout', _lib.layout); } catch (err) {} renderBookList(_lastBookListData); } else if (a === 'add') _libAddMenu(); else if (a === 'sel-done') { _lib.selecting = null; renderBookList(_lastBookListData); } else if (a === 'sel-folders') _libAssign(Array.from(_lib.selecting || [])); return; } const id = Number(card.dataset.libBook); if (_lib.selecting) { if (_lib.selecting.has(id)) _lib.selecting.delete(id); else _lib.selecting.add(id); renderBookList(_lastBookListData); return; } const b = _lastBookListData.find(x => x.id === id); if (b && b.keyOk === false) { customAlert('Dieses Buch lässt sich mit deinem Schlüssel nicht entschlüsseln. Über ⋮ → „Schlüssel einfügen …“ den richtigen Schlüssel eintragen.'); return; } openBook(id); }, true); // A long press on a book: choosing several. listEl.addEventListener('pointerdown', e => { const card = e.target.closest('[data-lib-book]'); if (!card || e.target.closest('button')) return; clearTimeout(pressTimer); pressTimer = setTimeout(() => { const id = Number(card.dataset.libBook); _lib.selecting = _lib.selecting || new Set(); _lib.selecting.add(id); pressed = id; renderBookList(_lastBookListData); }, 550); }); ['pointerup', 'pointercancel', 'pointerleave'].forEach(n => listEl.addEventListener(n, () => clearTimeout(pressTimer))); listEl.addEventListener('input', e => { if (!e.target.classList.contains('lib-q')) return; _lib.q = e.target.value; renderBookList(_lastBookListData); }); listEl.addEventListener('change', e => { if (!e.target.classList.contains('lib-sort')) return; _lib.sort = e.target.value; try { localStorage.setItem('diora_lib_sort', _lib.sort); } catch (err) {} renderBookList(_lastBookListData); }); // Back (Android): out of choosing, out of a search. const back = window.__dioraBack; window.__dioraBack = function () { const shown = $('tab-books') && $('tab-books').style.display !== 'none'; if (shown && _lib.selecting) { _lib.selecting = null; renderBookList(_lastBookListData); return true; } if (shown && (_lib.q || _lib.searching)) { _lib.q = ''; _lib.searching = false; renderBookList(_lastBookListData); return true; } return back ? back() : false; }; if (window.DioraCovers) { DioraCovers.onReady(id => { const b = (_lastBookListData || []).find(x => x.id === id); if (b && b.cover === 'generated') return; 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()}))); }); } _libLoadPrefs(); })(); function _libAddMenu() { const wrap = document.createElement('div'); wrap.className = 'dfo-scrim'; const cloud = !(window.DioraNative && DioraNative.isReaderApp && DioraNative.isReaderApp()); wrap.innerHTML = ``; wrap.addEventListener('click', e => { const a = e.target.closest('[data-a]'); if (e.target === wrap || e.target.closest('[data-x]')) { wrap.remove(); return; } if (!a) return; wrap.remove(); if (a.dataset.a === 'file') { const f = $('book-file-input'); if (f) f.click(); } else if (a.dataset.a === 'cloud') toggleCloudImport(); else { _lib.selecting = new Set(); renderBookList(_lastBookListData); } }); document.body.appendChild(wrap); } // ---- styles --------------------------------------------------------------------------------------------- (function () { const style = document.createElement('style'); style.textContent = [ 'html.lib-v2 #tab-books .book-drop-zone, html.lib-v2 #tab-books .book-list-filter,', 'html.lib-v2 #book-upload-area > button.btn, html.lib-v2 #book-upload-area > .nui-add-row { display: none !important; }', '#book-list .lib-head { display: flex; align-items: center; gap: 6px; margin: 4px 0 8px; }', '#book-list .lib-h { margin: 0 auto 0 0; font-size: 18px; }', '#book-list .lib-q { flex: 0 1 260px; min-width: 0; padding: 8px 10px; border-radius: 6px; border: 1px solid var(--border, #2a2a2a); background: var(--bg, #000); color: inherit; font: inherit; }', '#book-list .lib-sort { padding: 7px 6px; border-radius: 6px; border: 1px solid var(--border, #2a2a2a); background: var(--bg, #000); color: inherit; font: inherit; font-size: 13px; max-width: 140px; }', '#book-list .lib-icon { min-width: 38px; min-height: 38px; border-radius: 19px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 17px; cursor: pointer; }', '#book-list .lib-add { background: var(--accent, #e63946); border-color: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 20px; }', '#book-list .lib-chips { margin: 0 0 10px; }', '#book-list .lib-selbar { display: flex; align-items: center; gap: 8px; padding: 8px 10px; margin: 0 0 10px; border-radius: 8px; background: var(--raised, #222); }', '#book-list .lib-selbar span { flex: 1; }', '#book-list .lib-btn, .lib-sheet-item { min-height: 36px; padding: 0 14px; border-radius: 18px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 14px; cursor: pointer; }', '.lib-sheet-item { display: block; width: 100%; text-align: left; border: none; border-radius: 0; border-bottom: 1px solid var(--border, #2a2a2a); min-height: 46px; }', '.lib-danger { color: var(--accent, #e63946); }', '#book-list .lib-continue { display: flex; align-items: center; gap: 12px; padding: 10px; margin: 0 0 16px; border: 1px solid var(--border, #2a2a2a); border-radius: 10px; background: var(--card, #0b0b0b); cursor: pointer; }', '#book-list .lib-kicker { color: var(--accent, #e63946); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }', '#book-list .lib-play { flex: none; width: 46px; height: 46px; border-radius: 50%; background: var(--accent, #e63946); color: var(--on-accent, #fff); display: flex; align-items: center; justify-content: center; font-size: 17px; }', '#book-list .lib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 18px 14px; }', '@media (max-width: 480px) { #book-list .lib-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 10px; } }', '#book-list .lib-card { display: flex; flex-direction: column; gap: 3px; min-width: 0; cursor: pointer; }', '#book-list .lib-coverbox { position: relative; display: block; }', '#book-list .lib-cover { display: block; width: 100%; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 4px; background: var(--panel, #161616); box-shadow: 0 1px 3px rgba(0,0,0,.35); }', '#book-list .lib-nocover { display: flex; flex-direction: column; justify-content: space-between; padding: 8px; box-sizing: border-box; overflow: hidden; background: hsl(var(--h), 28%, 24%); color: hsl(var(--h), 40%, 88%); }', '#book-list .lib-nocover span { font-size: 12px; line-height: 1.25; display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; }', '#book-list .lib-nocover b { font-size: 26px; opacity: .5; align-self: flex-end; }', '#book-list .lib-thumb { width: 34px; height: 51px; flex: none; border-radius: 3px; object-fit: cover; }', '#book-list .lib-thumb.lib-nocover { padding: 2px; }', '#book-list .lib-thumb.lib-nocover span { display: none; }', '#book-list .lib-thumb.lib-nocover b { font-size: 15px; align-self: center; margin: auto; }', '#book-list .lib-thumb-l { width: 52px; height: 78px; }', '#book-list .lib-badge { position: absolute; left: 6px; bottom: 6px; padding: 1px 6px; border-radius: 9px; background: var(--accent, #e63946); color: var(--on-accent, #fff); font-size: 11px; }', '#book-list .lib-card .lib-more { position: absolute; right: 2px; top: 2px; width: 30px; height: 30px; border-radius: 15px; border: none; background: rgba(0,0,0,.45); color: #fff; font-size: 16px; cursor: pointer; opacity: 0; }', '#book-list .lib-card:hover .lib-more, #book-list .lib-card:focus-within .lib-more { opacity: 1; }', '@media (hover: none) { #book-list .lib-card .lib-more { opacity: .85; } }', '#book-list .lib-bar { display: block; height: 3px; border-radius: 2px; background: var(--border, #2a2a2a); overflow: hidden; margin-top: 4px; }', '#book-list .lib-bar > span { display: block; height: 100%; background: var(--accent, #e63946); }', '#book-list .lib-title { font-size: 13px; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }', '#book-list .lib-meta { font-size: 12px; color: var(--muted, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '#book-list .lib-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }', '#book-list .lib-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border, #2a2a2a); cursor: pointer; }', '#book-list .lib-row .lib-more { background: none; border: none; color: inherit; font-size: 18px; min-width: 36px; min-height: 36px; cursor: pointer; }', '#book-list .lib-state { font-size: 12px; color: var(--muted, #9a9a9a); white-space: nowrap; }', '#book-list .lib-done { color: var(--accent, #e63946); }', '#book-list .lib-sel .lib-cover, #book-list .lib-sel .lib-thumb { outline: 3px solid var(--accent, #e63946); outline-offset: 2px; }', '#book-list .lib-row.lib-sel { background: var(--hover, rgba(255,255,255,.05)); }', '#book-list .lib-group { margin: 18px 0 8px; font-size: 13px; font-weight: 400; color: var(--muted, #9a9a9a); }', '#book-list .lib-count { opacity: .7; }', '#book-list .lib-empty { color: var(--muted, #9a9a9a); font-size: 14px; }', 'body.eink #book-list .lib-add, body.eink #book-list .lib-play { background: #000; color: #fff; }', '#tab-books.lib-drop { outline: 2px dashed var(--accent, #e63946); outline-offset: -6px; }', '#book-upload-status { display: block; margin: 0 0 10px; color: var(--muted, #9a9a9a); font-size: 13px; }', '.cloud-chosen { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 6px 0; }', '.cloud-chosen:empty { display: none; }', '.cloud-entry .cloud-check { width: 18px; height: 18px; margin: 0 8px 0 2px; flex: none; }', 'body.eink #book-list .lib-cover { box-shadow: none; border: 1px solid #000; }', ].join('\n'); document.head.appendChild(style); })();