diff --git a/web.py b/web.py index 7009170..913e4eb 100644 --- a/web.py +++ b/web.py @@ -790,6 +790,10 @@ let activePlayerIdx = null; let allFiles = []; // dayId -> boolean, persists expanded state across re-renders const dayExpanded = new Map(); +// cross-file day section navigation (populated by ★ Highlights) +let dayActiveSections = []; +let dayActiveSectionCursor = -1; +let dayActiveId = null; function groupByDay(files) { const map = new Map(); @@ -941,6 +945,24 @@ async function analyse(idx, filename, cell, btn) { // J = previous section, K = next section (only when focus is not in an input) document.addEventListener('keydown', e => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + if (e.key !== 'j' && e.key !== 'J' && e.key !== 'k' && e.key !== 'K') return; + e.preventDefault(); + + // Day-level cross-file navigation when Highlights have been loaded + if (dayActiveSections.length) { + if (e.key === 'j' || e.key === 'J') { + const ni = dayActiveSectionCursor > 0 ? dayActiveSectionCursor - 1 : -1; + if (ni >= 0) jumpToDaySection(ni); + else announce('Beginning of day sections'); + } else { + const ni = dayActiveSectionCursor + 1; + if (ni < dayActiveSections.length) jumpToDaySection(ni); + else announce('End of day sections'); + } + return; + } + + // Per-file section navigation if (activePlayerIdx === null) return; const sections = sectionMap.get(activePlayerIdx) || []; if (!sections.length) return; @@ -949,7 +971,6 @@ document.addEventListener('keydown', e => { const preroll = getPreroll(); if (e.key === 'j' || e.key === 'J') { - e.preventDefault(); const cur = audio.currentTime; let targetIdx = -1; for (let i = sections.length - 1; i >= 0; i--) { @@ -963,8 +984,7 @@ document.addEventListener('keydown', e => { } else { announce('Beginning of sections'); } - } else if (e.key === 'k' || e.key === 'K') { - e.preventDefault(); + } else { const cur = audio.currentTime; let jumped = false; for (let i = 0; i < sections.length; i++) { @@ -1077,13 +1097,12 @@ function renderFiles(files) { document.getElementById('empty').style.display = visible ? 'none' : ''; const days = groupByDay(files); - let isFirst = true; + const today = new Date().toISOString().slice(0, 10); days.forEach((dayFiles, day) => { const dayId = 'day-' + day.replace(/-/g, ''); - if (!dayExpanded.has(dayId)) dayExpanded.set(dayId, isFirst); + if (!dayExpanded.has(dayId)) dayExpanded.set(dayId, day === today); const expanded = dayExpanded.get(dayId); - isFirst = false; const totalSize = dayFiles.reduce((a, f) => a + f.size, 0); const totalDur = dayFiles.reduce((a, f) => a + (f.duration || 0), 0); @@ -1218,6 +1237,11 @@ function renderFiles(files) { } }); document.getElementById('dayhl-' + dayId).hidden = true; + if (dayActiveId === dayId) { + dayActiveSections = []; + dayActiveSectionCursor = -1; + dayActiveId = null; + } } }); @@ -1327,6 +1351,23 @@ async function dayHighlights(dayId, analyzableFiles) { return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0'); }; + // Build cross-file section list for J/K navigation and chips + dayActiveSections = []; + positioned.forEach(({ f, data, fileStart }) => { + (data.sections || []).forEach(s => { + dayActiveSections.push({ + fileIdx: f._idx, + filename: f.name, + start: s.start, + end: s.end, + absStart: fileStart + s.start, + }); + }); + }); + dayActiveSections.sort((a, b) => a.absStart - b.absStart); + dayActiveSectionCursor = -1; + dayActiveId = dayId; + const box = document.createElement('div'); box.className = 'wbox'; box.style.marginBottom = '4px'; @@ -1337,6 +1378,27 @@ async function dayHighlights(dayId, analyzableFiles) { labels.innerHTML = `${esc(fmtHM(minT))}${esc(fmtHM((minT+maxT)/2))}${esc(fmtHM(maxT))}`; box.appendChild(labels); + if (dayActiveSections.length) { + const chips = document.createElement('div'); + chips.className = 'chips'; + chips.setAttribute('role', 'list'); + chips.setAttribute('aria-label', 'Day loud sections — click to jump, J/K to step across files'); + dayActiveSections.forEach((sec, si) => { + const c = document.createElement('button'); + c.className = 'chip'; + c.setAttribute('role', 'listitem'); + c.title = sec.filename + ' @ ' + fmtT(sec.start); + const d = new Date(sec.absStart * 1000); + const hms = d.getHours().toString().padStart(2,'0') + ':' + + d.getMinutes().toString().padStart(2,'0') + ':' + + d.getSeconds().toString().padStart(2,'0'); + c.textContent = hms; + c.addEventListener('click', () => jumpToDaySection(si)); + chips.appendChild(c); + }); + box.appendChild(chips); + } + const summary = document.createElement('div'); summary.className = 'quiet'; summary.style.marginTop = '4px'; @@ -1348,6 +1410,43 @@ async function dayHighlights(dayId, analyzableFiles) { if (btn) btn.disabled = false; } +function jumpToDaySection(si) { + if (si < 0 || si >= dayActiveSections.length) return; + dayActiveSectionCursor = si; + const sec = dayActiveSections[si]; + const { fileIdx, filename, start, end } = sec; + + // Close the previous player if switching to a different file + if (activePlayerIdx !== null && activePlayerIdx !== fileIdx) { + const prevProw = document.getElementById('prow-' + activePlayerIdx); + if (prevProw && !prevProw.hidden) { + document.getElementById('aud-' + activePlayerIdx)?.pause(); + prevProw.hidden = true; + const prevPbtn = document.getElementById('pbtn-' + activePlayerIdx); + if (prevPbtn) { + prevPbtn.setAttribute('aria-expanded', 'false'); + prevPbtn.textContent = '▶ Play'; + prevPbtn.setAttribute('aria-label', 'Play ' + (recMap.get(activePlayerIdx) || '')); + } + } + } + + // Open this file's player + const pbtn = document.getElementById('pbtn-' + fileIdx); + if (pbtn && pbtn.getAttribute('aria-expanded') !== 'true') togglePlayer(fileIdx, filename); + activePlayerIdx = fileIdx; + + const audio = document.getElementById('aud-' + fileIdx); + if (!audio) return; + const seekTo = Math.max(0, start - getPreroll()); + const doSeek = () => { audio.currentTime = seekTo; }; + if (audio.readyState >= 1) doSeek(); + else audio.addEventListener('loadedmetadata', doSeek, { once: true }); + + setCutFields(fileIdx, start, end); + announce(`Day section ${si + 1} of ${dayActiveSections.length}: ${fmtT(start)}–${fmtT(end)} in ${filename}`); +} + function applyFilters() { const nameQ = document.getElementById('filter-name').value.toLowerCase().trim(); const fromD = document.getElementById('filter-from').value;