diff --git a/app/ui/composer/elements/gallery.css b/app/ui/composer/elements/gallery.css index d781adf2..e0998644 100644 --- a/app/ui/composer/elements/gallery.css +++ b/app/ui/composer/elements/gallery.css @@ -31,6 +31,8 @@ } .cx-cell-badge { left: var(--sp-1); } .cx-cell-duration { right: var(--sp-1); font-variant-numeric: tabular-nums; } +.cx-cell-flags { position: absolute; top: 2px; right: 2px; display: flex; gap: 2px; pointer-events: none; } +.cx-cell-flag { font-size: 10px; font-weight: 700; line-height: 1; padding: 2px 4px; border-radius: var(--r-1); background: var(--accent); color: var(--void); } .cx-cell-name { font-size: clamp(10px, calc(var(--cell, 96px) * .115), 13px); line-height: var(--lh-snug); padding: 0 var(--sp-3) var(--sp-3); diff --git a/app/ui/composer/elements/gallery.js b/app/ui/composer/elements/gallery.js index 921b96bf..a76687db 100644 --- a/app/ui/composer/elements/gallery.js +++ b/app/ui/composer/elements/gallery.js @@ -34,6 +34,8 @@ function thumbOf(item, cls = "cx-cell-thumb", marks = true) { } if (marks && item.badge) thumb.append(el("span", { cls: "cx-cell-badge", text: item.badge })); if (marks && item.duration) thumb.append(el("span", { cls: "cx-cell-duration", text: item.duration })); + // `flags` [{text, title}]: what this item IS to the work (a reference, say): small and solid, in every view, readable at a glance. + if ((item.flags || []).length) thumb.append(el("span", { cls: "cx-cell-flags", children: item.flags.map((f) => el("span", { cls: "cx-cell-flag", text: f.text, attrs: { title: f.title || f.text } })) })); return thumb; } diff --git a/app/ui/features/media/hub.test.js b/app/ui/features/media/hub.test.js index 30d8d0ee..2ed61514 100644 --- a/app/ui/features/media/hub.test.js +++ b/app/ui/features/media/hub.test.js @@ -36,13 +36,15 @@ test("the selected scene's references are marked on their tiles, and the menu ta 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)); - assert.match(named("dog.png").textContent, /ref 1/); - assert.match(named("cat.png").textContent, /ref 2/); - assert.doesNotMatch(named("owl.png").textContent, /ref/); + const flag = (n) => [...named(n).querySelectorAll(".cx-cell-flag")].map((x) => x.textContent).join(); + assert.deepEqual([flag("dog.png"), flag("cat.png"), flag("owl.png")], ["R1", "R2", ""]); + const clear = [...host.querySelectorAll("button")].find((b) => b.textContent.startsWith("Clear references")); + assert.ok(clear && !clear.hidden && /\(2\)/.test(clear.textContent), "one click takes them all off"); document.querySelectorAll(".cx-popover").forEach((n) => n.remove()); named("dog.png").dispatchEvent(new window.MouseEvent("contextmenu", { bubbles: true, cancelable: true })); const stop = [...document.querySelectorAll(".cx-menu-item")].find((b) => b.textContent.includes("Stop using as a reference")); assert.ok(stop && !stop.disabled, "not greyed: it can be taken off"); stop.click(); - assert.deepEqual(set, [["s1", "references", ["m1"]]]); + clear.click(); + assert.deepEqual(set, [["s1", "references", ["m1"]], ["s1", "references", []]]); }); diff --git a/app/ui/features/media/index.js b/app/ui/features/media/index.js index 19b5483e..ab1e373a 100644 --- a/app/ui/features/media/index.js +++ b/app/ui/features/media/index.js @@ -90,7 +90,8 @@ export default { await gone(ids); } }); bulk.node.hidden = true; - host.append(c.toolbar.default({ items: [c.text.sm({ text: "Media" })], trailing: [selectBtn, bulk] }).node, drop.node, note.node, seg("Show", FILTERS, "filter").node, sort.node, + const clearRefs = c.button.sm({ label: "Clear references", tone: "ghost", title: "Stop using every reference picture in the selected scene", onClick: () => p.selected && p.setScene(p.selected.id, "references", []) }); + host.append(c.toolbar.default({ items: [c.text.sm({ text: "Media" })], trailing: [clearRefs, selectBtn, bulk] }).node, drop.node, note.node, seg("Show", FILTERS, "filter").node, sort.node, seg("View", VIEWS, "view").node, seg("Tile size", DENSITY, "cols").node, shelf.node); let drawn = ""; @@ -108,13 +109,16 @@ export default { shelf.set([g]); if (g.setCols) g.setCols(Number(show.cols)); // what the selected scene uses is marked on its tile, so a reference can be told from the rest - const role = (m) => [sc && sc.source_image === m.id ? "source" : null, refs.includes(m.id) ? `ref ${refs.indexOf(m.id) + 1}` : null].filter(Boolean); - g.setItems(shown.map((m) => ({ id: m.id, label: m.name, thumb: m.kind === "audio" ? undefined : `${base(m.id)}/thumb`, badge: [...role(m), m.kind].join(" · ") }))); + const flags = (m) => [sc && sc.source_image === m.id ? { text: "◐", title: "The selected scene's resolution source" } : null, + refs.includes(m.id) ? { text: `R${refs.indexOf(m.id) + 1}`, title: `Reference ${refs.indexOf(m.id) + 1} of ${refs.length} for the selected scene` } : null].filter(Boolean); + g.setItems(shown.map((m) => ({ id: m.id, label: m.name, thumb: m.kind === "audio" ? undefined : `${base(m.id)}/thumb`, badge: m.kind, flags: flags(m) }))); g.setValue(selecting ? [...chosen] : sc ? [sc.source_image, ...refs].filter(Boolean) : []); selectBtn.setLabel(selecting ? "Done" : "Select"); bulk.node.hidden = !selecting; bulk.setLabel(`Delete (${chosen.size})`); bulk.setDisabled(!chosen.size); + clearRefs.node.hidden = !refs.length; + clearRefs.setLabel(`Clear references (${refs.length})`); } async function refresh() { try { items = (await api.media()).media || []; learn(items); app.say("bin"); } catch (err) { tell(err.message); }