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() : '';
let h = 0;
const titleStr = rec.title || 'Untitled';
for (let i = 0; i < titleStr.length; i++) h = (h * 31 + titleStr.charCodeAt(i)) % 360;
const h2 = (h + 40) % 360;
const c1 = `hsl(${h}, 55%, 42%)`, c2 = `hsl(${h2}, 58%, 30%)`;
// 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;"` : `style="--bk1:${c1};--bk2:${c2}"`;
return `Audio files will be removed.
'
+ '
Character sheets
'
+ '
This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.
'
+ '
'
+ ''
+ ''
+ ''
+ '
';
chars.className = '';
}
}
if (lib) {
lib.hidden = (view !== 'library');
if (view === 'library') {
lib.classList.remove('card'); // Remove card styling so it takes full width/height naturally
lib.style.boxShadow = 'none';
lib.style.border = 'none';
lib.style.background = 'transparent';
readerRenderLibrary(); // Re-fetch on open
}
}
if (typeof refreshWorkflowCrumbs === 'function') {
refreshWorkflowCrumbs(view === 'cast' ? 'cast' : view === 'chars' ? 'chars' : 'source');
}
};
window.navReaderView = function (view) {
window.showReaderView(view);
};
window.readerJumpToPage = function (pageNum) {
const pg = parseInt(pageNum, 10);
if (!pg) return Promise.resolve(false);
if (typeof navTo === 'function') navTo('s-reader');
window.showReaderView('main');
let tries = 0;
const waitFrame = () => new Promise(resolve => requestAnimationFrame(resolve));
const jump = async () => {
await waitFrame();
const pageState = readerState.pages?.[pg - 1];
const pageDiv = pageState?.pageDiv;
if (pageDiv && !pageDiv.hidden) {
await readerRenderPage(pg - 1);
pageDiv.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
readerRenderVisible();
return true;
}
if (tries++ < 20) {
await new Promise(resolve => setTimeout(resolve, 100));
return jump();
}
if (typeof toast === 'function') toast('Source page is not loaded in Reader yet', 'info');
return false;
};
return jump();
};
window.readerOnShow = async function () {
const sel = $('reader-backend-select');
if (sel && (!sel.value || sel.options.length <= 1)) {
if (typeof availableTtsBackends === 'function' && !availableTtsBackends().length &&
typeof refreshTtsBackendAvailability === 'function') {
try { await refreshTtsBackendAvailability(); } catch (_) {}
}
if (typeof availableTtsBackends === 'function' && availableTtsBackends().length) {
sel.innerHTML = ttsBackendOptions(sel.value);
sel.disabled = false;
}
}
readerUpdateBackendHint();
if (window.VoicePicker) VoicePicker.upgrade('reader-voice-select');
readerRenderLibrary();
if (window._readerStartView) {
window.showReaderView(window._readerStartView);
window._readerStartView = null;
}
};