feat: casting badge, navigate-away/back, live character count (v1.12.3)

- Pulsing blue dot on the Read Aloud nav item while casting is active
- Navigating away and returning restores the cast panel automatically
- Reassigning a segment's speaker decrements the old count so the
  Characters Found panel stays in sync; speakers at 0 disappear

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-27 19:45:41 +02:00
parent 4f795e820c
commit 7a75d1ecf4
5 changed files with 51 additions and 6 deletions

View File

@ -9,6 +9,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.12.3] — 2026-06-27
### Added
- **Casting activity indicator**: pulsing blue dot on the Read Aloud nav item while audiobook casting is in progress, so users can see it's working from any section.
- **Restore cast view on return**: navigating away during casting and then back to Read Aloud automatically restores the casting panel instead of showing the blank PDF view.
### Fixed
- **Character count out of sync on reassignment**: reassigning a segment's speaker now decrements the old speaker's count in the Characters Found panel, so totals stay accurate when corrections are made. Speakers that drop to 0 lines are removed from the panel automatically.
---
## [1.12.2] — 2026-06-27 ## [1.12.2] — 2026-06-27
### Added ### Added

View File

@ -26,7 +26,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── --> <!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css"> <link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.12.2"> <link rel="stylesheet" href="/static/style.css?v=1.12.3">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -336,7 +336,7 @@
<script src="/static/vendor/wavesurfer-regions.min.js"></script> <script src="/static/vendor/wavesurfer-regions.min.js"></script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton --> <!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.12.2"></script> <script src="/static/loader.js?v=1.12.3"></script>
</body> </body>
</html> </html>

View File

