diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f701e..9adb26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/static/index.html b/static/index.html index f4a987e..be4f637 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@ - + @@ -336,7 +336,7 @@ - + diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 956c477..396666e 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -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]) => `${escHtml(n)}${info.count}`).join('') : 'reading…'; @@ -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,12 +996,13 @@ 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; const llm_url = overrideUrl || audiobookLlmUrl(), language = audiobookLang(); let model = overrideModel || audiobookLlmModel(); - + const view = audiobookCastView(unknownIdxs.length, llm_url, model); 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]; _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; const llm_url = overrideUrl || audiobookLlmUrl(), language = audiobookLang(); let model = overrideModel || audiobookLlmModel(); diff --git a/static/nav.js b/static/nav.js index d47158b..2eed2e0 100644 --- a/static/nav.js +++ b/static/nav.js @@ -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); diff --git a/static/style.css b/static/style.css index d84446d..c64dd43 100644 --- a/static/style.css +++ b/static/style.css @@ -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; }