Add View Characters + recast-all/selected once a book is already cast (v1.13.1)
"Cast Characters" always blindly regenerated the whole cast from scratch, even for a book already fully cast - no way to just look at what's there or touch up a handful of characters without redoing everyone. Once clGetAllByTagOrBook finds existing characters for this book, the button becomes a split control: View Characters (jump to the Library overview) plus a dropdown for Recast all or Recast selected... (checkbox picker that re-scans the book but only saves updates for the characters checked). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
04531b25f8
commit
204bc3a6c6
@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.13.1] — 2026-07-06
|
||||
|
||||
### Added
|
||||
- **"View Characters" + recast options once a book's already cast** — Read Aloud's Casting view no longer offers a single blind "Cast Characters" button once this book already has saved characters. It becomes a split button: **View Characters** jumps straight to the Library's character overview, and the dropdown caret offers **Recast all** (previous behavior) or **Recast selected…**, which opens a checkbox picker of the existing cast and only refreshes the ones you check — everyone else's saved sheet is left untouched. (Extraction still re-scans the whole book either way, since any page could mention any character; "selected" just controls what gets saved afterward, not what gets read.)
|
||||
|
||||
---
|
||||
|
||||
## [1.13.0] — 2026-07-06
|
||||
|
||||
### Changed
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.13.0">
|
||||
<meta name="app-version" content="1.13.1">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.13.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.13.1">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.13.0"></script>
|
||||
<script src="/static/loader.js?v=1.13.1"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1012,6 +1012,68 @@ function highlightText(text, names) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// Small dropdown anchored to the "Recast options" caret next to "View
|
||||
// Characters", once a book's already been cast before — offers the existing
|
||||
// full recast plus a targeted "recast just these characters" flow.
|
||||
function _abOpenRecastCharsMenu(anchorEl, bookTitle, existingChars) {
|
||||
document.querySelectorAll('.ab-recast-menu').forEach(el => el.remove());
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'ab-recast-menu';
|
||||
menu.innerHTML =
|
||||
'<button type="button" data-action="all"><span class="mdi mdi-refresh"></span> Recast all</button>' +
|
||||
'<button type="button" data-action="selected"><span class="mdi mdi-checkbox-multiple-marked-outline"></span> Recast selected…</button>';
|
||||
document.body.appendChild(menu);
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
menu.style.top = (rect.bottom + 4) + 'px';
|
||||
menu.style.left = Math.max(10, rect.right - 190) + 'px';
|
||||
const close = () => { menu.remove(); document.removeEventListener('mousedown', onDoc); };
|
||||
const onDoc = (e) => { if (!menu.contains(e.target) && e.target !== anchorEl) close(); };
|
||||
setTimeout(() => document.addEventListener('mousedown', onDoc), 0);
|
||||
menu.querySelector('[data-action="all"]').addEventListener('click', () => {
|
||||
close();
|
||||
if (typeof window.csForReader === 'function') window.csForReader();
|
||||
});
|
||||
menu.querySelector('[data-action="selected"]').addEventListener('click', () => {
|
||||
close();
|
||||
_abOpenRecastSelectPopup(bookTitle, existingChars);
|
||||
});
|
||||
}
|
||||
|
||||
// Checkbox picker for "recast selected" — re-scans the whole book (extraction
|
||||
// is passage-by-passage, so any page could mention any character) but only
|
||||
// saves updated sheets for the characters checked here.
|
||||
function _abOpenRecastSelectPopup(bookTitle, existingChars) {
|
||||
const ov = document.createElement('div');
|
||||
ov.className = 'audiobook-overlay';
|
||||
ov.innerHTML = '<div class="audiobook-box">'
|
||||
+ '<div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Recast selected characters</div>'
|
||||
+ '<div class="audiobook-msg">Re-scans the whole book, but only refreshes the characters you pick below — everyone else\'s sheet is left as-is.</div>'
|
||||
+ '<div class="ab-recast-select-list">'
|
||||
+ existingChars.map(c => '<label><input type="checkbox" class="ab-recast-cb" value="' + escHtml(c.name) + '"> ' + escHtml(c.name) + '</label>').join('')
|
||||
+ '</div>'
|
||||
+ '<div class="audiobook-actions">'
|
||||
+ '<button class="btn-secondary btn-sm" id="ab-recast-select-all">Select all</button>'
|
||||
+ '<span style="flex:1"></span>'
|
||||
+ '<button class="btn-secondary btn-sm" id="ab-recast-cancel">Cancel</button>'
|
||||
+ '<button class="btn-primary btn-sm" id="ab-recast-confirm">Recast selected</button>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
document.body.appendChild(ov);
|
||||
ov.querySelector('#ab-recast-cancel').addEventListener('click', () => ov.remove());
|
||||
ov.querySelector('#ab-recast-select-all').addEventListener('click', (e) => {
|
||||
const boxes = ov.querySelectorAll('.ab-recast-cb');
|
||||
const allChecked = [...boxes].every(b => b.checked);
|
||||
boxes.forEach(b => { b.checked = !allChecked; });
|
||||
e.currentTarget.textContent = allChecked ? 'Select all' : 'Select none';
|
||||
});
|
||||
ov.querySelector('#ab-recast-confirm').addEventListener('click', async () => {
|
||||
const names = [...ov.querySelectorAll('.ab-recast-cb:checked')].map(b => b.value);
|
||||
if (!names.length) { toast('Select at least one character', 'error'); return; }
|
||||
ov.remove();
|
||||
if (typeof window.csForReaderSelective === 'function') await window.csForReaderSelective(names);
|
||||
});
|
||||
}
|
||||
|
||||
// Live casting view: a scrolling feed of attributed lines + a character roster
|
||||
// that fills up as speakers are discovered. Far clearer than a bare bar.
|
||||
function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) {
|
||||
@ -3105,6 +3167,33 @@ ABSOLUTE REGELN:
|
||||
else if (typeof csForReader === 'function') csForReader();
|
||||
else toast('Character sheets not loaded yet', 'error');
|
||||
});
|
||||
// If this book's already been cast before, "Cast Characters" blindly
|
||||
// re-running the full generation over again isn't the useful default
|
||||
// anymore — offer to jump straight to the existing overview, or recast
|
||||
// just a hand-picked subset, instead of only "do it all again".
|
||||
(async () => {
|
||||
const bookTitle = window.readerState?.title || '';
|
||||
if (!bookTitle || typeof clGetAllByTagOrBook !== 'function') return;
|
||||
let existing = [];
|
||||
try { existing = await clGetAllByTagOrBook(bookTitle); } catch (_) {}
|
||||
if (!existing.length) return;
|
||||
const castBtn = foot.querySelector('#ab-cv-cast-chars');
|
||||
if (!castBtn) return;
|
||||
const wrap = document.createElement('span');
|
||||
wrap.className = 'ab-cv-castchars-group';
|
||||
wrap.innerHTML =
|
||||
'<button class="btn-secondary btn-sm ab-cv-castchars-view" title="Open this book\'s characters in the Library"><span class="mdi mdi-account-box-multiple-outline"></span> View Characters</button>' +
|
||||
'<button class="btn-secondary btn-sm ab-cv-castchars-menu" title="Recast options"><span class="mdi mdi-chevron-down"></span></button>';
|
||||
castBtn.replaceWith(wrap);
|
||||
wrap.querySelector('.ab-cv-castchars-view').addEventListener('click', () => {
|
||||
if (typeof navTo === 'function') navTo('s-library');
|
||||
if (typeof navLibraryView === 'function') navLibraryView('characters');
|
||||
});
|
||||
wrap.querySelector('.ab-cv-castchars-menu').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
_abOpenRecastCharsMenu(e.currentTarget, bookTitle, existing);
|
||||
});
|
||||
})();
|
||||
panel.classList.add('ab-castpanel-done');
|
||||
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false);
|
||||
},
|
||||
|
||||
@ -778,6 +778,32 @@ async function csForReader() {
|
||||
toast(sheets.length + ' character sheets saved — Library → Characters / Cast', 'success');
|
||||
}
|
||||
|
||||
// Refresh sheet data for a hand-picked subset of an already-cast book's
|
||||
// characters, leaving everyone else's saved sheet untouched. Extraction is
|
||||
// still passage-by-passage over the whole book (any page might mention any
|
||||
// character), so this takes as long as a full recast — it just discards the
|
||||
// results for characters the user didn't pick, rather than skipping work.
|
||||
async function csForReaderSelective(selectedNames) {
|
||||
const wanted = new Set((selectedNames || []).map(n => String(n).trim().toLowerCase()));
|
||||
if (!wanted.size) { toast('No characters selected', 'error'); return; }
|
||||
const text = csReaderText();
|
||||
const book = readerState.title || 'Untitled book';
|
||||
const knownRoster = csKnownReaderRoster();
|
||||
const sheets = await csGenerate(text, null, knownRoster);
|
||||
if (!sheets) return;
|
||||
const filtered = sheets.filter(s => wanted.has(String(s.name || '').trim().toLowerCase()));
|
||||
if (!filtered.length) { toast('None of the selected characters turned up in this pass — try again or pick different ones', 'error'); return; }
|
||||
const ab = (typeof _audiobook !== 'undefined') ? _audiobook : window._audiobook;
|
||||
if (ab?.roster) {
|
||||
const counts = new Map();
|
||||
ab.roster.forEach(function (info, name) { counts.set(String(name).trim().toLowerCase(), info.count || 0); });
|
||||
csAttachLineCounts(filtered, counts);
|
||||
}
|
||||
await csSaveToLibrary(book, filtered);
|
||||
csGoToLibrary();
|
||||
toast(filtered.length + ' character' + (filtered.length !== 1 ? 's' : '') + ' recast — Library → Characters / Cast', 'success');
|
||||
}
|
||||
|
||||
async function csForRehearser() {
|
||||
const title = $('reh-script-title')?.value.trim() || 'Character sheets';
|
||||
const book = $('reh-script-title')?.value.trim() || 'Untitled script';
|
||||
@ -802,6 +828,7 @@ async function csForRehearser() {
|
||||
}
|
||||
|
||||
window.csForReader = csForReader;
|
||||
window.csForReaderSelective = csForReaderSelective;
|
||||
window.csForRehearser = csForRehearser;
|
||||
$('reader-charsheets-btn')?.addEventListener('click', csForReader);
|
||||
$('reh-charsheets-btn')?.addEventListener('click', csForRehearser);
|
||||
|
||||
@ -5616,6 +5616,24 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
.audiobook-msg { color: var(--subtext); font-size: 13px; margin: 10px 0 12px; }
|
||||
.audiobook-box .reader-synth-track { width: 100%; }
|
||||
.audiobook-actions { margin-top: 14px; text-align: right; display: flex; gap: 8px; justify-content: flex-end; }
|
||||
/* "View Characters ▾" split button + its recast dropdown, shown once a book
|
||||
already has cast characters instead of a single blind "Cast Characters". */
|
||||
.ab-cv-castchars-group { display: inline-flex; margin-right: 8px; }
|
||||
.ab-cv-castchars-group .ab-cv-castchars-view { border-radius: 6px 0 0 6px; }
|
||||
.ab-cv-castchars-group .ab-cv-castchars-menu { border-radius: 0 6px 6px 0; border-left: 0; padding: 0 8px; }
|
||||
.ab-recast-menu {
|
||||
position: fixed; z-index: 2001; background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,.3); padding: 4px; min-width: 180px;
|
||||
display: flex; flex-direction: column; gap: 1px;
|
||||
}
|
||||
.ab-recast-menu button {
|
||||
display: flex; align-items: center; gap: 8px; padding: 7px 10px; font-size: 12.5px;
|
||||
border: none; background: none; color: var(--text); border-radius: 5px; cursor: pointer; text-align: left;
|
||||
}
|
||||
.ab-recast-menu button:hover { background: var(--panel); }
|
||||
.ab-recast-select-list { max-height: 280px; overflow-y: auto; margin: 10px 0; text-align: left; border: 1px solid var(--border); border-radius: 6px; padding: 4px; }
|
||||
.ab-recast-select-list label { display: flex; align-items: center; gap: 8px; padding: 5px 6px; cursor: pointer; font-size: 13px; border-radius: 4px; }
|
||||
.ab-recast-select-list label:hover { background: var(--panel); }
|
||||
.audiobook-count { font-weight: 500; font-size: 12px; color: var(--subtext); margin-left: auto; }
|
||||
.audiobook-preview-box { width: min(760px, 94vw); }
|
||||
.audiobook-preview-box .audiobook-title { flex-wrap: wrap; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user