diff --git a/movie_editor/frontend/mediabrowser.js b/movie_editor/frontend/mediabrowser.js index 8a6e5829..b7dd95af 100644 --- a/movie_editor/frontend/mediabrowser.js +++ b/movie_editor/frontend/mediabrowser.js @@ -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.")); diff --git a/movie_editor/frontend/player.js b/movie_editor/frontend/player.js index bc64c48e..d09a492e 100644 --- a/movie_editor/frontend/player.js +++ b/movie_editor/frontend/player.js @@ -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) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index a6666e6d..b0bdc95e 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -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 }), diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index 1193b112..7b4ec677 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -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; }