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 @@