diff --git a/CHANGELOG.md b/CHANGELOG.md index 9adb26d..00863af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.12.4] — 2026-06-27 + +### Added +- **Expandable context dividers in Recast Unknown**: the `⋯` gaps between scattered Unknown segments now show a count ("42 lines hidden — click to expand") and expand inline on click, revealing all segments between two Unknown passages so the user can see who is speaking before and after to make a better assignment. + +--- + ## [1.12.3] — 2026-06-27 ### Added diff --git a/static/index.html b/static/index.html index be4f637..3de401b 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 396666e..6b42a99 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -881,14 +881,35 @@ STRIKTE FORMAT- UND TEXTREGELN: feed.appendChild(frag); trim(); renderRoster(); }, note(text) { const r = document.createElement('div'); r.className = 'ab-cv-note'; r.textContent = text; feed.appendChild(r); trim(); }, - // Visual gap (three dots) marking that the passages before/after are not - // contiguous — used by Recast unknown, which only shows scattered segments. - divider() { + // Visual gap marking that the passages before/after are not contiguous. + // prevIdx / nextIdx are indices into _audiobook.segments for the gap boundaries. + // Click expands the divider to show all segments in the gap inline. + divider(prevIdx, nextIdx) { this.clearProcessing(); const r = document.createElement('div'); - r.className = 'ab-cv-divider'; - r.textContent = '⋯'; - r.title = 'There is more text before and after this passage'; + r.className = 'ab-cv-divider ab-cv-divider-expand'; + const gapFrom = Math.max(0, (prevIdx >= 0 ? prevIdx + 1 : 0)); + const segsAll = _audiobook.segments || []; + const gapTo = Math.min(segsAll.length - 1, (nextIdx >= 0 ? nextIdx - 1 : segsAll.length - 1)); + const gapCount = Math.max(0, gapTo - gapFrom + 1); + r.innerHTML = `⋯ ${gapCount > 0 ? gapCount + ' line' + (gapCount !== 1 ? 's' : '') + ' hidden — ' : ''}click to expand`; + r.title = 'Click to reveal the text between these passages'; + r.addEventListener('click', () => { + const gap = segsAll.slice(gapFrom, gapTo + 1); + if (!gap.length) { r.remove(); return; } + const frag = document.createDocumentFragment(); + for (const s of gap) { + const isNarr = s.type !== 'dialogue' || !s.speaker || s.speaker.toLowerCase() === 'narrator'; + const spk = isNarr ? 'Narrator' : s.speaker; + const c = colorFor(spk); + const row = document.createElement('div'); + row.className = 'ab-cv-row ab-cv-ctx-row' + (isNarr ? ' is-narr' : ''); + row.__seg = s; + row.innerHTML = `${escHtml(spk)}${s.emotion ? ' (${escHtml(s.emotion)})' : ''}${highlightText(s.text || '')}`; + frag.appendChild(row); + } + r.replaceWith(frag); + }); feed.appendChild(r); trim(); }, // Page-break marker in the casting feed (from source PDF page boundaries). @@ -1028,7 +1049,7 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel) { // These Unknown lines come from scattered places in the document. Mark a // gap with "⋯" whenever this passage isn't directly after the previous one. - if (i !== prevIdx + 1) view.divider(); + if (i !== prevIdx + 1) view.divider(prevIdx, i); prevIdx = i; const start = Math.max(0, i - 12); diff --git a/static/style.css b/static/style.css index c64dd43..9d72f11 100644 --- a/static/style.css +++ b/static/style.css @@ -4513,6 +4513,10 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami /* ── Recast unknown: gap between non-contiguous passages ───────────────── */ .ab-cv-divider { text-align: center; color: var(--subtext); font-size: 20px; letter-spacing: .35em; line-height: 1; padding: 10px 0 6px; opacity: .55; user-select: none; } +.ab-cv-divider-expand { cursor: pointer; font-size: 14px; letter-spacing: normal; transition: opacity .15s; } +.ab-cv-divider-expand:hover { opacity: .9; color: var(--accent); } +.ab-cv-divider-hint { font-size: 11px; font-weight: 500; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; vertical-align: middle; } +.ab-cv-ctx-row { opacity: .7; background: color-mix(in srgb, var(--panel) 60%, transparent); border-left: 2px solid var(--border); } .reader-synth-prog { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; } .reader-synth-track { width: 130px; height: 6px; border-radius: 4px; background: var(--panel); overflow: hidden; } .reader-synth-fill { height: 100%; width: 0; background: rgba(234,179,8,.9); transition: width .15s; }