feat: Shift+J/K/U/I jump to first/last/loudest/quietest section

Holding Shift with the section-navigation keys jumps straight to the
extreme in that direction instead of stepping one at a time:
- Shift+J / Shift+K -> first / last section in time order
- Shift+U / Shift+I -> loudest / quietest section

Works in both clip-queue navigation (stepClip gains a jump flag) and
in-player full-file navigation. Visible key-hint note and README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 07:33:43 +02:00
parent 5a9518e262
commit 682b0522d3
2 changed files with 24 additions and 11 deletions
+22 -9
View File
@@ -410,18 +410,20 @@ const advanceMode = () =>
document.querySelector('input[name="clip-adv"]:checked')?.value || 'off';
// Step the clip queue by dir (±1): in time order, or with byScore down/up
// the loudest-first ranking (next = next quieter).
function stepClip(dir, byScore) {
// the loudest-first ranking (next = next quieter). With jump=true, go straight
// to the extreme in that direction (first/last section, or loudest/quietest).
function stepClip(dir, byScore, jump) {
if (!clipQueue.length) return;
if (byScore) {
const order = scoreOrder(clipQueue);
const pos = order.indexOf(clipCursor); // -1: nothing played yet
const next = pos === -1 ? (dir > 0 ? 0 : -1) : pos + dir;
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]);
else announce(dir < 0 ? 'Loudest highlight reached' : 'End of highlights');
return;
}
const i = clipCursor + dir;
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');
}
@@ -534,8 +536,9 @@ async function analyse(idx, filename, cell, btn, force = false) {
}
// J/K = previous/next section in time order, U/I = up/down the loudest-first
// ranking, O = open the current clip in the full file.
// Only when focus is not in an input.
// ranking, O = open the current clip in the full file. Holding Shift jumps to
// the extreme in that direction: Shift+J/K = first/last section in time,
// Shift+U/I = loudest/quietest. Only when focus is not in an input.
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
@@ -545,10 +548,11 @@ document.addEventListener('keydown', e => {
if (key === 'o') { openClipInFile(); return; }
const dir = (key === 'j' || key === 'u') ? -1 : 1;
const byScore = key === 'u' || key === 'i';
const jump = e.shiftKey;
// Clip queue navigation (a chip was clicked or day highlights are loaded)
if (clipQueue.length) {
stepClip(dir, byScore);
stepClip(dir, byScore, jump);
return;
}
@@ -570,10 +574,12 @@ document.addEventListener('keydown', e => {
if (byScore) {
// U/I walk the ranking; the anchor is the section the playhead sits in
// (incl. its preroll lead-in). Anywhere else, I starts at the loudest.
// Shift jumps straight to the loudest (Shift+U) or quietest (Shift+I).
const order = scoreOrder(all);
const curIdx = all.findIndex(s => cur >= s.start - preroll - 0.5 && cur <= s.end + 0.5);
const pos = curIdx === -1 ? -1 : order.indexOf(curIdx);
const next = pos === -1 ? (dir > 0 ? 0 : -1) : pos + dir;
const next = jump ? (dir > 0 ? order.length - 1 : 0)
: pos === -1 ? (dir > 0 ? 0 : -1) : pos + dir;
if (next >= 0 && next < order.length)
jumpTo(all[order[next]], next, order.length, 'Highlight');
else
@@ -581,6 +587,13 @@ document.addEventListener('keydown', e => {
return;
}
// Shift+J/K jump to the first/last section regardless of the playhead.
if (jump) {
const i = dir < 0 ? 0 : all.length - 1;
jumpTo(all[i], i, all.length, 'Section');
return;
}
if (dir < 0) {
for (let i = all.length - 1; i >= 0; i--) {
if (all[i].start < cur - 1) { jumpTo(all[i], i, all.length, 'Section'); return; }
@@ -1012,7 +1025,7 @@ async function dayHighlights(dayId, analyzableFiles) {
const note = document.createElement('p');
note.className = 'quiet';
note.style.marginTop = '6px';
note.textContent = 'J / K plays through all sections in time order, U / I by loudness (loudest first)';
note.textContent = 'J / K plays through all sections in time order, U / I by loudness (loudest first). Hold Shift to jump to the first / last section (Shift + J / K) or the loudest / quietest (Shift + U / I)';
box.appendChild(note);
const MAX_DAY_CHIPS = 50;