// ── 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,
// a Cards/Table view toggle, and a Sort dropdown (all persisted, like Script
// Rehearser's cast-list controls).
const viewMode = localStorage.getItem('ttsvc_libchars_view') === 'table' ? 'table' : 'cards';
const SORT_OPTIONS = [
['tier', 'Rolle (Haupt zuerst)'],
['alpha', 'Alphabet'],
['lines', 'Anzahl Zeilen'],
['gender', 'Geschlecht'],
['voice', 'Stimme zugewiesen'],
];
const sortMode = SORT_OPTIONS.some(function (o) { return o[0] === localStorage.getItem('ttsvc_libchars_sort'); })
? localStorage.getItem('ttsvc_libchars_sort') : 'tier';
const bar = document.createElement('div');
bar.className = 'lib-chars-toolbar';
bar.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 || '') + '
'
+ '
'
+ ''
+ ''
+ '
'
+ '
'
+ '';
}
// sourceIdxs (optional): indices into the character's sources list — shown
// as small clickable "[N]" links next to the label so a reader who doubts a
// fact can jump straight to the exact page + quote it was extracted from,
// instead of scanning the whole "Quellen im Text" list at the bottom.
function _lcdFieldEdit(label, value, sheetKey, sourceIdxs) {
const v = _libStr(value);
const links = (sourceIdxs || []).map(function (idx) {
return '' + (sourceIdxs.indexOf(idx) + 1) + '';
}).join('');
return '
'
) : '';
const sourcesList = Array.isArray(sh.sources) ? sh.sources.filter(function (s) { return s && (s.quote || s.page != null); }) : [];
// Group source indices by the sheet field they were extracted for
// (line_hint), so each detail field below can show "jump to this exact
// citation" links instead of making the reader search the full list.
const sourcesByField = {};
sourcesList.forEach(function (s, idx) {
const key = _libStr(s.line_hint || s.hint || '').trim().toLowerCase();
if (!key) return;
(sourcesByField[key] = sourcesByField[key] || []).push(idx);
});
const sourcesHtml = sourcesList.length ? (
'