// ── Library Characters / Cast workspace ─────────────────────────────────────
// Loaded after library.js; uses its shared production helpers and exports
// libraryRenderCharacters for the Library section router.
// ── Characters / Cast ─────────────────────────────────────────────────────────
async function libraryRenderCharacters() {
const container = document.getElementById('lib-chars-list');
if (!container) return;
container.innerHTML = '
Loading characters…
';
let all = [];
try { all = (typeof clGetAll === 'function') ? await clGetAll() : []; } catch (_) { all = []; }
const byId = new Map(all.map(function (rec) { return [rec.id, rec]; }));
if (!all.length) {
container.innerHTML = '
'
+ ''
+ '
'
+ '
'
+ ''
+ '
No characters yet.
'
+ '
Open a book in Read Aloud, cast it as an audiobook, then click Cast Characters to generate character sheets — or import an existing cast from SillyTavern.
'
+ '
';
container.querySelector('#lib-chars-import').addEventListener('click', function () {
if (typeof stImportDialog === 'function') stImportDialog('', function () { libraryRenderCharacters(); });
});
return;
}
// Group by book (production)
const byBook = {};
all.forEach(function (rec) {
const bk = rec.book || 'Unsorted';
if (!byBook[bk]) byBook[bk] = [];
byBook[bk].push(rec);
});
container.innerHTML = '';
// Global toolbar — import a cast from SillyTavern into a new/unsorted production
const bar = document.createElement('div');
bar.className = 'lib-chars-toolbar';
bar.innerHTML = '';
bar.querySelector('#lib-chars-import').addEventListener('click', function () {
if (typeof stImportDialog === 'function') stImportDialog('', function () { libraryRenderCharacters(); });
});
container.appendChild(bar);
const productions = document.createDocumentFragment();
Object.keys(byBook).sort().forEach(function (book) {
const chars = byBook[book].sort(function (a, b) {
const tierOrder = { main: 0, supporting: 1, minor: 2 };
const ta = tierOrder[String(a.sheet?.tier || 'minor').toLowerCase()] ?? 2;
const tb = tierOrder[String(b.sheet?.tier || 'minor').toLowerCase()] ?? 2;
return ta - tb || (a.name || '').localeCompare(b.name || '');
});
const cov = libBookCover(book);
const prod = document.createElement('div');
prod.className = 'lib-chars-production';
prod.innerHTML = '
';
}
function _lcdField(label, value, multiline) {
const v = _libStr(value);
if (!v) return '';
return '
'
+ '
' + label + '
'
+ '
' + (multiline ? escHtml(v) : escHtml(v)) + '
'
+ '
';
}
function _lcdSection(icon, label, fields) {
const body = fields.join('');
if (!body) return '';
return '
'
+ '
' + label + '
'
+ body
+ '
';
}
function _lcdSectionFull(icon, label, fields) {
const body = fields.join('');
if (!body) return '';
return '
'
+ '
' + label + '
'
+ body
+ '
';
}
// A collapsible "ready to copy" box for one of the four external-tool prompts
// (Voice Design / Character Image / SillyTavern / Concept Art). Closed by
// default; editable in place via the same contenteditable+data-sheet-key
// convention the rest of the detail view uses, so edits autosave for free.
function _lcdPromptBox(label, value, sheetKey) {
const has = !!(value && String(value).trim());
return ''
+ '' + escHtml(label) + (has ? '' : ' — not generated yet') + ''
+ '
'
+ '
' + escHtml(value || '') + '
'
+ ''
+ '
'
+ '';
}
function _lcdFieldEdit(label, value, sheetKey) {
const v = _libStr(value);
return '