feat: loudness walk (U/I) keeps its own cursor, independent of J/K
The clip bar had one shared cursor, so pressing U/I re-derived the loudness rank from wherever you currently were. A J/K detour to review clips around an interesting spot therefore hijacked the loudness walk: returning to U/I continued from the J/K position, not from where the ranking left off. Add a separate scoreCursor that only U/I (and "highlights only" auto- advance) move; J/K never touches it. So: U/I to a loud moment, J/K to review the time-adjacent clips, U/I again resumes the ranking exactly where you left it. scoreCursor resets to -1 on every explicit jump / queue re-arm (hideClipBar, chip clicks, day-highlights arm) so the next U/I re-anchors on the selected section. Also label the position count "by time" (J/K) or "by loudness" (U/I) so the blind user can hear which dimension the count is in — the two looked identical before and switched meaning silently when changing keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+33
-9
@@ -349,12 +349,20 @@ function seekToSection(idx, filename, startSec, endSec, sectionIdx) {
|
||||
// day's); J/K and the Prev/Next buttons step through it in time order, U/I
|
||||
// step through the loudest-first ranking. The auto-advance radio picks what
|
||||
// plays when a clip ends: nothing, the next in time, or the next by loudness.
|
||||
//
|
||||
// Two cursors, deliberately independent. clipCursor is the time-order position
|
||||
// and follows every play (chip click, J/K, U/I). scoreCursor is the loudness
|
||||
// walk's own position in scoreOrder(): only U/I (and "highlights only" auto-
|
||||
// advance) move it; J/K detours to review nearby clips leave it alone, so
|
||||
// coming back to U/I resumes the ranking exactly where you left it. An explicit
|
||||
// jump (chip click, new queue) resets it to -1 so the next U/I re-anchors there.
|
||||
let clipQueue = [];
|
||||
let clipCursor = -1;
|
||||
let scoreCursor = -1;
|
||||
|
||||
// byScore: this play is part of a loudness walk (U/I or "highlights only"
|
||||
// auto-advance), so the position shown is the rank in the loudest-first
|
||||
// ranking ("3rd loudest of 426"), not the time-order index.
|
||||
// auto-advance), so the position shown is scoreCursor — the rank in the
|
||||
// loudest-first ranking, labelled "by loudness" — not the time-order index.
|
||||
function playClip(i, byScore) {
|
||||
if (i < 0 || i >= clipQueue.length) return;
|
||||
clipCursor = i;
|
||||
@@ -368,13 +376,15 @@ function playClip(i, byScore) {
|
||||
// Label = wall-clock time of occurrence (absStart from the filename clock);
|
||||
// falls back to in-file offsets for non-standard filenames. The visible
|
||||
// label and the screen-reader announcement use one identical string:
|
||||
// "17:09:56 to 17:09:57 · +30 dB (30 / 426)".
|
||||
// "17:09:56 to 17:09:57 · +30 dB (30 / 426 by time)" — the count is the
|
||||
// time-order index "by time" (J/K) or the loudness rank "by loudness" (U/I).
|
||||
const when = c.absStart != null
|
||||
? `${fmtClock(c.absStart)} to ${fmtClock(c.absStart + (c.end - c.start))}`
|
||||
: `${fmtDur(c.start)} to ${fmtDur(c.end)}`;
|
||||
const score = c.score != null ? ` · +${Math.round(c.score)} dB` : '';
|
||||
const pos = byScore ? scoreOrder(clipQueue).indexOf(i) : i;
|
||||
const text = `${when}${score} (${pos + 1} / ${clipQueue.length})`;
|
||||
const pos = byScore ? scoreCursor : i;
|
||||
const dim = byScore ? 'by loudness' : 'by time';
|
||||
const text = `${when}${score} (${pos + 1} / ${clipQueue.length} ${dim})`;
|
||||
const label = document.getElementById('clip-label');
|
||||
label.textContent = text;
|
||||
label.title = `${c.filename} @ ${fmtDur(c.start)}–${fmtDur(c.end)}`;
|
||||
@@ -392,6 +402,7 @@ function hideClipBar() {
|
||||
document.body.classList.remove('clip-open');
|
||||
clipQueue = [];
|
||||
clipCursor = -1;
|
||||
scoreCursor = -1;
|
||||
}
|
||||
|
||||
function playFileSection(idx, filename, si) {
|
||||
@@ -400,6 +411,7 @@ function playFileSection(idx, filename, si) {
|
||||
const epoch = f ? fileStartEpoch(f.date) : null;
|
||||
clipQueue = secs.map(s => ({fileIdx: idx, filename, start: s.start, end: s.end, score: s.score,
|
||||
absStart: epoch != null ? epoch + s.start : null}));
|
||||
scoreCursor = -1; // explicit jump: next U/I re-anchors here
|
||||
playClip(si);
|
||||
}
|
||||
|
||||
@@ -422,14 +434,23 @@ const advanceMode = () =>
|
||||
function stepClip(dir, byScore, jump) {
|
||||
if (!clipQueue.length) return;
|
||||
if (byScore) {
|
||||
// Walk scoreCursor, not the shared clipCursor: a J/K detour never moved
|
||||
// it, so we resume the ranking where we left off. scoreCursor === -1 means
|
||||
// the walk hasn't started since the queue was armed or an explicit jump —
|
||||
// anchor on the section currently selected (clipCursor), else the loudest.
|
||||
const order = scoreOrder(clipQueue);
|
||||
const pos = order.indexOf(clipCursor); // -1: nothing played yet
|
||||
const next = jump ? (dir > 0 ? order.length - 1 : 0)
|
||||
: pos === -1 ? (dir > 0 ? 0 : -1) : pos + dir;
|
||||
if (next >= 0 && next < order.length) playClip(order[next], true);
|
||||
let next;
|
||||
if (jump) next = dir > 0 ? order.length - 1 : 0;
|
||||
else if (scoreCursor !== -1) next = scoreCursor + dir;
|
||||
else {
|
||||
const anchor = clipCursor >= 0 ? order.indexOf(clipCursor) : -1;
|
||||
next = anchor === -1 ? (dir > 0 ? 0 : -1) : anchor + dir;
|
||||
}
|
||||
if (next >= 0 && next < order.length) { scoreCursor = next; playClip(order[next], true); }
|
||||
else announce(dir < 0 ? 'Loudest highlight reached' : 'End of highlights');
|
||||
return;
|
||||
}
|
||||
// J/K (time order) deliberately does NOT touch scoreCursor.
|
||||
const i = jump ? (dir > 0 ? clipQueue.length - 1 : 0) : clipCursor + dir;
|
||||
if (i >= 0 && i < clipQueue.length) playClip(i);
|
||||
else announce(dir < 0 ? 'Beginning of sections' : 'End of sections');
|
||||
@@ -926,6 +947,7 @@ function renderFiles(files) {
|
||||
dayActiveId = dayId;
|
||||
clipQueue = dayActiveSections;
|
||||
clipCursor = -1;
|
||||
scoreCursor = -1;
|
||||
return;
|
||||
}
|
||||
dayHighlights(dayId, hlFiles);
|
||||
@@ -1016,6 +1038,7 @@ async function dayHighlights(dayId, analyzableFiles) {
|
||||
// Arm the clip queue so J/K steps through the day immediately
|
||||
clipQueue = dayActiveSections;
|
||||
clipCursor = -1;
|
||||
scoreCursor = -1;
|
||||
|
||||
const box = document.createElement('div');
|
||||
box.className = 'wbox';
|
||||
@@ -1102,6 +1125,7 @@ async function dayHighlights(dayId, analyzableFiles) {
|
||||
function jumpToDaySection(si) {
|
||||
if (si < 0 || si >= dayActiveSections.length) return;
|
||||
clipQueue = dayActiveSections;
|
||||
scoreCursor = -1; // explicit jump: next U/I re-anchors here
|
||||
playClip(si);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user