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
### Added

View File

@ -26,7 +26,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.12.2">
<link rel="stylesheet" href="/static/style.css?v=1.12.3">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -336,7 +336,7 @@
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
<!-- 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>
</html>

View File

@ -290,6 +290,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
const closePanel = () => {
panel.hidden = true;
panel.innerHTML = '';
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false);
if (typeof window.navReaderView === 'function') window.navReaderView('main');
else if (typeof window.showReaderView === 'function') window.showReaderView('main');
else {
@ -497,7 +498,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
return html;
};
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
? 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>';
@ -618,6 +619,12 @@ STRIKTE FORMAT- UND TEXTREGELN:
function assignName(name) {
if (!assignModeSeg) return;
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.type = isNarrator ? 'narration' : 'dialogue';
if (isNarrator) assignModeSeg.emotion = '';
@ -965,6 +972,7 @@ ABSOLUTE REGELN:
foot.querySelector('#ab-cv-recast').addEventListener('click', () => applyPromptAndRun(audiobookCast));
foot.querySelector('#ab-cv-recast-unk').addEventListener('click', () => applyPromptAndRun(audiobookRecastUnknown));
panel.classList.add('ab-castpanel-done');
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false);
},
done() { closePanel(); },
};
@ -988,6 +996,7 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel) {
}
_audiobook.running = true; _audiobook.cancel = false;
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(true);
const ac = new AbortController(); _audiobook.abort = () => ac.abort();
if (overrideUrl && typeof overrideUrl !== 'string') overrideUrl = null;
@ -1118,6 +1127,7 @@ async function audiobookCast(overrideUrl, overrideModel) {
: [text];
_audiobook.running = true; _audiobook.cancel = false;
if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(true);
const ac = new AbortController();
_audiobook.abort = () => ac.abort();

View File

@ -124,6 +124,15 @@
// 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.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 (typeof window.initCollapsibleCards === 'function') window.initCollapsibleCards();
setStoredSection(sectionId);
@ -163,6 +172,20 @@
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) {
var sectionId = TAB_SECTION_MAP[name];
if (sectionId) showSection(sectionId);

View File

@ -176,6 +176,7 @@ body {
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-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-tree { display: none; flex-direction: column; padding: 2px 0 6px; }
.nav-tree.open { display: flex; }