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()
This commit is contained in:
parent
4d0e715982
commit
36e668455f
1 changed files with 99 additions and 7 deletions
|
|
@ -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 @@
|
|||
<option value="">All assets</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="media-panel-search">
|
||||
<input type="search" id="mediaSearch" placeholder="Search clips…" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="media-asset-list" id="mediaAssetList"></div>
|
||||
</div>
|
||||
|
||||
|
|
@ -439,7 +486,11 @@
|
|||
<button class="tl-tool-btn" id="redoBtn" title="Redo (Ctrl+Shift+Z)">↪</button>
|
||||
<div class="tl-sep"></div>
|
||||
<button class="tl-tool-btn" id="helpBtn" title="Keyboard shortcuts (?)">?</button>
|
||||
<span class="tl-save-status" id="saveStatus"></span>
|
||||
<div class="tl-seq-info">
|
||||
<span class="tl-seq-fps" id="seqFps"></span>
|
||||
<span class="tl-seq-duration" id="seqDuration"></span>
|
||||
<span class="tl-save-status" id="saveStatus"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-container" id="timelineContainer"></div>
|
||||
</div>
|
||||
|
|
@ -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 = '<div style="padding:var(--sp-4) var(--sp-3);font-size:var(--text-xs);color:var(--text-tertiary);">' +
|
||||
(query ? 'No clips match "' + esc(query) + '"' : 'No assets in this bin.') + '</div>';
|
||||
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;
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue