fix: stop casting feed from hijacking scroll and deleting visible rows

- Auto-scroll to bottom is now gated on the user being within 80px of
  the bottom. Scrolling up to review or edit a line freezes the feed
  in place — new rows are still added, but the viewport doesn't move.
- Row-trimming is likewise suppressed while the user is scrolled up,
  so old lines stay visible as long as they're being read/edited.
  Trim limit raised from 80 → 600 rows so almost nothing is evicted.
- A floating "↓ Live" pill button appears at the bottom of the feed
  whenever the user has scrolled up. Clicking it returns to the live
  bottom and re-enables auto-scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-26 22:47:25 +02:00
parent a978e39e49
commit 6b52a346d5
5 changed files with 52 additions and 7 deletions

View File

@ -9,6 +9,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.9.6] — 2026-06-26
### Fixed
- **Casting feed — scroll hijacking**: the feed no longer forces the view to the bottom while you are scrolled up reviewing or editing earlier lines. Auto-scroll only fires when you are already within 80px of the bottom.
- **Casting feed — vanishing text**: the trimming limit was raised from 80 to 600 rows, and trimming is now suppressed while you're scrolled up, so older lines stay visible as long as you're looking at them.
### Added
- **Casting feed — "↓ Live" jump button**: a floating pill button appears at the bottom of the feed whenever you've scrolled up. Click it to immediately return to the live bottom of the feed and re-enable auto-scroll.
---
## [1.9.5] — 2026-06-26 ## [1.9.5] — 2026-06-26
### Added ### Added

View File

@ -1 +1 @@
1.9.5 1.9.6

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.9.5"> <link rel="stylesheet" href="/static/style.css?v=1.9.6">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -308,7 +308,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.9.5"></script> <script src="/static/loader.js?v=1.9.6"></script>
</body> </body>
</html> </html>

View File

@ -226,7 +226,10 @@ function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) {
<div id="ab-cv-fill-text" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:600; color:var(--text); text-shadow: 0px 1px 2px var(--bg), 0px -1px 2px var(--bg); pointer-events:none;">0%</div> <div id="ab-cv-fill-text" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:600; color:var(--text); text-shadow: 0px 1px 2px var(--bg), 0px -1px 2px var(--bg); pointer-events:none;">0%</div>
</div> </div>
<div class="ab-cv-body"> <div class="ab-cv-body">
<div class="ab-cv-feed" id="ab-cv-feed"></div> <div class="ab-cv-feed-wrap">
<div class="ab-cv-feed" id="ab-cv-feed"></div>
<button class="ab-cv-jump-btn" id="ab-cv-jump-btn" hidden title="Jump to latest"><span class="mdi mdi-chevron-double-down"></span> Live</button>
</div>
<div class="ab-cv-side"> <div class="ab-cv-side">
<div class="ab-cv-side-head">Characters found</div> <div class="ab-cv-side-head">Characters found</div>
<div class="ab-cv-chars" id="ab-cv-chars"><span class="ab-cv-empty">reading</span></div> <div class="ab-cv-chars" id="ab-cv-chars"><span class="ab-cv-empty">reading</span></div>
@ -463,8 +466,29 @@ STRIKTE FORMAT- UND TEXTREGELN:
? 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>';
}; };
const MAXROWS = 80; const MAXROWS = 600; // keep most of the book visible; only trim when user is at bottom
const trim = () => { while (feed.childElementCount > MAXROWS) feed.removeChild(feed.firstChild); feed.scrollTop = feed.scrollHeight; }; // Whether the user has scrolled up to review/edit earlier content.
// While true: auto-scroll and trim are suppressed so their position is preserved.
let _userScrolled = false;
const jumpBtn = panel.querySelector('#ab-cv-jump-btn');
feed.addEventListener('scroll', () => {
const atBottom = feed.scrollTop + feed.clientHeight >= feed.scrollHeight - 80;
_userScrolled = !atBottom;
if (jumpBtn) jumpBtn.hidden = atBottom;
}, { passive: true });
const _scrollToBottom = () => { feed.scrollTop = feed.scrollHeight; };
if (jumpBtn) {
jumpBtn.addEventListener('click', () => {
_userScrolled = false;
jumpBtn.hidden = true;
_scrollToBottom();
});
}
const trim = () => {
if (_userScrolled) return; // don't touch the feed while user is reading/editing
while (feed.childElementCount > MAXROWS) feed.removeChild(feed.firstChild);
_scrollToBottom();
};
// Manual character assignment logic // Manual character assignment logic
let assignModeSeg = null; let assignModeSeg = null;

View File

@ -4153,7 +4153,17 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
} }
.ab-cv-body { display: grid; grid-template-columns: 1fr 220px; gap: 12px; padding: 12px; flex: 1; min-height: 0; } .ab-cv-body { display: grid; grid-template-columns: 1fr 220px; gap: 12px; padding: 12px; flex: 1; min-height: 0; }
.ab-cv-feed { min-height: 0; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; background: var(--panel); display: flex; flex-direction: column; gap: 3px; } .ab-cv-feed-wrap { position: relative; min-height: 0; display: flex; flex-direction: column; }
.ab-cv-feed { flex: 1; min-height: 0; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; background: var(--panel); display: flex; flex-direction: column; gap: 3px; }
.ab-cv-jump-btn {
position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 4px;
padding: 5px 14px; font-size: 12px; font-weight: 700;
background: var(--accent); color: #fff; border: none; border-radius: 20px;
cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,.25); z-index: 5;
white-space: nowrap;
}
.ab-cv-jump-btn:hover { filter: brightness(1.1); }
.ab-cv-row { .ab-cv-row {
display: block; display: block;
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;