feat(editor): double-click media to set a clip's anchor

The inner <img> hijacked the card's drag with a native image drag, so the
drop payload was a URL, not a media id — and it got worse the bigger the
image. Opt the image out and give the drag a cheap ghost.
This commit is contained in:
digital-garbage
2026-08-22 19:35:44 +03:00
parent 897efde50d
commit f31401760d
2 changed files with 37 additions and 2 deletions
+6
View File
@@ -65,6 +65,12 @@
looked exactly like one where the feature was switched off.
### Fixed
- **Double-click an image in the Media bin to make it the selected clip's anchor.** Dragging
was the only way in, and it could not be relied on: an `<img>` is natively draggable, so
inside the card the browser started its own image drag whose payload every drop target
rejects — worse the larger the image. The image now opts out of dragging, and the drag
ghost is the thumbnail rather than a snapshot of the full-size card.
- **Preview playback could stall at "Video is loading…" and stay there**, with the transport
buttons dead. A media reset leaves a `<video>` with no metadata and no load running, and
every recovery path keyed on the element having errored — which a reset does not do. Play
+31 -2
View File
@@ -87,6 +87,12 @@
const img = el("img");
img.src = url;
img.loading = "lazy";
// An <img> is natively draggable, and inside a draggable card it WINS: the browser
// starts its own image drag, whose dataTransfer carries a URL instead of our
// application/funpack-media id, so every drop target rejects it. It also has to
// decode and rasterize the full-size bitmap first, which is why big images felt like
// they "resisted" dragging. Opting the image out hands the drag back to the card.
img.draggable = false;
thumb.append(img);
return;
}
@@ -278,11 +284,18 @@
+ (previewing ? " previewing" : "")
+ (picked ? " picked" : ""));
card.draggable = true;
card.addEventListener("dragstart", (e) => { e.dataTransfer.setData("application/funpack-media", m.id); e.dataTransfer.effectAllowed = "copy"; });
card.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("application/funpack-media", m.id);
e.dataTransfer.effectAllowed = "copy";
// Drag the card's own thumbnail as the ghost: the default is a snapshot of the whole
// card, which for a full-resolution source image is a large rasterize on every drag.
const th = card.querySelector(".media-thumb");
if (th) { try { e.dataTransfer.setDragImage(th, 20, 20); } catch (_) {} }
});
card.title = mediaSelectMode
? `${m.name}\nClick to toggle selection`
: m.kind === "image"
? `${m.name}\nImage · click to preview · drag onto timeline to add a clip, or onto a clip to set its anchor`
? `${m.name}\nImage · click to preview · double-click to set it as the selected clip's anchor · drag onto the timeline to add a clip`
: m.kind === "audio"
? `${m.name}\nAudio · click to preview · add via timeline + Add → Audio`
: m.kind === "video"
@@ -378,6 +391,22 @@
S.previewMedia(m.id);
}
};
// Double-click = set this image as the selected clip's i2v anchor: the same assignment
// dragging it onto the clip performs. Drag was the ONLY way in, and a high-resolution
// image can refuse to drag at all (the browser builds a full-size drag image first), so
// the feature was unreachable for exactly the pictures most worth using as an anchor.
card.ondblclick = (e) => {
if (e.target.closest(".media-name-actions, .media-act, .media-name")) return;
if (mediaSelectMode || m.kind !== "image") return;
e.preventDefault(); e.stopPropagation();
const cur = S.get();
const sc = cur.selectedSceneId ? S.scene(cur.selectedSceneId) : null;
if (!sc || !S.isGenerativeScene(sc)) {
alert("Select a generated clip on the timeline first — the anchor is set on that clip.");
return;
}
S.assignMediaToScene(sc.id, m.id);
};
return card;
}