Files
olivv-cs-ComfyUI-FunPack/app/ui/features/overlays/index.js
T

72 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Overlays: text and pictures laid over the cut. Adds lanes to the timeline (through app.timelineLanes) and a "+ Overlay" button.
import { composer as c } from "../../composer/composer.js";
import * as ov from "../../shell/overlays.js";
import { learn } from "../../shell/bin.js";
import { onMediaDrop } from "../../shell/dnd.js";
import { openDialog } from "./dialog.js";
const ID = "o:";
const short = (o) => String((o.kind === "text" ? o.text : o.label) || "Overlay").split("\n")[0].slice(0, 40);
export default {
id: "overlays",
mount: "timeline.toolbar",
needs: ["project", "playhead", "api"],
setup({ host, app }) {
const p = app.project;
const edit = (fn) => p.edit(fn);
const pictures = async () => { const list = (await app.api.media()).media || []; learn(list); return list.filter((m) => m.kind === "image"); };
async function open(kind, existing) {
let bin = [];
if (kind === "image") { try { bin = await pictures(); } catch (err) { return c.toast.warn({ text: `Could not read the media bin: ${err.message}` }); } }
openDialog({ kind, existing, bin, projectWidth: p.project ? p.project.width : undefined, onSave: (s) => edit((pr) => {
if (existing) return ov.update(pr, existing.id, s);
const at = app.playhead.at, made = kind === "text" ? ov.addText(pr, at, s) : ov.addImage(pr, s.media_ref, s.label, at);
return ov.update(pr, made.id, s);
}) });
}
const confirmRemove = async (o) => { if (await c.modal.dialogue({ title: "Remove overlay", message: `Remove “${short(o)}” from the cut?`, tone: "danger", confirmLabel: "Remove" }).result) edit((pr) => ov.remove(pr, o.id)); };
const entry = {
owns: (id) => id.startsWith(ID),
lanes(pr) {
const known = new Set(ov.lanesOf(pr).map((l) => l.id));
const lanes = [...(ov.overlaysOf(pr).some((o) => !known.has(o.lane_id)) ? [{ id: "", label: "Unplaced" }] : []), ...ov.lanesOf(pr)]; // a v4 project's overlays without a lane still show, at the bottom as they render
return [...lanes].reverse().map((lane) => ({ id: `ov:${lane.id}`, label: lane.label, kind: "overlay", move: true,
clips: ov.overlaysOf(pr).filter((o) => (lane.id ? o.lane_id === lane.id : !known.has(o.lane_id))).map((o) => ({ id: ID + o.id, start: o.start_sec || 0, dur: o.duration_sec || 0.1, trim: true, title: short(o),
head: [o.kind === "text" ? "T" : "▣"],
actions: [{ label: "✎", title: "Edit", onClick: () => open(o.kind, o) }, { label: "▲", title: "Draw above the others (higher lane)", onClick: () => edit((x) => ov.restack(x, o.id, 1)) },
{ label: "▼", title: "Draw below the others (lower lane)", onClick: () => edit((x) => ov.restack(x, o.id, -1)) }, { label: "✕", title: "Remove", onClick: () => confirmRemove(o) }] })) }));
},
move: (id, d) => edit((pr) => ov.move(pr, id.slice(ID.length), d)),
trim: (id, edge, d) => edit((pr) => ov.trim(pr, id.slice(ID.length), edge, d)),
select: (id) => { const o = ov.find(p.project, id.slice(ID.length)); if (o) open(o.kind, o); },
};
app.timelineLanes.push(entry);
app.say("timeline.lanes"); // the stage draws what it has; tell it there is more
const add = c.button.menu({ label: "+ Overlay", tone: "ghost", disabled: !p.project, onClick: () => c.menu.dropdown({ anchor: add,
items: [{ id: "text", label: "Text…" }, { id: "image", label: "Picture…" }, { separator: true }, { id: "lane", label: "New lane on top" },
{ id: "unlane", label: "Remove top lane", danger: true, disabled: !ov.lanesOf(p.project).length }],
onPick: (id) => {
if (id === "text" || id === "image") return open(id);
if (id === "lane") return edit((pr) => ov.addLane(pr));
const top = ov.lanesOf(p.project).slice(-1)[0];
if (!top) return;
if (top && !ov.overlaysOf(p.project).some((o) => o.lane_id === top.id)) return edit((pr) => ov.removeLane(pr, top.id));
c.modal.dialogue({ title: "Remove lane", message: `Remove “${top.label}” and everything on it?`, tone: "danger", confirmLabel: "Remove" }).result.then((yes) => yes && edit((pr) => ov.removeLane(pr, top.id)));
} }) });
host.append(add.node);
const offDrop = onMediaDrop(".cx-nle-lane.cx-nle-lane-overlay", async (item, lane, e) => { // a picture dropped on an overlay lane becomes an overlay there
if (item.kind !== "image" || !p.project) return c.toast.warn({ text: "Only a picture can be laid over the cut." });
let name = "Image"; try { name = ((await app.api.media()).media || []).find((m) => m.id === item.id).name; } catch { /* keep the generic label */ }
const row = [...document.querySelectorAll(".cx-nle-lane.cx-nle-lane-overlay")].indexOf(lane), lanes = ov.lanesOf(p.project), laneId = (lanes[lanes.length - 1 - row] || {}).id; // lanes draw top-first
const sec = app.timelineView && app.timelineView.timeAt ? app.timelineView.timeAt(e.clientX) : app.playhead.at;
edit((pr) => ov.addImage(pr, item.id, name, sec, laneId));
});
const off = app.on(() => { add.setDisabled && add.setDisabled(!p.project); });
return () => { off(); offDrop(); const at = app.timelineLanes.indexOf(entry); if (at >= 0) app.timelineLanes.splice(at, 1); add.node.remove(); app.say("timeline.lanes"); };
},
};