Preview media bin images on click instead of assigning

Clicking an image previews it in the player; click outside the image
to return to the timeline view. Scene anchors are assigned only via
drag-drop onto clips or the inspector dropdown.
This commit is contained in:
digital-garbage
2026-06-10 16:28:24 +03:00
parent b196311076
commit d5d0fe51f0
4 changed files with 58 additions and 10 deletions
+8 -7
View File
@@ -108,7 +108,7 @@
const drop = el("div", "mediabin");
drop.append(el("div", "big", "🎞"));
drop.append(el("div", null, "Drop images & clips here"));
drop.append(el("div", "pj-meta", "or click to browse · drag onto a clip to set its source"));
drop.append(el("div", "pj-meta", "or click to browse · drag onto a clip to set its anchor"));
const file = el("input"); file.type = "file"; file.accept = "image/*,video/*"; file.multiple = true; file.style.display = "none";
file.onchange = () => { if (file.files.length) S.uploadMedia([...file.files]); file.value = ""; };
drop.onclick = () => file.click();
@@ -119,10 +119,12 @@
const grid = el("div", "media-grid");
(st.mediaBin || []).forEach((m) => {
const card = el("div", "media-card");
const card = el("div", "media-card" + (st.mediaPreviewId === m.id ? " previewing" : ""));
card.draggable = true;
card.addEventListener("dragstart", (e) => { e.dataTransfer.setData("application/funpack-media", m.id); e.dataTransfer.effectAllowed = "copy"; });
card.title = `${m.name}\nClick to assign to selected scene · drag onto a clip`;
card.title = m.kind === "image"
? `${m.name}\nClick to preview · drag onto a clip to set anchor`
: `${m.name}\nDrag onto a clip to set anchor`;
const thumb = el("div", "media-thumb");
if (m.kind === "image") { const img = el("img"); img.src = API.mediaUrl(m.id); img.loading = "lazy"; thumb.append(img); }
else thumb.append(el("span", "media-icon", m.kind === "video" ? "▶" : "◆"));
@@ -131,10 +133,9 @@
const del = el("button", "media-del", "✕"); del.title = "Delete asset";
del.onclick = (e) => { e.stopPropagation(); if (confirm(`Delete "${m.name}"?`)) S.deleteMedia(m.id); };
card.append(del);
card.onclick = () => {
if (!st.selectedSceneId) { alert("Select a scene first to assign this asset."); return; }
S.assignMediaToScene(st.selectedSceneId, m.id);
};
if (m.kind === "image") {
card.onclick = () => S.previewMedia(m.id);
}
grid.append(card);
});
if (!(st.mediaBin || []).length) grid.append(el("div", "pj-meta", "No media yet."));
+20 -1
View File
@@ -485,8 +485,12 @@
clear(body);
const previewAsset = st.mediaPreviewId
? (st.mediaBin || []).find((m) => m.id === st.mediaPreviewId)
: null;
// ── canvas (video + placeholder) ────────────────────────────────────────
const canvas = el("div", "pm-canvas");
const canvas = el("div", "pm-canvas" + (previewAsset?.kind === "image" ? " media-previewing" : ""));
_slateEl = el("div", "pm-slate");
_slateEl.style.display = "none";
_slateEl.append(el("div", "pm-slate-title"));
@@ -498,6 +502,8 @@
canvas.append(stage); // pooled videos live here; only the active one is visible
canvas.append(_slateEl);
canvas.append(_badgeEl);
} else if (previewAsset?.kind === "image") {
canvas.append(stage);
} else if (["queuing", "running", "pending"].includes(gen.state)) {
const splash = el("div", "pm-gen-splash");
splash.append(el("div", "pm-gen-icon", "⚙"));
@@ -532,6 +538,19 @@
}
canvas.append(ro);
}
if (previewAsset?.kind === "image") {
const layer = el("div", "pm-media-preview");
const label = el("div", "pm-media-preview-label", previewAsset.name || "Media preview");
const img = el("img", "pm-media-preview-img");
img.src = API.mediaUrl(previewAsset.id);
img.draggable = false;
img.onclick = (e) => e.stopPropagation();
layer.append(label, img);
canvas.append(layer);
canvas.onclick = (e) => {
if (!e.target.closest(".pm-media-preview-img")) S.clearMediaPreview();
};
}
body.append(canvas);
// ── minibar (segment strips + scrub needle) ──────────────────────────────
+23 -2
View File
@@ -21,6 +21,7 @@
saving: false,
models: { slots: [] }, // pluggable node config (shared with Models modal)
mediaBin: [], // uploaded assets [{id,name,kind,...}]
mediaPreviewId: null, // transient image preview in the player (not scene assignment)
shortcuts: [], // prompt shortcut library
imageTargets: [], // where an image asset can be wired [{value,label}]
ratingLabels: [], // FunPack Studio V2 rating options
@@ -1056,7 +1057,11 @@
for (const f of files) { try { await API.uploadMedia(f); } catch (e) { console.error("upload failed", e); } }
await loadMedia();
}
async function deleteMedia(id) { try { await API.deleteMedia(id); } catch (_) {} await loadMedia(); }
async function deleteMedia(id) {
try { await API.deleteMedia(id); } catch (_) {}
if (state.mediaPreviewId === id) state.mediaPreviewId = null;
await loadMedia();
}
async function loadTransitions() { try { state.transitions = (await API.transitions()).transitions || []; } catch (_) {} notify(); }
async function loadShortcuts() { try { state.shortcuts = (await API.shortcuts()).shortcuts || []; } catch (_) { state.shortcuts = []; } notify(); }
@@ -1106,6 +1111,22 @@
return true;
}
function previewMedia(mediaId) {
const m = (state.mediaBin || []).find((x) => x.id === mediaId);
if (!m || m.kind !== "image") return;
if (state.mediaPreviewId === mediaId) {
clearMediaPreview();
return;
}
try { window.Player?.pause?.(); } catch (_) {}
set({ mediaPreviewId: mediaId });
}
function clearMediaPreview() {
if (!state.mediaPreviewId) return;
set({ mediaPreviewId: null });
}
function assignMediaToScene(sceneId, mediaId) {
const s = scene(sceneId); if (!s) return;
const t = (s.source?.type === "mixed") ? "mixed"
@@ -1140,7 +1161,7 @@
resizeScene, splitScene, snapFrames,
refreshPreview, syncFromPreview, applyGlobalPrompt, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink,
setConditioningSlot, setSamplerSlot, setSamplerInput, setSamplerInputNow, unsetSamplerInput, setStudioInput, setStudioInputNow,
loadMedia, uploadMedia, deleteMedia, assignMediaToScene,
loadMedia, uploadMedia, deleteMedia, previewMedia, clearMediaPreview, assignMediaToScene,
loadShortcuts, saveShortcut, deleteShortcut, importShortcuts, loadTransitions, saveTransition, deleteTransition, importTransitions,
applyTransitionToSelection, insertShortcutIntoSelection,
setSceneRating: (id, v) => patchScene(id, { rating: v }),
+7
View File
@@ -242,6 +242,7 @@ input[type=number] { font-family: var(--font-mono); }
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 8px; }
.media-card { position: relative; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; cursor: pointer; background: var(--ink-0); }
.media-card:hover { border-color: var(--muted); }
.media-card.previewing { border-color: var(--teal); box-shadow: 0 0 0 1px color-mix(in srgb, var(--teal) 35%, transparent); }
.media-thumb { height: 56px; display: flex; align-items: center; justify-content: center; background: #060606; }
.media-thumb img { width: 100%; height: 100%; object-fit: cover; }
.media-icon { font-size: 20px; color: var(--faint); }
@@ -302,6 +303,12 @@ input[type=number] { font-family: var(--font-mono); }
/* video canvas — fills the zone above transport */
.pm-canvas { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; overflow: hidden; min-height: 0; background: #060606; }
.pm-canvas::after { content: ""; position: absolute; inset: 0; pointer-events: none; box-shadow: inset 0 0 140px 10px rgba(0,0,0,.65); z-index: 1; }
.pm-canvas.media-previewing .pm-stage,
.pm-canvas.media-previewing .pm-slate,
.pm-canvas.media-previewing .pm-segment-badge { visibility: hidden; }
.pm-media-preview { position: absolute; inset: 0; z-index: 5; display: flex; align-items: center; justify-content: center; cursor: default; }
.pm-media-preview-img { max-width: 100%; max-height: 100%; object-fit: contain; position: relative; z-index: 1; cursor: default; }
.pm-media-preview-label { position: absolute; top: 10px; left: 10px; z-index: 2; font-size: 10.5px; font-family: var(--font-mono); color: var(--teal); background: #000a; border: 1px solid color-mix(in srgb, var(--teal) 35%, var(--line)); border-radius: 12px; padding: 3px 9px; pointer-events: none; }
.pm-stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 0; }
.pm-video { max-width: 100%; max-height: 100%; display: none; object-fit: contain; position: relative; z-index: 0; }
.pm-video.active { display: block; }