@ -290,6 +290,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
const closePanel = () => { const closePanel = () => {
panel.hidden = true; panel.hidden = true;
panel.innerHTML = ''; panel.innerHTML = '';
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false);
if (typeof window.navReaderView === 'function') window.navReaderView('main'); if (typeof window.navReaderView === 'function') window.navReaderView('main');
else if (typeof window.showReaderView === 'function') window.showReaderView('main'); else if (typeof window.showReaderView === 'function') window.showReaderView('main');
else { else {
@ -497,7 +498,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
return html; return html;
}; };
const renderRoster = () => { const renderRoster = () => {
const items = [...roster.entries()].sort((a, b) => b[1].count - a[1].count); const items = [...roster.entries()].filter(([, info]) => info.count > 0).sort((a, b) => b[1].count - a[1].count);
chars.innerHTML = items.length chars.innerHTML = items.length
? items.map(([n, info]) => `<span class="ab-chip" style="--c:${info.color}"><span class="ab-chip-dot"></span>${escHtml(n)}<b>${info.count}</b></span>`).join('') ? items.map(([n, info]) => `<span class="ab-chip" style="--c:${info.color}"><span class="ab-chip-dot"></span>${escHtml(n)}<b>${info.count}</b></span>`).join('')
: '<span class="ab-cv-empty">reading…</span>'; : '<span class="ab-cv-empty">reading…</span>';
@ -618,6 +619,12 @@ STRIKTE FORMAT- UND TEXTREGELN:
function assignName(name) { function assignName(name) {
if (!assignModeSeg) return; if (!assignModeSeg) return;
const isNarrator = name.toLowerCase() === 'narrator'; const isNarrator = name.toLowerCase() === 'narrator';
// Decrement old speaker's count before reassigning
const _oldSpk = assignModeSeg.speaker;
if (_oldSpk && roster.has(_oldSpk)) {
const _old = roster.get(_oldSpk);
if (_old.count > 0) _old.count--;
}
assignModeSeg.speaker = isNarrator ? 'Narrator' : name; assignModeSeg.speaker = isNarrator ? 'Narrator' : name;
assignModeSeg.type = isNarrator ? 'narration' : 'dialogue'; assignModeSeg.type = isNarrator ? 'narration' : 'dialogue';
if (isNarrator) assignModeSeg.emotion = ''; if (isNarrator) assignModeSeg.emotion = '';
@ -965,6 +972,7 @@ ABSOLUTE REGELN:
foot.querySelector('#ab-cv-recast').addEventListener('click', () => applyPromptAndRun(audiobookCast)); foot.querySelector('#ab-cv-recast').addEventListener('click', () => applyPromptAndRun(audiobookCast));
foot.querySelector('#ab-cv-recast-unk').addEventListener('click', () => applyPromptAndRun(audiobookRecastUnknown)); foot.querySelector('#ab-cv-recast-unk').addEventListener('click', () => applyPromptAndRun(audiobookRecastUnknown));
panel.classList.add('ab-castpanel-done'); panel.classList.add('ab-castpanel-done');
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false);
}, },
done() { closePanel(); }, done() { closePanel(); },
}; };
@ -988,12 +996,13 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel) {
} }
_audiobook.running = true; _audiobook.cancel = false; _audiobook.running = true; _audiobook.cancel = false;
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(true);
const ac = new AbortController(); _audiobook.abort = () => ac.abort(); const ac = new AbortController(); _audiobook.abort = () => ac.abort();
if (overrideUrl && typeof overrideUrl !== 'string') overrideUrl = null; if (overrideUrl && typeof overrideUrl !== 'string') overrideUrl = null;
const llm_url = overrideUrl || audiobookLlmUrl(), language = audiobookLang(); const llm_url = overrideUrl || audiobookLlmUrl(), language = audiobookLang();
let model = overrideModel || audiobookLlmModel(); let model = overrideModel || audiobookLlmModel();
const view = audiobookCastView(unknownIdxs.length, llm_url, model); const view = audiobookCastView(unknownIdxs.length, llm_url, model);
view.processing('Waking up LLM model (this may take a few minutes if cold-booting)…'); view.processing('Waking up LLM model (this may take a few minutes if cold-booting)…');
@ -1118,9 +1127,10 @@ async function audiobookCast(overrideUrl, overrideModel) {
: [text]; : [text];
_audiobook.running = true; _audiobook.cancel = false; _audiobook.running = true; _audiobook.cancel = false;
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(true);
const ac = new AbortController(); const ac = new AbortController();
_audiobook.abort = () => ac.abort(); _audiobook.abort = () => ac.abort();
if (overrideUrl && typeof overrideUrl !== 'string') overrideUrl = null; if (overrideUrl && typeof overrideUrl !== 'string') overrideUrl = null;
const llm_url = overrideUrl || audiobookLlmUrl(), language = audiobookLang(); const llm_url = overrideUrl || audiobookLlmUrl(), language = audiobookLang();
let model = overrideModel || audiobookLlmModel(); let model = overrideModel || audiobookLlmModel();

View File

@ -124,6 +124,15 @@
// Leaving the Read Aloud reader: stop playback so audio doesn't keep running // Leaving the Read Aloud reader: stop playback so audio doesn't keep running
if (sectionId !== 's-reader' && typeof window.readerStop === 'function') window.readerStop(); if (sectionId !== 's-reader' && typeof window.readerStop === 'function') window.readerStop();
if (sectionId === 's-reader' && typeof window.readerOnShow === 'function') window.readerOnShow(); if (sectionId === 's-reader' && typeof window.readerOnShow === 'function') window.readerOnShow();
// If a cast session panel is active, restore it instead of showing the main PDF view
if (sectionId === 's-reader') {
var _castPnl = document.getElementById('reader-audiobook-panel');
if (_castPnl && _castPnl.querySelector('#ab-cv-feed')) {
setTimeout(function () {
if (typeof window.showReaderView === 'function') window.showReaderView('cast');
}, 0);
}
}
if (sectionId === 's-library' && typeof window.libraryRender === 'function') window.libraryRender(window._libraryView || 'books'); if (sectionId === 's-library' && typeof window.libraryRender === 'function') window.libraryRender(window._libraryView || 'books');
if (typeof window.initCollapsibleCards === 'function') window.initCollapsibleCards(); if (typeof window.initCollapsibleCards === 'function') window.initCollapsibleCards();
setStoredSection(sectionId); setStoredSection(sectionId);
@ -163,6 +172,20 @@
if (tabName) runSideEffects(tabName); if (tabName) runSideEffects(tabName);
}; };
// Pulsing dot on the Read Aloud nav item while casting is in progress
window.setNavCastingBadge = function (active) {
var el = document.querySelector('[data-nav-section="s-reader"]');
if (!el) return;
var existing = el.querySelector('.nav-casting-dot');
if (active && !existing) {
var dot = document.createElement('span');
dot.className = 'nav-casting-dot';
el.appendChild(dot);
} else if (!active && existing) {
existing.remove();
}
};
window.switchTab = function (name) { window.switchTab = function (name) {
var sectionId = TAB_SECTION_MAP[name]; var sectionId = TAB_SECTION_MAP[name];
if (sectionId) showSection(sectionId); if (sectionId) showSection(sectionId);

View File

@ -176,6 +176,7 @@ body {
border-radius: 10px; padding: 1px 6px; min-width: 18px; text-align: center; border-radius: 10px; padding: 1px 6px; min-width: 18px; text-align: center;
} }
.nav-chevron { font-size: 14px; color: var(--subtext); transition: transform .2s; margin-left: 2px; } .nav-chevron { font-size: 14px; color: var(--subtext); transition: transform .2s; margin-left: 2px; }
.nav-casting-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex-shrink: 0; margin-left: auto; animation: pulse 1s infinite; }
.nav-chevron.open { transform: rotate(180deg); } .nav-chevron.open { transform: rotate(180deg); }
.nav-tree { display: none; flex-direction: column; padding: 2px 0 6px; } .nav-tree { display: none; flex-direction: column; padding: 2px 0 6px; }
.nav-tree.open { display: flex; } .nav-tree.open { display: flex; }