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:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user