More UI improvements

This commit is contained in:
space-nuko
2023-05-10 18:15:02 -05:00
parent fb697ca1a1
commit e4497e2537
16 changed files with 19993 additions and 18673 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,11 @@
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode } from "@litegraph-ts/core"; import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem } from "@litegraph-ts/core";
import type ComfyApp from "./components/ComfyApp"; import type ComfyApp from "./components/ComfyApp";
import queueState from "./stores/queueState"; import queueState from "./stores/queueState";
import { get } from "svelte/store"; import { get } from "svelte/store";
import uiState from "./stores/uiState"; import uiState from "./stores/uiState";
import layoutState from "./stores/layoutState"; import layoutState from "./stores/layoutState";
import { Watch } from "@litegraph-ts/nodes-basic";
import { ComfyReroute } from "./nodes";
export type SerializedGraphCanvasState = { export type SerializedGraphCanvasState = {
offset: Vector2, offset: Vector2,
@@ -250,4 +252,88 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
} }
} }
private reinstantiate(_value: IContextMenuItem, _options, mouseEvent, prevMenu, node?: LGraphNode) {
if ((node as any).isBackendNode)
return
const newNode = LiteGraph.createNode(node.type);
for (let index = 0; index < newNode.inputs.length; index++) {
const newInput = newNode.inputs[index];
const oldInput = node.inputs[index]
if (oldInput && newInput.type === oldInput.type) {
continue;
}
let link: LLink | null = null;
if (oldInput) {
link = node.getInputLink(index);
node.disconnectInput(index)
oldInput.type = newInput.type
oldInput.name = newInput.name
}
else {
node.addInput(newInput.name, newInput.type, newInput)
}
const reroute = LiteGraph.createNode(ComfyReroute);
reroute.properties.ignoreTypes = true;
node.graph.add(reroute)
const inputPos = node.getConnectionPos(true, index);
reroute.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
reroute.connect(0, node, index);
if (link != null)
node.graph.getNodeById(link.target_id).connect(link.target_slot, reroute, 0)
}
for (let index = 0; index < newNode.outputs.length; index++) {
const newOutput = newNode.outputs[index];
const oldOutput = node.outputs[index]
if (oldOutput && newOutput.type === oldOutput.type) {
continue;
}
let links = []
if (oldOutput) {
links = node.getOutputLinks(index)
node.disconnectOutput(index)
oldOutput.type = newOutput.type
oldOutput.name = newOutput.name
}
else {
node.addOutput(newOutput.name, newOutput.type, newOutput)
}
const reroute = LiteGraph.createNode(ComfyReroute);
reroute.properties.ignoreTypes = true;
node.graph.add(reroute)
const rerouteSize = reroute.computeSize();
const outputPos = node.getConnectionPos(false, index);
reroute.pos = [outputPos[0] + rerouteSize[0] + 20, outputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
node.connect(index, reroute, 0);
for (const link of links) {
reroute.connect(0, link.target_id, link.target_slot)
}
}
}
override getNodeMenuOptions(node: LGraphNode): ContextMenuItem[] {
const options = super.getNodeMenuOptions(node);
options.push(
{
content: "Reinstantiate",
has_submenu: false,
disabled: false,
callback: this.reinstantiate.bind(this)
},
)
return options
}
} }

View File

