The surfaces v4 has that v5 did not. Each announces itself and each can be absent without the rest noticing, which is the point of the exercise: the frontend should read as v4, and nothing in it should be load-bearing for anything else. * SCENE AND PROJECT, in the column v4 keeps them in, which now says which scene it is about. Length on the timeline is the scene's own CROP and says outright that a regenerate reads the project's -- the rule that mixing the two is how a per-scene value quietly becomes everyone's. * RATINGS on a scene, and a clip carries its rating as a colour on its edge. Stored, not yet read: nothing in v5 learns from them yet. * A RULER, and clips as wide as they are long, so the strip reads as time rather than a row of equal boxes. Both ends bounded -- one long clip must not become the whole strip, and one short one must stay hittable. * THE PICKER WHEEL, on the middle button anywhere. It shows whatever announced itself to actions.js, so a part of the app that grew a new action is in the wheel without the wheel being told, and a part that failed to load is simply not among them. v4's three pinned buttons had a fixed idea of what could go in them. * THE WAY IN, and it closes -- on Escape, on the backdrop, on its own button. v4's could only be completed: a page refresh was the only way out, which makes it a trap rather than a wizard. Dismissing is a real answer. * File > rename, and the listing moves with it rather than going stale until a reload. Two things the kit refused, correctly, and now nobody has to know: a wheel of fewer than two items throws (there is nothing to aim between), so the shell does not offer one; and a strip cell with no ceiling took the whole strip.
107 lines
3.7 KiB
JavaScript
107 lines
3.7 KiB
JavaScript
// The inspector: what the PROJECT is, and what THIS SCENE is.
|
|
//
|
|
// Two tabs because they are two different things and v4 proved that mixing them
|
|
// is how a per-scene value quietly becomes the project's. The rule this file
|
|
// exists to make visible: a regenerate reads the PROJECT, and a crop is the
|
|
// scene's own -- so Length appears in both places and means something different
|
|
// in each, and both say so.
|
|
|
|
import { composer } from "../composer/composer.js";
|
|
|
|
const RATINGS = [
|
|
{ value: "perfect", label: "Perfect" },
|
|
{ value: "good", label: "Good" },
|
|
{ value: "wrong", label: "Wrong" },
|
|
{ value: "awful", label: "Awful" },
|
|
];
|
|
|
|
export function createInspector({ project, onRename } = {}) {
|
|
const host = composer.region.stack({ gap: "sm", fill: true });
|
|
let tab = "scene";
|
|
|
|
const tabs = composer.tabs.underline({
|
|
value: tab, label: "Inspector",
|
|
tabs: [{ value: "scene", label: "Scene" }, { value: "project", label: "Project" }],
|
|
onChange: (next) => { tab = next; draw(); },
|
|
});
|
|
|
|
const rows = composer.region.stack({ gap: "sm", label: "Inspector rows" });
|
|
host.set([tabs, rows]);
|
|
|
|
function sceneRows() {
|
|
const scene = project.selected;
|
|
if (!scene) {
|
|
return [composer.emptyState.default({
|
|
icon: "▭", title: "No scene", hint: "Add one on the timeline." })];
|
|
}
|
|
const at = project.scenes.indexOf(scene) + 1;
|
|
return [
|
|
composer.label.section({ text: `Scene ${at}` }),
|
|
composer.settingsRow.default({
|
|
label: "Length on the timeline",
|
|
// The rule, said where it applies: this is a crop, and a regenerate
|
|
// does not read it.
|
|
hint: "Frames this clip plays for. Regenerating uses the project's length.",
|
|
control: composer.number.md({
|
|
value: scene.length ?? project.video.length ?? 1, min: 1, label: "Length",
|
|
onChange: (v) => project.setScene(scene.id, "length", v),
|
|
}),
|
|
}),
|
|
composer.settingsRow.default({
|
|
label: "Rating",
|
|
hint: "What you thought of it. Kept with the scene.",
|
|
control: composer.buttonGroup.md({
|
|
label: "Rating", value: scene.rating || undefined, items: RATINGS,
|
|
onChange: (v) => project.setScene(scene.id, "rating", v),
|
|
}),
|
|
}),
|
|
composer.settingsRow.default({
|
|
label: "Result",
|
|
hint: scene.result ? "This scene has been generated." : "Not generated yet.",
|
|
}),
|
|
];
|
|
}
|
|
|
|
function projectRows() {
|
|
const open = project.project;
|
|
if (!open) return [composer.hint.default({ text: "No project is open." })];
|
|
return [
|
|
composer.label.section({ text: "Project" }),
|
|
composer.settingsRow.default({
|
|
label: "Name",
|
|
control: composer.input.md({
|
|
value: open.name, label: "Project name",
|
|
onCommit: (v) => { if (onRename) onRename(v); },
|
|
}),
|
|
}),
|
|
composer.settingsRow.default({
|
|
label: "Scenes", hint: `${project.scenes.length} on the timeline`,
|
|
}),
|
|
composer.hint.default({
|
|
text: "Size and length live in the Constructor: they are what every scene "
|
|
+ "is generated at.",
|
|
}),
|
|
];
|
|
}
|
|
|
|
function draw() {
|
|
tabs.setValue(tab);
|
|
// Wrapped in a group, which is what draws the card AND is the container the
|
|
// stacking rule measures: settings rows outside one keep their 180px control
|
|
// column whatever the width, and a four-button group is then clipped.
|
|
const [head, ...body] = tab === "scene" ? sceneRows() : projectRows();
|
|
rows.set([head, composer.group.default({ rows: body })]);
|
|
}
|
|
|
|
draw();
|
|
|
|
return {
|
|
node: host.node,
|
|
draw,
|
|
/** Which tab is showing. Tests and the wheel both ask. */
|
|
get tab() { return tab; },
|
|
show(next) { tab = next; draw(); },
|
|
destroy() { host.destroy(); },
|
|
};
|
|
}
|