lane G M16: pathsStore — save/load/delete server-side camera paths UI
Unhides #btn-path-save / #path-load-select / #btn-path-del; save prompts a name and POSTs camPath.toJSON(), load fetches + camPath.fromJSON, delete confirms; dropdown refreshes after every mutation. Capsule mode: write buttons hide via write-ui, the load dropdown keeps working against baked api/paths (hides itself if unavailable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0314daec1a
commit
1c0422a025
@ -1,4 +1,4 @@
|
||||
// Server-side camera paths UI (spec M16, lane G). STUB — lane G fills THIS FILE ONLY.
|
||||
// Server-side camera paths UI (spec M16, lane G).
|
||||
//
|
||||
// Pre-wired by foundation2: the hidden controls in index.html — #btn-path-save (💾),
|
||||
// #path-load-select (dropdown), #btn-path-del (🗑) — this module's import + init() call in
|
||||
@ -6,13 +6,120 @@
|
||||
// GET /api/paths/{id} ({..., json: <camPath JSON>}), POST /api/paths {name, json:<text>}
|
||||
// (422 on invalid), DELETE /api/paths/{id}.
|
||||
//
|
||||
// When implementing: unhide the controls, set `ready = true`. Save = prompt for a name,
|
||||
// POST camPath.toJSON(); load = fetch the selected path and camPath.fromJSON(json); delete =
|
||||
// DELETE the selected id. Refresh the dropdown after every mutation. #btn-path-save and
|
||||
// #btn-path-del already carry the `write-ui` class, so capsule mode (contract #5) hides
|
||||
// them for free — the load dropdown may stay if you bake paths in, else hide it too.
|
||||
// Save = prompt for a name, POST camPath.toJSON(); load = fetch the selected path and
|
||||
// camPath.fromJSON(json); delete = DELETE the selected id; the dropdown refreshes after
|
||||
// every mutation. #btn-path-save / #btn-path-del carry `write-ui`, so capsule mode
|
||||
// (contract #5) hides them for free; the load dropdown stays — the capsule bakes
|
||||
// api/paths + api/paths/{id}, so loading keeps working zero-backend. If the listing
|
||||
// fetch fails in a capsule (older bundle without baked paths), the dropdown hides too.
|
||||
|
||||
import { state } from "./state.js";
|
||||
import { camPath } from "./camPath.js";
|
||||
|
||||
const PLACEHOLDER = "— saved paths —";
|
||||
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
async function api(path, options) {
|
||||
const resp = await fetch(state.apiBase + path, options);
|
||||
if (!resp.ok) throw new Error(`${path} -> HTTP ${resp.status}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
export const pathsStore = {
|
||||
ready: false,
|
||||
init() {}, // ponytail: stub — lane G replaces the body
|
||||
|
||||
init() {
|
||||
const saveBtn = el("btn-path-save");
|
||||
const select = el("path-load-select");
|
||||
const delBtn = el("btn-path-del");
|
||||
if (!saveBtn || !select || !delBtn) return;
|
||||
|
||||
saveBtn.addEventListener("click", () => this.save());
|
||||
delBtn.addEventListener("click", () => this.remove());
|
||||
select.addEventListener("change", () => this.load());
|
||||
|
||||
saveBtn.style.display = "";
|
||||
select.style.display = "";
|
||||
delBtn.style.display = "";
|
||||
this.ready = true;
|
||||
this.refresh(); // async initial fill of the dropdown
|
||||
},
|
||||
|
||||
/** Rebuild the dropdown from GET /api/paths, keeping the selection when it survives. */
|
||||
async refresh(selectId = null) {
|
||||
const select = el("path-load-select");
|
||||
const keep = selectId != null ? String(selectId) : select.value;
|
||||
let paths;
|
||||
try {
|
||||
paths = await api("/api/paths");
|
||||
} catch (err) {
|
||||
console.warn("[festival4d] saved-paths listing unavailable", err);
|
||||
if (state.capsule) select.style.display = "none"; // baked bundle without paths
|
||||
return;
|
||||
}
|
||||
select.innerHTML = "";
|
||||
const ph = document.createElement("option");
|
||||
ph.value = "";
|
||||
ph.textContent = PLACEHOLDER;
|
||||
select.appendChild(ph);
|
||||
for (const p of paths) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(p.id);
|
||||
opt.textContent = p.name;
|
||||
opt.title = p.created_at ?? "";
|
||||
select.appendChild(opt);
|
||||
}
|
||||
select.value = [...select.options].some((o) => o.value === keep) ? keep : "";
|
||||
},
|
||||
|
||||
/** 💾 Save the current camera path under a prompted name. */
|
||||
async save() {
|
||||
if (!camPath.keyframes.length) {
|
||||
window.alert("No keyframes to save — press K (or ⏺+) to add some first.");
|
||||
return;
|
||||
}
|
||||
const name = window.prompt("Name this camera path:", `path ${new Date().toLocaleString()}`);
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
const created = await api("/api/paths", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: name.trim(), json: camPath.toJSON() }),
|
||||
});
|
||||
await this.refresh(created.id); // leave the new path selected
|
||||
} catch (err) {
|
||||
console.error("[festival4d] saving camera path failed", err);
|
||||
}
|
||||
},
|
||||
|
||||
/** Load the selected saved path into camPath. */
|
||||
async load() {
|
||||
const id = el("path-load-select").value;
|
||||
if (!id) return;
|
||||
try {
|
||||
const data = await api(`/api/paths/${id}`);
|
||||
camPath.fromJSON(data.json);
|
||||
} catch (err) {
|
||||
console.error("[festival4d] loading camera path failed", err);
|
||||
this.refresh(); // it may have been deleted elsewhere — resync the dropdown
|
||||
}
|
||||
},
|
||||
|
||||
/** 🗑 Delete the selected saved path. */
|
||||
async remove() {
|
||||
const select = el("path-load-select");
|
||||
const id = select.value;
|
||||
if (!id) return;
|
||||
const name = select.options[select.selectedIndex]?.textContent ?? id;
|
||||
if (!window.confirm(`Delete saved path "${name}"?`)) return;
|
||||
try {
|
||||
await api(`/api/paths/${id}`, { method: "DELETE" });
|
||||
} catch (err) {
|
||||
console.error("[festival4d] deleting camera path failed", err);
|
||||
}
|
||||
await this.refresh();
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user