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
`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
the director, source modules, camera engine or storage. Replacement and disposal
release listeners; pending action feedback is limited to its current owner.
+1 -1
View File
@@ -2,7 +2,7 @@
## 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
editing/playback actions while retaining persistence, camera and layer sequencing.
Replacing rows revokes their old listeners. Disposal stops controls immediately;
+3 -10
View File
@@ -417,7 +417,7 @@ export class SceneDirector {
this._saveProject();
this._renderShotList();
},
create: () => this._createScene(),
create: (name) => this._createScene(name),
deleteScene: () => this._deleteSelectedScene(),
capture: () => this.captureShot(),
update: () => this.updateSelectedShot(),
@@ -474,9 +474,8 @@ export class SceneDirector {
return { scene, shot };
}
/** Prompt the user for a name and append a new empty scene to the project. */
_createScene() {
const sceneName = window.prompt('New scene name', `Scene ${this._project.scenes.length + 1}`);
/** Append a named empty scene after the controls accept the creation prompt. */
_createScene(sceneName) {
if (!sceneName) return;
const scene = {
@@ -498,9 +497,6 @@ export class SceneDirector {
const scene = this._getSelectedScene();
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);
// Restore default recipes if the user deleted all scenes
if (!this._project.scenes.length) {
@@ -596,9 +592,6 @@ export class SceneDirector {
const { scene, shot } = this._getShot(sceneId, shotId);
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);
this._selectedShotId = scene.shots[0]?.id || null;
this._saveProject();
+30 -3
View File
@@ -25,8 +25,6 @@ export class SceneControls {
this.run('selectScene', elements.select.value),
);
for (const [element, action] of Object.entries({
new: 'create',
delete: 'deleteScene',
capture: 'capture',
update: 'update',
next: 'next',
@@ -35,6 +33,8 @@ export class SceneControls {
})) {
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),
);
@@ -79,6 +79,33 @@ export class SceneControls {
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();
@@ -89,7 +116,7 @@ export class SceneControls {
rename: (sceneId, shotId, title) =>
this.run('renameShot', sceneId, shotId, title),
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();
}
});
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();
}
});