@@ -95,10 +95,10 @@
{/each} {/each}
</div> </div>
{#if isHidden(container) && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} /> <div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
{/if} {/if}
{#if showHandles} {#if showHandles}
<div class="handle handle-container" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if} {/if}
</Accordion> </Accordion>
</Block> </Block>

View File

@@ -91,10 +91,10 @@
{/each} {/each}
</div> </div>
{#if isHidden(container) && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} /> <div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
{/if} {/if}
{#if showHandles} {#if showHandles}
<div class="handle handle-container" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if} {/if}
</Block> </Block>
</div> </div>

View File

@@ -12,7 +12,6 @@
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import type { ComfyAPIStatus } from "$lib/api"; import type { ComfyAPIStatus } from "$lib/api";
import { SvelteToast, toast } from '@zerodevx/svelte-toast' import { SvelteToast, toast } from '@zerodevx/svelte-toast'
import defaultGraph from "$lib/defaultGraph"
import { LGraph } from "@litegraph-ts/core"; import { LGraph } from "@litegraph-ts/core";
import LightboxModal from "./LightboxModal.svelte"; import LightboxModal from "./LightboxModal.svelte";
@@ -132,7 +131,7 @@
async function doLoadDefault() { async function doLoadDefault() {
var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?"); var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?");
if (confirmed) { if (confirmed) {
await app.deserialize(defaultGraph) await app.initDefaultGraph();
} }
} }

View File

@@ -1,7 +1,6 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType } from "@litegraph-ts/core"; import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI, { type ComfyAPIQueueStatus } from "$lib/api" import ComfyAPI, { type ComfyAPIQueueStatus } from "$lib/api"
import defaultGraph from "$lib/defaultGraph"
import { getPngMetadata, importA1111 } from "$lib/pnginfo"; import { getPngMetadata, importA1111 } from "$lib/pnginfo";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
@@ -32,6 +31,7 @@ import uiState from "$lib/stores/uiState";
import { download, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils"; import { download, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
import notify from "$lib/notify"; import notify from "$lib/notify";
import configState from "$lib/stores/configState"; import configState from "$lib/stores/configState";
import { blankGraph } from "$lib/defaultGraph";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -436,7 +436,16 @@ export default class ComfyApp {
} }
async initDefaultGraph() { async initDefaultGraph() {
const state = structuredClone(defaultGraph) let state = null;
try {
const graphResponse = await fetch("/workflows/defaultWorkflow.json");
state = await graphResponse.json() as SerializedAppState;
}
catch (error) {
console.error("Failed to load default graph", error)
notify(`Failed to load default graph: ${error}`, { type: "error" })
state = structuredClone(blankGraph)
}
await this.deserialize(state) await this.deserialize(state)
} }
@@ -840,7 +849,7 @@ export default class ComfyApp {
const def = defs[backendNode.type]; const def = defs[backendNode.type];
const rawValues = def["input"]["required"][inputSlot.name][0]; const rawValues = def["input"]["required"][inputSlot.name][0];
console.warn("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, inputSlot.config.values.length) console.warn("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
comboNode.doAutoConfig(inputSlot, { includeProperties: new Set(["values"]), setWidgetTitle: false }) comboNode.doAutoConfig(inputSlot, { includeProperties: new Set(["values"]), setWidgetTitle: false })
comboNode.formatValues(rawValues) comboNode.formatValues(rawValues)

View File

@@ -110,10 +110,10 @@
{/each} {/each}
</div> </div>
{#if isHidden(container) && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} /> <div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
{/if} {/if}
{#if showHandles} {#if showHandles}
<div class="handle handle-container" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if} {/if}
</Block> </Block>
{:else} {:else}

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,9 @@ import queueState from "$lib/stores/queueState";
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core"; import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
import { get } from "svelte/store"; import { get } from "svelte/store";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import type { NotifyOptions } from "$lib/notify"; import type { NotifyOptions } from "$lib/notify";
import { convertComfyOutputToGradio } from "$lib/utils"; import { convertComfyOutputToGradio, uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
export class ComfyQueueEvents extends ComfyGraphNode { export class ComfyQueueEvents extends ComfyGraphNode {
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -579,3 +579,83 @@ LiteGraph.registerNodeType({
desc: "Wraps an event's parameter such that passing it into a ComfyWidgetNode's 'store' action will not trigger its 'changed' event", desc: "Wraps an event's parameter such that passing it into a ComfyWidgetNode's 'store' action will not trigger its 'changed' event",
type: "events/no_change" type: "events/no_change"
}) })
export interface ComfyUploadImageActionProperties extends ComfyGraphNodeProperties {
folderType: "output" | "temp"
lastUploadedImageFile: string | null
}
export class ComfyUploadImageAction extends ComfyGraphNode {
override properties: ComfyUploadImageActionProperties = {
tags: [],
folderType: "output",
lastUploadedImageFile: null
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "filename", type: "string" },
{ name: "trigger", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "input_filename", type: "string" },
{ name: "uploaded", type: BuiltInSlotType.EVENT }
],
}
private _promise = null;
displayWidget: ITextWidget;
constructor(title?: string) {
super(title);
this.displayWidget = this.addWidget<ITextWidget>(
"text",
"File",
this.properties.lastUploadedImageFile,
"lastUploadedImageFile"
);
this.displayWidget.disabled = true;
}
override onExecute() {
this.setOutputData(0, this.properties.lastUploadedImageFile)
}
override onAction(action: any, param: any) {
if (action !== "trigger" || this._promise != null)
return;
const filename = this.getInputData(0)
if (typeof filename !== "string" || !filename) {
return;
}
const data: GalleryOutputEntry = {
filename,
subfolder: "",
type: this.properties.folderType || "output"
}
this._promise = uploadImageToComfyUI(data)
.then((json: ComfyUploadImageAPIResponse) => {
console.debug("[UploadImageAction] Succeeded", json)
this.properties.lastUploadedImageFile = json.name;
this.triggerSlot(1, this.properties.lastUploadedImageFile);
this._promise = null;
})
.catch((e) => {
console.error("Error uploading:", e)
notify(`Error uploading image to ComfyUi: ${e}`, { type: "error", timeout: 10000 })
this.properties.lastUploadedImageFile = null;
this._promise = null;
})
}
}
LiteGraph.registerNodeType({
class: ComfyUploadImageAction,
title: "Comfy.UploadImageAction",
desc: "Uploads an image from the specified ComfyUI folder into its input folder",
type: "actions/store_images"
})

View File

@@ -1,6 +1,7 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core"; import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes"; import type { GalleryOutput } from "./ComfyWidgetNodes";
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties { export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties {
images: GalleryOutput | null, images: GalleryOutput | null,
@@ -12,10 +13,6 @@ export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties
type ImageCacheState = "none" | "uploading" | "failed" | "cached" type ImageCacheState = "none" | "uploading" | "failed" | "cached"
interface ComfyUploadImageAPIResponse {
name: string
}
/* /*
* A node that can act as both an input and output image node by uploading * A node that can act as both an input and output image node by uploading
* the output file into ComfyUI's input folder. * the output file into ComfyUI's input folder.
@@ -174,23 +171,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
this.properties.filenames[newIndex] = { filename: null, status: "uploading" } this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
this.onPropertyChanged("filenames", this.properties.filenames) this.onPropertyChanged("filenames", this.properties.filenames)
const url = `http://${location.hostname}:8188` // TODO make configurable const promise = uploadImageToComfyUI(data)
const params = new URLSearchParams(data)
const promise = fetch(url + "/view?" + params)
.then((r) => r.blob())
.then((blob) => {
console.debug("Fetchin", url, params)
const formData = new FormData();
formData.append("image", blob, data.filename);
return fetch(
new Request(url + "/upload/image", {
body: formData,
method: 'POST'
})
)
})
.then((r) => r.json())
.then((json: ComfyUploadImageAPIResponse) => { .then((json: ComfyUploadImageAPIResponse) => {
console.debug("Gottem", json) console.debug("Gottem", json)
if (lastGenNumber === this.properties.genNumber) { if (lastGenNumber === this.properties.genNumber) {

View File

@@ -4,6 +4,7 @@ import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"
export interface ComfyRerouteProperties extends ComfyGraphNodeProperties { export interface ComfyRerouteProperties extends ComfyGraphNodeProperties {
showOutputText: boolean; showOutputText: boolean;
horizontal: boolean; horizontal: boolean;
ignoreTypes: boolean;
} }
export default class ComfyReroute extends ComfyGraphNode { export default class ComfyReroute extends ComfyGraphNode {
@@ -24,7 +25,8 @@ export default class ComfyReroute extends ComfyGraphNode {
override properties: ComfyRerouteProperties = { override properties: ComfyRerouteProperties = {
tags: [], tags: [],
showOutputText: ComfyReroute.defaultVisibility, showOutputText: ComfyReroute.defaultVisibility,
horizontal: false horizontal: false,
ignoreTypes: false
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -61,6 +63,7 @@ export default class ComfyReroute extends ComfyGraphNode {
override onConnectionsChange(type: LConnectionKind, slotIndex: number, isConnected: boolean, link: LLink, ioSlot: (INodeInputSlot | INodeOutputSlot)) { override onConnectionsChange(type: LConnectionKind, slotIndex: number, isConnected: boolean, link: LLink, ioSlot: (INodeInputSlot | INodeOutputSlot)) {
this.applyOrientation(); this.applyOrientation();
this.canInheritSlotTypes = !this.properties.ignoreTypes;
super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot); super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
}; };

View File

@@ -608,7 +608,7 @@ export type GalleryOutputEntry = {
export interface ComfyGalleryProperties extends ComfyWidgetProperties { export interface ComfyGalleryProperties extends ComfyWidgetProperties {
index: number, index: number,
updateMode: "replace" | "append" updateMode: "replace" | "append",
} }
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> { export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
@@ -616,7 +616,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
tags: [], tags: [],
defaultValue: [], defaultValue: [],
index: 0, index: 0,
updateMode: "replace" updateMode: "replace",
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -629,7 +629,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
{ name: "selected_index", type: "number" }, { name: "selected_index", type: "number" },
{ name: "width", type: "number" }, { name: "width", type: "number" },
{ name: "height", type: "number" }, { name: "height", type: "number" },
{ name: "any_selected", type: "boolean" }, { name: "filename", type: "string" },
] ]
} }
@@ -644,12 +644,15 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
override outputIndex = null; override outputIndex = null;
override changedIndex = null; override changedIndex = null;
anyImageSelected: boolean = false; selectedFilename: string | null = null;
selectedIndexWidget: ITextWidget;
modeWidget: IComboWidget; modeWidget: IComboWidget;
constructor(name?: string) { constructor(name?: string) {
super(name, []) super(name, [])
this.selectedIndexWidget = this.addWidget("text", "Selected", String(this.properties.index), "index")
this.selectedIndexWidget.disabled = true;
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] }) this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
} }
@@ -662,10 +665,20 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
imageSize: Vector2 = [1, 1] imageSize: Vector2 = [1, 1]
override onExecute() { override onExecute() {
this.setOutputData(0, this.properties.index) const index = this.properties.index;
this.setOutputData(0, index)
this.setOutputData(1, this.imageSize[0]) this.setOutputData(1, this.imageSize[0])
this.setOutputData(2, this.imageSize[1]) this.setOutputData(2, this.imageSize[1])
this.setOutputData(3, this.anyImageSelected)
let filename: string | null = null;
if (index != null) {
const entry = get(this.value)[index];
if (entry)
filename = entry.name
}
this.setOutputData(3, filename)
} }
override onAction(action: any, param: any, options: { action_call?: string }) { override onAction(action: any, param: any, options: { action_call?: string }) {
@@ -700,11 +713,8 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
} }
override setValue(value: any, noChangedEvent: boolean = false) { override setValue(value: any, noChangedEvent: boolean = false) {
console.warn("SETVALUE", value)
super.setValue(value, noChangedEvent) super.setValue(value, noChangedEvent)
this.setProperty("index", null)
this.setProperty("index", 0)
this.anyImageSelected = false;
} }
} }
@@ -887,6 +897,9 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>>
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [
{ name: "store", type: BuiltInSlotType.ACTION }
],
outputs: [ outputs: [
{ name: "filename", type: "string" }, // TODO support batches { name: "filename", type: "string" }, // TODO support batches
{ name: "width", type: "number" }, { name: "width", type: "number" },
@@ -900,6 +913,7 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>>
override defaultValue = []; override defaultValue = [];
override outputIndex = null; override outputIndex = null;
override changedIndex = 3; override changedIndex = 3;
override storeActionName = "store";
override saveUserState = false; override saveUserState = false;
imageSize: Vector2 = [1, 1]; imageSize: Vector2 = [1, 1];
@@ -908,6 +922,18 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>>
super(name, []) super(name, [])
} }
override parseValue(value: any): GradioFileData[] {
if (value == null)
return []
if (typeof value === "string" && value !== "") { // Single filename
return [{ name: value, data: value, orig_name: value, is_file: true }]
}
else {
return []
}
}
override onExecute(param: any, options: object) { override onExecute(param: any, options: object) {
super.onExecute(param, options); super.onExecute(param, options);

View File

@@ -6,7 +6,8 @@ import { get } from "svelte/store"
import layoutState from "$lib/stores/layoutState" import layoutState from "$lib/stores/layoutState"
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import type { SerializedLGraph } from "@litegraph-ts/core"; import type { SerializedLGraph } from "@litegraph-ts/core";
import type { GalleryOutput } from "./nodes/ComfyWidgetNodes"; import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload";
export function clamp(n: number, min: number, max: number): number { export function clamp(n: number, min: number, max: number): number {
return Math.min(Math.max(n, min), max) return Math.min(Math.max(n, min), max)
@@ -125,10 +126,13 @@ export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileDat
// TODO configure backend URL // TODO configure backend URL
const url = `http://${location.hostname}:8188` // TODO make configurable const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(r) const params = new URLSearchParams(r)
return { const fileData: GradioFileData = {
name: null, name: r.filename,
orig_name: r.filename,
is_file: false,
data: url + "/view?" + params data: url + "/view?" + params
} }
return fileData
}); });
} }
@@ -145,3 +149,27 @@ export function jsonToJsObject(json: string): string {
.replace(regex, "$1:"); .replace(regex, "$1:");
}); });
} }
export interface ComfyUploadImageAPIResponse {
name: string
}
export async function uploadImageToComfyUI(data: GalleryOutputEntry): Promise<ComfyUploadImageAPIResponse> {
const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(data)
return fetch(url + "/view?" + params)
.then((r) => r.blob())
.then((blob) => {
console.debug("Fetchin", url, params)
const formData = new FormData();
formData.append("image", blob, data.filename);
return fetch(
new Request(url + "/upload/image", {
body: formData,
method: 'POST'
})
)
})
.then((r) => r.json())
}

View File

@@ -136,7 +136,7 @@
</script> </script>
<div class="wrapper comfy-combo" class:updated={lightUp}> <div class="wrapper comfy-combo" class:mobile={isMobile} class:updated={lightUp}>
{#key $valuesForCombo} {#key $valuesForCombo}
{#if node !== null && nodeValue !== null} {#if node !== null && nodeValue !== null}
{#if $valuesForCombo == null} {#if $valuesForCombo == null}
@@ -173,12 +173,14 @@
<VirtualList <VirtualList
items={filteredItems} items={filteredItems}
width="100%" width="100%"
height={300} height={isMobile ? 300 : 600}
itemCount={filteredItems?.length} itemCount={filteredItems?.length}
itemSize={50} itemSize={isMobile ? 50 : 24}
overscanCount={5}
scrollToIndex={hoverItemIndex}> scrollToIndex={hoverItemIndex}>
<div slot="item" <div slot="item"
class="comfy-select-item" class="comfy-select-item"
class:mobile={isMobile}
let:index={i} let:index={i}
let:style let:style
{style} {style}
@@ -260,7 +262,11 @@
.comfy-select-list { .comfy-select-list {
height: 300px; height: 300px;
width: 30rem; width: 30rem;
> :global(.virtual-list-wrapper) {
box-shadow: var(--block-shadow);
background-color: white; background-color: white;
}
.comfy-empty-list { .comfy-empty-list {
height: 100%; height: 100%;
@@ -268,12 +274,10 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: xx-large; font-size: xx-large;
color: var(--neutral-700) color: var(--neutral-400)
} }
.comfy-select-item { .comfy-select-item {
font-size: 16px;
padding: 1.2rem;
border: 1px solid var(--neutral-300); border: 1px solid var(--neutral-300);
border-top: none; border-top: none;
white-space: nowrap; white-space: nowrap;
@@ -281,6 +285,15 @@
text-overflow: ellipsis; text-overflow: ellipsis;
display: flex; display: flex;
align-items: center; align-items: center;
background-color: white;
font-size: 14px;
padding: 0.2rem;
&.mobile {
font-size: 16px;
padding: 1.2rem;
}
&.hover { &.hover {
color: white; color: white;

View File

@@ -30,7 +30,6 @@
node = widget.node as ComfyGalleryNode node = widget.node as ComfyGalleryNode
nodeValue = node.value; nodeValue = node.value;
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
node.anyImageSelected = false;
if ($nodeValue != null) { if ($nodeValue != null) {
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) { if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
@@ -135,22 +134,19 @@
// Update index // Update index
node.setProperty("index", e.detail.index as number) node.setProperty("index", e.detail.index as number)
node.anyImageSelected = true;
} }
$: if ($propsChanged > -1 && widget && $nodeValue) { $: if ($propsChanged > -1 && widget && $nodeValue) {
if (widget.attrs.variant === "image") { if (widget.attrs.variant === "image") {
selected_image = $nodeValue.length - 1 selected_image = $nodeValue.length - 1
node.setProperty("index", selected_image) node.setProperty("index", selected_image)
node.anyImageSelected = true;
} }
} }
else { else {
node.setProperty("index", null) node.setProperty("index", null)
node.anyImageSelected = false;
} }
$: node.anyImageSelected = selected_image != null; $: node.setProperty("index", selected_image)
</script> </script>
{#if widget && node && nodeValue && $nodeValue} {#if widget && node && nodeValue && $nodeValue}

View File

@@ -2,14 +2,13 @@
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Page, Navbar, Button, BlockTitle, Block, List, ListItem } from "framework7-svelte" import { Page, Navbar, Button, BlockTitle, Block, List, ListItem } from "framework7-svelte"
import defaultGraph from "$lib/defaultGraph";
export let app: ComfyApp | null = null; export let app: ComfyApp | null = null;
async function doLoadDefault() { async function doLoadDefault() {
var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?"); var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?");
if (confirmed) { if (confirmed) {
await app.deserialize(defaultGraph) await app.initDefaultGraph();
} }
} }
</script> </script>