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:
@@ -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."));
|
||||
|
||||
@@ -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) ──────────────────────────────
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user