Files
m0rtus59-ComfyUI-ComfySidebar/js/ui/mediaPreview.js
T

624 lines
26 KiB
JavaScript

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 = `<svg viewBox="0 0 24 24" width="14" height="14" style="margin-left: 2px; pointer-events: none;"><polygon points="6,4 20,12 6,20" fill="#ffffff"/></svg>`;
const stopIconSvg = `<svg viewBox="0 0 24 24" width="12" height="12" style="pointer-events: none;"><rect x="5" y="5" width="14" height="14" rx="2" fill="#ffffff"/></svg>`;
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 = `<i class="pi pi-folder" style="font-size: 10px;"></i><span>${folderName}</span>`;
const openHint = document.createElement("div");
openHint.className = "comfy-sidebar-file-subtext";
openHint.innerHTML = `<i class="pi pi-external-link" style="font-size: 9px;"></i><span>Open Folder</span>`;
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 = `<i class="pi pi-folder-open"></i><span>Open Location</span>`;
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 = `
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" style="width: 100%; height: 100%; filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));">
<circle cx="50" cy="50" r="16.65" fill="rgba(0, 0, 0, 0.55)" />
<polygon points="45.7,42.5 45.7,57.5 58.7,50" fill="rgba(255, 255, 255, 0.9)" />
</svg>
`;
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();
}
}