Compare commits

..
2 Commits
Author SHA1 Message Date
m0rtus59 c49859a75b Update README to improve feature announcement order
Reordered new feature announcements in the README for clarity.
2026-07-18 17:26:41 +05:00
M0rtus59 08454ff7db Added preview compare slider 2026-07-18 17:22:54 +05:00
4 changed files with 375 additions and 74 deletions
+3 -2
View File
@@ -11,8 +11,9 @@ A lightweight, high-performance ComfyUI extension that replaces the cluttered st
* **Quick Save Buttons**: Dedicated interactive shortcuts appear on hover, allowing you to instantly download generation outputs or export exact workflow JSON files with a single click.
* **Compact Adaptive Design**: A clean, smart layout that automatically balances single or multi-column grids based on your sidebar width. It maximizes viewable content and respects your active screen area without wasting precious space.
* **Live Queue Tracking**: Stay informed on your server state. Includes a visual progress bar that tracks generations dynamically and displays the exact node title currently being executed.
* **[NEW]** A compact topbar layout displaying both workflow tabs and the control panel on a single unified top bar. See the 'Comfy Layout' toggle in the settings.
* **[NEW]** Added toggles to hide buttons you don't need from the side panel.
* A compact topbar layout displaying both workflow tabs and the control panel on a single unified top bar. See the 'Comfy Layout' toggle in the settings.
* Toggles to hide buttons you don't need from the side panel.
* **[NEW]** Preview compare slider - just click the result image preview in the sidebar, and then click another to load it alongside the first one for pixel by pixel comparison.
<img width="385" height="336" alt="image" src="https://github.com/user-attachments/assets/743fbf85-9281-4561-9dd0-cd9af4d8c0e4" />
+341
View File
@@ -0,0 +1,341 @@
import { State } from "./state.js";
import { isVideoFormat } from "./utils.js";
let activeComparisonViewer = null;
let videoSyncActive = false;
let syncAnimationFrameId = null;
const createMediaElement = (src, muted = false) => {
const isVideo = isVideoFormat(src);
const el = isVideo ? document.createElement("video") : document.createElement("img");
if (isVideo) {
el.muted = muted;
el.playsInline = true;
el.autoplay = true;
el.loop = true;
el.controls = false; // Disable native controls to prevent layout shifts
}
Object.assign(el.style, {
gridArea: "1 / 1", maxWidth: "100%", maxHeight: "80vh",
objectFit: "contain", pointerEvents: "none"
});
el.src = isVideo ? src + "#t=0.001" : src;
return el;
};
// Video Playback Manager: Binds timeline progress bar, timeline scrubbing, and play/pause triggers
const setupVideoPlayback = (vid, container) => {
if (!vid) return;
const onMetaLoaded = () => {
vid.play().catch(()=>{});
};
vid.addEventListener("loadedmetadata", onMetaLoaded);
if (vid.readyState >= 1) onMetaLoaded();
// Synced Scrubber Progress Bar Overlay
const controlBar = document.createElement("div");
Object.assign(controlBar.style, {
position: "absolute", bottom: "16px", left: "50%", transform: "translateX(-50%)",
display: "flex", alignItems: "center", gap: "12px", background: "rgba(10,10,10,0.85)",
padding: "8px 16px", borderRadius: "8px", zIndex: "40", fontSize: "11px",
fontFamily: "monospace", color: "#eee", width: "80%", maxWidth: "500px",
boxShadow: "0 2px 8px rgba(0,0,0,0.5)", transform: "translate3d(-50%, 0, 0)",
pointerEvents: "auto"
});
const playBtn = document.createElement("span");
playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause";
playBtn.style.cursor = "pointer";
playBtn.style.fontSize = "14px";
playBtn.onclick = (e) => {
e.stopPropagation();
if (vid.paused) {
vid.play().catch(()=>{});
playBtn.className = "pi pi-pause";
} else {
vid.pause();
playBtn.className = "pi pi-play";
}
};
const scrubberContainer = document.createElement("div");
Object.assign(scrubberContainer.style, {
flex: "1", height: "4px", background: "#444", borderRadius: "2px",
position: "relative", cursor: "pointer"
});
const scrubberFill = document.createElement("div");
Object.assign(scrubberFill.style, {
width: "0%", height: "100%", background: "#3b82f6", borderRadius: "2px"
});
scrubberContainer.appendChild(scrubberFill);
const timeLabel = document.createElement("span");
timeLabel.textContent = "0:00 / 0:00";
const scrub = (e) => {
const rect = scrubberContainer.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const percent = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width));
const duration = vid.duration || 0;
if (duration > 0) {
vid.currentTime = (percent / 100) * duration;
scrubberFill.style.width = `${percent}%`;
}
};
let isScrubbing = false;
scrubberContainer.onmousedown = (e) => { e.stopPropagation(); isScrubbing = true; scrub(e); };
scrubberContainer.ontouchstart = (e) => { e.stopPropagation(); isScrubbing = true; scrub(e); };
const handleWindowMove = (e) => { if (isScrubbing) scrub(e); };
const handleWindowUp = () => { isScrubbing = false; };
window.addEventListener("mousemove", handleWindowMove);
window.addEventListener("touchmove", handleWindowMove);
window.addEventListener("mouseup", handleWindowUp);
window.addEventListener("touchend", handleWindowUp);
controlBar.append(playBtn, scrubberContainer, timeLabel);
container.appendChild(controlBar);
videoSyncActive = true;
const syncLoop = () => {
if (!videoSyncActive) return;
// Update progress & labels
const cur = vid.currentTime || 0;
const dur = vid.duration || 0;
if (dur > 0) {
scrubberFill.style.width = `${(cur / dur) * 100}%`;
const formatTime = (t) => {
const m = Math.floor(t / 60);
const s = Math.floor(t % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
};
timeLabel.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
}
// Dynamically match play button in case video loops
playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause";
syncAnimationFrameId = requestAnimationFrame(syncLoop);
};
requestAnimationFrame(syncLoop);
return () => {
window.removeEventListener("mousemove", handleWindowMove);
window.removeEventListener("touchmove", handleWindowMove);
window.removeEventListener("mouseup", handleWindowUp);
window.removeEventListener("touchend", handleWindowUp);
vid.removeEventListener("loadedmetadata", onMetaLoaded);
controlBar.remove();
};
};
function createComparisonViewer(baseSrc) {
const isBaseVideo = isVideoFormat(baseSrc);
// Mount the viewer overlay container directly inside ComfyUI's canvas parent.
// This allows menus/sidebars outside the canvas area to naturally render on top.
const canvasEl = document.querySelector("#graph-canvas, canvas");
const targetContainer = canvasEl ? canvasEl.parentNode : document.body;
// Ensure the canvas container is positioned relatively so our overlay stretches accurately
if (targetContainer && targetContainer !== document.body) {
targetContainer.style.position = "relative";
}
const container = document.createElement("div");
container.className = "comfy-sidebar-comparison-overlay";
Object.assign(container.style, {
position: "absolute", top: "0", left: "0", width: "100%", height: "100%",
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center",
zIndex: "10", // Above base LGraph grid but strictly below other UI components
pointerEvents: "auto", userSelect: "none", "-webkit-user-select": "none"
});
const header = document.createElement("div");
Object.assign(header.style, {
position: "absolute", top: "16px", display: "flex", gap: "16px",
zIndex: "30", color: "#aaa", fontSize: "12px", fontFamily: "sans-serif",
pointerEvents: "none"
});
const infoText = document.createElement("span");
infoText.textContent = isBaseVideo
? "Video playback. Press Esc to close."
: "Reference loaded. Click another card image in the sidebar to compare.";
header.appendChild(infoText);
container.appendChild(header);
// Close button
const closeBtn = document.createElement("span");
closeBtn.className = "pi pi-times";
closeBtn.title = "Close Comparison (Esc)";
Object.assign(closeBtn.style, {
position: "absolute", top: "16px", right: "24px", zIndex: "30",
cursor: "pointer", fontSize: "20px", color: "#aaa", transition: "color 0.15s ease"
});
closeBtn.onmouseenter = () => closeBtn.style.color = "#fff";
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
container.appendChild(closeBtn);
// Workspace wrapper holding both elements aligned pixel-perfectly
const wrapper = document.createElement("div");
Object.assign(wrapper.style, {
position: "relative", display: "grid", placeItems: "center",
maxWidth: "85%", maxHeight: "85%"
});
container.appendChild(wrapper);
// Close strictly on clicking empty background space of the overlay backdrop
container.onclick = (e) => {
if (e.target === container) {
destroy();
}
};
// Media A (Base reference - Left. Created unmuted to enable audio playback)
let mediaA = createMediaElement(baseSrc, false);
wrapper.appendChild(mediaA);
// Media B (Comparison target - Right)
let mediaB = null;
// Draggable Split Slider Handle
const slider = document.createElement("div");
Object.assign(slider.style, {
position: "absolute", top: "0", bottom: "0", left: "50%",
width: "2px", background: "#fff", cursor: "ew-resize",
zIndex: "25", display: "none", pointerEvents: "auto",
boxShadow: "0 0 8px rgba(0,0,0,0.5)"
});
const sliderButton = document.createElement("div");
Object.assign(sliderButton.style, {
position: "absolute", top: "50%", left: "50%",
transform: "translate(-50%, -50%)", width: "32px", height: "32px",
borderRadius: "50%", background: "#fff", color: "#333",
display: "flex", alignItems: "center", justifyContent: "center",
boxShadow: "0 2px 6px rgba(0,0,0,0.4)", pointerEvents: "auto",
userSelect: "none", webkitUserSelect: "none"
});
sliderButton.innerHTML = `<span class="pi pi-arrows-h" style="font-size: 12px;"></span>`;
slider.appendChild(sliderButton);
wrapper.appendChild(slider);
let splitRatio = 50; // Percentage
let isDragging = false;
let destroyVideoPlaybackFn = null;
const updateSliderPosition = (percent) => {
splitRatio = Math.max(0, Math.min(100, percent));
slider.style.left = `${splitRatio}%`;
if (mediaB) {
// Symmetrical split comparison layout: Image A on Left, Image B on Right
mediaB.style.clipPath = `polygon(${splitRatio}% 0, 100% 0, 100% 100%, ${splitRatio}% 100%)`;
}
};
const startDrag = (e) => {
e.preventDefault();
e.stopPropagation();
isDragging = true;
// Symmetrical Click Teleportation: Snaps split-line directly to mouse coordinates
const rect = wrapper.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const percent = ((clientX - rect.left) / rect.width) * 100;
updateSliderPosition(percent);
};
const doDrag = (e) => {
if (!isDragging) return;
const rect = wrapper.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const percent = ((clientX - rect.left) / rect.width) * 100;
updateSliderPosition(percent);
};
const endDrag = () => { isDragging = false; };
// Allows dragging/clicking from any region of the wrapper
wrapper.addEventListener("mousedown", startDrag);
wrapper.addEventListener("touchstart", startDrag);
window.addEventListener("mousemove", doDrag);
window.addEventListener("touchmove", doDrag);
window.addEventListener("mouseup", endDrag);
window.addEventListener("touchend", endDrag);
const destroy = () => {
window.removeEventListener("mousemove", doDrag);
window.removeEventListener("touchmove", doDrag);
window.removeEventListener("mouseup", endDrag);
window.removeEventListener("touchend", endDrag);
document.removeEventListener("keydown", handleKeys);
videoSyncActive = false;
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
if (destroyVideoPlaybackFn) destroyVideoPlaybackFn();
container.remove();
activeComparisonViewer = null;
};
closeBtn.onclick = destroy;
const handleKeys = (e) => {
if (e.key === "Escape") destroy();
if (e.key === " " && mediaB) {
e.preventDefault();
updateSliderPosition(splitRatio > 50 ? 0 : 100);
}
};
document.addEventListener("keydown", handleKeys);
targetContainer.appendChild(container);
// Initialize single-video playbacks if the base output is an MP4
if (isBaseVideo) {
destroyVideoPlaybackFn = setupVideoPlayback(mediaA, container);
}
return {
loadTarget(targetSrc) {
const isTargetVideo = isVideoFormat(targetSrc);
if (isBaseVideo) {
// If in Video Playback Mode: clicking another item replaces the player or opens image comparison
destroy();
showFullscreenPreview([targetSrc]);
return;
}
// If in Image Comparison Mode: ignore video targets ("images compare only to other images")
if (isTargetVideo) {
return;
}
if (mediaB) mediaB.remove();
mediaB = createMediaElement(targetSrc, false);
mediaB.style.pointerEvents = "none";
wrapper.appendChild(mediaB);
slider.style.display = "block";
infoText.textContent = "Drag the slider to compare. Click other card images to update target | Esc to close.";
updateSliderPosition(50);
}
};
}
export function showFullscreenPreview(imgSrcs) {
if (!imgSrcs || imgSrcs.length === 0) return;
// If the comparison overlay is already open, dynamically load the target
if (activeComparisonViewer) {
activeComparisonViewer.loadTarget(imgSrcs[0]);
return;
}
// Otherwise, open a fresh side-by-side comparison workspace
activeComparisonViewer = createComparisonViewer(imgSrcs[0]);
}
+28 -72
View File
@@ -2,41 +2,12 @@ import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { State, promptStates, cardElements, saveStatesToLocalStorage } from "./state.js";
import { isVideoFormat, matchesFilter, getRunOutputs } from "./utils.js";
import { showFullscreenPreview } from "./comparison.js";
// DI Hook for cyclic imports
export let syncQueueFn = async () => {};
export function setSyncQueue(fn) { syncQueueFn = fn; }
export function showFullscreenPreview(imgSrcs) {
if (!imgSrcs || imgSrcs.length === 0) return;
const overlay = document.createElement("div");
Object.assign(overlay.style, {
position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh",
background: "rgba(0,0,0,0.9)", zIndex: "10000", display: "flex",
flexDirection: "column", alignItems: "center", justifyContent: "center", cursor: "zoom-out"
});
const content = document.createElement("div");
Object.assign(content.style, { maxWidth: "90%", maxHeight: "85%", display: "flex", justifyContent: "center" });
imgSrcs.forEach(src => {
if (isVideoFormat(src)) {
const video = document.createElement("video");
video.src = src; video.autoplay = true; video.controls = true; video.loop = true;
video.style.maxWidth = "100%"; video.style.maxHeight = "100%";
content.appendChild(video);
} else {
const img = document.createElement("img");
img.src = src; img.style.maxWidth = "100%"; img.style.maxHeight = "100%"; img.style.objectFit = "contain";
content.appendChild(img);
}
});
overlay.appendChild(content);
overlay.onclick = () => overlay.remove();
document.body.appendChild(overlay);
}
export const updateSidebarBadge = (count) => {
const icons = document.querySelectorAll('.pi-images');
icons.forEach(icon => {
@@ -292,13 +263,13 @@ function renderCardImages(cardObj, state, keepAspect) {
const matchesTagType = currentMediaEl && (isVideo === (currentMediaEl.tagName.toLowerCase() === "video"));
if (matchesTagType) {
// --- IN-PLACE REUSE PATH (ELIMINATES BLINKING & SHAPE-SHIFTS) ---
// --- IN-PLACE REUSE PATH ---
const mediaEl = currentMediaEl;
// 1. Update interactive event listeners on reuse to bind correct closure variables
mediaEl.onclick = (ev) => {
ev.stopPropagation();
showFullscreenPreview(state.images.map(i => i.url ? i.url : window.location.origin + `/view?filename=${encodeURIComponent(i.filename)}&type=${encodeURIComponent(i.type || 'output')}&subfolder=${encodeURIComponent(i.subfolder || '')}`));
showFullscreenPreview([src]);
};
if (!isVideo) {
@@ -363,12 +334,15 @@ function renderCardImages(cardObj, state, keepAspect) {
Object.assign(wrapper.style, { position: "relative", width: "100%", display: "block" });
const thumb = isVideo ? document.createElement("video") : document.createElement("img");
// Explicit low stacking index combined with standard native webkit dragging force rules
Object.assign(thumb.style, {
width: "100%",
borderRadius: "2px",
display: "block",
cursor: "zoom-in",
webkitUserDrag: "element"
webkitUserDrag: "element",
zIndex: "1"
});
if (isVideo) {
@@ -380,7 +354,7 @@ function renderCardImages(cardObj, state, keepAspect) {
const playIcon = document.createElement("div");
Object.assign(playIcon.style, {
position: "absolute", top: "0", left: "0", width: "100%", height: "100%",
pointerEvents: "none", zIndex: "5", transition: "opacity 0.2s ease"
pointerEvents: "none", zIndex: "2", transition: "opacity 0.2s ease"
});
playIcon.innerHTML = `
@@ -397,47 +371,24 @@ function renderCardImages(cardObj, state, keepAspect) {
if (!keepAspect) { thumb.style.aspectRatio = "1 / 1"; thumb.style.objectFit = "cover"; }
// Keep native image tags naturally draggable
if (!isVideo) {
thumb.setAttribute("draggable", "true");
thumb.onload = () => {
if (keepAspect && thumb.naturalWidth && thumb.naturalHeight) {
thumb.style.aspectRatio = `${thumb.naturalWidth} / ${thumb.naturalHeight}`;
}
if (cardObj.dimEl) {
cardObj.dimEl.textContent = `${thumb.naturalWidth}x${thumb.naturalHeight}`;
cardObj.dimEl.style.display = "block";
}
};
thumb.src = src;
const dragStartHandler = (e) => {
State.currentDraggedImgData = { ...img, workflow: state.workflow };
try {
e.dataTransfer.setData("text/uri-list", src);
e.dataTransfer.setData("text/plain", src);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
e.stopPropagation();
};
thumb.addEventListener("dragstart", dragStartHandler);
thumb._currentDragStart = dragStartHandler;
} else {
thumb.setAttribute("draggable", "false");
thumb.onloadedmetadata = () => {
if (keepAspect && thumb.videoWidth && thumb.videoHeight) {
thumb.style.aspectRatio = `${thumb.videoWidth} / ${thumb.videoHeight}`;
}
if (cardObj.dimEl) {
cardObj.dimEl.textContent = `${thumb.videoWidth}x${thumb.videoHeight}`;
cardObj.dimEl.style.display = "block";
}
};
}
if (!isVideo) {
thumb.onload = () => { if (keepAspect && thumb.naturalWidth && thumb.naturalHeight) thumb.style.aspectRatio = `${thumb.naturalWidth} / ${thumb.naturalHeight}`; };
if (src.startsWith("blob:")) { const tempImg = new Image(); tempImg.onload = () => { thumb.src = src; thumb._lastBlob = src; }; tempImg.src = src; } else thumb.src = src;
} else {
thumb.onloadedmetadata = () => { if (keepAspect && thumb.videoWidth && thumb.videoHeight) thumb.style.aspectRatio = `${thumb.videoWidth} / ${thumb.videoHeight}`; };
thumb.src = src + "#t=0.001";
}
thumb.onclick = (ev) => {
ev.stopPropagation();
showFullscreenPreview(state.images.map(i => i.url ? i.url : window.location.origin + `/view?filename=${encodeURIComponent(i.filename)}&type=${encodeURIComponent(i.type || 'output')}&subfolder=${encodeURIComponent(i.subfolder || '')}`));
showFullscreenPreview([src]);
};
wrapper.insertBefore(thumb, wrapper.firstChild);
@@ -451,7 +402,7 @@ function renderCardImages(cardObj, state, keepAspect) {
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)"
boxShadow: "0 1px 3px rgba(0,0,0,0.4)", transform: "translate3d(-50%, 0, 0)" // Forces a GPU compositor stacking context to prevent video clipping
});
const prevBtn = document.createElement("span");
@@ -507,7 +458,7 @@ const handleGlobalClick = (e) => {
}
const sidebar = State.sidebarContainer;
const clickedInsideSidebar = sidebar && sidebar.contains(e.target);
const clickedFullscreenOverlay = e.target.closest('div[style*="zIndex: 10000"]');
const clickedFullscreenOverlay = e.target.closest('div[style*="zIndex: 999"]');
if (!clickedInsideSidebar && !clickedFullscreenOverlay) {
State.activeSubmenuPromptId = null;
@@ -541,7 +492,9 @@ export function renderDOM() {
borderRadius: "6px",
cursor: "pointer",
transition: "all 0.15s ease",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.3)"
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.3)",
zIndex: "20",
transform: "translateZ(0)" // Forces a GPU compositor stacking context to prevent video clipping
});
btn.onmouseenter = () => { btn.style.backgroundColor = "rgba(0, 0, 0, 0.95)"; btn.style.color = "#fff"; };
btn.onmouseleave = () => { btn.style.backgroundColor = "rgba(0, 0, 0, 0.75)"; btn.style.color = "#e2e8f0"; };
@@ -580,13 +533,14 @@ export function renderDOM() {
timerEl.className = "comfy-sidebar-card-timer";
timerEl.textContent = `Image ${index + 1}/${batchInfo.images.length}`;
timerEl.style.display = "block";
timerEl.style.transform = "translateZ(0)";
const dimEl = document.createElement("div");
Object.assign(dimEl.style, {
position: "absolute", top: "6px", right: "8px", fontSize: "10px",
fontFamily: "monospace", opacity: "0.7", background: "rgba(0, 0, 0, 0.6)",
padding: "2px 4px", borderRadius: "3px", pointerEvents: "none", zIndex: "5", color: "#fff",
display: "none"
display: "none", transform: "translateZ(0)"
});
const grid = document.createElement("div");
@@ -684,13 +638,14 @@ export function renderDOM() {
const node = st.workflow?.nodes?.find(n => String(n.id) === String(out.nodeId));
timerEl.textContent = node ? (node.title || node.type) : `Node #${out.nodeId}`;
timerEl.style.display = "block";
timerEl.style.transform = "translateZ(0)";
const dimEl = document.createElement("div");
Object.assign(dimEl.style, {
position: "absolute", top: "6px", right: "8px", fontSize: "10px",
fontFamily: "monospace", opacity: "0.7", background: "rgba(0, 0, 0, 0.6)",
padding: "2px 4px", borderRadius: "3px", pointerEvents: "none", zIndex: "5", color: "#fff",
display: "none"
display: "none", transform: "translateZ(0)"
});
const grid = document.createElement("div");
@@ -829,7 +784,8 @@ export function renderDOM() {
cursor: "pointer",
transition: "all 0.15s ease",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.3)",
zIndex: "20"
zIndex: "20",
transform: "translateZ(0)"
});
leftHoverBtn.onmouseenter = () => { leftHoverBtn.style.backgroundColor = "rgba(0, 0, 0, 0.95)"; leftHoverBtn.style.color = "#fff"; };
leftHoverBtn.onmouseleave = () => { leftHoverBtn.style.backgroundColor = "rgba(0, 0, 0, 0.75)"; leftHoverBtn.style.color = "#e2e8f0"; };
+3
View File
@@ -3,6 +3,7 @@ export const isVideoFormat = (url) => {
return s.includes(".mp4") || s.includes(".webm");
};
// Robust media output scanner supporting both arrays and single objects (gifs, mp4s, etc.)
export function findImagesInOutputs(outputs) {
const list = [];
if (!outputs) return list;
@@ -13,6 +14,8 @@ export function findImagesInOutputs(outputs) {
val.forEach(item => {
if (item && typeof item === 'object' && item.filename) list.push(item);
});
} else if (val && typeof val === 'object' && val.filename) {
list.push(val);
}
}
}