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;