feat(editor): fps-aware render, FPS selector in new-seq dialog, keyboard help overlay
- openSequence() and applyHistory() now pass state.seq.frame_rate to Timeline.render() instead of hardcoded 59.94 — clips render on the correct frame grid for every sequence - New-sequence panel gains a frame-rate selector (23.976 / 24 / 25 / 29.97 / 30 / 50 / 59.94 / 60); createNewSequence() posts frame_rate to the API - Press ? to open a keyboard shortcut help overlay; Escape to close
This commit is contained in:
parent
81c771a7be
commit
bfc2649909
1 changed files with 156 additions and 3 deletions
|
|
@ -219,6 +219,85 @@
|
|||
transition: background var(--t-fast);
|
||||
}
|
||||
.topbar-seq-name:hover { background: var(--bg-hover); }
|
||||
|
||||
/* ── Keyboard shortcut help overlay ───────────────────────── */
|
||||
.kbd-help-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 900;
|
||||
}
|
||||
.kbd-help-backdrop.open { display: block; }
|
||||
|
||||
.kbd-help-panel {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: 0 24px 64px rgba(0,0,0,0.45);
|
||||
width: 480px;
|
||||
max-width: 90vw;
|
||||
z-index: 901;
|
||||
flex-direction: column;
|
||||
}
|
||||
.kbd-help-panel.open { display: flex; }
|
||||
|
||||
.kbd-help-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--sp-4) var(--sp-5);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: 600;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.kbd-help-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--sp-5);
|
||||
padding: var(--sp-5);
|
||||
}
|
||||
|
||||
.kbd-section-title {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: var(--sp-3);
|
||||
}
|
||||
|
||||
.kbd-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
margin-bottom: var(--sp-2);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 7px;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-strong, var(--border));
|
||||
border-radius: var(--r-sm);
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
min-width: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -358,6 +437,8 @@
|
|||
<div class="tl-sep"></div>
|
||||
<button class="tl-tool-btn" id="undoBtn" title="Undo (Ctrl+Z)">↩</button>
|
||||
<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>
|
||||
<div class="timeline-container" id="timelineContainer"></div>
|
||||
|
|
@ -384,6 +465,19 @@
|
|||
<label class="form-label" for="newSeqName">Sequence name</label>
|
||||
<input type="text" id="newSeqName" placeholder="e.g. Rough Cut v1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="newSeqFps">Frame rate</label>
|
||||
<select id="newSeqFps" class="form-select">
|
||||
<option value="23.976">23.976 fps (Cinema)</option>
|
||||
<option value="24">24 fps</option>
|
||||
<option value="25">25 fps (PAL)</option>
|
||||
<option value="29.97">29.97 fps (NTSC)</option>
|
||||
<option value="30">30 fps</option>
|
||||
<option value="50">50 fps</option>
|
||||
<option value="59.94" selected>59.94 fps (HD default)</option>
|
||||
<option value="60">60 fps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide-panel-footer">
|
||||
<button class="btn btn-ghost" id="cancelSeqBtn">Cancel</button>
|
||||
|
|
@ -391,6 +485,39 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard shortcut help overlay -->
|
||||
<div class="kbd-help-backdrop" id="kbdHelpBackdrop"></div>
|
||||
<div class="kbd-help-panel" id="kbdHelpPanel" role="dialog" aria-label="Keyboard shortcuts">
|
||||
<div class="kbd-help-header">
|
||||
<span>Keyboard Shortcuts</span>
|
||||
<button class="btn btn-ghost btn-sm" id="closeKbdHelp" style="padding:0;width:28px;height:28px;">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 3l10 10M13 3L3 13"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="kbd-help-body">
|
||||
<div>
|
||||
<div class="kbd-section-title">Playback</div>
|
||||
<div class="kbd-row"><kbd>Space</kbd><span>Play / Pause</span></div>
|
||||
<div class="kbd-row"><kbd>J</kbd><span>Stop</span></div>
|
||||
<div class="kbd-row"><kbd>L</kbd><span>Play</span></div>
|
||||
<div class="kbd-section-title" style="margin-top:var(--sp-4)">In / Out</div>
|
||||
<div class="kbd-row"><kbd>I</kbd><span>Mark In</span></div>
|
||||
<div class="kbd-row"><kbd>O</kbd><span>Mark Out</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kbd-section-title">Tools</div>
|
||||
<div class="kbd-row"><kbd>V</kbd><span>Select</span></div>
|
||||
<div class="kbd-row"><kbd>C</kbd><span>Razor</span></div>
|
||||
<div class="kbd-row"><kbd>H</kbd><span>Hand</span></div>
|
||||
<div class="kbd-section-title" style="margin-top:var(--sp-4)">Edit</div>
|
||||
<div class="kbd-row"><kbd>Ctrl+Z</kbd><span>Undo</span></div>
|
||||
<div class="kbd-row"><kbd>Ctrl+⇧+Z</kbd><span>Redo</span></div>
|
||||
<div class="kbd-row"><kbd>Ctrl+S</kbd><span>Save</span></div>
|
||||
<div class="kbd-row"><kbd>?</kbd><span>This help</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=6"></script>
|
||||
<script src="js/timecode.js"></script>
|
||||
<script src="js/timeline.js"></script>
|
||||
|
|
@ -514,7 +641,7 @@ async function openSequence(id) {
|
|||
state.history = [cloneClips(state.seq.clips)];
|
||||
state.historyIdx = 0;
|
||||
document.getElementById('seqSelect').value = id;
|
||||
Timeline.render(state.seq.clips, { fps: 59.94 });
|
||||
Timeline.render(state.seq.clips, { fps: state.seq.frame_rate || 59.94 });
|
||||
updateProgramScrub();
|
||||
}
|
||||
|
||||
|
|
@ -865,7 +992,7 @@ function redo() {
|
|||
function applyHistory() {
|
||||
const clips = cloneClips(state.history[state.historyIdx]);
|
||||
state.seq.clips = clips;
|
||||
Timeline.render(clips, { fps: 59.94 });
|
||||
Timeline.render(clips, { fps: state.seq.frame_rate || 59.94 });
|
||||
markDirty();
|
||||
}
|
||||
|
||||
|
|
@ -891,6 +1018,7 @@ function setupToolbar() {
|
|||
document.getElementById('undoBtn').onclick = undo;
|
||||
document.getElementById('redoBtn').onclick = redo;
|
||||
document.getElementById('saveBtn').onclick = saveSequence;
|
||||
document.getElementById('helpBtn').onclick = openKbdHelp;
|
||||
document.getElementById('exportEdlBtn').onclick = function() {
|
||||
if (!state.seq) { toast('No sequence open', '', 'warning'); return; }
|
||||
exportSequenceEDL(state.seq.id, (state.seq.name || 'sequence') + '.edl');
|
||||
|
|
@ -902,6 +1030,9 @@ function setupKeyboard() {
|
|||
const tag = document.activeElement.tagName;
|
||||
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return;
|
||||
|
||||
if (e.key === 'Escape') { closeKbdHelp(); return; }
|
||||
if (e.key === '?') { e.preventDefault(); toggleKbdHelp(); return; }
|
||||
|
||||
if (e.code === 'Space') { e.preventDefault(); togglePgmPlay(); return; }
|
||||
if (e.key === 'j' || e.key === 'J') { stopPgm(); return; }
|
||||
if (e.key === 'k' || e.key === 'K') { stopPgm(); return; }
|
||||
|
|
@ -929,6 +1060,27 @@ function updateToolbarActive(tool) {
|
|||
Timeline.setTool(tool);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// KEYBOARD HELP OVERLAY
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
function openKbdHelp() {
|
||||
document.getElementById('kbdHelpPanel').classList.add('open');
|
||||
document.getElementById('kbdHelpBackdrop').classList.add('open');
|
||||
}
|
||||
function closeKbdHelp() {
|
||||
document.getElementById('kbdHelpPanel').classList.remove('open');
|
||||
document.getElementById('kbdHelpBackdrop').classList.remove('open');
|
||||
}
|
||||
function toggleKbdHelp() {
|
||||
if (document.getElementById('kbdHelpPanel').classList.contains('open')) closeKbdHelp();
|
||||
else openKbdHelp();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('closeKbdHelp').onclick = closeKbdHelp;
|
||||
document.getElementById('kbdHelpBackdrop').onclick = closeKbdHelp;
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
// NEW SEQUENCE PANEL
|
||||
// ════════════════════════════════════════════════════════════════
|
||||
|
|
@ -942,7 +1094,8 @@ function setupNewSeqPanel() {
|
|||
|
||||
async function createNewSequence() {
|
||||
const name = document.getElementById('newSeqName').value.trim() || 'Sequence ' + (state.sequences.length + 1);
|
||||
const r = await createSequence({ project_id: state.projectId, name: name });
|
||||
const fps = parseFloat(document.getElementById('newSeqFps').value) || 59.94;
|
||||
const r = await createSequence({ project_id: state.projectId, name: name, frame_rate: fps });
|
||||
if (!r.success) { toast('Failed to create sequence', r.error, 'error'); return; }
|
||||
closePanel('seq');
|
||||
document.getElementById('newSeqName').value = '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue