No saved books yet.
'; return; }
list.innerHTML = all.map(rec => {
const total = rec.sentenceCount || 0;
const synth = rec.synthCount || 0;
const readPct = total ? Math.round(((rec.idx || 0) / total) * 100) : 0;
const synthPct = total ? Math.round((synth / total) * 100) : 0;
const date = rec.updated ? new Date(rec.updated).toLocaleDateString() : '';
// A timestamp busts the cache if the cover was updated
const coverUrl = `${READER_API}/${rec.id}/cover?t=${new Date(rec.updated||Date.now()).getTime()}`;
const bgStyle = rec.hasCover ? `style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.8) 100%), url('${coverUrl}'); background-size: cover; background-position: center; color: #fff;"` : '';
const textStyle = rec.hasCover ? `style="color: #fff;"` : '';
const subtextStyle = rec.hasCover ? `style="color: rgba(255,255,255,0.7);"` : '';
const statsStyle = rec.hasCover ? `style="color: rgba(255,255,255,0.8);"` : '';
return `
${escHtml(rec.title)}
${total} sentences${rec.pageCount ? ' · ' + rec.pageCount + ' pg' : ''} · ${date}
${synthPct}% audio ${readPct}% read
`;
}).join('');
list.querySelectorAll('.reader-book').forEach(el => {
el.addEventListener('click', e => {
if (e.target.closest('.reader-book-del')) return;
readerOpenLibraryDoc(el.dataset.id);
});
// Drag & drop for cover image
el.addEventListener('dragover', e => {
if (e.dataTransfer && [...(e.dataTransfer.types || [])].includes('Files')) {
e.preventDefault();
el.style.borderColor = 'var(--accent)';
}
});
el.addEventListener('dragleave', () => { el.style.borderColor = ''; });
el.addEventListener('drop', async e => {
if (!(e.dataTransfer && [...(e.dataTransfer.types || [])].includes('Files'))) return;
e.preventDefault(); e.stopPropagation();
el.style.borderColor = '';
const f = e.dataTransfer.files?.[0];
if (!f || !f.type.startsWith('image/')) { toast('Please drop an image file', 'error'); return; }
try {
const id = el.dataset.id;
const r = await fetch(`${READER_API}/${id}/cover`, { method: 'PUT', body: f });
if (!r.ok) throw new Error(r.statusText);
// Touch the updated timestamp so the cache buster updates on re-render
await fetch(`${READER_API}/${id}/progress`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ updated: new Date().toISOString() }),
});
toast('Cover image updated', 'success');
readerRenderLibrary();
} catch (err) {
toast('Failed to upload cover: ' + (err.message || err), 'error');
}
});
});
list.querySelectorAll('.reader-book-del').forEach(btn => btn.addEventListener('click', e => {
e.stopPropagation();
if (confirm('Delete this saved document and its audio?')) readerDeleteLibraryDoc(btn.dataset.del);
}));
}
// ── Wiring ───────────────────────────────────────────────────────────────────
$('reader-file-input')?.addEventListener('change', function () {
const f = this.files?.[0]; this.value = '';
readerImportFile(f);
});
$('reader-fetch-voices-btn')?.addEventListener('click', readerFetchVoices);
$('reader-backend-select')?.addEventListener('change', () => {
const sel = $('reader-voice-select'); if (sel) sel.innerHTML = '