diff --git a/litegraph b/litegraph index 42adb8d..3ce3a47 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 42adb8dba1631da0a743486f4d8eee9748ce70c8 +Subproject commit 3ce3a478717580ad01778cf78d89285dba51dae4 diff --git a/src/lib/ImageViewer.ts b/src/lib/ImageViewer.ts index 8913f4d..9120722 100644 --- a/src/lib/ImageViewer.ts +++ b/src/lib/ImageViewer.ts @@ -6,7 +6,7 @@ export class ImageViewer { currentImages: string[] = [] selectedIndex: number = -1; currentGallery: HTMLDivElement | null = null; - private static _instance: ImageViewer; + static _instance: ImageViewer; static get instance(): ImageViewer { if (!ImageViewer._instance) @@ -62,6 +62,18 @@ export class ImageViewer { this.currentGallery = galleryElem; this.setModalImageSrc(imageUrls[index]) this.lightboxModal.style.display = "flex"; + + const left = this.lightboxModal.querySelector(".modalPrev") + const right = this.lightboxModal.querySelector(".modalNext") + if (imageUrls.length <= 1) { + left.style.display = "none" + right.style.display = "none" + } + else { + left.style.display = "block" + right.style.display = "block" + } + setTimeout(() => { this.modalImage.focus() }, 200) @@ -138,6 +150,9 @@ export class ImageViewer { } showLightbox(source: HTMLImageElement) { + if (this.lightboxModal.style.display != "none") + this.closeModal(); + const initiallyZoomed = true this.modalZoomSet(this.modalImage, initiallyZoomed) diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 22cd784..26c91fa 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1,57 +1,39 @@ -import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core"; -import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; -import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID, type QueueItemType } from "$lib/api" -import { importA1111, parsePNGMetadata } from "$lib/pnginfo"; -import EventEmitter from "events"; -import type TypedEmitter from "typed-emitter"; +import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyNodeID, type ComfyPromptRequest, type PromptID, type QueueItemType } from "$lib/api"; +import { parsePNGMetadata } from "$lib/pnginfo"; +import { BuiltInSlotType, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type INodeInputSlot, type LGraphNodeConstructor, type NodeID, type NodeTypeOpts, type SerializedLGraph, type SlotIndex } from "@litegraph-ts/core"; import A1111PromptModal from "./modal/A1111PromptModal.svelte"; +import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte"; import MissingNodeTypesModal from "./modal/MissingNodeTypesModal.svelte"; import WorkflowLoadErrorModal from "./modal/WorkflowLoadErrorModal.svelte"; -import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte"; +import * as nodes from "$lib/nodes/index"; -// Import nodes -import "@litegraph-ts/nodes-basic" -import "@litegraph-ts/nodes-events" -import "@litegraph-ts/nodes-logic" -import "@litegraph-ts/nodes-math" -import "@litegraph-ts/nodes-strings" -import "$lib/nodes/index" -import "$lib/nodes/widgets/index" -import "$lib/nodes/actions/index" -import * as nodes from "$lib/nodes/index" -import * as widgets from "$lib/nodes/widgets/index" - -import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas"; -import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; -import queueState from "$lib/stores/queueState"; -import { type SvelteComponentDev } from "svelte/internal"; -import type IComfyInputSlot from "$lib/IComfyInputSlot"; -import { defaultWorkflowAttributes, type LayoutState, type SerializedLayoutState, type WritableLayoutStateStore } from "$lib/stores/layoutStates"; -import { toast } from '@zerodevx/svelte-toast' -import ComfyGraph from "$lib/ComfyGraph"; -import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; -import { get, writable, type Writable } from "svelte/store"; -import { tick } from "svelte"; -import uiState from "$lib/stores/uiState"; -import { basename, capitalize, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils"; -import notify from "$lib/notify"; -import configState from "$lib/stores/configState"; -import { blankGraph } from "$lib/defaultGraph"; -import type { SerializedPromptOutput } from "$lib/utils"; -import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer"; -import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef"; -import { ComfyComboNode } from "$lib/nodes/widgets"; -import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111"; -import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt"; import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; -import selectionState from "$lib/stores/selectionState"; -import layoutStates from "$lib/stores/layoutStates"; -import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState"; -import workflowState from "$lib/stores/workflowState"; -import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from "$lib/convertVanillaWorkflow"; +import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas"; +import { isBackendNodeDefInputType, iterateNodeDefInputs, iterateNodeDefOutputs, type ComfyNodeDef } from "$lib/ComfyNodeDef"; +import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt"; +import convertVanillaWorkflow from "$lib/convertVanillaWorkflow"; +import { blankGraph } from "$lib/defaultGraph"; +import type IComfyInputSlot from "$lib/IComfyInputSlot"; +import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; +import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; +import { ComfyComboNode } from "$lib/nodes/widgets"; +import notify from "$lib/notify"; +import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111"; +import configState from "$lib/stores/configState"; +import layoutStates, { defaultWorkflowAttributes, type SerializedLayoutState } from "$lib/stores/layoutStates"; import modalState from "$lib/stores/modalState"; +import queueState from "$lib/stores/queueState"; +import selectionState from "$lib/stores/selectionState"; +import uiState from "$lib/stores/uiState"; +import workflowState, { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState"; +import type { SerializedPromptOutput } from "$lib/utils"; +import { basename, capitalize, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range } from "$lib/utils"; +import { tick } from "svelte"; +import { type SvelteComponentDev } from "svelte/internal"; +import { get, writable, type Writable } from "svelte/store"; +import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -556,9 +538,14 @@ export default class ComfyApp { this.ctrlDown = e.ctrlKey; // Queue prompt using ctrl or command + enter - if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) { + if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.code === "Enter" || e.keyCode === 10)) { + e.preventDefault(); this.runDefaultQueueAction(); } + else if ((e.ctrlKey) && (e.key === "s" || e.code === "KeyS")) { + e.preventDefault(); + this.saveStateToLocalStorage(); + } }); window.addEventListener("keyup", (e) => { this.shiftDown = e.shiftKey; diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index 7c445c9..4c18de4 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -223,8 +223,13 @@ expandAll = false } - $: if(!showModal) - selectedPrompt = null; + function closeModal() { + selectedPrompt = null + selectedImages = [] + showModal = false; + expandAll = false; + console.warn("CLOSEMODAL") + } let queued = false $: queued = Boolean($queueState.runningNodeID || $queueState.progress); @@ -238,9 +243,11 @@

Prompt Details

- {#if selectedPrompt} - - {/if} + + {#if selectedPrompt} + { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} images={selectedImages} {expandAll} /> + {/if} +
{#if modal != null && modal.svelteComponent != null} - + {/if}
{#if modal != null && modal.buttons?.length > 0} {#each modal.buttons as button} - {/each} diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 47f5012..fe6a6b9 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -4,7 +4,7 @@ export let showModal; // boolean export let closeOnClick = true; // boolean - export const closeDialog = _ => doClose(); + export const closeDialog = () => doClose(); let dialog; // HTMLDialogElement @@ -40,7 +40,7 @@ >
- +
diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte index 9f86644..971fbbb 100644 --- a/src/lib/components/PromptDisplay.svelte +++ b/src/lib/components/PromptDisplay.svelte @@ -8,14 +8,23 @@ import Gallery from "$lib/components/gradio/gallery/Gallery.svelte"; import { ImageViewer } from "$lib/ImageViewer"; import type { Styles } from "@gradio/utils"; - import { countNewLines } from "$lib/utils"; + import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils"; + import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte"; + import workflowState, { type ComfyWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState"; + import type { ComfyReceiveOutputNode } from "$lib/nodes/actions"; + import type ComfyApp from "./ComfyApp"; const splitLength = 50; export let prompt: SerializedPromptInputsAll; - export let images: string[] = []; + export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint export let isMobile: boolean = false; export let expandAll: boolean = false; + export let closeModal: () => void; + export let app: ComfyApp; + let isPromptOpen = expandAll; + + let selected_image: number | null = null; let galleryStyle: Styles = { grid_cols: [2], @@ -23,6 +32,32 @@ height: "var(--size-96)" } + let receiveTargets: WorkflowReceiveOutputTargets[] = []; + let comfyBoxImages = [] + 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); + } + else { + comfyBoxImages = [] + } + + $: if (comfyBoxImages.length > 0) { + if (selected_image != null) + litegraphType = "COMFYBOX_IMAGE" + else + litegraphType = "COMFYBOX_IMAGES" + receiveTargets = workflowState.findReceiveOutputTargets(litegraphType) + } + else { + litegraphType = "(none)" + receiveTargets = [] + } + function isInputLink(input: SerializedPromptInput): boolean { return Array.isArray(input) && input.length === 2 @@ -65,56 +100,98 @@ // TODO dialog renders over it // ImageViewer.instance.showLightbox(e.detail) } + + function sendOutput(workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode) { + if (workflow == null || targetNode == null) + return + + let value = null; + if (targetNode.properties.type === "COMFYBOX_IMAGE") { + if (selected_image != null) + value = comfyBoxImages[selected_image] + else + value = comfyBoxImages[0] + } + else if (targetNode.properties.type === "COMFYBOX_IMAGES") { + value = comfyBoxImages + } + + if (value == null) + return; + + targetNode.receiveOutput(value); + workflowState.setActiveWorkflow(app.lCanvas, workflow.id) + + closeModal(); + }
-
+
- {#each Object.entries(prompt) as [nodeID, inputs], i} - {@const classType = inputs.class_type} - {@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))} - {#if filtered.length > 0} -
- - - {#each filtered as [inputName, input]} - - -
- {#if isInputLink(input)} - Link {input[0]} -> {input[1]} - {:else if typeof input === "object"} + +
+ + {#each Object.entries(prompt) as [nodeID, inputs], i} + {@const classType = inputs.class_type} + {@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))} + {#if filtered.length > 0} +
+ + + {#each filtered as [inputName, input]} - - + +
+ {#if isInputLink(input)} + Link {input[0]} -> {input[1]} + {:else if typeof input === "object"} + + + + + {:else if isMultiline(input)} + {@const lines = Math.max(countNewLines(input), input.length / splitLength)} + + {:else} + + {/if} +
- {:else if isMultiline(input)} - {@const lines = Math.max(countNewLines(input), input.length / splitLength)} - - {:else} - - {/if} -
+ {/each} +
- {/each} - - -
- {/if} - {/each} +
+ {/if} + {/each} +
+
+ +
+ + + + Output type: {litegraphType} + {#if receiveTargets.length > 0} + sendOutput(e.detail.workflow, e.detail.targetNode)} /> + {:else} +
No receive output targets found across all workflows.
+ {/if} +
+
{#if images.length > 0} @@ -128,6 +205,7 @@ root={""} root_url={""} on:clicked={onGalleryImageClicked} + bind:selected_image />
@@ -148,6 +226,10 @@ @media (min-width: 1600px) { flex-direction: row; } + } + + .prompt-and-sends { + width: 50%; .scroll-container { position: relative; @@ -155,21 +237,6 @@ flex: 1 1 0%; } - .image-container { - position: relative; - flex: 1 1 0%; - max-width: 30vw; - - > :global(.block) { - height: 100%; - - :global(> .preview) { - height: 100%; - max-height: none !important; - } - } - } - .copy-button { display: flex; position: absolute; @@ -220,4 +287,23 @@ } } } + + .outputs-message { + padding: 0.5rem; + } + + .image-container { + position: relative; + flex: 1 1 0%; + width: 50%; + + > :global(.block) { + height: 100%; + + :global(> .preview) { + height: 100%; + max-height: none !important; + } + } + } diff --git a/src/lib/components/modal/A1111PromptModal.svelte b/src/lib/components/modal/A1111PromptModal.svelte index 6716a77..8261623 100644 --- a/src/lib/components/modal/A1111PromptModal.svelte +++ b/src/lib/components/modal/A1111PromptModal.svelte @@ -104,7 +104,6 @@ .accordion { background: var(--panel-background-fill); - :global(> .block .block) { background: var(--panel-background-fill); } diff --git a/src/lib/components/modal/ReceiveOutputTargets.svelte b/src/lib/components/modal/ReceiveOutputTargets.svelte new file mode 100644 index 0000000..9e1b28c --- /dev/null +++ b/src/lib/components/modal/ReceiveOutputTargets.svelte @@ -0,0 +1,79 @@ + + +
+ {#if receiveTargets.length > 0} + {#each receiveTargets as { workflow, targetNodes }} + + {workflow.attrs.title} + {#each targetNodes as targetNode} + +
+
+
➤ {targetNode.properties.name}
+ {#if targetNode.properties.description} +
{targetNode.properties.description}
+ {/if} +
+
+ +
+
+
+ {/each} +
+ {/each} + {:else} +
(No receive targets found.)
+ {/if} +
+ + diff --git a/src/lib/components/modal/SendOutputModal.svelte b/src/lib/components/modal/SendOutputModal.svelte new file mode 100644 index 0000000..905abe0 --- /dev/null +++ b/src/lib/components/modal/SendOutputModal.svelte @@ -0,0 +1,79 @@ + + + + +
+
+ + Type: {type} + + sendOutput(e.detail.workflow, e.detail.targetNode)} /> +
+
+ + {#if type === "COMFYBOX_IMAGE"} + + {/if} + +
+
+ + diff --git a/src/lib/nodeImports.ts b/src/lib/nodeImports.ts new file mode 100644 index 0000000..3cbf7fd --- /dev/null +++ b/src/lib/nodeImports.ts @@ -0,0 +1,9 @@ +import "@litegraph-ts/nodes-basic"; +import "@litegraph-ts/nodes-events"; +import "@litegraph-ts/nodes-logic"; +import "@litegraph-ts/nodes-math"; +import "@litegraph-ts/nodes-strings"; + +import "$lib/nodes/index"; +import "$lib/nodes/widgets/index"; +import "$lib/nodes/actions/index"; diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index 31f8b73..71b5496 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -150,9 +150,7 @@ export default class ComfyGraphNode extends LGraphNode { const link = currentNode.getUpstreamLink(); if (link !== null) { const node = this.graph.getNodeById(link.origin_id) as ComfyGraphNode; - console.warn(node.type) if (node.canInheritSlotTypes) { - console.log("REROUTE2", node) if (node === this) { // We've found a circle currentNode.disconnectInput(link.target_slot); @@ -193,7 +191,6 @@ export default class ComfyGraphNode extends LGraphNode { const node = this.graph.getNodeById(link.target_id) as ComfyGraphNode; if (node.canInheritSlotTypes) { - console.log("REROUTE", node) // Follow reroute nodes nodes.push(node); updateNodes.push(node); diff --git a/src/lib/nodes/actions/ComfyReceiveOutputNode.ts b/src/lib/nodes/actions/ComfyReceiveOutputNode.ts new file mode 100644 index 0000000..e5bc654 --- /dev/null +++ b/src/lib/nodes/actions/ComfyReceiveOutputNode.ts @@ -0,0 +1,91 @@ +import { BuiltInSlotType, LiteGraph, type IComboWidget, type SlotLayout, type SlotType, type ITextWidget, BASE_SLOT_TYPES, LGraphNode, type Vector2, BuiltInSlotShape, LGraphCanvas } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; +import { getLitegraphType } from "$lib/utils"; +import notify from "$lib/notify"; + +export interface ComfyReceiveOutputNodeProperties extends ComfyGraphNodeProperties { + name: string, + description: string, + type: SlotType +} + +function getOutputTypes(widget: IComboWidget, node: LGraphNode): string[] { + let result = [] + result = result.concat(Array.from(BASE_SLOT_TYPES)) + result.push("COMFYBOX_IMAGE") + result.push("COMFYBOX_IMAGES") + return result +} + +export default class ComfyReceiveOutputNode extends ComfyGraphNode { + override properties: ComfyReceiveOutputNodeProperties = { + tags: [], + name: "Image", + description: "Generic image input.", + type: "COMFYBOX_IMAGE" + } + + static slotLayout: SlotLayout = { + outputs: [ + { name: "received", type: BuiltInSlotType.EVENT } + ] + } + + override size: Vector2 = [180, 90]; + + nameWidget: ITextWidget; + descriptionWidget: ITextWidget; + typeWidget: IComboWidget; + + isActive: boolean = false; + + private _queue: any[] = [] + + constructor(title?: string) { + super(title) + + this.nameWidget = this.addWidget("text", "Name", this.properties.name, "name"); + this.descriptionWidget = this.addWidget("text", "Desc.", this.properties.description, "description", { multiline: true }); + this.typeWidget = this.addWidget("combo", "Type", "" + this.properties.type, "type", { values: getOutputTypes }); + } + + override onPropertyChanged(property: any, value: any) { + if (property === "type") { + const color = LGraphCanvas.DEFAULT_CONNECTION_COLORS_BY_TYPE[value] || LGraphCanvas.DEFAULT_CONNECTION_COLORS_BY_TYPE[BuiltInSlotType.EVENT]; + this.outputs[0].color_on = color + this.outputs[0].color_off = color + } + } + + override getTitle(): string { + if (this.flags.collapsed) { + return this.properties.name; + } + return this.title; + } + + override onExecute() { + while (this._queue.length > 0) + this.triggerSlot(0, this._queue.splice(0, 1)) + } + + receiveOutput(value: any) { + const type = getLitegraphType(value); + console.warn("receive", this.id, value, type) + + if (type !== this.properties.type) { + console.error(`Output type mismatch! ${type} != ${this.properties.type}`) + notify("Output type mismatch!", { type: "error" }) + return; + } + + this._queue.push(value); + } +} + +LiteGraph.registerNodeType({ + class: ComfyReceiveOutputNode, + title: "Comfy.ReceiveOutput", + desc: "Receives a workflow output sent from elsewhere", + type: "events/receive_output" +}) diff --git a/src/lib/nodes/actions/ComfySendOutputAction.ts b/src/lib/nodes/actions/ComfySendOutputAction.ts new file mode 100644 index 0000000..1f6fece --- /dev/null +++ b/src/lib/nodes/actions/ComfySendOutputAction.ts @@ -0,0 +1,83 @@ +import modalState, { type ModalData, type ModalState } from "$lib/stores/modalState"; +import { getLitegraphType } from "$lib/utils"; +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte"; +import notify from "$lib/notify"; +import workflowState from "$lib/stores/workflowState"; +import { get } from "svelte/store"; +import type ComfyApp from "$lib/components/ComfyApp"; + +export interface ComfySendOutputActionProperties extends ComfyGraphNodeProperties { +} + +export default class ComfySendOutputAction extends ComfyGraphNode { + override properties: ComfySendOutputActionProperties = { + tags: [], + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "value", type: "*" }, + { name: "trigger", type: BuiltInSlotType.ACTION } + ], + } + + isActive: boolean = false; + + override onAction(action: any, param: any) { + const value = this.getInputData(0); + if (value == null) { + notify("No workflow data to send!", { type: "error" }) + return; + } + + if (this.isActive) + return; + + let type = getLitegraphType(value); + const receiveTargets = workflowState.findReceiveOutputTargets(type); + + this.isActive = true; + + const doSend = (modal: ModalData) => { + this.isActive = false; + + const { workflow, targetNode } = get(modal.state) as SendOutputModalResult; + console.warn("send", workflow, targetNode); + + if (workflow == null || targetNode == null) + return + + const app = (window as any).app as ComfyApp; + if (app == null) { + console.error("Couldn't get app!") + return + } + + targetNode.receiveOutput(value); + workflowState.setActiveWorkflow(app.lCanvas, workflow.id) + } + + modalState.pushModal({ + title: "Send Output", + closeOnClick: true, + showCloseButton: true, + svelteComponent: SendOutputModal, + svelteProps: { + value, + type, + receiveTargets + }, + onClose: doSend + }) + }; +} + +LiteGraph.registerNodeType({ + class: ComfySendOutputAction, + title: "Comfy.SendOutputAction", + desc: "Sends a workflow output elsewhere", + type: "actions/send_output" +}) diff --git a/src/lib/nodes/actions/index.ts b/src/lib/nodes/actions/index.ts index 383601a..553c09a 100644 --- a/src/lib/nodes/actions/index.ts +++ b/src/lib/nodes/actions/index.ts @@ -8,3 +8,5 @@ export { default as ComfySetNodeModeAdvancedAction } from "./ComfySetNodeModeAdv export { default as ComfySetPromptThumbnailsAction } from "./ComfySetPromptThumbnailsAction" export { default as ComfyStoreImagesAction } from "./ComfyStoreImagesAction" export { default as ComfySwapAction } from "./ComfySwapAction" +export { default as ComfySendOutputAction } from "./ComfySendOutputAction" +export { default as ComfyReceiveOutputNode } from "./ComfyReceiveOutputNode" diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index 738410f..e232708 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -1,3 +1,5 @@ +import "$lib/nodes/ComfyGraphNode"; + export { default as ComfyReroute } from "./ComfyReroute" export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode" export { default as ComfyValueControl } from "./ComfyValueControl" diff --git a/src/lib/nodes/widgets/index.ts b/src/lib/nodes/widgets/index.ts index 51794cf..496e62e 100644 --- a/src/lib/nodes/widgets/index.ts +++ b/src/lib/nodes/widgets/index.ts @@ -1,4 +1,5 @@ export { default as ComfyWidgetNode } from "./ComfyWidgetNode" + export { default as ComfyButtonNode } from "./ComfyButtonNode" export { default as ComfyCheckboxNode } from "./ComfyCheckboxNode" export { default as ComfyComboNode } from "./ComfyComboNode" diff --git a/src/lib/stores/modalState.ts b/src/lib/stores/modalState.ts index bb22c7e..6da90c0 100644 --- a/src/lib/stores/modalState.ts +++ b/src/lib/stores/modalState.ts @@ -1,24 +1,28 @@ import type { SvelteComponentDev } from "svelte/internal"; -import { writable, type Writable } from "svelte/store"; +import { get, writable, type Writable } from "svelte/store"; import { v4 as uuidv4 } from "uuid"; +export type ModalState = Record; + export type ModalButton = { name: string, variant: "primary" | "secondary", - onClick: () => void, + onClick: (state: ModalData) => void, closeOnClick?: boolean } export interface ModalData { id: string, title?: string, - onClose?: () => void, + onClose?: (modal: ModalState) => void, svelteComponent?: typeof SvelteComponentDev, svelteProps: Record, buttons: ModalButton[], showCloseButton: boolean, - closeOnClick: boolean + closeOnClick: boolean, + state: Writable, + close: () => void, } -export interface ModalState { +export interface ModalStates { activeModals: ModalData[] } @@ -28,20 +32,23 @@ export interface ModalStateOps { closeAllModals: () => void, } -export type WritableModalStateStore = Writable & ModalStateOps; -const store: Writable = writable( +export type WritableModalStateStore = Writable & ModalStateOps; +const store: Writable = writable( { activeModals: [] }) function pushModal(data: Partial) { + const id = uuidv4() const modal: ModalData = { showCloseButton: true, closeOnClick: true, buttons: [], svelteProps: {}, + state: writable({}), ...data, - id: uuidv4(), + id, + close: () => closeModal(id) } store.update(s => { @@ -51,7 +58,13 @@ function pushModal(data: Partial) { } function closeModal(id: string) { + const modal = get(store).activeModals.find(m => m.id === id); + if (modal == null) + return; + store.update(s => { + if (modal.onClose) + modal.onClose(modal) s.activeModals = s.activeModals.filter(m => m.id !== id) return s; }) diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts index b86d323..122db4a 100644 --- a/src/lib/stores/workflowState.ts +++ b/src/lib/stores/workflowState.ts @@ -1,5 +1,5 @@ import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas'; -import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph, LiteGraph } from '@litegraph-ts/core'; +import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph, LiteGraph, type SlotType, NodeMode } from '@litegraph-ts/core'; import { get, writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store'; import { defaultWorkflowAttributes, type SerializedLayoutState, type WritableLayoutStateStore } from './layoutStates'; @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from "uuid"; import type ComfyGraphCanvas from '$lib/ComfyGraphCanvas'; import { blankGraph } from '$lib/defaultGraph'; import type { SerializedAppState } from '$lib/components/ComfyApp'; +import type ComfyReceiveOutputNode from '$lib/nodes/actions/ComfyReceiveOutputNode'; type ActiveCanvas = { canvas: LGraphCanvas | null; @@ -232,6 +233,11 @@ export type WorkflowState = { activeWorkflow: ComfyWorkflow | null, } +export type WorkflowReceiveOutputTargets = { + workflow: ComfyWorkflow, + targetNodes: ComfyReceiveOutputNode[] +} + type WorkflowStateOps = { getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null getWorkflowByGraph: (graph: LGraph) => ComfyWorkflow | null @@ -243,7 +249,8 @@ type WorkflowStateOps = { addWorkflow: (canvas: ComfyGraphCanvas, data: ComfyWorkflow) => void, closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void, closeAllWorkflows: (canvas: ComfyGraphCanvas) => void, - setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number) => ComfyWorkflow | null + setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number | WorkflowInstID) => ComfyWorkflow | null, + findReceiveOutputTargets: (type: SlotType | SlotType[]) => WorkflowReceiveOutputTargets[] } export type WritableWorkflowStateStore = Writable & WorkflowStateOps; @@ -345,7 +352,7 @@ function closeAllWorkflows(canvas: ComfyGraphCanvas) { closeWorkflow(canvas, 0) } -function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkflow | null { +function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number | WorkflowInstID): ComfyWorkflow | null { const state = get(store); if (state.openedWorkflows.length === 0) { @@ -354,6 +361,10 @@ function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkfl return null; } + if (typeof index === "string") { + index = state.openedWorkflows.findIndex(w => w.id === index) + } + if (index < 0 || index >= state.openedWorkflows.length) return state.activeWorkflow; @@ -375,6 +386,31 @@ function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkfl return workflow; } +function findReceiveOutputTargets(type: SlotType | SlotType[]): WorkflowReceiveOutputTargets[] { + let result = [] + + const state = get(store); + + if (!Array.isArray(type)) + type = [type] + + const types = new Set(type); + + for (const workflow of state.openedWorkflows) { + const targetNodes = workflow.graph + // can't use class here because of circular import + .findNodesByTypeRecursive("events/receive_output") + .filter(n => { + return types.has(n.properties.type) && n.mode === NodeMode.ALWAYS + }) + + if (targetNodes.length > 0) + result.push({ workflow, targetNodes }); + } + + return result; +} + const workflowStateStore: WritableWorkflowStateStore = { ...store, @@ -389,5 +425,6 @@ const workflowStateStore: WritableWorkflowStateStore = closeWorkflow, closeAllWorkflows, setActiveWorkflow, + findReceiveOutputTargets } export default workflowStateStore; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2e55ffe..d9eb7cb 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,7 @@ import { type WidgetLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates"; import selectionState from "$lib/stores/selectionState"; import type { FileData as GradioFileData } from "@gradio/upload"; -import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID } from "@litegraph-ts/core"; +import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID, type SlotType } from "@litegraph-ts/core"; import { get } from "svelte/store"; import type { ComfyNodeID } from "./api"; import { type SerializedPrompt } from "./components/ComfyApp"; @@ -507,6 +507,21 @@ export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string { return convertComfyOutputToComfyURL(image.comfyUIFile) } +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") || "" + + // If at least filename and type exist then we're good + if (filename != null && type != null) { + return { filename, type, subfolder } + } + + return null; +} + export function showLightbox(images: string[], index: number, e: Event) { e.preventDefault() if (!images) @@ -516,3 +531,31 @@ export function showLightbox(images: string[], index: number, e: Event) { e.stopPropagation() } + +export function getLitegraphType(param: any): SlotType | null { + if (param == null) + return null; + + switch (typeof param) { + case "string": + return "string" + case "number": + case "bigint": + return "number" + case "boolean": + return "boolean" + case "object": + if (isComfyBoxImageMetadata(param)) { + return "COMFYBOX_IMAGE" + } + else if (isComfyBoxImageMetadataArray(param)) { + return "COMFYBOX_IMAGES" + } + return null; + case "symbol": + case "undefined": + case "function": + default: + return null; + } +} diff --git a/src/lib/widgets/ImageUploadWidget.svelte b/src/lib/widgets/ImageUploadWidget.svelte index eb5c9cb..7c9b72a 100644 --- a/src/lib/widgets/ImageUploadWidget.svelte +++ b/src/lib/widgets/ImageUploadWidget.svelte @@ -14,6 +14,7 @@ import notify from "$lib/notify"; import NumberInput from "$lib/components/NumberInput.svelte"; import type { ComfyImageEditorNode } from "$lib/nodes/widgets"; + import { ImageViewer } from "$lib/ImageViewer"; export let widget: WidgetLayout | null = null; export let isMobile: boolean = false; @@ -176,6 +177,17 @@ }, 1000); } + function openLightbox() { + if (!$nodeValue || $nodeValue.length === 0) + return; + + const comfyImage = $nodeValue[0]; + const comfyURL = comfyBoxImageToComfyURL(comfyImage); + const images = [comfyURL] + + ImageViewer.instance.showModal(images, 0); + } + let status = "empty"; let uploadError = null; @@ -233,7 +245,7 @@ on:upload_error={onUploadError} on:clear={onClear} on:change={onChange} - on:image_clicked={openImageEditor} + on:image_clicked={openLightbox} /> {:else}
@@ -249,7 +261,7 @@ on:upload_error={onUploadError} on:clear={onClear} on:change={onChange} - on:image_clicked={openImageEditor} + on:image_clicked={openLightbox} />
diff --git a/src/main-desktop.ts b/src/main-desktop.ts index f2b74ce..3d56e90 100644 --- a/src/main-desktop.ts +++ b/src/main-desktop.ts @@ -1,3 +1,6 @@ +// Run node registration before anthing else, in the proper order +import "$lib/nodeImports"; + import ComfyApp from '$lib/components/ComfyApp'; import { configureLitegraph } from '$lib/init'; import App from './App.svelte'; diff --git a/src/main-mobile.ts b/src/main-mobile.ts index 560c795..6969dc3 100644 --- a/src/main-mobile.ts +++ b/src/main-mobile.ts @@ -1,3 +1,6 @@ +// Run node registration before anthing else, in the proper order +import "$lib/nodeImports"; + import AppMobile from './AppMobile.svelte'; import Framework7 from 'framework7/lite-bundle'; import Framework7Svelte from 'framework7-svelte';