From 03df8882f671acef55f8ea4ea6b01c68fb2d54d3 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:25:22 +0300 Subject: [PATCH] feat: Pin reference to slot RN in a reference's tile menu, beside Stop using as a reference Co-Authored-By: Claude Sonnet 5.5 --- app/ui/features/media/hub.test.js | 23 +++++++++++++++++++++++ app/ui/features/media/index.js | 14 +++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/ui/features/media/hub.test.js b/app/ui/features/media/hub.test.js index 804940b0..c347dc9b 100644 --- a/app/ui/features/media/hub.test.js +++ b/app/ui/features/media/hub.test.js @@ -63,3 +63,26 @@ test("a changed pin repaints the slot numbers on the tiles even when the list of fire("project"); assert.deepEqual([flag("cat.png"), flag("dog.png")], ["R1", "R2"], "unpinned: back to list order"); }); + +test("a reference's tile menu pins it at the slot number it holds, for every scene, and takes the pin off again", async () => { + const doc = { fixed_references: {} }; + const project = { selected: { id: "s1", references: ["m1", "m2"] }, project: doc, setScene: () => {}, edit: (fn) => fn(doc) }; + const api = { media: async () => ({ media: [{ id: "m1", name: "cat.png", kind: "image" }, { id: "m2", name: "dog.png", kind: "image" }, { id: "m3", name: "owl.png", kind: "image" }] }) }; + const host = document.createElement("div"); + hub.setup({ host, app: { project, api, mediaPeek: {}, say: () => {}, on: () => () => {} } }); + await new Promise((r) => setTimeout(r, 10)); + const named = (n) => [...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes(n)); + const menuFor = (n) => { + document.querySelectorAll(".cx-popover").forEach((x) => x.remove()); + named(n).dispatchEvent(new window.MouseEvent("contextmenu", { bubbles: true, cancelable: true })); + return [...document.querySelectorAll(".cx-menu-item")].map((b) => b.textContent); + }; + assert.ok(menuFor("dog.png").includes("Pin reference to slot R2")); + assert.ok(!menuFor("owl.png").some((t) => t.includes("slot R")), "a picture that is no reference has nothing to pin"); + menuFor("dog.png"); + [...document.querySelectorAll(".cx-menu-item")].find((b) => b.textContent === "Pin reference to slot R2").click(); + assert.deepEqual(doc.fixed_references, { m2: 2 }); + assert.ok(menuFor("dog.png").includes("Unpin reference from slot R2")); + [...document.querySelectorAll(".cx-menu-item")].find((b) => b.textContent === "Unpin reference from slot R2").click(); + assert.deepEqual(doc.fixed_references, {}); +}); diff --git a/app/ui/features/media/index.js b/app/ui/features/media/index.js index 2f54d0dc..e1ca3a31 100644 --- a/app/ui/features/media/index.js +++ b/app/ui/features/media/index.js @@ -32,12 +32,20 @@ export default { p.setScene(sc.id, "source_image", item.id); draw(); }; + /** The slot number this picture holds in the selected scene, and whether it is pinned there: null when it is not one of its references. */ + const slotOf = (id) => { + const sc = p.selected; + if (!sc) return null; + const plan = sceneReferences(sc, (p.project || {}).fixed_references), i = plan.ids.indexOf(id); + return i < 0 ? null : { n: referenceNumbers(plan.ids, plan.pins)[i], pinned: id in plan.pins }; + }; + const pinItem = (it) => { const s = slotOf(it.id); return s ? [{ id: "pinref", label: s.pinned ? `Unpin reference from slot R${s.n}` : `Pin reference to slot R${s.n}` }] : []; }; const extras = (it) => (app.mediaMenu || []).flatMap((f) => { try { return f(it, items.find((m) => m.id === it.id)) || []; } catch { return []; } }); // entries other features add to a tile's menu: { id, label, run } const context = (it, e) => c.menu.context({ x: e.clientX, y: e.clientY, onPick: (id) => (act[id] ? act[id](it) : extras(it).find((x) => x.id === id)?.run()), items: [{ id: "look", label: "Look at it on the monitor" }, ...(p.selected && p.selected.source_image === it.id ? [{ id: "unres", label: "Stop using as the selected scene's resolution source" }] : [{ id: "res", label: "Use as the selected scene's resolution source", disabled: !p.selected || it.kind === "audio" || !items.some((m) => m.id === it.id && m.kind === "image") }]), p.selected && (p.selected.references || []).includes(it.id) ? { id: "unref", label: "Stop using as a reference for the selected scene" } - : { id: "ref", label: "Use as a reference for the selected scene", disabled: !p.selected }, { id: "rename", label: "Rename…" }, + : { id: "ref", label: "Use as a reference for the selected scene", disabled: !p.selected }, ...pinItem(it), { id: "rename", label: "Rename…" }, { id: "export", label: "Save to your computer", disabled: !items.some((m) => m.id === it.id && m.kind !== "audio") }, ...extras(it).map(({ id, label, disabled }) => ({ id, label, disabled })), { separator: true }, { id: "delete", label: "Delete from the bin", danger: true }] }); /** Delete these from the bin, and nothing in this project may keep pointing at them. */ async function gone(ids) { @@ -59,6 +67,10 @@ export default { const act = { look: (it) => peek(it), res: (it) => pick(it), ref: (it) => p.setScene(p.selected.id, "references", [...(p.selected.references || []), it.id]), + pinref: (it) => { // pin at the number it holds now, for every scene, or take the pin off + const s = slotOf(it.id); + if (s) p.edit((pr) => { const next = { ...(pr.fixed_references || {}) }; if (s.pinned) delete next[it.id]; else next[it.id] = s.n; pr.fixed_references = next; return true; }); + }, unref: (it) => p.setScene(p.selected.id, "references", (p.selected.references || []).filter((r) => r !== it.id)), unres: () => p.setScene(p.selected.id, "source_image", ""), rename: async (it) => {