import { isImageFormat, isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "../utils/utils.js"; import { openFileOrFolder } from "./mediaActions.js"; import { stopAllAudioPlayback, isAudioViewerOpen, setCurrentlyPlayingAudio, getCurrentlyPlayingAudio } from "./audioController.js"; import { showFullscreenPreview } from "../utils/comparison.js"; import { store } from "../core/store.js"; export function render3DCardPreview(cardObj, wrapper, src, img, state) { let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper"); const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; const imageAssets = state?.images?.filter(i => isImageFormat(i.filename || i.url)) || []; const lastImageAsset = imageAssets.length > 0 ? imageAssets[imageAssets.length - 1] : null; const bgImgSrc = lastImageAsset ? (lastImageAsset.url || window.location.origin + `/view?filename=${encodeURIComponent(lastImageAsset.filename)}&type=${lastImageAsset.type || 'output'}&subfolder=${encodeURIComponent(lastImageAsset.subfolder || '')}`) : null; if (!preview3D) { wrapper.innerHTML = ""; preview3D = document.createElement("div"); preview3D.className = "comfy-sidebar-3d-wrapper"; if (bgImgSrc) { const bgImg = document.createElement("img"); bgImg.className = "comfy-sidebar-3d-preview-img"; bgImg.src = bgImgSrc; bgImg.alt = "3D Thumbnail Preview"; preview3D.appendChild(bgImg); } const overlay = document.createElement("div"); overlay.className = "comfy-sidebar-3d-overlay"; const badge = document.createElement("span"); badge.className = "comfy-sidebar-3d-badge"; const ext = (img.filename || src).split('.').pop().toUpperCase(); badge.textContent = ext || "3D"; const icon = document.createElement("span"); icon.className = "pi pi-box comfy-sidebar-3d-icon"; const title = document.createElement("span"); title.className = "comfy-sidebar-3d-title"; title.textContent = img.filename || "3D Model"; overlay.append(badge, icon, title); preview3D.appendChild(overlay); wrapper.appendChild(preview3D); } else { const bgImg = preview3D.querySelector(".comfy-sidebar-3d-preview-img"); if (bgImg && bgImgSrc && bgImg.src !== bgImgSrc) { bgImg.src = bgImgSrc; } else if (!bgImg && bgImgSrc) { const newBgImg = document.createElement("img"); newBgImg.className = "comfy-sidebar-3d-preview-img"; newBgImg.src = bgImgSrc; newBgImg.alt = "3D Thumbnail Preview"; preview3D.insertBefore(newBgImg, preview3D.firstChild); } const title = preview3D.querySelector(".comfy-sidebar-3d-title"); if (title) title.textContent = img.filename || "3D Model"; } cardObj.firstImgElement = preview3D; preview3D.onclick = (ev) => { ev.stopPropagation(); if (ev.ctrlKey || ev.metaKey) { openFileOrFolder(img); return; } showFullscreenPreview([fullUrl], ev.shiftKey); }; preview3D.setAttribute("draggable", "true"); preview3D.ondragstart = (e) => { e.stopPropagation(); const filename = img.filename || "model.glb"; const mimeType = "application/octet-stream"; try { e.dataTransfer.setData("text/uri-list", fullUrl); e.dataTransfer.setData("text/plain", fullUrl); e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullUrl}`); if (state && state.workflow) { e.dataTransfer.setData("application/json", JSON.stringify(state.workflow)); } } catch (err) {} e.dataTransfer.effectAllowed = "copy"; }; } export function renderAudioCardPreview(cardObj, wrapper, src, img, state) { let previewAudio = wrapper.querySelector(".comfy-sidebar-audio-wrapper"); const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; const filename = img.filename || getFilenameFromUrl(src) || "audio.wav"; const ext = filename.split('.').pop().toUpperCase(); const playIconSvg = ``; const stopIconSvg = ``; if (!previewAudio) { wrapper.innerHTML = ""; previewAudio = document.createElement("div"); previewAudio.className = "comfy-sidebar-audio-wrapper"; const topRow = document.createElement("div"); Object.assign(topRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", paddingLeft: "42px" }); const title = document.createElement("span"); title.className = "comfy-sidebar-audio-title"; title.textContent = filename; const badge = document.createElement("span"); badge.className = "comfy-sidebar-audio-badge"; badge.textContent = ext || "AUDIO"; topRow.append(title, badge); const centerArea = document.createElement("div"); Object.assign(centerArea.style, { position: "relative", width: "100%", display: "flex", alignItems: "center", justifyContent: "center", margin: "6px 0" }); const soundwave = document.createElement("div"); soundwave.className = "comfy-sidebar-soundwave-container"; soundwave.style.width = "100%"; const barHeights = [10, 16, 24, 18, 30, 22, 14, 28, 34, 20, 12, 26, 32, 18, 14, 22, 30, 16, 12]; barHeights.forEach((h, i) => { const bar = document.createElement("div"); bar.className = "comfy-sidebar-soundwave-bar"; bar.style.height = `${h}px`; bar.style.animationDelay = `${(i * 0.08).toFixed(2)}s`; soundwave.appendChild(bar); }); const playBtn = document.createElement("button"); Object.assign(playBtn.style, { position: "absolute", width: "36px", height: "36px", borderRadius: "50%", background: "rgba(22, 22, 30, 0.88)", border: "1px solid rgba(255, 255, 255, 0.25)", color: "#ffffff", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.65)", zIndex: "5", transition: "all 0.15s ease", outline: "none" }); playBtn.innerHTML = playIconSvg; centerArea.append(soundwave, playBtn); const bottomRow = document.createElement("div"); Object.assign(bottomRow.style, { display: "flex", flexDirection: "column", gap: "4px", width: "100%", padding: "0 34px", boxSizing: "border-box" }); const scrubber = document.createElement("div"); Object.assign(scrubber.style, { width: "100%", height: "4px", background: "#334155", borderRadius: "2px", position: "relative", cursor: "pointer" }); const scrubberFill = document.createElement("div"); Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#c084fc", borderRadius: "2px" }); scrubber.appendChild(scrubberFill); const timeLabel = document.createElement("div"); Object.assign(timeLabel.style, { fontSize: "9px", fontFamily: "monospace", color: "#94a3b8", textAlign: "center" }); timeLabel.textContent = "0:00 / 0:00"; bottomRow.append(scrubber, timeLabel); const audioEl = document.createElement("audio"); audioEl.src = fullUrl; audioEl.preload = "metadata"; const formatTime = (t) => { const m = Math.floor(t / 60); const s = Math.floor(t % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; }; const resetAudioUI = () => { playBtn.innerHTML = playIconSvg; previewAudio.classList.remove("playing"); scrubberFill.style.width = "0%"; timeLabel.textContent = `0:00 / ${formatTime(audioEl.duration || 0)}`; }; audioEl._onResetUI = resetAudioUI; audioEl.onloadedmetadata = () => { timeLabel.textContent = `0:00 / ${formatTime(audioEl.duration || 0)}`; }; audioEl.ontimeupdate = () => { if (audioEl.duration > 0) { const percent = (audioEl.currentTime / audioEl.duration) * 100; scrubberFill.style.width = `${percent}%`; timeLabel.textContent = `${formatTime(audioEl.currentTime)} / ${formatTime(audioEl.duration)}`; } }; audioEl.onended = () => { resetAudioUI(); if (getCurrentlyPlayingAudio() === audioEl) stopAllAudioPlayback(); }; const togglePlay = (e) => { e.stopPropagation(); if (isAudioViewerOpen()) { stopAllAudioPlayback(); showFullscreenPreview([fullUrl]); return; } if (audioEl.paused) { setCurrentlyPlayingAudio(audioEl); audioEl.play().catch(()=>{}); playBtn.innerHTML = stopIconSvg; previewAudio.classList.add("playing"); } else { audioEl.pause(); audioEl.currentTime = 0; resetAudioUI(); stopAllAudioPlayback(); } }; playBtn.onclick = togglePlay; playBtn.onmouseenter = () => { playBtn.style.background = "#9333ea"; playBtn.style.borderColor = "#c084fc"; playBtn.style.transform = "scale(1.08)"; }; playBtn.onmouseleave = () => { playBtn.style.background = previewAudio.classList.contains("playing") ? "rgba(147, 51, 234, 0.85)" : "rgba(22, 22, 30, 0.88)"; playBtn.style.borderColor = "rgba(255, 255, 255, 0.25)"; playBtn.style.transform = "scale(1.0)"; }; scrubber.onclick = (e) => { e.stopPropagation(); const rect = scrubber.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; if (audioEl.duration > 0) audioEl.currentTime = pos * audioEl.duration; }; previewAudio.append(topRow, centerArea, bottomRow, audioEl); wrapper.appendChild(previewAudio); previewAudio.onclick = (e) => { if (e.target.closest('button, input') || e.target === scrubber || e.target === scrubberFill) return; stopAllAudioPlayback(); if (e.ctrlKey || e.metaKey) { openFileOrFolder(img); return; } showFullscreenPreview([fullUrl]); }; } cardObj.firstImgElement = previewAudio; previewAudio.setAttribute("draggable", "true"); previewAudio.ondragstart = (e) => { e.stopPropagation(); const mimeType = "audio/wav"; try { e.dataTransfer.setData("text/uri-list", fullUrl); e.dataTransfer.setData("text/plain", fullUrl); e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullUrl}`); if (state && state.workflow) { e.dataTransfer.setData("application/json", JSON.stringify(state.workflow)); } } catch (err) {} e.dataTransfer.effectAllowed = "copy"; }; } export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state) { let previewFile = wrapper.querySelector(".comfy-sidebar-file-wrapper"); const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; const filename = img.filename || getFilenameFromUrl(src) || "output_file"; const ext = (filename.split('.').pop() || "FILE").toUpperCase(); const folderName = img.subfolder ? `${img.subfolder}/` : (img.type || "output"); if (!previewFile) { wrapper.innerHTML = ""; previewFile = document.createElement("div"); previewFile.className = "comfy-sidebar-file-wrapper"; const topRow = document.createElement("div"); Object.assign(topRow.style, { display: "flex", justifyContent: "flex-end", width: "100%", minHeight: "18px" }); const badge = document.createElement("span"); badge.className = "comfy-sidebar-file-badge"; badge.textContent = ext; topRow.appendChild(badge); const centerArea = document.createElement("div"); Object.assign(centerArea.style, { display: "flex", alignItems: "center", gap: "8px", width: "100%", margin: "2px 0 6px 0" }); const icon = document.createElement("span"); icon.className = "pi pi-file comfy-sidebar-file-icon"; const title = document.createElement("span"); title.className = "comfy-sidebar-file-title"; title.textContent = filename; title.title = filename; centerArea.append(icon, title); const bottomRow = document.createElement("div"); Object.assign(bottomRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }); const folderInfo = document.createElement("div"); folderInfo.className = "comfy-sidebar-file-subtext"; folderInfo.innerHTML = `${folderName}`; const openHint = document.createElement("div"); openHint.className = "comfy-sidebar-file-subtext"; openHint.innerHTML = `Open Folder`; bottomRow.append(folderInfo, openHint); previewFile.append(topRow, centerArea, bottomRow); wrapper.appendChild(previewFile); } else { const title = previewFile.querySelector(".comfy-sidebar-file-title"); if (title) { title.textContent = filename; title.title = filename; } const badge = previewFile.querySelector(".comfy-sidebar-file-badge"); if (badge) badge.textContent = ext; } cardObj.firstImgElement = previewFile; previewFile.onclick = (ev) => { ev.stopPropagation(); openFileOrFolder(img); }; previewFile.setAttribute("draggable", "true"); previewFile.ondragstart = (e) => { e.stopPropagation(); const mimeType = "application/octet-stream"; try { e.dataTransfer.setData("text/uri-list", fullUrl); e.dataTransfer.setData("text/plain", fullUrl); e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullUrl}`); if (state && state.workflow) { e.dataTransfer.setData("application/json", JSON.stringify(state.workflow)); } } catch (err) {} e.dataTransfer.effectAllowed = "copy"; }; } export function renderCardImages(cardObj, state, onNavigateBatch) { cardObj.currentImageIndex = cardObj.currentImageIndex || 0; if (cardObj.currentImageIndex >= state.images.length) { cardObj.currentImageIndex = 0; } const idx = cardObj.currentImageIndex; const img = state.images[idx]; if (!img) { cardObj.grid.innerHTML = ""; cardObj.firstImgElement = null; if (cardObj.dimEl) cardObj.dimEl.style.display = "none"; return; } const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; const isVideo = isVideoFormat(src) || isVideoFormat(img.filename); const is3D = is3DFormat(src) || is3DFormat(img.filename); const isAudio = isAudioFormat(src) || isAudioFormat(img.filename); const isImage = isImageFormat(src) || isImageFormat(img.filename) || (!isVideo && !is3D && !isAudio && !img.filename?.includes(".")); let wrapper = cardObj.grid.querySelector(".comfy-sidebar-media-wrapper"); if (!wrapper) { cardObj.grid.innerHTML = ""; wrapper = document.createElement("div"); wrapper.className = "comfy-sidebar-media-wrapper"; Object.assign(wrapper.style, { position: "relative", width: "100%", display: "block" }); cardObj.grid.appendChild(wrapper); } let ctrlOverlay = wrapper.querySelector(".comfy-sidebar-ctrl-overlay"); if (!ctrlOverlay) { ctrlOverlay = document.createElement("div"); ctrlOverlay.className = "comfy-sidebar-ctrl-overlay"; ctrlOverlay.innerHTML = `Open Location`; wrapper.appendChild(ctrlOverlay); } if (is3D) { render3DCardPreview(cardObj, wrapper, src, img, state); return; } if (isAudio) { renderAudioCardPreview(cardObj, wrapper, src, img, state); return; } if (!isImage && !isVideo) { renderGenericFileCardPreview(cardObj, wrapper, src, img, state); return; } let mediaEl = wrapper.querySelector("img, video"); const needsRebuild = !mediaEl || (isVideo !== (mediaEl.tagName.toLowerCase() === "video")); if (needsRebuild) { if (mediaEl) mediaEl.remove(); mediaEl = isVideo ? document.createElement("video") : document.createElement("img"); Object.assign(mediaEl.style, { width: "100%", borderRadius: "2px", display: "block", cursor: "zoom-in", webkitUserDrag: "element", zIndex: "1" }); wrapper.insertBefore(mediaEl, wrapper.firstChild); } cardObj.firstImgElement = mediaEl; mediaEl.onclick = (ev) => { ev.stopPropagation(); if (ev.ctrlKey || ev.metaKey) { openFileOrFolder(img); return; } showFullscreenPreview([src], ev.shiftKey); }; mediaEl.onerror = async () => { if (src && !src.startsWith("blob:") && !is3D && !isAudio) { try { const res = await fetch(src, { method: "HEAD" }); if (res.status === 404) { store.deletePrompt(state.pid); if (cardObj.element) cardObj.element.remove(); return; } } catch (e) {} renderGenericFileCardPreview(cardObj, wrapper, src, img, state); } }; const applyDimensions = (width, height) => { if (cardObj.dimEl && width && height) { cardObj.dimEl.textContent = `${width}x${height}`; cardObj.dimEl.style.display = "block"; } }; const isUnfinished = state.status ? (state.status !== "completed") : false; if (isUnfinished) { mediaEl.setAttribute("draggable", "false"); mediaEl.style.cursor = "grab"; if (mediaEl._currentDragStart) { mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart); delete mediaEl._currentDragStart; } } else { mediaEl.setAttribute("draggable", "true"); mediaEl.style.cursor = "zoom-in"; if (isVideo) { const fullSrc = src.startsWith("http") ? src : window.location.origin + src; const filename = img.filename || "output.mp4"; const mimeType = src.includes(".webm") ? "video/webm" : "video/mp4"; const videoDragHandler = (e) => { try { e.dataTransfer.setData("text/uri-list", fullSrc); e.dataTransfer.setData("text/plain", fullSrc); e.dataTransfer.setData("DownloadURL", `${mimeType}:${filename}:${fullSrc}`); if (state && state.workflow) { e.dataTransfer.setData("application/json", JSON.stringify(state.workflow)); } } catch (err) {} e.dataTransfer.effectAllowed = "copy"; e.stopPropagation(); }; if (mediaEl._currentDragStart) { mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart); } mediaEl.addEventListener("dragstart", videoDragHandler); mediaEl._currentDragStart = videoDragHandler; } else { if (mediaEl._currentDragStart) { mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart); delete mediaEl._currentDragStart; } } } if (isVideo) { mediaEl.muted = true; mediaEl.playsInline = true; mediaEl.preload = "metadata"; mediaEl.loop = true; mediaEl.onloadedmetadata = () => { applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight); }; if (mediaEl.readyState >= 1) { applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight); } let playIcon = wrapper.querySelector(".comfy-sidebar-play-icon"); if (!playIcon) { playIcon = document.createElement("div"); playIcon.className = "comfy-sidebar-play-icon"; Object.assign(playIcon.style, { position: "absolute", top: "0", left: "0", width: "100%", height: "100%", pointerEvents: "none", zIndex: "2", transition: "opacity 0.2s ease" }); playIcon.innerHTML = ` `; wrapper.appendChild(playIcon); } mediaEl.onmouseenter = () => { playIcon.style.opacity = "0"; mediaEl.play().catch(()=>{}); }; mediaEl.onmouseleave = () => { playIcon.style.opacity = "1"; mediaEl.pause(); }; } else { mediaEl.onload = () => { applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight); if (state._oldPreviewBlobUrl) { try { URL.revokeObjectURL(state._oldPreviewBlobUrl); } catch(e){} delete state._oldPreviewBlobUrl; } }; if (mediaEl.complete && mediaEl.naturalWidth) { applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight); } const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon"); if (playIcon) playIcon.remove(); } const currentSrc = mediaEl.getAttribute("src") || mediaEl.src; if (currentSrc !== src && currentSrc !== (currentSrc + "#t=0.001")) { if (src.startsWith("blob:") && !isVideo) { const tempImg = new Image(); tempImg.onload = () => { const oldBlob = mediaEl._lastBlob; mediaEl.src = src; mediaEl._lastBlob = src; applyDimensions(tempImg.naturalWidth, tempImg.naturalHeight); if (oldBlob && oldBlob !== src && oldBlob.startsWith("blob:")) { try { URL.revokeObjectURL(oldBlob); } catch(e){} } }; tempImg.src = src; } else { mediaEl.src = isVideo ? src + "#t=0.001" : src; } } else { if (isVideo) { if (mediaEl.videoWidth) applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight); } else { if (mediaEl.naturalWidth) applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight); } } let navBar = wrapper.querySelector(".comfy-sidebar-batch-navbar"); if (state.images.length > 1) { if (!navBar) { navBar = document.createElement("div"); navBar.className = "comfy-sidebar-batch-navbar"; Object.assign(navBar.style, { position: "absolute", bottom: "6px", left: "50%", display: "flex", alignItems: "center", gap: "8px", background: "rgba(0,0,0,0.75)", padding: "4px 10px", borderRadius: "12px", zIndex: "15", fontSize: "10px", fontFamily: "monospace", color: "#eee", userSelect: "none", pointerEvents: "auto", boxShadow: "0 1px 3px rgba(0,0,0,0.4)", transform: "translate3d(-50%, 0, 0)" }); const prevBtn = document.createElement("span"); prevBtn.className = "pi pi-chevron-left"; prevBtn.style.cursor = "pointer"; prevBtn.onclick = (ev) => { ev.stopPropagation(); cardObj.currentImageIndex = (cardObj.currentImageIndex - 1 + state.images.length) % state.images.length; renderCardImages(cardObj, state, onNavigateBatch); }; const label = document.createElement("span"); label.className = "comfy-sidebar-batch-label"; label.textContent = `${idx + 1}/${state.images.length}`; label.style.cursor = "pointer"; label.title = "View all images in this batch"; label.onclick = (ev) => { ev.stopPropagation(); if (typeof onNavigateBatch === "function") { onNavigateBatch(state); } }; const nextBtn = document.createElement("span"); nextBtn.className = "pi pi-chevron-right"; nextBtn.style.cursor = "pointer"; nextBtn.onclick = (ev) => { ev.stopPropagation(); cardObj.currentImageIndex = (cardObj.currentImageIndex + 1) % state.images.length; renderCardImages(cardObj, state, onNavigateBatch); }; navBar.append(prevBtn, label, nextBtn); wrapper.appendChild(navBar); } const label = navBar.querySelector(".comfy-sidebar-batch-label"); if (label) label.textContent = `${idx + 1}/${state.images.length}`; } else { if (navBar) navBar.remove(); } }