refactor: keep Scene prompts with their controls

This commit is contained in:
Sameh Khamis committed 2026-09-13 03:35:22 -07:00
1 parent d42036d74f
commit 9b6d34cb8a
5 files changed
+77 -15

No files matched your search

+1 -1
View File
@@ -323,7 +323,7 @@ original Display groups, independent scroll positions and current focus owner.
## Scene controls ## Scene controls
`ui/scenes` owns Scene panel input, project/shot rows, playback button and runtime `ui/scenes` owns Scene prompts, panel input, project/shot rows, playback button and runtime
presentation. It receives project reads and explicit actions, with no imports of presentation. It receives project reads and explicit actions, with no imports of
the director, source modules, camera engine or storage. Replacement and disposal the director, source modules, camera engine or storage. Replacement and disposal
release listeners; pending action feedback is limited to its current owner. release listeners; pending action feedback is limited to its current owner.
+1 -1
View File
@@ -2,7 +2,7 @@
## Scene control ownership ## Scene control ownership
Scene controls own panel listeners, shot rows, playback/recording presentation Scene controls own creation/deletion prompts, panel listeners, shot rows, playback/recording presentation
and keyboard cancellation. The director supplies project reads and explicit and keyboard cancellation. The director supplies project reads and explicit
editing/playback actions while retaining persistence, camera and layer sequencing. editing/playback actions while retaining persistence, camera and layer sequencing.
Replacing rows revokes their old listeners. Disposal stops controls immediately; Replacing rows revokes their old listeners. Disposal stops controls immediately;
+3 -10
View File
@@ -417,7 +417,7 @@ export class SceneDirector {
this._saveProject(); this._saveProject();
this._renderShotList(); this._renderShotList();
}, },
create: () => this._createScene(), create: (name) => this._createScene(name),
deleteScene: () => this._deleteSelectedScene(), deleteScene: () => this._deleteSelectedScene(),
capture: () => this.captureShot(), capture: () => this.captureShot(),
update: () => this.updateSelectedShot(), update: () => this.updateSelectedShot(),
@@ -474,9 +474,8 @@ export class SceneDirector {
return { scene, shot }; return { scene, shot };
} }
/** Prompt the user for a name and append a new empty scene to the project. */ /** Append a named empty scene after the controls accept the creation prompt. */
_createScene() { _createScene(sceneName) {
const sceneName = window.prompt('New scene name', `Scene ${this._project.scenes.length + 1}`);
if (!sceneName) return; if (!sceneName) return;
const scene = { const scene = {
@@ -498,9 +497,6 @@ export class SceneDirector {
const scene = this._getSelectedScene(); const scene = this._getSelectedScene();
if (!scene) return; if (!scene) return;
const ok = window.confirm(`Delete scene "${scene.title}" and all shots?`);
if (!ok) return;
this._project.scenes = this._project.scenes.filter((item) => item.id !== scene.id); this._project.scenes = this._project.scenes.filter((item) => item.id !== scene.id);
// Restore default recipes if the user deleted all scenes // Restore default recipes if the user deleted all scenes
if (!this._project.scenes.length) { if (!this._project.scenes.length) {
@@ -596,9 +592,6 @@ export class SceneDirector {
const { scene, shot } = this._getShot(sceneId, shotId); const { scene, shot } = this._getShot(sceneId, shotId);
if (!scene || !shot) return; if (!scene || !shot) return;
const ok = window.confirm(`Delete shot "${shot.title}"?`);
if (!ok) return;
scene.shots = scene.shots.filter((item) => item.id !== shot.id); scene.shots = scene.shots.filter((item) => item.id !== shot.id);
this._selectedShotId = scene.shots[0]?.id || null; this._selectedShotId = scene.shots[0]?.id || null;
this._saveProject(); this._saveProject();
+30 -3
View File
@@ -25,8 +25,6 @@ export class SceneControls {
this.run('selectScene', elements.select.value), this.run('selectScene', elements.select.value),
); );
for (const [element, action] of Object.entries({ for (const [element, action] of Object.entries({
new: 'create',
delete: 'deleteScene',
capture: 'capture', capture: 'capture',
update: 'update', update: 'update',
next: 'next', next: 'next',
@@ -35,6 +33,8 @@ export class SceneControls {
})) { })) {
this.listen(elements[element], 'click', () => this.run(action)); 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.listen(elements.start, 'click', () =>
this.run('start', this.read().selectedSceneId), this.run('start', this.read().selectedSceneId),
); );
@@ -79,6 +79,33 @@ export class SceneControls {
if (!this.destroyed) renderSceneOptions(this.elements.select, this.read()); 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() { renderShotList() {
if (this.destroyed) return; if (this.destroyed) return;
for (const remove of this.rowRemovers.splice(0)) remove(); for (const remove of this.rowRemovers.splice(0)) remove();
@@ -89,7 +116,7 @@ export class SceneControls {
rename: (sceneId, shotId, title) => rename: (sceneId, shotId, title) =>
this.run('renameShot', sceneId, shotId, title), this.run('renameShot', sceneId, shotId, title),
load: (sceneId, shotId) => this.run('load', sceneId, shotId), load: (sceneId, shotId) => this.run('load', sceneId, shotId),
remove: (sceneId, shotId) => this.run('deleteShot', sceneId, shotId), remove: (sceneId, shotId) => this.deleteShot(sceneId, shotId),
}); });
} }
+42
View File
@@ -262,3 +262,45 @@ test('Scene action failures are visible only while that action still owns presen
f.restore(); f.restore();
} }
}); });
test('creation and deletion prompts precede their model actions and remain inert after disposal', () => {
const f = fixture();
try {
let prompts = 0;
window.prompt = () => {
prompts++;
return null;
};
window.confirm = () => {
prompts++;
return false;
};
f.elements.new.click();
f.elements.delete.click();
f.owner.deleteShot('scene-a', 'shot-a');
assert.deepEqual(f.calls, []);
window.prompt = () => {
prompts++;
return 'New title';
};
window.confirm = () => {
prompts++;
return true;
};
f.elements.new.click();
f.elements.delete.click();
f.owner.deleteShot('scene-a', 'shot-a');
assert.deepEqual(f.calls, [
['create', 'New title'],
['deleteScene'],
['deleteShot', 'scene-a', 'shot-a'],
]);
f.owner.destroy();
f.owner.createScene();
f.owner.deleteSelectedScene();
f.owner.deleteShot('scene-a', 'shot-a');
assert.equal(prompts, 6);
} finally {
f.restore();
}
});