feat: day-collapsed-by-default, cross-file highlights navigation

- All days collapsed by default; only today auto-expands
- Day Highlights chips now show absolute HH:MM:SS timestamps and are
  clickable — jumps to the right file's player, auto-opening it and
  closing the previous one when switching files
- J/K keys navigate across all files in the highlighted day (day-level
  mode) instead of staying within a single file; falls back to per-file
  mode when no day highlights are active
- Collapsing a day clears its cross-file section state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 20:13:19 +02:00
parent d84056929a
commit f7e7d5bfaa
+105 -6
View File
@@ -790,6 +790,10 @@ let activePlayerIdx = null;
let allFiles = []; let allFiles = [];
// dayId -> boolean, persists expanded state across re-renders // dayId -> boolean, persists expanded state across re-renders
const dayExpanded = new Map(); const dayExpanded = new Map();
// cross-file day section navigation (populated by ★ Highlights)
let dayActiveSections = [];
let dayActiveSectionCursor = -1;
let dayActiveId = null;
function groupByDay(files) { function groupByDay(files) {
const map = new Map(); 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) // J = previous section, K = next section (only when focus is not in an input)
document.addEventListener('keydown', e => { document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; 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; if (activePlayerIdx === null) return;
const sections = sectionMap.get(activePlayerIdx) || []; const sections = sectionMap.get(activePlayerIdx) || [];
if (!sections.length) return; if (!sections.length) return;
@@ -949,7 +971,6 @@ document.addEventListener('keydown', e => {
const preroll = getPreroll(); const preroll = getPreroll();
if (e.key === 'j' || e.key === 'J') { if (e.key === 'j' || e.key === 'J') {
e.preventDefault();
const cur = audio.currentTime; const cur = audio.currentTime;
let targetIdx = -1; let targetIdx = -1;
for (let i = sections.length - 1; i >= 0; i--) { for (let i = sections.length - 1; i >= 0; i--) {
@@ -963,8 +984,7 @@ document.addEventListener('keydown', e => {
} else { } else {
announce('Beginning of sections'); announce('Beginning of sections');
} }
} else if (e.key === 'k' || e.key === 'K') { } else {
e.preventDefault();
const cur = audio.currentTime; const cur = audio.currentTime;
let jumped = false; let jumped = false;
for (let i = 0; i < sections.length; i++) { for (let i = 0; i < sections.length; i++) {
@@ -1077,13 +1097,12 @@ function renderFiles(files) {
document.getElementById('empty').style.display = visible ? 'none' : ''; document.getElementById('empty').style.display = visible ? 'none' : '';
const days = groupByDay(files); const days = groupByDay(files);
let isFirst = true; const today = new Date().toISOString().slice(0, 10);
days.forEach((dayFiles, day) => { days.forEach((dayFiles, day) => {
const dayId = 'day-' + day.replace(/-/g, ''); 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); const expanded = dayExpanded.get(dayId);
isFirst = false;
const totalSize = dayFiles.reduce((a, f) => a + f.size, 0); const totalSize = dayFiles.reduce((a, f) => a + f.size, 0);
const totalDur = dayFiles.reduce((a, f) => a + (f.duration || 0), 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; 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'); 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'); const box = document.createElement('div');
box.className = 'wbox'; box.className = 'wbox';
box.style.marginBottom = '4px'; box.style.marginBottom = '4px';
@@ -1337,6 +1378,27 @@ async function dayHighlights(dayId, analyzableFiles) {
labels.innerHTML = `<span>${esc(fmtHM(minT))}</span><span>${esc(fmtHM((minT+maxT)/2))}</span><span>${esc(fmtHM(maxT))}</span>`; labels.innerHTML = `<span>${esc(fmtHM(minT))}</span><span>${esc(fmtHM((minT+maxT)/2))}</span><span>${esc(fmtHM(maxT))}</span>`;
box.appendChild(labels); 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'); const summary = document.createElement('div');
summary.className = 'quiet'; summary.className = 'quiet';
summary.style.marginTop = '4px'; summary.style.marginTop = '4px';
@@ -1348,6 +1410,43 @@ async function dayHighlights(dayId, analyzableFiles) {
if (btn) btn.disabled = false; 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() { function applyFilters() {
const nameQ = document.getElementById('filter-name').value.toLowerCase().trim(); const nameQ = document.getElementById('filter-name').value.toLowerCase().trim();
const fromD = document.getElementById('filter-from').value; const fromD = document.getElementById('filter-from').value;