Global modal system
This commit is contained in:
@@ -7,11 +7,9 @@
|
|||||||
import LightboxModal from "./LightboxModal.svelte";
|
import LightboxModal from "./LightboxModal.svelte";
|
||||||
import Sidebar from "./Sidebar.svelte";
|
import Sidebar from "./Sidebar.svelte";
|
||||||
import SidebarItem from "./SidebarItem.svelte";
|
import SidebarItem from "./SidebarItem.svelte";
|
||||||
// import Modal from "./Modal.svelte";
|
|
||||||
// import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
|
|
||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte";
|
import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte";
|
||||||
|
import GlobalModal from "./GlobalModal.svelte";
|
||||||
|
|
||||||
export let app: ComfyApp = undefined;
|
export let app: ComfyApp = undefined;
|
||||||
let hasShownUIHelpToast: boolean = false;
|
let hasShownUIHelpToast: boolean = false;
|
||||||
@@ -36,11 +34,6 @@
|
|||||||
document.getElementById("app-root").classList.remove("dark")
|
document.getElementById("app-root").classList.remove("dark")
|
||||||
}
|
}
|
||||||
|
|
||||||
// let showModal: boolean = false;
|
|
||||||
//
|
|
||||||
// $: showModal = $a1111Prompt != null
|
|
||||||
//
|
|
||||||
// let selectedTab
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -49,20 +42,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<!--
|
|
||||||
<Modal bind:showModal on:close={() => ($a1111Prompt = null)}>
|
|
||||||
<div slot="header" class="prompt-modal-header">
|
|
||||||
<h1 style="padding-bottom: 1rem;">A1111 Prompt Details</h1>
|
|
||||||
</div>
|
|
||||||
<A1111PromptDisplay prompt={$a1111Prompt} />
|
|
||||||
<div slot="buttons" let:closeDialog>
|
|
||||||
<Button variant="secondary" on:click={closeDialog}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div id="main" class:dark={uiTheme === "gradio-dark"}>
|
<div id="main" class:dark={uiTheme === "gradio-dark"}>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<Sidebar selected="generate">
|
<Sidebar selected="generate">
|
||||||
@@ -74,6 +53,7 @@
|
|||||||
</Sidebar>
|
</Sidebar>
|
||||||
</div>
|
</div>
|
||||||
<LightboxModal />
|
<LightboxModal />
|
||||||
|
<GlobalModal/>
|
||||||
</div>
|
</div>
|
||||||
<SvelteToast options={toastOptions} />
|
<SvelteToast options={toastOptions} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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 { 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 type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||||
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
||||||
import { getPngMetadata, importA1111, parsePNGMetadata } from "$lib/pnginfo";
|
import { importA1111, parsePNGMetadata } from "$lib/pnginfo";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
|
import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
|
||||||
|
|
||||||
|
|
||||||
// Import nodes
|
// Import nodes
|
||||||
import "@litegraph-ts/nodes-basic"
|
import "@litegraph-ts/nodes-basic"
|
||||||
@@ -28,7 +30,7 @@ import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
|||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
|
import { basename, download, graphToGraphVis, 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";
|
import { blankGraph } from "$lib/defaultGraph";
|
||||||
@@ -45,7 +47,7 @@ import layoutStates from "$lib/stores/layoutStates";
|
|||||||
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||||
import workflowState from "$lib/stores/workflowState";
|
import workflowState from "$lib/stores/workflowState";
|
||||||
import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from "$lib/convertVanillaWorkflow";
|
import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from "$lib/convertVanillaWorkflow";
|
||||||
import convertVanillaWorkflow from "$lib/convertVanillaWorkflow";
|
import modalState from "$lib/stores/modalState";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
|
|
||||||
@@ -142,6 +144,11 @@ type CanvasState = {
|
|||||||
canvas: ComfyGraphCanvas,
|
canvas: ComfyGraphCanvas,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkflowLoadError = {
|
||||||
|
message: string,
|
||||||
|
error: Error
|
||||||
|
}
|
||||||
|
|
||||||
function isComfyBoxWorkflow(data: any): data is SerializedAppState {
|
function isComfyBoxWorkflow(data: any): data is SerializedAppState {
|
||||||
return data != null && (typeof data === "object") && data.comfyBoxWorkflow;
|
return data != null && (typeof data === "object") && data.comfyBoxWorkflow;
|
||||||
}
|
}
|
||||||
@@ -167,7 +174,6 @@ export default class ComfyApp {
|
|||||||
ctrlDown: boolean = false;
|
ctrlDown: boolean = false;
|
||||||
selectedGroupMoving: boolean = false;
|
selectedGroupMoving: boolean = false;
|
||||||
alreadySetup: Writable<boolean> = writable(false);
|
alreadySetup: Writable<boolean> = writable(false);
|
||||||
a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
|
|
||||||
|
|
||||||
private queueItems: PromptQueueItem[] = [];
|
private queueItems: PromptQueueItem[] = [];
|
||||||
private processingQueue: boolean = false;
|
private processingQueue: boolean = false;
|
||||||
@@ -266,10 +272,10 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
convertVanillaWorkflow(workflow: ComfyVanillaWorkflow): SerializedAppState {
|
convertVanillaWorkflow(workflow: ComfyVanillaWorkflow, title: string): SerializedAppState {
|
||||||
const attrs: WorkflowAttributes = {
|
const attrs: WorkflowAttributes = {
|
||||||
...defaultWorkflowAttributes,
|
...defaultWorkflowAttributes,
|
||||||
title: "ComfyUI Workflow"
|
title
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas: SerializedGraphCanvasState = {
|
const canvas: SerializedGraphCanvasState = {
|
||||||
@@ -579,8 +585,8 @@ export default class ComfyApp {
|
|||||||
return workflow;
|
return workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
async openVanillaWorkflow(data: SerializedLGraph) {
|
async openVanillaWorkflow(data: SerializedLGraph, filename: string) {
|
||||||
const converted = this.convertVanillaWorkflow(data)
|
const converted = this.convertVanillaWorkflow(data, basename(filename))
|
||||||
console.info("WORKFLWO", converted)
|
console.info("WORKFLWO", converted)
|
||||||
notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" })
|
notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" })
|
||||||
await this.openWorkflow(converted);
|
await this.openWorkflow(converted);
|
||||||
@@ -818,7 +824,7 @@ export default class ComfyApp {
|
|||||||
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxWorkflow));
|
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxWorkflow));
|
||||||
} else if (pngInfo.workflow) {
|
} else if (pngInfo.workflow) {
|
||||||
const workflow = JSON.parse(pngInfo.workflow);
|
const workflow = JSON.parse(pngInfo.workflow);
|
||||||
await this.openVanillaWorkflow(workflow);
|
await this.openVanillaWorkflow(workflow, file.name);
|
||||||
} else if (pngInfo.parameters) {
|
} else if (pngInfo.parameters) {
|
||||||
const parsed = parseA1111(pngInfo.parameters)
|
const parsed = parseA1111(pngInfo.parameters)
|
||||||
if ("error" in parsed) {
|
if ("error" in parsed) {
|
||||||
@@ -826,11 +832,19 @@ export default class ComfyApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const converted = convertA1111ToStdPrompt(parsed)
|
const converted = convertA1111ToStdPrompt(parsed)
|
||||||
this.a1111Prompt.set({
|
const a1111Info: A1111PromptAndInfo = {
|
||||||
infotext: pngInfo.parameters,
|
infotext: pngInfo.parameters,
|
||||||
parsedInfotext: parsed,
|
parsedInfotext: parsed,
|
||||||
stdPrompt: converted,
|
stdPrompt: converted,
|
||||||
imageFile: file
|
imageFile: file
|
||||||
|
}
|
||||||
|
modalState.pushModal({
|
||||||
|
title: "A1111 Prompt Details",
|
||||||
|
svelteComponent: A1111PromptDisplay,
|
||||||
|
svelteProps: {
|
||||||
|
prompt: a1111Info
|
||||||
|
},
|
||||||
|
showCloseButton: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -846,7 +860,7 @@ export default class ComfyApp {
|
|||||||
await this.openWorkflow(result);
|
await this.openWorkflow(result);
|
||||||
}
|
}
|
||||||
else if (isVanillaWorkflow(result)) {
|
else if (isVanillaWorkflow(result)) {
|
||||||
await this.openVanillaWorkflow(result);
|
await this.openVanillaWorkflow(result, file.name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@@ -990,6 +1004,5 @@ export default class ComfyApp {
|
|||||||
* Clean current state
|
* Clean current state
|
||||||
*/
|
*/
|
||||||
clean() {
|
clean() {
|
||||||
this.a1111Prompt.set(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from "@gradio/button";
|
import { Button } from "@gradio/button";
|
||||||
import type ComfyApp from "./ComfyApp";
|
import type ComfyApp from "./ComfyApp";
|
||||||
|
import DropZone from "./DropZone.svelte";
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
export let transitioning: boolean = false;
|
export let transitioning: boolean = false;
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
<div class="wrapper litegraph">
|
<div class="wrapper litegraph">
|
||||||
<div class="canvas-wrapper pane-wrapper">
|
<div class="canvas-wrapper pane-wrapper">
|
||||||
<canvas id="graph-canvas" />
|
<canvas id="graph-canvas" />
|
||||||
|
<DropZone {app} />
|
||||||
</div>
|
</div>
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
{#if !transitioning}
|
{#if !transitioning}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import Spinner from "./Spinner.svelte";
|
import Spinner from "./Spinner.svelte";
|
||||||
import PromptDisplay from "./PromptDisplay.svelte";
|
import PromptDisplay from "./PromptDisplay.svelte";
|
||||||
import { ListIcon as List } from "svelte-feather-icons";
|
import { ListIcon as List } from "svelte-feather-icons";
|
||||||
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo } from "$lib/utils"
|
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils"
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { QueueItemType } from "$lib/api";
|
import type { QueueItemType } from "$lib/api";
|
||||||
import { ImageViewer } from "$lib/ImageViewer";
|
import { ImageViewer } from "$lib/ImageViewer";
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
if (entry.workflowID != null) {
|
if (entry.workflowID != null) {
|
||||||
const workflow = workflowState.getWorkflow(entry.workflowID);
|
const workflow = workflowState.getWorkflow(entry.workflowID);
|
||||||
if (workflow != null && workflow.attrs.title) {
|
if (workflow != null && workflow.attrs.title) {
|
||||||
message = `Workflow: ${workflow.attrs.title}`
|
message = `${workflow.attrs.title}`
|
||||||
}
|
}
|
||||||
if (subgraphs?.length > 0)
|
if (subgraphs?.length > 0)
|
||||||
message += ` (${subgraphs.join(', ')})`
|
message += ` (${subgraphs.join(', ')})`
|
||||||
@@ -208,7 +208,6 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div class="queue">
|
<div class="queue">
|
||||||
<!-- <DropZone {app} /> -->
|
|
||||||
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
||||||
{#if _entries.length > 0}
|
{#if _entries.length > 0}
|
||||||
{#each _entries as entry}
|
{#each _entries as entry}
|
||||||
@@ -230,7 +229,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="queue-entry-details">
|
<div class="queue-entry-details">
|
||||||
<div class="queue-entry-message">
|
<div class="queue-entry-message">
|
||||||
{entry.message}
|
{truncateString(entry.message, 20)}
|
||||||
</div>
|
</div>
|
||||||
<div class="queue-entry-submessage">
|
<div class="queue-entry-submessage">
|
||||||
{entry.submessage}
|
{entry.submessage}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { cubicIn } from 'svelte/easing';
|
import { cubicIn } from 'svelte/easing';
|
||||||
|
import { truncateString } from '$lib/utils';
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
export let uiTheme: string = "gradio-dark" // TODO config
|
export let uiTheme: string = "gradio-dark" // TODO config
|
||||||
@@ -235,7 +236,7 @@
|
|||||||
class:selected={item.id === $workflowState.activeWorkflowID}
|
class:selected={item.id === $workflowState.activeWorkflowID}
|
||||||
on:click={() => app.setActiveWorkflow(item.id)}>
|
on:click={() => app.setActiveWorkflow(item.id)}>
|
||||||
<span class="workflow-tab-title">
|
<span class="workflow-tab-title">
|
||||||
{workflow.attrs.title}
|
{truncateString(workflow.attrs.title, 32)}
|
||||||
{#if workflow.isModified}
|
{#if workflow.isModified}
|
||||||
*
|
*
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { writable, type Writable } from "svelte/store";
|
import modalState from "$lib/stores/modalState";
|
||||||
import type ComfyApp from "./ComfyApp";
|
import type ComfyApp from "./ComfyApp";
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
let a1111Prompt: Writable<any | null> = writable(null);
|
|
||||||
let dropZone: HTMLDivElement | null = null;
|
let dropZone: HTMLDivElement | null = null;
|
||||||
let disabled = false;
|
let disabled = false;
|
||||||
|
|
||||||
$: a1111Prompt = app.a1111Prompt;
|
$: disabled = $modalState.activeModals.length > 0;
|
||||||
|
|
||||||
$: disabled = a1111Prompt && $a1111Prompt;
|
|
||||||
|
|
||||||
$: if (disabled) {
|
$: if (disabled) {
|
||||||
hideDropZone();
|
hideDropZone();
|
||||||
|
|||||||
44
src/lib/components/GlobalModal.svelte
Normal file
44
src/lib/components/GlobalModal.svelte
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import modalState, { type ModalData } from "$lib/stores/modalState";
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import Modal from "./Modal.svelte";
|
||||||
|
|
||||||
|
function onClose(modal: ModalData | null) {
|
||||||
|
if (modal == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (modal.onClose)
|
||||||
|
modal.onClose()
|
||||||
|
|
||||||
|
modalState.closeModal(modal.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each $modalState.activeModals as modal(modal.id)}
|
||||||
|
<Modal showModal={true} on:close={() => onClose(modal)}>
|
||||||
|
<div slot="header" class="modal-header">
|
||||||
|
{#if modal != null}
|
||||||
|
<h1 style="padding-bottom: 1rem;">{modal.title}</h1>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<svelte:fragment>
|
||||||
|
{#if modal != null && modal.svelteComponent != null}
|
||||||
|
<svelte:component this={modal.svelteComponent} {...modal.svelteProps}/>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
<div slot="buttons" let:closeDialog>
|
||||||
|
{#if modal != null && modal.buttons?.length > 0}
|
||||||
|
{#each modal.buttons as button}
|
||||||
|
<Button variant={button.variant} on:click={button.onClick}>
|
||||||
|
{button.name}
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if modal.showCloseButton}
|
||||||
|
<Button variant="secondary" on:click={closeDialog}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/each}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
|
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
|
||||||
import { ImageViewer } from "$lib/ImageViewer";
|
import { ImageViewer } from "$lib/ImageViewer";
|
||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
|
import { countNewLines } from "$lib/utils";
|
||||||
|
|
||||||
const splitLength = 50;
|
const splitLength = 50;
|
||||||
|
|
||||||
@@ -29,10 +30,6 @@
|
|||||||
&& typeof input[1] === "number"
|
&& typeof input[1] === "number"
|
||||||
}
|
}
|
||||||
|
|
||||||
function countNewLines(str: string): number {
|
|
||||||
return str.split(/\r\n|\r|\n/).length
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMultiline(input: any): boolean {
|
function isMultiline(input: any): boolean {
|
||||||
return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1);
|
return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot } from "@litegraph-ts/core";
|
import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot } from "@litegraph-ts/core";
|
||||||
import type { SerializedAppState } from "./components/ComfyApp";
|
import type { SerializedAppState } from "./components/ComfyApp";
|
||||||
import layoutStates, { defaultWorkflowAttributes, type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "./stores/layoutStates";
|
import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "./stores/layoutStates";
|
||||||
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
|
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
|
||||||
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
|
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
|
||||||
import ComfyApp from "./components/ComfyApp";
|
import ComfyApp from "./components/ComfyApp";
|
||||||
@@ -12,6 +12,7 @@ import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode"
|
|||||||
import { v4 as uuidv4 } from "uuid"
|
import { v4 as uuidv4 } from "uuid"
|
||||||
import type ComfyWidgetNode from "./nodes/widgets/ComfyWidgetNode";
|
import type ComfyWidgetNode from "./nodes/widgets/ComfyWidgetNode";
|
||||||
import { ComfyGalleryNode } from "./nodes/widgets";
|
import { ComfyGalleryNode } from "./nodes/widgets";
|
||||||
|
import { countNewLines } from "./utils";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The workflow type used by base ComfyUI
|
* The workflow type used by base ComfyUI
|
||||||
@@ -40,7 +41,6 @@ const vanillaToComfyBoxNodeMapping: Record<string, string> = {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Version of LGraphNode.getConnectionPos but for serialized nodes.
|
* Version of LGraphNode.getConnectionPos but for serialized nodes.
|
||||||
*
|
|
||||||
* TODO handle other node types! (horizontal, hardcoded slot pos, collapsed...)
|
* TODO handle other node types! (horizontal, hardcoded slot pos, collapsed...)
|
||||||
*/
|
*/
|
||||||
function getConnectionPos(node: SerializedLGraphNode, is_input: boolean, slotNumber: number, out: Vector2 = [0, 0]): Vector2 {
|
function getConnectionPos(node: SerializedLGraphNode, is_input: boolean, slotNumber: number, out: Vector2 = [0, 0]): Vector2 {
|
||||||
@@ -63,7 +63,7 @@ function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node:
|
|||||||
comfyWidgetNode.flags.collapsed = true;
|
comfyWidgetNode.flags.collapsed = true;
|
||||||
const size: Vector2 = [0, 0];
|
const size: Vector2 = [0, 0];
|
||||||
|
|
||||||
// Compute collapsed size, sinze computeSize() ignores the collapsed flag
|
// Compute collapsed size, since computeSize() ignores the collapsed flag
|
||||||
// LiteGraph only computes it if the node is rendered
|
// LiteGraph only computes it if the node is rendered
|
||||||
const fontSize = LiteGraph.NODE_TEXT_SIZE;
|
const fontSize = LiteGraph.NODE_TEXT_SIZE;
|
||||||
size[0] = Math.min(
|
size[0] = Math.min(
|
||||||
@@ -81,6 +81,14 @@ function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node:
|
|||||||
else
|
else
|
||||||
serWidgetNode.pos[0] += 20;
|
serWidgetNode.pos[0] += 20;
|
||||||
serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2;
|
serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2;
|
||||||
|
|
||||||
|
if (widgetNodeType === "ui/text" && typeof value === "string" && value.indexOf("\n") != -1) {
|
||||||
|
const lineCount = countNewLines(value);
|
||||||
|
serWidgetNode.properties.multiline = true;
|
||||||
|
serWidgetNode.properties.lines = lineCount + 2
|
||||||
|
serWidgetNode.properties.maxLines = lineCount + 2
|
||||||
|
}
|
||||||
|
|
||||||
vanillaWorkflow.nodes.push(serWidgetNode)
|
vanillaWorkflow.nodes.push(serWidgetNode)
|
||||||
|
|
||||||
return [comfyWidgetNode, serWidgetNode];
|
return [comfyWidgetNode, serWidgetNode];
|
||||||
@@ -97,6 +105,59 @@ function connectSerializedNodes(vanillaWorkflow: ComfyVanillaWorkflow, originNod
|
|||||||
vanillaWorkflow.links.push([newLinkID, originNode.id, originSlot, targetNode.id, targetSlot, connInput.type])
|
vanillaWorkflow.links.push([newLinkID, originNode.id, originSlot, targetNode.id, targetSlot, connInput.type])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Converts all the IDs in the serialized graph into UUID format
|
||||||
|
*/
|
||||||
|
function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
|
||||||
|
const nodeIDs: Record<NodeID, UUID> = {};
|
||||||
|
const linkIDs: Record<LinkID, UUID> = {};
|
||||||
|
|
||||||
|
const getNodeID = (id: NodeID): UUID => {
|
||||||
|
if (typeof id === "string")
|
||||||
|
return id
|
||||||
|
nodeIDs[id] ||= uuidv4();
|
||||||
|
return nodeIDs[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLinkID = (id: LinkID): UUID => {
|
||||||
|
if (typeof id === "string")
|
||||||
|
return id
|
||||||
|
linkIDs[id] ||= uuidv4();
|
||||||
|
return linkIDs[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of vanillaWorkflow.nodes) {
|
||||||
|
node.id = getNodeID(node.id);
|
||||||
|
if (node.inputs != null) {
|
||||||
|
for (const input of node.inputs) {
|
||||||
|
if (input.link != null) {
|
||||||
|
input.link = getLinkID(input.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.outputs != null) {
|
||||||
|
for (const output of node.outputs) {
|
||||||
|
if (output.links != null)
|
||||||
|
output.links = output.links.map(getLinkID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const link of vanillaWorkflow.links) {
|
||||||
|
link[0] = getLinkID(link[0])
|
||||||
|
link[1] = getNodeID(link[1])
|
||||||
|
link[3] = getNodeID(link[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Recurse!
|
||||||
|
for (const node of vanillaWorkflow.nodes) {
|
||||||
|
if (node.type === "graph/subgraph") {
|
||||||
|
rewriteIDsInGraph((node as any).subgraph as SerializedLGraph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): ComfyWorkflow {
|
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): ComfyWorkflow {
|
||||||
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
|
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
|
||||||
const { root, left, right } = layoutState.initDefaultLayout();
|
const { root, left, right } = layoutState.initDefaultLayout();
|
||||||
@@ -104,6 +165,8 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
|
|||||||
// TODO will need to convert IDs to UUIDs
|
// TODO will need to convert IDs to UUIDs
|
||||||
const idToUUID: Record<NodeID | LinkID, UUID> = {}
|
const idToUUID: Record<NodeID | LinkID, UUID> = {}
|
||||||
|
|
||||||
|
rewriteIDsInGraph(vanillaWorkflow);
|
||||||
|
|
||||||
for (const [id, node] of Object.entries(vanillaWorkflow.nodes)) {
|
for (const [id, node] of Object.entries(vanillaWorkflow.nodes)) {
|
||||||
const newType = vanillaToComfyBoxNodeMapping[node.type];
|
const newType = vanillaToComfyBoxNodeMapping[node.type];
|
||||||
if (newType != null) {
|
if (newType != null) {
|
||||||
@@ -201,10 +264,22 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
|
|||||||
widgetNodeType,
|
widgetNodeType,
|
||||||
value);
|
value);
|
||||||
|
|
||||||
|
switch (widgetNodeType) {
|
||||||
|
case "ui/number":
|
||||||
|
serWidgetNode.properties.min = inputOpts?.min || 0;
|
||||||
|
serWidgetNode.properties.max = inputOpts?.max || 100;
|
||||||
|
serWidgetNode.properties.step = inputOpts?.step || 1;
|
||||||
|
break;
|
||||||
|
case "ui/text":
|
||||||
|
serWidgetNode.properties.multiline = inputOpts?.multiline || false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (group == null)
|
if (group == null)
|
||||||
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
|
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
|
||||||
|
|
||||||
layoutState.addWidget(group, comfyWidgetNode)
|
const widget = layoutState.addWidget(group, comfyWidgetNode)
|
||||||
|
widget.attrs.title = inputName;
|
||||||
|
|
||||||
const connOutputIndex = serWidgetNode.outputs?.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
|
const connOutputIndex = serWidgetNode.outputs?.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
|
||||||
if (connOutputIndex != null) {
|
if (connOutputIndex != null) {
|
||||||
@@ -244,7 +319,8 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
|
|||||||
if (group == null)
|
if (group == null)
|
||||||
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
|
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
|
||||||
|
|
||||||
layoutState.addWidget(group, comfyGalleryNode)
|
const widget = layoutState.addWidget(group, comfyGalleryNode)
|
||||||
|
widget.attrs.title = "Output"
|
||||||
|
|
||||||
const connInputIndex = serGalleryNode.inputs?.findIndex(o => o.name === comfyGalleryNode.storeActionName)
|
const connInputIndex = serGalleryNode.inputs?.findIndex(o => o.name === comfyGalleryNode.storeActionName)
|
||||||
if (connInputIndex != null) {
|
if (connInputIndex != null) {
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
|
|||||||
|
|
||||||
export interface ComfyTextProperties extends ComfyWidgetProperties {
|
export interface ComfyTextProperties extends ComfyWidgetProperties {
|
||||||
multiline: boolean;
|
multiline: boolean;
|
||||||
|
lines: number;
|
||||||
|
maxLines: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ComfyTextNode extends ComfyWidgetNode<string> {
|
export default class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||||
override properties: ComfyTextProperties = {
|
override properties: ComfyTextProperties = {
|
||||||
tags: [],
|
tags: [],
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
multiline: false
|
multiline: false,
|
||||||
|
lines: 5,
|
||||||
|
maxLines: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
|
|||||||
@@ -428,6 +428,38 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
defaultValue: "gallery",
|
defaultValue: "gallery",
|
||||||
refreshPanelOnChange: true
|
refreshPanelOnChange: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Text
|
||||||
|
{
|
||||||
|
name: "multiline",
|
||||||
|
type: "boolean",
|
||||||
|
location: "nodeProps",
|
||||||
|
editable: true,
|
||||||
|
validNodeTypes: ["ui/text"],
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lines",
|
||||||
|
type: "number",
|
||||||
|
location: "nodeProps",
|
||||||
|
editable: true,
|
||||||
|
validNodeTypes: ["ui/text"],
|
||||||
|
defaultValue: 5,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
step: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "maxLines",
|
||||||
|
type: "number",
|
||||||
|
location: "nodeProps",
|
||||||
|
editable: true,
|
||||||
|
validNodeTypes: ["ui/text"],
|
||||||
|
defaultValue: 5,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
step: 1
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
66
src/lib/stores/modalState.ts
Normal file
66
src/lib/stores/modalState.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
export type ModalButton = {
|
||||||
|
name: string,
|
||||||
|
variant: "primary" | "secondary",
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
export interface ModalData {
|
||||||
|
id: string,
|
||||||
|
title: string,
|
||||||
|
onClose?: () => void,
|
||||||
|
svelteComponent?: typeof SvelteComponentDev,
|
||||||
|
svelteProps?: Record<string, any>,
|
||||||
|
buttons?: ModalButton[],
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}
|
||||||
|
export interface ModalState {
|
||||||
|
activeModals: ModalData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalStateOps {
|
||||||
|
pushModal: (data: Partial<ModalData>) => void,
|
||||||
|
closeModal: (id: string) => void,
|
||||||
|
closeAllModals: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WritableModalStateStore = Writable<ModalState> & ModalStateOps;
|
||||||
|
const store: Writable<ModalState> = writable(
|
||||||
|
{
|
||||||
|
activeModals: []
|
||||||
|
})
|
||||||
|
|
||||||
|
function pushModal(data: Partial<ModalData>) {
|
||||||
|
const modal: ModalData = {
|
||||||
|
title: "Modal",
|
||||||
|
...data,
|
||||||
|
id: uuidv4(),
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(s => {
|
||||||
|
s.activeModals.push(modal);
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(id: string) {
|
||||||
|
store.update(s => {
|
||||||
|
s.activeModals = s.activeModals.filter(m => m.id !== id)
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllModals() {
|
||||||
|
store.set({ activeModals: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalStateStore: WritableModalStateStore =
|
||||||
|
{
|
||||||
|
...store,
|
||||||
|
pushModal,
|
||||||
|
closeModal,
|
||||||
|
closeAllModals
|
||||||
|
}
|
||||||
|
export default modalStateStore;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas';
|
import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas';
|
||||||
import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph } from '@litegraph-ts/core';
|
import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph, LiteGraph } from '@litegraph-ts/core';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
import { defaultWorkflowAttributes, type SerializedLayoutState, type WritableLayoutStateStore } from './layoutStates';
|
import { defaultWorkflowAttributes, type SerializedLayoutState, type WritableLayoutStateStore } from './layoutStates';
|
||||||
@@ -76,6 +76,11 @@ export class ComfyWorkflow {
|
|||||||
*/
|
*/
|
||||||
isModified: boolean = false;
|
isModified: boolean = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Missing node types encountered when deserializing the graph
|
||||||
|
*/
|
||||||
|
missingNodeTypes: string[];
|
||||||
|
|
||||||
get layout(): WritableLayoutStateStore | null {
|
get layout(): WritableLayoutStateStore | null {
|
||||||
return layoutStates.getLayout(this.id)
|
return layoutStates.getLayout(this.id)
|
||||||
}
|
}
|
||||||
@@ -170,6 +175,18 @@ export class ComfyWorkflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) {
|
deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) {
|
||||||
|
this.missingNodeTypes = []
|
||||||
|
|
||||||
|
for (let n of data.graph.nodes) {
|
||||||
|
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||||
|
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||||
|
|
||||||
|
// Find missing node types
|
||||||
|
if (!(n.type in LiteGraph.registered_node_types)) {
|
||||||
|
this.missingNodeTypes.push(n.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure loadGraphData does not trigger any state changes in layoutState
|
// Ensure loadGraphData does not trigger any state changes in layoutState
|
||||||
// (isConfiguring is set to true here)
|
// (isConfiguring is set to true here)
|
||||||
// lGraph.configure will add new nodes, triggering onNodeAdded, but we
|
// lGraph.configure will add new nodes, triggering onNodeAdded, but we
|
||||||
|
|||||||
@@ -19,6 +19,25 @@ export function range(size: number, startAt: number = 0): ReadonlyArray<number>
|
|||||||
return [...Array(size).keys()].map(i => i + startAt);
|
return [...Array(size).keys()].map(i => i + startAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function countNewLines(str: string): number {
|
||||||
|
return str.split(/\r\n|\r|\n/).length
|
||||||
|
}
|
||||||
|
|
||||||
|
export function basename(filepath: string): string {
|
||||||
|
const filename = filepath.split('/').pop().split('\\').pop();
|
||||||
|
return filename.split('.').slice(0, -1).join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncateString(str: string, num: number): string {
|
||||||
|
if (num <= 0)
|
||||||
|
return "…";
|
||||||
|
|
||||||
|
if (str.length <= num) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.slice(0, num) + "…";
|
||||||
|
}
|
||||||
|
|
||||||
export function* enumerate<T>(iterable: Iterable<T>): Iterable<[number, T]> {
|
export function* enumerate<T>(iterable: Iterable<T>): Iterable<[number, T]> {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
for (const value of iterable) {
|
for (const value of iterable) {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
bind:value={$nodeValue}
|
bind:value={$nodeValue}
|
||||||
label={widget.attrs.title}
|
label={widget.attrs.title}
|
||||||
disabled={isDisabled(widget)}
|
disabled={isDisabled(widget)}
|
||||||
lines={node.properties.multiline ? 5 : 1}
|
lines={node.properties.multiline ? node.properties.lines : 1}
|
||||||
max_lines={node.properties.multiline ? 5 : 1}
|
max_lines={node.properties.multiline ? node.properties.maxLines : 1}
|
||||||
show_label={widget.attrs.title !== ""}
|
show_label={widget.attrs.title !== ""}
|
||||||
on:change
|
on:change
|
||||||
on:submit
|
on:submit
|
||||||
|
|||||||
Reference in New Issue
Block a user