import { isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "./utils.js"; import { create3DViewer } from "./viewer3d.js"; import { createTextReader } from "./text_reader.js"; import { stopAllAudioPlayback } from "../ui/audioController.js"; import { SidebarOverlay } from "./overlay.js"; let activeComparisonViewer = null; export function isAudioViewerOpen() { return !!(activeComparisonViewer && activeComparisonViewer.isAudio); } const getClientX = (e) => { if (e.touches && e.touches.length > 0) return e.touches[0].clientX; if (e.changedTouches && e.changedTouches.length > 0) return e.changedTouches[0].clientX; return e.clientX || 0; }; const getClientY = (e) => { if (e.touches && e.touches.length > 0) return e.touches[0].clientY; if (e.changedTouches && e.changedTouches.length > 0) return e.changedTouches[0].clientY; return e.clientY || 0; }; function createAudioViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () => {}) { let togglePlayFn = () => {}; const overlay = new SidebarOverlay({ onDestroy, onKeyDown: (e) => { if (e.key === " ") { e.preventDefault(); togglePlayFn(); } } }); const playerBox = document.createElement("div"); Object.assign(playerBox.style, { background: "linear-gradient(135deg, #1e1333 0%, #0f172a 100%)", border: "1px solid rgba(255,255,255,0.15)", borderRadius: "12px", padding: "24px 28px", width: "90%", maxWidth: "520px", boxShadow: "0 8px 32px rgba(0,0,0,0.7)", display: "flex", flexDirection: "column", gap: "16px", zIndex: "20" }); const parsedFilename = getFilenameFromUrl(baseSrc) || "audio.wav"; const parsedExt = (parsedFilename.split(".").pop() || "AUDIO").toUpperCase(); const titleRow = document.createElement("div"); Object.assign(titleRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between" }); const title = document.createElement("span"); title.textContent = parsedFilename; Object.assign(title.style, { fontSize: "14px", fontWeight: "bold", color: "#f1f5f9", fontFamily: "monospace", maxWidth: "75%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }); const badge = document.createElement("span"); badge.className = "comfy-sidebar-audio-badge"; badge.style.position = "static"; badge.textContent = parsedExt; titleRow.append(title, badge); const soundwave = document.createElement("div"); soundwave.className = "comfy-sidebar-soundwave-container"; soundwave.style.height = "56px"; const barHeights = [14, 22, 38, 28, 48, 36, 20, 42, 54, 32, 18, 40, 50, 30, 22, 36, 48, 24, 18, 34, 46, 26, 16]; barHeights.forEach((h, i) => { const bar = document.createElement("div"); bar.className = "comfy-sidebar-soundwave-bar"; bar.style.height = `${h}px`; bar.style.width = "4px"; bar.style.animationDelay = `${(i * 0.06).toFixed(2)}s`; soundwave.appendChild(bar); }); const scrubberContainer = document.createElement("div"); Object.assign(scrubberContainer.style, { width: "100%", height: "6px", background: "#334155", borderRadius: "3px", position: "relative", cursor: "pointer" }); const scrubberFill = document.createElement("div"); Object.assign(scrubberFill.style, { width: "0%", height: "100%", background: "#c084fc", borderRadius: "3px" }); scrubberContainer.appendChild(scrubberFill); const controlsRow = document.createElement("div"); Object.assign(controlsRow.style, { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }); const leftControls = document.createElement("div"); Object.assign(leftControls.style, { display: "flex", alignItems: "center", gap: "10px" }); const playIconSvg = ``; const stopIconSvg = ``; const playBtn = document.createElement("button"); Object.assign(playBtn.style, { width: "36px", height: "36px", borderRadius: "50%", background: "#a855f7", border: "none", color: "#fff", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "14px" }); playBtn.innerHTML = playIconSvg; const timeLabel = document.createElement("span"); Object.assign(timeLabel.style, { fontSize: "11px", fontFamily: "monospace", color: "#94a3b8" }); timeLabel.textContent = "0:00 / 0:00"; leftControls.append(playBtn, timeLabel); const rightControls = document.createElement("div"); Object.assign(rightControls.style, { display: "flex", alignItems: "center", gap: "8px" }); const loopBtn = document.createElement("button"); Object.assign(loopBtn.style, { background: "transparent", border: "1px solid #444", color: "#aaa", borderRadius: "4px", padding: "4px 8px", fontSize: "11px", cursor: "pointer" }); loopBtn.innerHTML = ``; loopBtn.title = "Toggle Loop"; const speedSelect = document.createElement("select"); Object.assign(speedSelect.style, { background: "#1e293b", border: "1px solid #444", color: "#eee", borderRadius: "4px", padding: "2px 4px", fontSize: "11px", outline: "none" }); ["0.5x", "1.0x", "1.5x", "2.0x"].forEach(spd => { const opt = document.createElement("option"); opt.value = spd.replace("x", ""); opt.textContent = spd; if (spd === "1.0x") opt.selected = true; speedSelect.appendChild(opt); }); rightControls.append(loopBtn, speedSelect); controlsRow.append(leftControls, rightControls); playerBox.append(titleRow, soundwave, scrubberContainer, controlsRow); overlay.container.appendChild(playerBox); const audioEl = document.createElement("audio"); audioEl.src = baseSrc; audioEl.preload = "metadata"; overlay.container.appendChild(audioEl); const formatTime = (t) => { const m = Math.floor(t / 60); const s = Math.floor(t % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; }; 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 = () => { playBtn.innerHTML = playIconSvg; playerBox.classList.remove("playing"); scrubberFill.style.width = "0%"; }; togglePlayFn = () => { if (audioEl.paused) { stopAllAudioPlayback(); audioEl.play().catch(()=>{}); playBtn.innerHTML = stopIconSvg; playerBox.classList.add("playing"); } else { audioEl.pause(); audioEl.currentTime = 0; playBtn.innerHTML = playIconSvg; playerBox.classList.remove("playing"); scrubberFill.style.width = "0%"; timeLabel.textContent = `0:00 / ${formatTime(audioEl.duration || 0)}`; } }; playBtn.onclick = togglePlayFn; loopBtn.onclick = () => { audioEl.loop = !audioEl.loop; loopBtn.style.borderColor = audioEl.loop ? "#a855f7" : "#444"; loopBtn.style.color = audioEl.loop ? "#c084fc" : "#aaa"; }; speedSelect.onchange = (e) => { audioEl.playbackRate = Number(e.target.value); }; scrubberContainer.onclick = (e) => { const rect = scrubberContainer.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; if (audioEl.duration > 0) audioEl.currentTime = pos * audioEl.duration; }; overlay.addCleanup(() => { audioEl.pause(); audioEl.src = ""; }); audioEl.play().then(() => { playBtn.innerHTML = stopIconSvg; playerBox.classList.add("playing"); }).catch(()=>{}); return { isAudio: true, loadTarget(targetSrc) { if (!isAudioFormat(targetSrc)) { overlay.destroy(); onSwitchMedia(targetSrc); return; } const newFilename = getFilenameFromUrl(targetSrc) || "audio.wav"; title.textContent = newFilename; badge.textContent = (newFilename.split(".").pop() || "AUDIO").toUpperCase(); audioEl.src = targetSrc; audioEl.play().then(() => { playBtn.innerHTML = stopIconSvg; playerBox.classList.add("playing"); }).catch(()=>{}); }, destroy: () => overlay.destroy() }; } 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; } Object.assign(el.style, { gridArea: "1 / 1", maxWidth: "calc(100vw - 120px)", maxHeight: "calc(100vh - 120px)", width: "auto", height: "auto", objectFit: "contain", pointerEvents: isVideo ? "none" : "auto", userSelect: "none", webkitUserSelect: "none", display: "block" }); el.draggable = false; el.ondragstart = (e) => e.preventDefault(); el.src = isVideo ? src + "#t=0.001" : src; return el; }; const setupVideoPlayback = (vid, container) => { if (!vid) return () => {}; let videoSyncActive = true; let syncAnimationFrameId = null; const onMetaLoaded = () => { vid.play().catch(()=>{}); }; vid.addEventListener("loadedmetadata", onMetaLoaded); if (vid.readyState >= 1) onMetaLoaded(); const controlBar = document.createElement("div"); Object.assign(controlBar.style, { position: "absolute", bottom: "16px", left: "50%", display: "flex", alignItems: "center", gap: "12px", background: "var(--comfy-input-bg, rgba(18, 18, 18, 0.9))", border: "1px solid var(--border-color, rgba(255, 255, 255, 0.15))", padding: "8px 16px", borderRadius: "8px", zIndex: "40", fontSize: "11px", fontFamily: "monospace", color: "var(--fg-color, #eee)", width: "80%", maxWidth: "500px", boxShadow: "0 4px 12px rgba(0,0,0,0.3)", 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 = getClientX(e); 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); const syncLoop = () => { if (!videoSyncActive) return; 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)}`; } playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause"; syncAnimationFrameId = requestAnimationFrame(syncLoop); }; requestAnimationFrame(syncLoop); return () => { videoSyncActive = false; if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId); 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, onDestroy = () => {}) { const isBaseVideo = isVideoFormat(baseSrc); let splitRatio = 50; let mediaB = null; const overlay = new SidebarOverlay({ onDestroy, onKeyDown: (e) => { if (e.key === " " && mediaB) { e.preventDefault(); updateSliderPosition(splitRatio > 50 ? 0 : 100); } } }); const scrollContainer = document.createElement("div"); Object.assign(scrollContainer.style, { position: "absolute", top: "0", left: "0", width: "100%", height: "100%", display: "flex", overflow: "auto", boxSizing: "border-box", padding: "48px 24px", scrollbarWidth: "thin", scrollbarColor: "#555 rgba(0, 0, 0, 0.3)", zIndex: "15" }); overlay.container.appendChild(scrollContainer); const header = document.createElement("div"); Object.assign(header.style, { position: "absolute", top: "16px", left: "50%", transform: "translateX(-50%)", display: "flex", gap: "16px", zIndex: "30", color: "var(--desc-color, #aaa)", fontSize: "12px", fontFamily: "sans-serif", pointerEvents: "none", background: "var(--comfy-input-bg, rgba(18, 18, 18, 0.85))", border: "1px solid var(--border-color, rgba(255, 255, 255, 0.15))", padding: "6px 14px", borderRadius: "6px", backdropFilter: "blur(6px)", boxShadow: "0 2px 8px rgba(0,0,0,0.3)", maxWidth: "85%", textAlign: "center" }); const infoText = document.createElement("span"); infoText.textContent = isBaseVideo ? "Video playback. Press Esc to close." : "Click image to zoom (100%/Fit) | Shift+Click another card to compare."; header.appendChild(infoText); overlay.container.appendChild(header); const wrapper = document.createElement("div"); Object.assign(wrapper.style, { position: "relative", display: "grid", placeItems: "center", margin: "auto", flexShrink: "0", cursor: isBaseVideo ? "default" : "zoom-in" }); scrollContainer.appendChild(wrapper); let mediaA = createMediaElement(baseSrc, false); wrapper.appendChild(mediaA); let isZoomed = false; 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" }); sliderButton.innerHTML = ``; slider.appendChild(sliderButton); wrapper.appendChild(slider); const updateSliderPosition = (percent) => { splitRatio = Math.max(0, Math.min(100, percent)); slider.style.left = `${splitRatio}%`; if (mediaB) { mediaB.style.clipPath = `polygon(${splitRatio}% 0, 100% 0, 100% 100%, ${splitRatio}% 100%)`; } }; const cleanupMedia = (el) => { if (el && el.tagName === "VIDEO") { el.pause(); el.src = ""; el.load(); } }; const syncImageScales = () => { if (!mediaA) return; const wA = mediaA.naturalWidth || mediaA.videoWidth || 0; const hA = mediaA.naturalHeight || mediaA.videoHeight || 0; if (!mediaB) { slider.style.display = "none"; if (mediaA) mediaA.style.pointerEvents = isBaseVideo ? "none" : "auto"; if (isZoomed && wA && hA) { wrapper.style.maxWidth = "none"; wrapper.style.maxHeight = "none"; wrapper.style.width = `${wA}px`; wrapper.style.height = `${hA}px`; wrapper.style.cursor = "zoom-out"; mediaA.style.maxWidth = "none"; mediaA.style.maxHeight = "none"; mediaA.style.width = "100%"; mediaA.style.height = "100%"; mediaA.style.objectFit = "fill"; } else { wrapper.style.maxWidth = "none"; wrapper.style.maxHeight = "none"; wrapper.style.width = "auto"; wrapper.style.height = "auto"; wrapper.style.cursor = isBaseVideo ? "default" : "zoom-in"; mediaA.style.maxWidth = "calc(100vw - 120px)"; mediaA.style.maxHeight = "calc(100vh - 120px)"; mediaA.style.width = "auto"; mediaA.style.height = "auto"; mediaA.style.objectFit = "contain"; } return; } const wB = mediaB.naturalWidth || mediaB.videoWidth || 0; const hB = mediaB.naturalHeight || mediaB.videoHeight || 0; if (!wA || !hA || !wB || !hB) return; const arA = wA / hA; const arB = wB / hB; const arDiff = Math.abs(arA - arB) / Math.max(arA, arB); if (arDiff > 0.05) { infoText.textContent = `Aspect ratio mismatch (${wA}x${hA} vs ${wB}x${hB}). Images must have matching proportions to compare.`; infoText.style.color = "#f87171"; setTimeout(() => { if (infoText) { infoText.style.color = "#aaa"; infoText.textContent = "Click image to zoom (100%/Fit) | Shift+Click another card to compare."; } }, 3500); cleanupMedia(mediaB); mediaB.remove(); mediaB = null; slider.style.display = "none"; syncImageScales(); return; } slider.style.display = "block"; if (mediaA) mediaA.style.pointerEvents = "none"; if (mediaB) mediaB.style.pointerEvents = "none"; infoText.style.color = "#aaa"; infoText.textContent = "Drag slider to compare. Click image to zoom (100%/Fit) | Shift+Click another card to compare | Esc to close."; updateSliderPosition(50); const maxW = Math.max(wA, wB); const maxH = Math.max(hA, hB); if (isZoomed) { wrapper.style.maxWidth = "none"; wrapper.style.maxHeight = "none"; wrapper.style.width = `${maxW}px`; wrapper.style.height = `${maxH}px`; wrapper.style.cursor = "zoom-out"; [mediaA, mediaB].forEach(el => { if (el) { el.style.maxWidth = "none"; el.style.maxHeight = "none"; el.style.width = "100%"; el.style.height = "100%"; el.style.objectFit = "fill"; } }); } else { wrapper.style.maxWidth = "none"; wrapper.style.maxHeight = "none"; wrapper.style.width = "auto"; wrapper.style.height = "auto"; wrapper.style.cursor = "zoom-in"; [mediaA, mediaB].forEach(el => { if (el) { el.style.maxWidth = "calc(100vw - 120px)"; el.style.maxHeight = "calc(100vh - 120px)"; el.style.width = "auto"; el.style.height = "auto"; el.style.objectFit = "contain"; } }); } }; mediaA.onload = syncImageScales; if (mediaA.complete) syncImageScales(); const toggleZoom = () => { if (isBaseVideo) return; isZoomed = !isZoomed; syncImageScales(); if (!isZoomed) { scrollContainer.scrollTop = 0; scrollContainer.scrollLeft = 0; } }; let isDraggingSlider = false, dragMoved = false, dragStartX = 0, dragStartY = 0; const startDrag = (e) => { if (e.button && e.button !== 0) return; dragStartX = getClientX(e); dragStartY = getClientY(e); dragMoved = false; isDraggingSlider = true; }; const doDrag = (e) => { if (!isDraggingSlider) return; const clientX = getClientX(e); const clientY = getClientY(e); if (!dragMoved && (Math.abs(clientX - dragStartX) > 4 || Math.abs(clientY - dragStartY) > 4)) { dragMoved = true; } if (dragMoved && mediaB) { const rect = wrapper.getBoundingClientRect(); const percent = ((clientX - rect.left) / rect.width) * 100; updateSliderPosition(percent); } }; const endDrag = () => { if (!isDraggingSlider) return; isDraggingSlider = false; if (dragMoved) setTimeout(() => { dragMoved = false; }, 50); }; wrapper.addEventListener("mousedown", startDrag); wrapper.addEventListener("touchstart", startDrag); window.addEventListener("mousemove", doDrag); window.addEventListener("touchmove", doDrag); window.addEventListener("mouseup", endDrag); window.addEventListener("touchend", endDrag); overlay.addCleanup(() => { window.removeEventListener("mousemove", doDrag); window.removeEventListener("touchmove", doDrag); window.removeEventListener("mouseup", endDrag); window.removeEventListener("touchend", endDrag); }); wrapper.addEventListener("click", () => { if (dragMoved || isBaseVideo) return; toggleZoom(); }); scrollContainer.addEventListener("click", (e) => { if (dragMoved) return; if (e.target === scrollContainer || e.target === overlay.container) overlay.destroy(); }); if (isBaseVideo) { const destroyVideoPlayback = setupVideoPlayback(mediaA, overlay.container); overlay.addCleanup(destroyVideoPlayback); } overlay.addCleanup(() => { cleanupMedia(mediaA); cleanupMedia(mediaB); }); return { is3D: false, isAudio: false, isText: false, loadTarget(targetSrc, isShiftClick) { const isTargetVideo = isVideoFormat(targetSrc); const isTarget3D = is3DFormat(targetSrc); const isTargetAudio = isAudioFormat(targetSrc); const isTargetText = typeof targetSrc === "object" && targetSrc.text; if (isBaseVideo || isTargetVideo || isTarget3D || isTargetAudio || isTargetText) { overlay.destroy(); showFullscreenPreview([targetSrc], isShiftClick); return; } if (isShiftClick) { if (mediaB) { cleanupMedia(mediaB); mediaB.remove(); mediaB = null; } mediaB = createMediaElement(targetSrc, false); mediaB.style.pointerEvents = "none"; mediaB.onload = syncImageScales; wrapper.appendChild(mediaB); if (mediaB.complete && mediaB.naturalWidth) syncImageScales(); } else { if (mediaB) { cleanupMedia(mediaB); mediaB.remove(); mediaB = null; } slider.style.display = "none"; mediaA.src = targetSrc; mediaA.onload = () => { infoText.style.color = "#aaa"; infoText.textContent = "Click image to zoom (100%/Fit) | Shift+Click another card to compare."; syncImageScales(); }; } }, destroy: () => overlay.destroy() }; } export function showFullscreenPreview(imgSrcs, isShiftClick = false) { if (!imgSrcs || imgSrcs.length === 0) return; const item = imgSrcs[0]; // Handle Text Output if (typeof item === "object" && item.text) { if (activeComparisonViewer) { if (activeComparisonViewer.isText) { activeComparisonViewer.loadTarget(item); return; } activeComparisonViewer.destroy(); activeComparisonViewer = null; } activeComparisonViewer = createTextReader( item, (target) => showFullscreenPreview([target]), () => { activeComparisonViewer = null; } ); return; } const src = typeof item === "string" ? item : (item.url || item.filename || ""); // Delegate 3D formats if (is3DFormat(src)) { if (activeComparisonViewer) { if (activeComparisonViewer.is3D) { activeComparisonViewer.loadTarget(src); return; } activeComparisonViewer.destroy(); activeComparisonViewer = null; } activeComparisonViewer = create3DViewer( src, (targetSrc) => showFullscreenPreview([targetSrc]), () => { activeComparisonViewer = null; } ); return; } // Delegate Audio formats if (isAudioFormat(src)) { if (activeComparisonViewer) { if (activeComparisonViewer.isAudio) { activeComparisonViewer.loadTarget(src); return; } activeComparisonViewer.destroy(); activeComparisonViewer = null; } activeComparisonViewer = createAudioViewer( src, (targetSrc) => showFullscreenPreview([targetSrc]), () => { activeComparisonViewer = null; } ); return; } // Delegate 2D/Video formats if (activeComparisonViewer) { if (activeComparisonViewer.is3D || activeComparisonViewer.isAudio || activeComparisonViewer.isText) { activeComparisonViewer.destroy(); activeComparisonViewer = null; } else { activeComparisonViewer.loadTarget(src, isShiftClick); return; } } activeComparisonViewer = createComparisonViewer( src, () => { activeComparisonViewer = null; } ); }