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

243 lines
7.5 KiB
JavaScript

/** Own Scene panel listeners and presentation through project reads and actions. */
import {
sceneElements,
renderSceneOptions,
renderSceneShots,
presentSceneSelection,
presentSceneButtons,
presentSceneProgress,
presentSceneRuntime,
} from './scenePresentation.js';
export class SceneControls {
constructor({ read, actions, subscribe, elements = sceneElements() }) {
this.read = read;
this.actions = actions;
this.elements = elements;
this.destroyed = false;
this.actionGeneration = 0;
this.removers = [];
this.rowRemovers = [];
this.playbackKeyRemover = null;
if (!elements.select) return;
if (subscribe)
this.unsubscribe = subscribe((notification) =>
this.present(notification),
);
else {
this.renderSceneSelect();
this.renderShotList();
}
this.listen(elements.select, 'change', () =>
this.run('selectScene', elements.select.value),
);
for (const [element, action] of Object.entries({
capture: 'capture',
update: 'update',
next: 'next',
export: 'export',
download: 'download',
})) {
this.listen(elements[element], 'click', () => this.run(action));
}
this.listen(elements.new, 'click', () => this.createScene());
this.listen(elements.delete, 'click', () => this.deleteSelectedScene());
this.listen(elements.start, 'click', () =>
this.run('start', this.read().selectedSceneId),
);
this.listen(elements.stop, 'click', () => this.run('stop', 'Stopped'));
this.listen(elements.import, 'click', () => elements.file?.click());
this.listen(elements.file, 'change', async () => {
const file = elements.file?.files?.[0];
if (!file) return;
await this.run(
this.actions.reviewImport ? 'reviewImport' : 'import',
file,
);
if (!this.destroyed) elements.file.value = '';
});
if (!subscribe) {
this.updateStatus('Ready');
this.setProgress(0);
this.setButtons(false);
}
}
/** Apply only the presentation affected by an action; selection keeps row identity. */
present({ state, change, initial }) {
if (this.destroyed) return;
const type = change?.type;
if (
initial ||
[
'scene-options-changed',
'scene-created',
'scene-deleted',
'project-imported',
].includes(type)
)
this.renderSceneSelect();
if (
initial ||
[
'shots-changed',
'scene-created',
'scene-deleted',
'shot-captured',
'shot-updated',
'shot-renamed',
'shot-deleted',
'project-imported',
].includes(type)
)
this.renderShotList();
if (type === 'selection-changed')
presentSceneSelection(this.elements.shots, state.selectedShotId);
if (initial || type === 'buttons-changed') this.setButtons(state.running);
if (initial || type === 'progress-changed')
this.setProgress(state.progress);
if (initial || type === 'status-changed') this.updateStatus(state.status);
if (initial || type === 'runtime-changed')
this.updateRuntime(state.runtime);
if (initial || type === 'playback-presentation')
this.setPlaybackActive(state.playbackActive);
if (initial || type === 'playback-keyboard')
this.setPlaybackKeyboardEnabled(state.keyboardEnabled);
if (type === 'project-exported') this.updateStatus(state.status);
if (type === 'shot-loaded') this.updateStatus(state.status);
if (type === 'run-event' && change.event === 'shot_start')
this.setButtons(state.running);
}
listen(target, type, callback, removers = this.removers) {
if (!target) return;
const listener = (event) => {
if (!this.destroyed) return callback(event);
};
target.addEventListener(type, listener);
removers.push(() => target.removeEventListener(type, listener));
}
run(action, ...args) {
if (this.destroyed) return;
const generation = ++this.actionGeneration;
const failed = () => {
if (!this.destroyed && generation === this.actionGeneration)
this.updateStatus('Scene action failed');
};
try {
const result = this.actions[action](...args);
return result?.then ? Promise.resolve(result).catch(failed) : result;
} catch {
failed();
}
}
renderSceneSelect() {
if (!this.destroyed) renderSceneOptions(this.elements.select, this.read());
}
createScene() {
if (this.destroyed) return;
const name = window.prompt(
'New scene name',
`Scene ${this.read().scenes.length + 1}`,
);
if (name) this.run('create', name);
}
deleteSelectedScene() {
if (this.destroyed) return;
const state = this.read();
const scene = state.scenes.find(
(item) => item.id === state.selectedSceneId,
);
if (scene && window.confirm(`Delete scene "${scene.title}" and all shots?`))
this.run('deleteScene');
}
deleteShot(sceneId, shotId) {
if (this.destroyed) return;
const scene = this.read().scenes.find((item) => item.id === sceneId);
const shot = scene?.shots.find((item) => item.id === shotId);
if (shot && window.confirm(`Delete shot "${shot.title}"?`))
this.run('deleteShot', sceneId, shotId);
}
renderShotList() {
if (this.destroyed) return;
for (const remove of this.rowRemovers.splice(0)) remove();
renderSceneShots(this.elements.shots, this.read(), {
listen: (element, type, callback) =>
this.listen(element, type, callback, this.rowRemovers),
select: (id) => {
this.run('selectShot', id);
if (!this.destroyed)
presentSceneSelection(
this.elements.shots,
this.read().selectedShotId,
);
},
rename: (sceneId, shotId, title) =>
this.run('renameShot', sceneId, shotId, title),
load: (sceneId, shotId) => this.run('load', sceneId, shotId),
remove: (sceneId, shotId) => this.deleteShot(sceneId, shotId),
});
}
setButtons(running) {
if (!this.destroyed)
presentSceneButtons(this.elements, running, this.read().hasRun);
}
setProgress(progress) {
if (!this.destroyed) presentSceneProgress(this.elements.progress, progress);
}
updateStatus(text) {
if (!this.destroyed && this.elements.status)
this.elements.status.textContent = text;
}
updateRuntime(text) {
if (!this.destroyed) presentSceneRuntime(this.elements.runtime, text);
}
setPlaybackActive(active) {
if (this.destroyed && active) return;
document.body.classList.toggle('scene-playback-mode', active);
}
setPlaybackKeyboardEnabled(enabled) {
if (!enabled) {
this.playbackKeyRemover?.();
this.playbackKeyRemover = null;
} else if (!this.destroyed && !this.playbackKeyRemover) {
const onKeyDown = (event) => {
if (!this.destroyed && event.key === 'Escape' && this.read().running)
this.run('stop', 'Stopped (Esc)');
};
document.addEventListener('keydown', onKeyDown);
this.playbackKeyRemover = () =>
document.removeEventListener('keydown', onKeyDown);
}
}
destroy() {
if (this.destroyed) return;
this.destroyed = true;
this.actionGeneration++;
this.unsubscribe?.();
this.unsubscribe = null;
this.setPlaybackKeyboardEnabled(false);
for (const remove of [
...this.removers.splice(0),
...this.rowRemovers.splice(0),
])
remove();
if (this.elements.shots) this.elements.shots.textContent = '';
presentSceneRuntime(this.elements.runtime, '');
this.setPlaybackActive(false);
}
}