Files
gods-eye-view/src/ui/scenePresentation.js
T

157 lines
5.2 KiB
JavaScript

/** Scene panel elements and text-only project presentation. */
export function sceneElements(root = document) {
const ids = {
panel: 'scene-panel',
select: 'scene-select',
new: 'scene-new-btn',
delete: 'scene-delete-btn',
capture: 'scene-capture-btn',
update: 'scene-update-shot-btn',
shots: 'scene-shot-list',
start: 'scene-start-btn',
stop: 'scene-stop-btn',
next: 'scene-next-btn',
export: 'scene-export-btn',
import: 'scene-import-btn',
file: 'scene-import-file',
download: 'scene-download-btn',
status: 'scene-status',
progress: 'scene-progress-fill',
runtime: 'scene-runtime',
};
return Object.fromEntries(
Object.entries(ids).map(([name, id]) => [name, root.getElementById(id)]),
);
}
export function renderSceneOptions(element, { scenes, selectedSceneId }) {
if (!element) return;
element.textContent = '';
for (const scene of scenes) {
const option = document.createElement('option');
option.value = scene.id;
option.textContent = scene.title;
element.appendChild(option);
}
if (selectedSceneId) element.value = selectedSceneId;
}
export function renderSceneShots(
element,
state,
{ listen, select, rename, load, remove },
) {
if (!element) return;
const scene = state.scenes.find((item) => item.id === state.selectedSceneId);
element.textContent = '';
if (!scene || scene.shots.length === 0) {
const empty = document.createElement('div');
empty.className = 'scene-shot-empty';
empty.textContent = 'No shots yet. Use CAPTURE SHOT to save current look.';
element.appendChild(empty);
return;
}
for (const shot of scene.shots) {
const row = document.createElement('div');
row.className = 'scene-shot-row';
row.dataset.sceneShotId = shot.id;
row.classList.toggle('active', shot.id === state.selectedShotId);
const top = document.createElement('div');
top.className = 'scene-shot-top';
const label = document.createElement('div');
label.className = 'scene-shot-label';
label.textContent = shot.title;
label.title = 'Double-click to rename';
listen(label, 'click', () => select(shot.id));
listen(label, 'dblclick', () => {
if (label.children.length) return;
const input = document.createElement('input');
input.type = 'text';
input.className = 'scene-shot-rename';
input.setAttribute('aria-label', 'Shot name');
input.value = shot.title;
let finished = false;
const finish = (save) => {
if (finished) return;
finished = true;
const title = input.value.trim();
label.textContent = shot.title;
if (save && title && title !== shot.title)
rename(scene.id, shot.id, title);
};
listen(input, 'keydown', (event) => {
event.stopPropagation();
if (event.key === 'Enter' || event.key === 'Escape') {
event.preventDefault();
finish(event.key === 'Enter');
}
});
listen(input, 'blur', () => finish(true));
label.textContent = '';
label.appendChild(input);
input.focus();
input.select();
});
const actions = document.createElement('div');
actions.className = 'scene-shot-actions';
const loadButton = document.createElement('button');
loadButton.className = 'scene-shot-btn';
loadButton.textContent = 'LOAD';
listen(loadButton, 'click', () => load(scene.id, shot.id));
const deleteButton = document.createElement('button');
deleteButton.className = 'scene-shot-btn scene-shot-danger';
deleteButton.textContent = 'DEL';
listen(deleteButton, 'click', () => remove(scene.id, shot.id));
actions.appendChild(loadButton);
actions.appendChild(deleteButton);
top.appendChild(label);
top.appendChild(actions);
const meta = document.createElement('div');
meta.className = 'scene-shot-meta';
const mode = shot.visual?.detection?.mode || 'OFF';
const style = shot.visual?.style || 'normal';
meta.textContent = `${style.toUpperCase()} · ${mode} · ${shot.durationSec.toFixed(1)}s + ${shot.holdSec.toFixed(1)}s`;
row.appendChild(top);
row.appendChild(meta);
element.appendChild(row);
}
}
export function presentSceneSelection(element, selectedShotId) {
for (const row of element?.children || []) {
row.classList.toggle('active', row.dataset.sceneShotId === selectedShotId);
}
}
export function presentSceneButtons(elements, running, hasRun) {
for (const name of [
'start',
'next',
'select',
'new',
'delete',
'capture',
'update',
'export',
'import',
]) {
if (elements[name]) elements[name].disabled = running;
}
if (elements.stop) elements.stop.disabled = !running;
if (elements.download) elements.download.disabled = !hasRun;
elements.panel?.classList.toggle('running', running);
}
export function presentSceneProgress(element, progress) {
if (!element) return;
const percentage = Math.round(Math.max(0, Math.min(1, progress)) * 100);
element.style.width = `${percentage}%`;
element.textContent = `${percentage}%`;
}
export function presentSceneRuntime(element, text) {
if (!element) return;
element.textContent = text;
element.classList.toggle('active', !!text);
}