From 36e668455f2dad34ba82c9fabce08d937972cc3b Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 19 May 2026 23:27:25 -0400 Subject: [PATCH] feat(editor): media-panel search, sequence duration badge, parseFloat guard - Media panel gains a search input that filters the clip list in real time (case-insensitive match on display_name / filename) - Timeline toolbar shows total sequence duration (e.g. 00:05:23;14) and frame rate, updated whenever clips change or a sequence is opened - parseFloat() guard on state.seq.frame_rate so a NUMERIC string from Postgres never leaks into Timeline.render() / applyHistory() --- services/web-ui/public/editor.html | 106 +++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 7 deletions(-) diff --git a/services/web-ui/public/editor.html b/services/web-ui/public/editor.html index ce601d1..bccd830 100644 --- a/services/web-ui/public/editor.html +++ b/services/web-ui/public/editor.html @@ -112,6 +112,31 @@ color: var(--text-tertiary); } + .media-panel-search { + padding: var(--sp-2) var(--sp-3); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + } + + .media-panel-search input { + width: 100%; + height: 24px; + padding: 0 var(--sp-2); + font-size: var(--text-xs); + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-sm); + color: var(--text-primary); + outline: none; + box-sizing: border-box; + } + .media-panel-search input:focus { + border-color: var(--accent-border); + } + .media-panel-search input::placeholder { + color: var(--text-tertiary); + } + .media-asset-list { flex: 1; overflow-y: auto; @@ -193,8 +218,27 @@ .tl-sep { width: 1px; height: 18px; background: var(--border); } - .tl-save-status { + .tl-seq-info { margin-left: auto; + display: flex; + align-items: center; + gap: var(--sp-3); + } + + .tl-seq-duration { + font-size: var(--text-xs); + font-variant-numeric: tabular-nums; + font-family: var(--font-mono, monospace); + color: var(--text-secondary); + letter-spacing: .03em; + } + + .tl-seq-fps { + font-size: var(--text-xs); + color: var(--text-tertiary); + } + + .tl-save-status { font-size: var(--text-xs); color: var(--text-tertiary); } @@ -425,6 +469,9 @@ +
@@ -439,7 +486,11 @@
- +
+ + + +
@@ -593,6 +644,11 @@ document.addEventListener('DOMContentLoaded', async () => { }); })(); + // Media search + document.getElementById('mediaSearch').addEventListener('input', function() { + renderMediaList(); + }); + await loadProject(); await loadSequences(); await loadMediaAssets(); @@ -641,8 +697,10 @@ async function openSequence(id) { state.history = [cloneClips(state.seq.clips)]; state.historyIdx = 0; document.getElementById('seqSelect').value = id; - Timeline.render(state.seq.clips, { fps: state.seq.frame_rate || 59.94 }); + const fps = parseFloat(state.seq.frame_rate) || 59.94; + Timeline.render(state.seq.clips, { fps }); updateProgramScrub(); + updateSeqInfo(); } // ════════════════════════════════════════════════════════════════ @@ -668,9 +726,23 @@ function fmtMs(ms) { } function renderMediaList() { - const list = document.getElementById('mediaAssetList'); + const list = document.getElementById('mediaAssetList'); + const query = (document.getElementById('mediaSearch').value || '').trim().toLowerCase(); + const assets = query + ? state.assets.filter(a => + (a.display_name || '').toLowerCase().includes(query) || + (a.filename || '').toLowerCase().includes(query)) + : state.assets; + list.innerHTML = ''; - state.assets.forEach(function(asset) { + + if (assets.length === 0) { + list.innerHTML = '
' + + (query ? 'No clips match "' + esc(query) + '"' : 'No assets in this bin.') + '
'; + return; + } + + assets.forEach(function(asset) { const el = document.createElement('div'); el.className = 'media-asset-item'; @@ -708,6 +780,23 @@ function renderMediaList() { }); } +// ════════════════════════════════════════════════════════════════ +// SEQUENCE INFO BADGE +// ════════════════════════════════════════════════════════════════ +function updateSeqInfo() { + if (!state.seq) { + document.getElementById('seqDuration').textContent = ''; + document.getElementById('seqFps').textContent = ''; + return; + } + const fps = parseFloat(state.seq.frame_rate) || 59.94; + const clips = state.seq.clips || []; + const maxOut = clips.reduce(function(m, c) { return Math.max(m, c.timeline_out_frames || 0); }, 0); + const durTC = maxOut > 0 ? TC.framesToTC(maxOut) : '00:00:00;00'; + document.getElementById('seqDuration').textContent = durTC; + document.getElementById('seqFps').textContent = fps + ' fps'; +} + // ════════════════════════════════════════════════════════════════ // SOURCE MONITOR // ════════════════════════════════════════════════════════════════ @@ -920,6 +1009,7 @@ function onClipsChanged(clips) { state.history.push(cloneClips(clips)); state.historyIdx = state.history.length - 1; state.seq.clips = clips; + updateSeqInfo(); markDirty(); } @@ -992,7 +1082,9 @@ function redo() { function applyHistory() { const clips = cloneClips(state.history[state.historyIdx]); state.seq.clips = clips; - Timeline.render(clips, { fps: state.seq.frame_rate || 59.94 }); + const fps = parseFloat(state.seq.frame_rate) || 59.94; + Timeline.render(clips, { fps }); + updateSeqInfo(); markDirty(); } @@ -1077,7 +1169,7 @@ function toggleKbdHelp() { } document.addEventListener('DOMContentLoaded', function() { - document.getElementById('closeKbdHelp').onclick = closeKbdHelp; + document.getElementById('closeKbdHelp').onclick = closeKbdHelp; document.getElementById('kbdHelpBackdrop').onclick = closeKbdHelp; });