diff --git a/public/workflows/defaultWorkflow.json b/public/workflows/defaultWorkflow.json index 7890270..d54daac 100644 --- a/public/workflows/defaultWorkflow.json +++ b/public/workflows/defaultWorkflow.json @@ -61,7 +61,7 @@ ], "title": "UI.Gallery", "properties": { - "tags": [], + "tags": ["gen"], "defaultValue": [], "index": 3, "updateMode": "append", @@ -1694,7 +1694,7 @@ ], "title": "UI.Gallery", "properties": { - "tags": [], + "tags": ["hr"], "defaultValue": [], "index": 1, "updateMode": "append", diff --git a/src/lib/api.ts b/src/lib/api.ts index 492aa2b..ec54872 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -101,6 +101,7 @@ export type ComfyUIPromptExtraData = { } type ComfyAPIEvents = { + // JSON status: (status: ComfyAPIStatusResponse | null, error?: Error | null) => void, progress: (progress: Progress) => void, reconnecting: () => void, @@ -111,6 +112,9 @@ type ComfyAPIEvents = { execution_cached: (promptID: PromptID, nodes: ComfyNodeID[]) => void, execution_interrupted: (error: ComfyInterruptedError) => void, execution_error: (error: ComfyExecutionError) => void, + + // Binary + b_preview: (imageBlob: Blob) => void } export default class ComfyAPI { @@ -126,7 +130,7 @@ export default class ComfyAPI { } /** - * Poll status for colab and other things that don't support websockets. + * Poll status for colab and other things that don't support websockets. */ private pollQueue() { setInterval(async () => { @@ -176,6 +180,7 @@ export default class ComfyAPI { this.socket = new WebSocket( `ws${window.location.protocol === "https:" ? "s" : ""}://${hostname}:${port}/ws${existingSession}` ); + this.socket.binaryType = "arraybuffer"; this.socket.addEventListener("open", () => { opened = true; @@ -204,38 +209,64 @@ export default class ComfyAPI { this.socket.addEventListener("message", (event) => { try { - const msg = JSON.parse(event.data); - switch (msg.type) { - case "status": - if (msg.data.sid) { - this.clientId = msg.data.sid; - sessionStorage["Comfy.SessionId"] = this.clientId; - } - this.eventBus.emit("status", { execInfo: { queueRemaining: msg.data.status.exec_info.queue_remaining } }); - break; - case "progress": - this.eventBus.emit("progress", msg.data as Progress); - break; - case "executing": - this.eventBus.emit("executing", msg.data.prompt_id, msg.data.node); - break; - case "executed": - this.eventBus.emit("executed", msg.data.prompt_id, msg.data.node, msg.data.output); - break; - case "execution_start": - this.eventBus.emit("execution_start", msg.data.prompt_id); - break; - case "execution_cached": - this.eventBus.emit("execution_cached", msg.data.prompt_id, msg.data.nodes); - break; - case "execution_interrupted": - this.eventBus.emit("execution_interrupted", msg.data); - break; - case "execution_error": - this.eventBus.emit("execution_error", msg.data); - break; - default: - console.warn("Unhandled message:", event.data); + if (event.data instanceof ArrayBuffer) { + const view = new DataView(event.data); + const eventType = view.getUint32(0); + const buffer = event.data.slice(4); + switch (eventType) { + case 1: + const view2 = new DataView(event.data); + const imageType = view2.getUint32(0) + let imageMime: string + switch (imageType) { + case 1: + default: + imageMime = "image/jpeg"; + break; + case 2: + imageMime = "image/png" + } + const imageBlob = new Blob([buffer.slice(4)], { type: imageMime }); + this.eventBus.emit("b_preview", imageBlob); + break; + default: + throw new Error(`Unknown binary websocket message of type ${eventType}`); + } + } + else { + const msg = JSON.parse(event.data); + switch (msg.type) { + case "status": + if (msg.data.sid) { + this.clientId = msg.data.sid; + sessionStorage["Comfy.SessionId"] = this.clientId; + } + this.eventBus.emit("status", { execInfo: { queueRemaining: msg.data.status.exec_info.queue_remaining } }); + break; + case "progress": + this.eventBus.emit("progress", msg.data as Progress); + break; + case "executing": + this.eventBus.emit("executing", msg.data.prompt_id, msg.data.node); + break; + case "executed": + this.eventBus.emit("executed", msg.data.prompt_id, msg.data.node, msg.data.output); + break; + case "execution_start": + this.eventBus.emit("execution_start", msg.data.prompt_id); + break; + case "execution_cached": + this.eventBus.emit("execution_cached", msg.data.prompt_id, msg.data.nodes); + break; + case "execution_interrupted": + this.eventBus.emit("execution_interrupted", msg.data); + break; + case "execution_error": + this.eventBus.emit("execution_error", msg.data); + break; + default: + console.warn("Unhandled message:", event.data); + } } } catch (error) { console.error("Error handling message", event.data, error); diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 97abc08..0f0369c 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -651,6 +651,10 @@ export default class ComfyApp { } }); + this.api.addEventListener("b_preview", (imageBlob: Blob) => { + queueState.previewUpdated(imageBlob); + }); + const config = get(configState); if (config.pollSystemStatsInterval > 0) { diff --git a/src/lib/components/ComfyBoxWorkflowsView.svelte b/src/lib/components/ComfyBoxWorkflowsView.svelte index 4fd5430..722a1dd 100644 --- a/src/lib/components/ComfyBoxWorkflowsView.svelte +++ b/src/lib/components/ComfyBoxWorkflowsView.svelte @@ -386,6 +386,9 @@ + + + showLightbox(entry.images, i, e)} - src={image} + src={imageURL} + loading="lazy" alt="thumbnail" /> {/each} diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte index b96e2c7..ee9de16 100644 --- a/src/lib/components/PromptDisplay.svelte +++ b/src/lib/components/PromptDisplay.svelte @@ -8,7 +8,7 @@ import Gallery from "$lib/components/gradio/gallery/Gallery.svelte"; import { ImageViewer } from "$lib/ImageViewer"; import type { Styles } from "@gradio/utils"; - import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils"; + import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines, type ComfyImageLocation, convertComfyOutputToComfyURL } from "$lib/utils"; import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte"; import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState"; import type { ComfyReceiveOutputNode } from "$lib/nodes/actions"; @@ -17,7 +17,7 @@ const splitLength = 50; export let prompt: SerializedPromptInputsAll; - export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint + export let images: ComfyImageLocation[] = []; export let isMobile: boolean = false; export let expandAll: boolean = false; export let closeModal: () => void; @@ -36,10 +36,7 @@ let litegraphType = "(none)" $: if (images.length > 0) { - // since the image links come from gradio, have to parse the URL for the - // ComfyImageLocation params - comfyBoxImages = images.map(comfyURLToComfyFile) - .map(comfyFileToComfyBoxMetadata); + comfyBoxImages = images.map(comfyFileToComfyBoxMetadata); } else { comfyBoxImages = [] @@ -199,7 +196,7 @@
{ @@ -18,7 +19,8 @@ export default class ComfyGalleryNode extends ComfyWidgetNode = { } }; +export enum OutputThumbnailsMode { + Auto, + AlwaysThumbnail, + AlwaysFullSize +} + +const defOutputThumbnails: ConfigDefEnum<"outputThumbnails", OutputThumbnailsMode> = { + name: "outputThumbnails", + type: "enum", + defaultValue: OutputThumbnailsMode.Auto, + category: "ui", + description: "If enabled, send back smaller sized output image thumbnails for gallery/queue/history. Enable if you have slow network or are using Colab.", + options: { + values: [ + { + value: OutputThumbnailsMode.Auto, + label: "Autodetect" + }, + { + value: OutputThumbnailsMode.AlwaysThumbnail, + label: "Always use thumbnails" + }, + { + value: OutputThumbnailsMode.AlwaysFullSize, + label: "Always use full size" + }, + ] + } +}; + const defAlwaysStripUserState: ConfigDefBoolean<"alwaysStripUserState"> = { name: "alwaysStripUserState", type: "boolean", @@ -207,6 +237,7 @@ export const CONFIG_DEFS = [ defComfyUIHostname, defComfyUIPort, defNotifications, + defOutputThumbnails, defAlwaysStripUserState, defPromptForWorkflowName, defConfirmWhenUnloadingUnsavedChanges, diff --git a/src/lib/stores/layoutStates.ts b/src/lib/stores/layoutStates.ts index bae1f34..31c4457 100644 --- a/src/lib/stores/layoutStates.ts +++ b/src/lib/stores/layoutStates.ts @@ -615,6 +615,14 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ validNodeTypes: ["ui/gallery"], defaultValue: true }, + { + name: "showPreviews", + type: "boolean", + location: "nodeProps", + editable: true, + validNodeTypes: ["ui/gallery"], + defaultValue: true + }, // ImageUpload { diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index 9e45774..f716630 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -22,6 +22,7 @@ type QueueStateOps = { executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void, executionError: (error: ComfyExecutionError) => CompletedQueueEntry | null, progressUpdated: (progress: Progress) => void + previewUpdated: (imageBlob: Blob) => void getQueueEntry: (promptID: PromptID) => QueueEntry | null; afterQueued: (workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void queueItemDeleted: (type: QueueItemType, id: PromptID) => void; @@ -88,6 +89,11 @@ export type QueueState = { */ runningNodeID: ComfyNodeID | null; + /* + * Currently executing prompt if any + */ + runningPromptID: PromptID | null; + /* * Nodes which should be rendered as "executing" in the frontend (green border). * This includes the running node and all its parent subgraphs @@ -98,6 +104,12 @@ export type QueueState = { * Progress for the current node reported by the frontend */ progress: Progress | null, + + /* + * Image preview URL + */ + previewURL: string | null, + /** * If true, user pressed the "Interrupt" button in the frontend. Disable the * button and wait until the next prompt starts running to re-enable it @@ -115,6 +127,7 @@ const store: Writable = writable({ runningNodeID: null, executingNodes: new Set(), progress: null, + preview: null, isInterrupting: false }) @@ -171,6 +184,19 @@ function progressUpdated(progress: Progress) { }) } +function previewUpdated(imageBlob: Blob) { + console.debug("[queueState] previewUpdated", imageBlob?.type) + store.update(s => { + if (s.runningNodeID == null) { + s.previewURL = null; + return s; + } + + s.previewURL = URL.createObjectURL(imageBlob); + return s; + }) +} + function statusUpdated(status: ComfyAPIStatusResponse | null) { console.debug("[queueState] statusUpdated", status) store.update((s) => { @@ -296,6 +322,7 @@ function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) entry.nodesRan.add(runningNodeID) } s.runningNodeID = runningNodeID; + s.runningPromptID = promptID; if (entry?.extraData?.workflowID) { const workflow = workflowState.getWorkflow(entry.extraData.workflowID); @@ -337,7 +364,9 @@ function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) console.debug("[queueState] Could not find in pending! (executingUpdated)", promptID) } s.progress = null; + s.previewURL = null; s.runningNodeID = null; + s.runningPromptID = null; s.executingNodes.clear(); } entry_ = entry; @@ -362,7 +391,9 @@ function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) { } s.isInterrupting = false; // TODO move to start s.progress = null; + s.previewURL = null; s.runningNodeID = null; + s.runningPromptID = null; s.executingNodes.clear(); return s }) @@ -380,7 +411,9 @@ function executionError(error: ComfyExecutionError): CompletedQueueEntry | null console.error("[queueState] Could not find in pending! (executionError)", error.prompt_id) } s.progress = null; + s.previewURL = null; s.runningNodeID = null; + s.runningPromptID = null; s.executingNodes.clear(); return s }) @@ -416,6 +449,7 @@ function executionStart(promptID: PromptID) { } s.isInterrupting = false; s.runningNodeID = null; + s.runningPromptID = promptID; s.executingNodes.clear(); return s }) @@ -480,7 +514,9 @@ function queueCleared(type: QueueItemType) { s.queuePending.set([]); s.queueRemaining = 0; s.runningNodeID = null; + s.runningPromptID = null; s.progress = null; + s.previewURL = null; s.executingNodes.clear(); } else { @@ -535,6 +571,7 @@ const queueStateStore: WritableQueueStateStore = historyUpdated, statusUpdated, progressUpdated, + previewUpdated, executionStart, executingUpdated, executionCached, diff --git a/src/lib/stores/uiQueueState.ts b/src/lib/stores/uiQueueState.ts index a361c22..d16af8a 100644 --- a/src/lib/stores/uiQueueState.ts +++ b/src/lib/stores/uiQueueState.ts @@ -1,7 +1,8 @@ import type { PromptID, QueueItemType } from '$lib/api'; +import type { ComfyImageLocation } from "$lib/utils"; import { get, writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store'; -import queueState, { type CompletedQueueEntry, type QueueEntry } from './queueState'; +import queueState, { QueueEntryStatus, type CompletedQueueEntry, type QueueEntry } from './queueState'; import type { WorkflowError } from './workflowState'; import { convertComfyOutputToComfyURL } from '$lib/utils'; @@ -13,7 +14,7 @@ export type QueueUIEntry = { submessage: string, date?: string, status: QueueUIEntryStatus, - images?: string[], // URLs + images?: ComfyImageLocation[], // URLs details?: string, // shown in a tooltip on hover error?: WorkflowError } @@ -94,13 +95,12 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que const thumbnails = entry.extraData?.thumbnails if (thumbnails) { - result.images = thumbnails.map(convertComfyOutputToComfyURL); + result.images = [...thumbnails] } const outputs = Object.values(entry.outputs) .filter(o => o.images) .flatMap(o => o.images) - .map(convertComfyOutputToComfyURL); if (outputs) { result.images = result.images.concat(outputs) } @@ -114,7 +114,6 @@ function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry { const images = Object.values(entry.entry.outputs) .filter(o => o.images) .flatMap(o => o.images) - .map(convertComfyOutputToComfyURL); result.images = images if (entry.message) @@ -132,6 +131,8 @@ function updateFromQueue(queuePending: QueueEntry[], queueRunning: QueueEntry[]) // newest entries appear at the top s.queuedEntries = queuePending.map((e) => convertPendingEntry(e, "pending")).reverse(); s.runningEntries = queueRunning.map((e) => convertPendingEntry(e, "running")).reverse(); + s.queuedEntries.sort((a, b) => a.entry.number - b.entry.number) + s.runningEntries.sort((a, b) => a.entry.number - b.entry.number) s.queueUIEntries = s.queuedEntries.concat(s.runningEntries); console.warn("[ComfyQueue] BUILDQUEUE", s.queuedEntries.length, s.runningEntries.length) return s; diff --git a/src/lib/stores/uiState.ts b/src/lib/stores/uiState.ts index e28bc90..2fb7e96 100644 --- a/src/lib/stores/uiState.ts +++ b/src/lib/stores/uiState.ts @@ -10,6 +10,7 @@ export type UIState = { autoAddUI: boolean, uiUnlocked: boolean, uiEditMode: UIEditMode, + hidePreviews: boolean, reconnecting: boolean, forceSaveUserState: boolean | null, @@ -30,6 +31,7 @@ const store: Writable = writable( autoAddUI: true, uiUnlocked: false, uiEditMode: "widgets", + hidePreviews: false, reconnecting: false, forceSaveUserState: null, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 68bdff3..7b1df82 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -9,6 +9,7 @@ import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workf import { ImageViewer } from "./ImageViewer"; import configState from "$lib/stores/configState"; import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte"; +import { OutputThumbnailsMode } from "./stores/configDefs"; export function clamp(n: number, min: number, max: number): number { if (max <= min) @@ -300,31 +301,80 @@ export function convertComfyOutputToGradio(output: SerializedPromptOutput): Grad export function convertComfyOutputEntryToGradio(r: ComfyImageLocation): GradioFileData { const url = configState.getBackendURL(); - const params = new URLSearchParams(r) const fileData: GradioFileData = { name: r.filename, orig_name: r.filename, is_file: false, - data: url + "/view?" + params + data: convertComfyOutputToComfyURL(r) } return fileData } -export function convertComfyOutputToComfyURL(output: string | ComfyImageLocation): string { +function convertComfyPreviewTypeToString(preview: ComfyImagePreviewType): string { + const arr = [] + switch (preview.format) { + case ComfyImagePreviewFormat.JPEG: + arr.push("jpeg") + break; + case ComfyImagePreviewFormat.WebP: + default: + arr.push("webp") + break; + } + + arr.push(String(preview.quality)) + + return arr.join(";") +} + +export function convertComfyOutputToComfyURL(output: string | ComfyImageLocation, thumbnail: boolean = false): string { if (typeof output === "string") return output; - const params = new URLSearchParams(output) + const paramsObj = { + filename: output.filename, + subfolder: output.subfolder, + type: output.type + } + + if (thumbnail) { + let doThumbnail: boolean; + + switch (get(configState).outputThumbnails) { + case OutputThumbnailsMode.AlwaysFullSize: + doThumbnail = false; + break; + case OutputThumbnailsMode.AlwaysThumbnail: + doThumbnail = true; + break; + case OutputThumbnailsMode.Auto: + default: + // TODO detect colab, etc. + if (isMobileBrowser(navigator.userAgent)) { + doThumbnail = true; + } + else { + doThumbnail = false; + } + break; + } + + if (doThumbnail) { + output.preview = { + format: ComfyImagePreviewFormat.WebP, + quality: 80 + } + } + } + + if (output.preview != null) + paramsObj["preview"] = convertComfyPreviewTypeToString(output.preview) + + const params = new URLSearchParams(paramsObj) const url = configState.getBackendURL(); return url + "/view?" + params } -export function convertGradioFileDataToComfyURL(image: GradioFileData, type: ComfyUploadImageType = "input"): string { - const baseUrl = configState.getBackendURL(); - const params = new URLSearchParams({ filename: image.name, subfolder: "", type }) - return `${baseUrl}/view?${params}` -} - export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: ComfyUploadImageType = "input"): ComfyImageLocation { if (!fileData.is_file) throw "Can't convert blob data to comfy output!" @@ -336,18 +386,6 @@ export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, typ } } -export function convertFilenameToComfyURL(filename: string, - subfolder: string = "", - type: "input" | "output" | "temp" = "output"): string { - const params = new URLSearchParams({ - filename, - subfolder, - type - }) - const url = configState.getBackendURL(); - return url + "/view?" + params -} - export function jsonToJsObject(json: string): string { // Try to parse, to see if it's real JSON JSON.parse(json); @@ -413,6 +451,16 @@ export interface SerializedPromptOutput { [key: string]: any } +export enum ComfyImagePreviewFormat { + WebP = "webp", + JPEG = "jpeg", +} + +export type ComfyImagePreviewType = { + format: ComfyImagePreviewFormat, + quality: number +} + /** Raw output entry as received from ComfyUI's backend */ export type ComfyImageLocation = { /* Filename with extension in the subfolder. */ @@ -420,7 +468,19 @@ export type ComfyImageLocation = { /* Subfolder in the containing folder. */ subfolder: string, /* Base ComfyUI folder where the image is located. */ - type: ComfyUploadImageType + type: ComfyUploadImageType, + /* + * Preview information + * + * "format;quality" + * + * ex) + * webp;50 -> webp, quality 50 + * webp;50 -> webp, quality 50 + * jpeg;80 -> rgb, jpeg, quality 80 + * + */ + preview?: ComfyImagePreviewType } /* @@ -544,27 +604,54 @@ export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string { return convertComfyOutputToComfyURL(image.comfyUIFile) } +function parseComfyUIPreviewType(previewStr: string): ComfyImagePreviewType { + let split = previewStr.split(";") + let format = ComfyImagePreviewFormat.WebP; + if (split[0] === "webp") + format = ComfyImagePreviewFormat.WebP; + else if (split[0] === "jpeg") + format = ComfyImagePreviewFormat.JPEG; + + let quality = parseInt(split[0]) + if (isNaN(quality)) + quality = 80 + + return { format, quality } +} + export function comfyURLToComfyFile(urlString: string): ComfyImageLocation | null { const url = new URL(urlString); const params = new URLSearchParams(url.search); const filename = params.get("filename") const type = params.get("type") as ComfyUploadImageType; const subfolder = params.get("subfolder") || "" + const previewStr = params.get("preview") || null; + let preview = null + + if (previewStr != null) { + preview = parseComfyUIPreviewType(preview); + } // If at least filename and type exist then we're good if (filename != null && type != null) { - return { filename, type, subfolder } + return { filename, type, subfolder, preview } } return null; } -export function showLightbox(images: string[], index: number, e: Event) { +export function showLightbox(images: ComfyImageLocation[] | string[], index: number, e: Event) { e.preventDefault() if (!images) return - ImageViewer.instance.showModal(images, index); + let images_: string[] + if (typeof images[0] === "object") + images_ = (images as ComfyImageLocation[]).map(v => convertComfyOutputToComfyURL(v)) + else + images_ = (images as string[]) + + ImageViewer.instance.showModal(images_, index); e.stopPropagation() } diff --git a/src/lib/widgets/GalleryWidget.svelte b/src/lib/widgets/GalleryWidget.svelte index c820d34..0ce01ee 100644 --- a/src/lib/widgets/GalleryWidget.svelte +++ b/src/lib/widgets/GalleryWidget.svelte @@ -11,7 +11,11 @@ import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils"; import { f7 } from "framework7-svelte"; import type { ComfyGalleryNode } from "$lib/nodes/widgets"; - import { showMobileLightbox } from "$lib/components/utils"; + import { showMobileLightbox } from "$lib/components/utils"; + import queueState from "$lib/stores/queueState"; + import uiState from "$lib/stores/uiState"; + import { loadImage } from "./utils"; + import Spinner from "$lib/components/Spinner.svelte"; export let widget: WidgetLayout | null = null; export let isMobile: boolean = false; @@ -25,6 +29,47 @@ $: widget && setNodeValue(widget); + function tagsMatch(tags: string[] | null): boolean { + if(tags != null && tags.length > 0) + return node.properties.tags.length > 0 && node.properties.tags.every(t => tags.includes(t)); + else + return node.properties.tags.length === 0; + } + + let previewURL: string | null; + let previewImage: HTMLImageElement | null = null; + let previewElem: HTMLImageElement | null = null + $: { + previewURL = $queueState.previewURL; + + if (previewURL && $queueState.runningPromptID != null && !$uiState.hidePreviews && node.properties.showPreviews) { + const queueEntry = queueState.getQueueEntry($queueState.runningPromptID) + if (queueEntry != null) { + const tags = queueEntry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs; + if (tagsMatch(tags)) { + loadImage(previewURL).then((img) => { + previewImage = img; + }) + } + else { + previewImage = null; + } + } + else { + previewImage = null; + } + } + else { + previewImage = null; + } + } + + function showPreview() { + } + + function hidePreview() { + } + function setNodeValue(widget: WidgetLayout) { if (widget) { node = widget.node as ComfyGalleryNode @@ -34,6 +79,8 @@ imageHeight = node.imageHeight selected_image = node.selectedImage; forceSelectImage = node.forceSelectImage; + previewURL = null; + previewImage = null; if ($nodeValue != null) { if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) { @@ -108,6 +155,11 @@