Merge pull request #20 from space-nuko/properties-pane2
Properties pane
This commit is contained in:
21
README.md
21
README.md
@@ -6,6 +6,14 @@ This project is *still under construction* and many features are missing, be awa
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the latest release [here](https://nightly.link/space-nuko/ComfyBox/workflows/build-and-publish/master/ComfyBox-dist.zip) and extract it somewhere
|
||||
2. Start the ComfyUI backend with `python main.py --enable-cors-header`
|
||||
3. In the folder you extracted open the `run.bat`/`run.sh` script (requires Python 3 to be on your PATH). Alternatively you can serve the contents of the folder with a web server.
|
||||
|
||||
## NOTE
|
||||
|
||||
This frontend isn't compatible with regular ComfyUI's workflow format since extra metadata is saved like panel layout, so you'll have to spend a bit of time recreating them. This project also isn't compatible with regular ComfyUI's frontend extension format, but useful extensions can be integrated into this repo with some effort.
|
||||
@@ -13,16 +21,19 @@ This frontend isn't compatible with regular ComfyUI's workflow format since extr
|
||||
## Proposed Features
|
||||
- All the power of ComfyUI with more convenience on top
|
||||
- Autocreation of UI widgets from your workflow, quickly creating a personalized dashboard
|
||||
- Custom widget and node types
|
||||
- Look up queued and finished generations and their configs in realtime
|
||||
- Arrange the UI however you like and attach custom classes/styles to each widget
|
||||
- Custom widget types
|
||||
- See the status of queued and finished generations and their configs in realtime
|
||||
- Development with TypeScript
|
||||
|
||||
## Requirements
|
||||
## Development
|
||||
|
||||
### Requirements
|
||||
|
||||
- `pnpm`
|
||||
- An installation of vanilla [ComfyUI](https://github.com/comfyanonymous/ComfyUI) for the backend
|
||||
|
||||
## Installation
|
||||
### Installation
|
||||
|
||||
1. Clone the repo with submodules:
|
||||
|
||||
@@ -33,5 +44,5 @@ git clone https://github.com/space-nuko/ComfyBox --recursive
|
||||
2. `pnpm install`
|
||||
4. `pnpm build:css`
|
||||
5. `pnpm dev`
|
||||
6. Start ComfyUI as usual with `python main.py --enable-cors-header`
|
||||
6. Start ComfyUI with `python main.py --enable-cors-header`
|
||||
7. Visit `http://localhost:3000` in your browser
|
||||
|
||||
11
bin/run.bat
Normal file
11
bin/run.bat
Normal file
@@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
|
||||
echo Starting ComfyBox.
|
||||
echo Be sure you've started ComfyUI already using this command:
|
||||
echo[
|
||||
echo python main.py --enable-cors-header
|
||||
echo[
|
||||
echo Serving at http://localhost:8000
|
||||
echo[
|
||||
|
||||
python -m http.server 8000
|
||||
11
bin/run.sh
Normal file
11
bin/run.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
echo "Starting ComfyBox."
|
||||
echo "Be sure you've started ComfyUI already using this command:"
|
||||
echo ""
|
||||
echo " python main.py --enable-cors-header"
|
||||
echo ""
|
||||
echo "Serving at http://localhost:8000"
|
||||
echo ""
|
||||
|
||||
python -m http.server 8000
|
||||
0
dist/.keep
vendored
0
dist/.keep
vendored
Submodule litegraph updated: 115bef46a5...6cbae97b3c
@@ -27,6 +27,7 @@
|
||||
"svelte-dnd-action": "^0.9.22",
|
||||
"typescript": "^5.0.3",
|
||||
"vite": "^4.3.1",
|
||||
"vite-plugin-static-copy": "^0.14.0",
|
||||
"vite-tsconfig-paths": "^4.0.8",
|
||||
"vitest": "^0.25.8"
|
||||
},
|
||||
@@ -45,6 +46,7 @@
|
||||
"@litegraph-ts/nodes-basic": "workspace:*",
|
||||
"@litegraph-ts/nodes-events": "workspace:*",
|
||||
"@litegraph-ts/nodes-math": "workspace:*",
|
||||
"@litegraph-ts/nodes-strings": "workspace:*",
|
||||
"@litegraph-ts/tsconfig": "workspace:*",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.1",
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@@ -43,6 +43,9 @@ importers:
|
||||
'@litegraph-ts/nodes-math':
|
||||
specifier: workspace:*
|
||||
version: link:litegraph/packages/nodes-math
|
||||
'@litegraph-ts/nodes-strings':
|
||||
specifier: workspace:*
|
||||
version: link:litegraph/packages/nodes-strings
|
||||
'@litegraph-ts/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:litegraph/packages/tsconfig
|
||||
@@ -122,6 +125,9 @@ importers:
|
||||
vite:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1(sass@1.61.0)
|
||||
vite-plugin-static-copy:
|
||||
specifier: ^0.14.0
|
||||
version: 0.14.0(vite@4.3.1)
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^4.0.8
|
||||
version: 4.0.8(typescript@5.0.3)(vite@4.3.1)
|
||||
@@ -805,6 +811,22 @@ importers:
|
||||
specifier: ^4.2.1
|
||||
version: 4.3.1
|
||||
|
||||
litegraph/packages/nodes-strings:
|
||||
dependencies:
|
||||
'@litegraph-ts/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
devDependencies:
|
||||
'@litegraph-ts/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../tsconfig
|
||||
typescript:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
vite:
|
||||
specifier: ^4.2.1
|
||||
version: 4.3.1
|
||||
|
||||
litegraph/packages/tsconfig: {}
|
||||
|
||||
packages:
|
||||
@@ -7065,6 +7087,19 @@ packages:
|
||||
vite: 4.3.1(sass@1.61.0)
|
||||
dev: false
|
||||
|
||||
/vite-plugin-static-copy@0.14.0(vite@4.3.1):
|
||||
resolution: {integrity: sha512-RMFmb4czomcrsbQBiUZs9HcDGN3kxGvF+OrtkfTVocp12CuoUCuJQhcY26RK35A6KS4WasGzEwcYZqHMjkAvVw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
vite: ^3.0.0 || ^4.0.0
|
||||
dependencies:
|
||||
chokidar: 3.5.3
|
||||
fast-glob: 3.2.12
|
||||
fs-extra: 11.1.1
|
||||
picocolors: 1.0.0
|
||||
vite: 4.3.1(sass@1.61.0)
|
||||
dev: true
|
||||
|
||||
/vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.1):
|
||||
resolution: {integrity: sha512-p04zH+Ey+NT78571x0pdX7nVRIJSlmKVvYryFglSWOK3Hc72eDL0+JJfbyQiugaIBApJkaEqbBQvqpsFZOSVGg==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4 } from "@litegraph-ts/core";
|
||||
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode } from "@litegraph-ts/core";
|
||||
import type ComfyApp from "./components/ComfyApp";
|
||||
import queueState from "./stores/queueState";
|
||||
import { get } from "svelte/store";
|
||||
import uiState from "./stores/uiState";
|
||||
import layoutState from "./stores/layoutState";
|
||||
|
||||
export type SerializedGraphCanvasState = {
|
||||
offset: Vector2,
|
||||
@@ -101,7 +103,39 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
}
|
||||
}
|
||||
|
||||
private alignToGrid(node: LGraphNode, ctx: CanvasRenderingContext2D) {
|
||||
const x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
|
||||
const y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE);
|
||||
|
||||
const shiftX = x - node.pos[0];
|
||||
let shiftY = y - node.pos[1];
|
||||
|
||||
let w, h;
|
||||
if (node.flags.collapsed) {
|
||||
w = node._collapsed_width;
|
||||
h = LiteGraph.NODE_TITLE_HEIGHT;
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||
} else {
|
||||
w = node.size[0];
|
||||
h = node.size[1];
|
||||
let titleMode = node.titleMode
|
||||
if (titleMode !== TitleMode.TRANSPARENT_TITLE && titleMode !== TitleMode.NO_TITLE) {
|
||||
h += LiteGraph.NODE_TITLE_HEIGHT;
|
||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||
}
|
||||
}
|
||||
const f = ctx.fillStyle;
|
||||
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
|
||||
ctx.fillRect(shiftX, shiftY, w, h);
|
||||
ctx.fillStyle = f;
|
||||
}
|
||||
|
||||
override drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void {
|
||||
if ((window as any)?.app?.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
|
||||
this.alignToGrid(node, ctx)
|
||||
}
|
||||
|
||||
// Fade out inactive nodes
|
||||
var editor_alpha = this.editor_alpha;
|
||||
if (node.mode === NodeMode.NEVER) { // never
|
||||
this.editor_alpha = 0.4;
|
||||
@@ -196,4 +230,23 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
override onSelectionChange(nodes: Record<number, LGraphNode>) {
|
||||
const ls = get(layoutState)
|
||||
ls.currentSelectionNodes = Object.values(nodes)
|
||||
ls.currentSelection = []
|
||||
layoutState.set(ls)
|
||||
}
|
||||
|
||||
override onNodeMoved(node: LGraphNode) {
|
||||
if (super.onNodeMoved)
|
||||
super.onNodeMoved(node);
|
||||
|
||||
if ((window as any)?.app?.shiftDown) {
|
||||
// Ensure all selected nodes are realigned
|
||||
for (const id in this.selected_nodes) {
|
||||
this.selected_nodes[id].alignToGrid();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,17 @@
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
export let showHandles: boolean = false;
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
let children: IDragItem[] | null = null;
|
||||
const flipDurationMs = 100;
|
||||
|
||||
$: if (container) {
|
||||
children = $layoutState.allItems[container.id].children;
|
||||
attrsChanged = container.attrsChanged
|
||||
}
|
||||
else {
|
||||
children = null;
|
||||
attrsChanged = null
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
@@ -38,26 +41,23 @@
|
||||
</script>
|
||||
|
||||
{#if container && children}
|
||||
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')}"
|
||||
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
{#key $attrsChanged}
|
||||
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
||||
class:hide-block={container.attrs.blockVariant === "hidden"}
|
||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)}
|
||||
class:root-container={zIndex === 0}
|
||||
class:is-executing={container.isNodeExecuting}
|
||||
class:container-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1}>
|
||||
class:edit={edit}>
|
||||
<Block>
|
||||
{#if container.attrs.showTitle}
|
||||
{#if container.attrs.title !== ""}
|
||||
<label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}>
|
||||
<BlockTitle>
|
||||
{#if $uiState.uiEditMode === "widgets"}
|
||||
<input class="edit-title" bind:value={container.attrs.title} type="text" minlength="1" />
|
||||
{:else}
|
||||
{container.attrs.title}
|
||||
{/if}
|
||||
</BlockTitle>
|
||||
<BlockTitle>{container.attrs.title}</BlockTitle>
|
||||
</label>
|
||||
{/if}
|
||||
<div class="v-pane"
|
||||
class:empty={children.length === 0}
|
||||
class:edit={$uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
class:edit={edit}
|
||||
use:dndzone="{{
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
@@ -70,8 +70,12 @@
|
||||
on:finalize="{handleFinalize}"
|
||||
>
|
||||
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||
{@const hidden = item?.attrs?.hidden}
|
||||
<div class="animation-wrapper"
|
||||
animate:flip={{duration:flipDurationMs}}>
|
||||
class:hidden={hidden}
|
||||
animate:flip={{duration:flipDurationMs}}
|
||||
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
|
||||
>
|
||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
|
||||
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||
@@ -79,11 +83,15 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if container.attrs.hidden && edit}
|
||||
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
|
||||
{/if}
|
||||
{#if showHandles}
|
||||
<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}
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@@ -97,6 +105,10 @@
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
&:not(.edit) > .animation-wrapper.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
border-width: 3px;
|
||||
border-color: var(--color-grey-400);
|
||||
@@ -134,10 +146,41 @@
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
> :global(*) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
> :global(.padded) {
|
||||
padding: 10px 12px 0px 10px;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.block) {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.edit > :global(.block) {
|
||||
border-color: var(--color-pink-500);
|
||||
border-width: 2px;
|
||||
border-style: dashed !important;
|
||||
margin: 0.2em;
|
||||
padding: 1.4em;
|
||||
}
|
||||
|
||||
:global(.hide-block > .block) {
|
||||
padding: 0.5em 0.25em;
|
||||
box-shadow: unset;
|
||||
border-width: 0;
|
||||
border-color: unset;
|
||||
border-radius: unset;
|
||||
background: var(--block-background-fill);
|
||||
width: 100%;
|
||||
line-height: var(--line-sm);
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--layout-gap);
|
||||
@@ -177,14 +220,6 @@
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
|
||||
&:not(.edit) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: grab;
|
||||
z-index: 99999;
|
||||
@@ -195,6 +230,11 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 100;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
background-color: #add8e680;
|
||||
}
|
||||
@@ -249,14 +289,6 @@
|
||||
color: var(--input-placeholder-color);
|
||||
}
|
||||
|
||||
.container-edit-outline > :global(.block) {
|
||||
border-color: var(--color-pink-500);
|
||||
border-width: 2px;
|
||||
border-style: dashed !important;
|
||||
margin: 0.2em;
|
||||
padding: 1.4em;
|
||||
}
|
||||
|
||||
.widget-edit-outline {
|
||||
border: 2px dashed var(--color-blue-400);
|
||||
margin: 0.2em;
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { get } from "svelte/store";
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import { Button } from "@gradio/button";
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import ComfyUIPane from "./ComfyUIPane.svelte";
|
||||
import ComfyApp, { type SerializedAppState } from "./ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import { Checkbox, TextBox } from "@gradio/form"
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
@@ -16,6 +17,7 @@
|
||||
import { LGraph } from "@litegraph-ts/core";
|
||||
import LightboxModal from "./LightboxModal.svelte";
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
import ComfyProperties from "./ComfyProperties.svelte";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
|
||||
export let app: ComfyApp = undefined;
|
||||
@@ -23,9 +25,11 @@
|
||||
let queue: ComfyQueue = undefined;
|
||||
let mainElem: HTMLDivElement;
|
||||
let uiPane: ComfyUIPane = undefined;
|
||||
let props: ComfyProperties = undefined;
|
||||
let containerElem: HTMLDivElement;
|
||||
let resizeTimeout: NodeJS.Timeout | null;
|
||||
let hasShownUIHelpToast: boolean = false;
|
||||
let uiTheme: string = "anapnoe";
|
||||
|
||||
let debugLayout: boolean = false;
|
||||
|
||||
@@ -43,7 +47,8 @@
|
||||
|
||||
function queuePrompt() {
|
||||
console.log("Queuing!");
|
||||
app.queuePrompt(0, 1);
|
||||
const workflow = $layoutState.attrs.defaultSubgraph;
|
||||
app.queuePrompt(0, 1, workflow);
|
||||
}
|
||||
|
||||
$: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
|
||||
@@ -52,11 +57,11 @@
|
||||
$: if ($uiState.uiEditMode)
|
||||
$layoutState.currentSelection = []
|
||||
|
||||
let graphSize = null;
|
||||
let graphSize = 0;
|
||||
|
||||
function toggleGraph() {
|
||||
if (graphSize == 0) {
|
||||
graphSize = 100;
|
||||
graphSize = 50;
|
||||
app.resizeCanvas();
|
||||
}
|
||||
else {
|
||||
@@ -64,15 +69,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
let sidebarSize = 20;
|
||||
let propsSidebarSize = 0; //15;
|
||||
|
||||
function toggleSidebar() {
|
||||
if (sidebarSize == 0) {
|
||||
sidebarSize = 20;
|
||||
function toggleProps() {
|
||||
if (propsSidebarSize == 0) {
|
||||
propsSidebarSize = 15;
|
||||
app.resizeCanvas();
|
||||
}
|
||||
else {
|
||||
sidebarSize = 0;
|
||||
propsSidebarSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let queueSidebarSize = 15;
|
||||
|
||||
function toggleQueue() {
|
||||
if (queueSidebarSize == 0) {
|
||||
queueSidebarSize = 15;
|
||||
app.resizeCanvas();
|
||||
}
|
||||
else {
|
||||
queueSidebarSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +158,8 @@
|
||||
(window as any).app = app;
|
||||
(window as any).appPane = uiPane;
|
||||
|
||||
await import('../../scss/ux.scss');
|
||||
|
||||
refreshView();
|
||||
})
|
||||
|
||||
@@ -149,10 +168,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if uiTheme === "anapnoe"}
|
||||
<link rel="stylesheet" href="/src/scss/ux.scss">
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div id="main">
|
||||
<div id="dropzone" class="dropzone"></div>
|
||||
<div id="container" bind:this={containerElem}>
|
||||
<Splitpanes theme="comfy" on:resize={refreshView}>
|
||||
<Pane bind:size={propsSidebarSize}>
|
||||
<div class="sidebar-wrapper pane-wrapper">
|
||||
<ComfyProperties bind:this={props} />
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
|
||||
<Pane>
|
||||
@@ -165,7 +195,7 @@
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
<Pane bind:size={sidebarSize}>
|
||||
<Pane bind:size={queueSidebarSize}>
|
||||
<div class="sidebar-wrapper pane-wrapper">
|
||||
<ComfyQueue bind:this={queue} />
|
||||
</div>
|
||||
@@ -174,13 +204,16 @@
|
||||
</div>
|
||||
<div id="bottombar">
|
||||
<Button variant="primary" on:click={queuePrompt}>
|
||||
Run
|
||||
Queue Prompt
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleGraph}>
|
||||
Toggle Graph
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleSidebar}>
|
||||
Toggle Sidebar
|
||||
<Button variant="secondary" on:click={toggleProps}>
|
||||
Toggle Props
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleQueue}>
|
||||
Toggle Queue
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doSave}>
|
||||
Save
|
||||
@@ -197,14 +230,23 @@
|
||||
<Button variant="secondary" on:click={doRefreshCombos}>
|
||||
🔄
|
||||
</Button>
|
||||
<Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/>
|
||||
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||
<label for="enable-ui-editing">Enable UI Editing</label>
|
||||
<label class="label" for="enable-ui-editing">
|
||||
<BlockTitle>Enable UI Editing</BlockTitle>
|
||||
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="widgets">Widgets</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label" for="ui-theme">
|
||||
<BlockTitle>Theme</BlockTitle>
|
||||
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
||||
<option value="">None</option>
|
||||
<option value="anapnoe">Anapnoe</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<LightboxModal />
|
||||
</div>
|
||||
@@ -213,7 +255,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
#container {
|
||||
height: calc(100vh - 60px);
|
||||
height: calc(100vh - 70px);
|
||||
max-width: 100vw;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
@@ -293,7 +335,7 @@
|
||||
}
|
||||
|
||||
:global(.splitpanes.comfy) {
|
||||
max-height: calc(100vh - 60px);
|
||||
max-height: calc(100vh - 70px);
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
@@ -304,4 +346,8 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label.label > :global(span) {
|
||||
top: 20%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,8 @@ import type TypedEmitter from "typed-emitter";
|
||||
import "@litegraph-ts/nodes-basic"
|
||||
import "@litegraph-ts/nodes-events"
|
||||
import "@litegraph-ts/nodes-math"
|
||||
import "@litegraph-ts/nodes-strings"
|
||||
import "$lib/nodes/index"
|
||||
import * as nodes from "$lib/nodes/index"
|
||||
|
||||
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
|
||||
@@ -24,10 +26,13 @@ import { toast } from '@zerodevx/svelte-toast'
|
||||
import ComfyGraph from "$lib/ComfyGraph";
|
||||
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
import { get } from "svelte/store";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { promptToGraphVis } from "$lib/utils";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
|
||||
LiteGraph.catch_exceptions = false;
|
||||
LiteGraph.CANVAS_GRID_SIZE = 32;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// Load default visibility
|
||||
@@ -52,9 +57,11 @@ export type SerializedPromptInputs = {
|
||||
class_type: string
|
||||
}
|
||||
|
||||
export type SerializedPromptOutput = Record<string, SerializedPromptInputs>
|
||||
|
||||
export type SerializedPrompt = {
|
||||
workflow: SerializedLGraph,
|
||||
output: Record<string, SerializedPromptInputs>
|
||||
output: SerializedPromptOutput
|
||||
}
|
||||
|
||||
export type Progress = {
|
||||
@@ -318,12 +325,12 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
// Distinguish frontend/backend connections
|
||||
const BACKEND_TYPES = ["CLIP", "CLIP_VISION", "CLIP_VISION_OUTPUT", "CONDITIONING", "CONTROL_NET", "IMAGE", "LATENT", "MASK", "MODEL", "STYLE_MODEL", "VAE"]
|
||||
const BACKEND_TYPES = ["CLIP", "CLIP_VISION", "CLIP_VISION_OUTPUT", "CONDITIONING", "CONTROL_NET", "LATENT", "MASK", "MODEL", "STYLE_MODEL", "VAE"]
|
||||
for (const type of BACKEND_TYPES) {
|
||||
setColor(type, "orange")
|
||||
}
|
||||
|
||||
setColor("OUTPUT", "rebeccapurple")
|
||||
setColor("IMAGE", "rebeccapurple")
|
||||
setColor(BuiltInSlotType.EVENT, "lightseagreen")
|
||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||
}
|
||||
@@ -418,7 +425,7 @@ export default class ComfyApp {
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt(): Promise<SerializedPrompt> {
|
||||
async graphToPrompt(tag: string | null = null): Promise<SerializedPrompt> {
|
||||
// Run frontend-only logic
|
||||
this.lGraph.runStep(1)
|
||||
|
||||
@@ -436,6 +443,11 @@ export default class ComfyApp {
|
||||
|
||||
const node = node_ as ComfyBackendNode;
|
||||
|
||||
if (tag && node.tags.indexOf(tag) === -1) {
|
||||
console.debug("Skipping tagged node", tag, node.tags)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.mode === NodeMode.NEVER) {
|
||||
// Don't serialize muted nodes
|
||||
continue;
|
||||
@@ -449,6 +461,11 @@ export default class ComfyApp {
|
||||
const inp = node.inputs[i];
|
||||
const inputLink = node.getInputLink(i)
|
||||
const inputNode = node.getInputNode(i)
|
||||
|
||||
if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inputLink || !inputNode) {
|
||||
if ("config" in inp) {
|
||||
const defaultValue = (inp as IComfyInputSlot).config?.defaultValue
|
||||
@@ -487,17 +504,36 @@ export default class ComfyApp {
|
||||
if (parent) {
|
||||
const seen = {}
|
||||
let link = node.getInputLink(i);
|
||||
while (parent && !parent.isBackendNode) {
|
||||
|
||||
const isValidParent = (parent: ComfyGraphNode) => {
|
||||
if (!parent || parent.isBackendNode)
|
||||
return false;
|
||||
if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
while (isValidParent(parent)) {
|
||||
link = parent.getInputLink(link.origin_slot);
|
||||
if (link && !seen[link.id]) {
|
||||
seen[link.id] = true
|
||||
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
|
||||
const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
|
||||
if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) {
|
||||
console.debug("Skipping tagged parent node", tag, node.tags)
|
||||
parent = null;
|
||||
}
|
||||
else {
|
||||
parent = inputNode;
|
||||
}
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (link && parent && parent.isBackendNode) {
|
||||
if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1)
|
||||
continue;
|
||||
|
||||
const input = node.inputs[i]
|
||||
// TODO can null be a legitimate value in some cases?
|
||||
// Nodes like CLIPLoader will never have a value in the frontend, hence "null".
|
||||
@@ -520,15 +556,19 @@ export default class ComfyApp {
|
||||
if (Array.isArray(output[o].inputs[i])
|
||||
&& output[o].inputs[i].length === 2
|
||||
&& !output[output[o].inputs[i][0]]) {
|
||||
console.debug("Prune removed node link", o, i, output[o].inputs[i])
|
||||
delete output[o].inputs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn({ workflow, output })
|
||||
console.warn(promptToGraphVis({ workflow, output }))
|
||||
|
||||
return { workflow, output };
|
||||
}
|
||||
|
||||
async queuePrompt(num: number, batchCount: number = 1) {
|
||||
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
|
||||
this.queueItems.push({ num, batchCount });
|
||||
|
||||
// Only have one action process the items so each one gets a unique seed correctly
|
||||
@@ -536,14 +576,23 @@ export default class ComfyApp {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag === "")
|
||||
tag = null;
|
||||
|
||||
this.processingQueue = true;
|
||||
try {
|
||||
while (this.queueItems.length) {
|
||||
({ num, batchCount } = this.queueItems.pop());
|
||||
console.log(`Queue get! ${num} ${batchCount}`);
|
||||
console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
const p = await this.graphToPrompt();
|
||||
for (const node of this.lGraph._nodes_in_order) {
|
||||
if ("beforeQueued" in node) {
|
||||
(node as ComfyGraphNode).beforeQueued();
|
||||
}
|
||||
}
|
||||
|
||||
const p = await this.graphToPrompt(tag);
|
||||
|
||||
try {
|
||||
await this.api.queuePrompt(num, p);
|
||||
@@ -624,12 +673,8 @@ export default class ComfyApp {
|
||||
if ("config" in input) {
|
||||
const comfyInput = input as IComfyInputSlot;
|
||||
|
||||
console.warn("RefreshCombo", comfyInput.defaultWidgetNode, comfyInput)
|
||||
|
||||
if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) {
|
||||
comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
|
||||
|
||||
console.warn("RefreshCombo", comfyInput.config.values, def["input"]["required"][comfyInput.name])
|
||||
const inputNode = node.getInputNode(index)
|
||||
|
||||
if (inputNode && "doAutoConfig" in inputNode) {
|
||||
|
||||
56
src/lib/components/ComfyComboProperty.svelte
Normal file
56
src/lib/components/ComfyComboProperty.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let value: string = "";
|
||||
export let values: string[] = [""];
|
||||
export let name: string = "";
|
||||
let value_: string = ""
|
||||
|
||||
$: value;
|
||||
$: handleChange(value);
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: string;
|
||||
submit: undefined;
|
||||
blur: undefined;
|
||||
}>();
|
||||
|
||||
function handleChange(val: string) {
|
||||
console.debug("combo handleChange", val, value_)
|
||||
if (val != value_)
|
||||
dispatch("change", val);
|
||||
value_ = val
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="select-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="select">
|
||||
<select on:blur bind:value>
|
||||
{#each values as value}
|
||||
<option {value}>
|
||||
{value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style lang="scss">
|
||||
.select-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
|
||||
select {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-title {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
</style>
|
||||
45
src/lib/components/ComfyNumberProperty.svelte
Normal file
45
src/lib/components/ComfyNumberProperty.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let value: number = 0;
|
||||
export let step: number = 1;
|
||||
export let name: string = "";
|
||||
let value_: number = 0;
|
||||
|
||||
$: value;
|
||||
$: handleChange(value);
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: number;
|
||||
submit: undefined;
|
||||
blur: undefined;
|
||||
}>();
|
||||
|
||||
function handleChange(val: number) {
|
||||
if (val != value_)
|
||||
dispatch("change", val);
|
||||
value_ = val
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="number-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="number">
|
||||
<input type="number" bind:value {step}>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style lang="scss">
|
||||
.number-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.number {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
310
src/lib/components/ComfyProperties.svelte
Normal file
310
src/lib/components/ComfyProperties.svelte
Normal file
@@ -0,0 +1,310 @@
|
||||
<script lang="ts">
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { TextBox, Checkbox } from "@gradio/form";
|
||||
import { LGraphNode } from "@litegraph-ts/core"
|
||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||
import { get } from "svelte/store"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||
|
||||
let target: IDragItem | null = null;
|
||||
let node: LGraphNode | null = null;
|
||||
|
||||
$: if ($layoutState.currentSelection.length > 0) {
|
||||
const targetId = $layoutState.currentSelection.slice(-1)[0]
|
||||
target = $layoutState.allItems[targetId].dragItem
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
}
|
||||
else {
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
else if ($layoutState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $layoutState.currentSelectionNodes[0]
|
||||
}
|
||||
else {
|
||||
target = null
|
||||
node = null;
|
||||
}
|
||||
|
||||
let targetType: string = "???"
|
||||
|
||||
$: {
|
||||
if (node != null)
|
||||
targetType = node.type || "Node"
|
||||
else if (target)
|
||||
targetType = "Group"
|
||||
else
|
||||
targetType = ""
|
||||
}
|
||||
|
||||
function validNodeProperty(spec: AttributesSpec, node: LGraphNode): boolean {
|
||||
if (spec.validNodeTypes) {
|
||||
return spec.validNodeTypes.indexOf(node.type) !== -1;
|
||||
}
|
||||
return spec.name in node.properties
|
||||
}
|
||||
|
||||
function updateAttribute(entry: AttributesSpec, target: IDragItem, value: any) {
|
||||
if (target) {
|
||||
const name = entry.name
|
||||
console.warn("updateAttribute", name, value)
|
||||
|
||||
target.attrs[name] = value
|
||||
target.attrsChanged.set(!get(target.attrsChanged))
|
||||
|
||||
if (node && "propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateProperty(entry: AttributesSpec, value: any) {
|
||||
if (node) {
|
||||
const name = entry.name
|
||||
console.warn("updateProperty", name, value)
|
||||
|
||||
node.properties[name] = value;
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getVar(node: LGraphNode, entry: AttributesSpec) {
|
||||
let value = node[entry.name]
|
||||
if (entry.serialize)
|
||||
value = entry.serialize(value)
|
||||
console.debug("[ComfyProperties] getVar", entry, value, node)
|
||||
return value
|
||||
}
|
||||
|
||||
function updateVar(entry: any, value: any) {
|
||||
if (node) {
|
||||
const name = entry.name
|
||||
console.warn("updateProperty", name, value)
|
||||
|
||||
if (entry.deserialize)
|
||||
value = entry.deserialize(value)
|
||||
|
||||
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
|
||||
node[name] = value;
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateWorkflowAttribute(entry: AttributesSpec, value: any) {
|
||||
const name = entry.name
|
||||
console.warn("updateWorkflowAttribute", name, value)
|
||||
|
||||
$layoutState.attrs[name] = value
|
||||
$layoutState = $layoutState
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="props">
|
||||
<div class="top">
|
||||
<div class="target-name">
|
||||
<span>
|
||||
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
|
||||
{#if targetType !== ""}
|
||||
<span class="type">({targetType})</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="props-entries">
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
<span class="title">{category.categoryName}</span>
|
||||
</span>
|
||||
</div>
|
||||
{#each category.specs as spec(spec.name)}
|
||||
{#if spec.location === "widget" && target && spec.name in target.attrs}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
on:input={(e) => updateAttribute(spec, target, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={target.attrs[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={target.attrs[spec.name]}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if node}
|
||||
{#if spec.location === "nodeProps" && validNodeProperty(spec, node)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={node.properties[spec.name]}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
on:input={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={node.properties[spec.name]}
|
||||
label={spec.name}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name]}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if spec.location === "nodeVars" && spec.name in node}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
on:input={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
step={1}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.props-entry {
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.target-name {
|
||||
border-color: var(--neutral-400);
|
||||
background: var(--neutral-300);
|
||||
padding: 0.8rem 1.0rem;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
padding: 0.4rem 1.0rem;
|
||||
border-color: var(--neutral-300);
|
||||
background: var(--neutral-200);
|
||||
}
|
||||
|
||||
.target-name, .category-name {
|
||||
border-width: var(--block-border-width);
|
||||
|
||||
.type {
|
||||
color: var(--neutral-500);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
/* width: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0.5em; */
|
||||
}
|
||||
</style>
|
||||
@@ -57,18 +57,18 @@
|
||||
|
||||
$: if (entries) {
|
||||
_entries = []
|
||||
for (const entry of entries) {
|
||||
for (const outputs of Object.values(entry.outputs)) {
|
||||
const allImages = outputs.images.map(r => {
|
||||
// TODO configure backend URL
|
||||
const url = "http://localhost:8188/view?"
|
||||
const params = new URLSearchParams(r)
|
||||
return url + params
|
||||
});
|
||||
|
||||
_entries.push({ allImages, name: "Output" })
|
||||
}
|
||||
}
|
||||
// for (const entry of entries) {
|
||||
// for (const outputs of Object.values(entry.outputs)) {
|
||||
// const allImages = outputs.images.map(r => {
|
||||
// // TODO configure backend URL
|
||||
// const url = "http://localhost:8188/view?"
|
||||
// const params = new URLSearchParams(r)
|
||||
// return url + params
|
||||
// });
|
||||
//
|
||||
// _entries.push({ allImages, name: "Output" })
|
||||
// }
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import BlockContainer from "./BlockContainer.svelte"
|
||||
import { type Writable } from "svelte/store"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
|
||||
export let dragItem: IDragItem | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
let container: ContainerLayout | null = null;
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let widget: WidgetLayout | null = null;
|
||||
let showHandles: boolean = false;
|
||||
|
||||
@@ -17,14 +21,23 @@
|
||||
dragItem = null;
|
||||
container = null;
|
||||
widget = null;
|
||||
attrsChanged = null;
|
||||
propsChanged = null;
|
||||
}
|
||||
else if (dragItem.type === "container") {
|
||||
container = dragItem as ContainerLayout;
|
||||
attrsChanged = container.attrsChanged;
|
||||
widget = null;
|
||||
propsChanged = null;
|
||||
}
|
||||
else if (dragItem.type === "widget") {
|
||||
widget = dragItem as WidgetLayout;
|
||||
attrsChanged = widget.attrsChanged;
|
||||
container = null;
|
||||
if (widget.node && "propsChanged" in widget.node)
|
||||
propsChanged = (widget.node as ComfyWidgetNode).propsChanged
|
||||
else
|
||||
propsChanged = null;
|
||||
}
|
||||
|
||||
$: showHandles = $uiState.uiEditMode === "widgets" // TODO
|
||||
@@ -35,21 +48,38 @@
|
||||
$: if ($queueState && widget && widget.node) {
|
||||
dragItem.isNodeExecuting = $queueState.runningNodeId === widget.node.id;
|
||||
}
|
||||
|
||||
function getWidgetClass() {
|
||||
const title = widget.node.type.replace("/", "-").replace(".", "-")
|
||||
return `widget--${title}`
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if container}
|
||||
{#key $attrsChanged}
|
||||
<BlockContainer {container} {classes} {zIndex} {showHandles} />
|
||||
{/key}
|
||||
{:else if widget && widget.node}
|
||||
<div class="widget" class:widget-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
{#key $attrsChanged}
|
||||
{#key $propsChanged}
|
||||
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
|
||||
class:edit={edit}
|
||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
|
||||
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
|
||||
class:hidden={widget.attrs.hidden}
|
||||
>
|
||||
<svelte:component this={widget.node.svelteComponentType} {widget} />
|
||||
</div>
|
||||
{#if widget.attrs.hidden && edit}
|
||||
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
|
||||
{/if}
|
||||
{#if showHandles}
|
||||
<div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
||||
{/if}
|
||||
{/key}
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@@ -66,6 +96,10 @@
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.hidden:not(.edit) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: grab;
|
||||
z-index: 99999;
|
||||
@@ -76,6 +110,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handle-hidden {
|
||||
background-color: #40404080;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
background-color: #add8e680;
|
||||
}
|
||||
@@ -85,7 +123,7 @@
|
||||
color: var(--neutral-400);
|
||||
}
|
||||
|
||||
.widget-edit-outline {
|
||||
.edit {
|
||||
border: 2px dashed var(--color-blue-400);
|
||||
margin: 0.2em;
|
||||
padding: 0.2em;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,18 +2,21 @@ import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnec
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||
|
||||
export interface ComfyAfterQueuedAction extends Record<any, any> {
|
||||
prompt: SerializedPrompt
|
||||
export interface ComfyQueueEventsProperties extends Record<any, any> {
|
||||
prompt: SerializedPrompt | null
|
||||
}
|
||||
|
||||
export class ComfyAfterQueuedAction extends ComfyGraphNode {
|
||||
override properties: ComfyCopyActionProperties = {
|
||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||
override properties: ComfyQueueEventsProperties = {
|
||||
prompt: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
{ name: "beforeQueued", type: BuiltInSlotType.EVENT },
|
||||
{ name: "afterQueued", type: BuiltInSlotType.EVENT },
|
||||
{ name: "prompt", type: "*" }
|
||||
],
|
||||
@@ -21,17 +24,22 @@ export class ComfyAfterQueuedAction extends ComfyGraphNode {
|
||||
|
||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||
if (property === "value") {
|
||||
this.setOutputData(0, this.properties.prompt)
|
||||
this.setOutputData(2, this.properties.prompt)
|
||||
}
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
this.setOutputData(0, this.properties.prompt)
|
||||
this.setOutputData(2, this.properties.prompt)
|
||||
}
|
||||
|
||||
override beforeQueued() {
|
||||
this.setProperty("value", null)
|
||||
this.triggerSlot(0, "bang")
|
||||
}
|
||||
|
||||
override afterQueued(p: SerializedPrompt) {
|
||||
this.setProperty("value", p)
|
||||
this.triggerSlot(0, "bang")
|
||||
this.triggerSlot(1, "bang")
|
||||
}
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
@@ -41,10 +49,52 @@ export class ComfyAfterQueuedAction extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyAfterQueuedAction,
|
||||
title: "Comfy.AfterQueuedAction",
|
||||
class: ComfyQueueEvents,
|
||||
title: "Comfy.QueueEvents",
|
||||
desc: "Triggers a 'bang' event when a prompt is queued.",
|
||||
type: "actions/after_queued"
|
||||
type: "actions/queue_events"
|
||||
})
|
||||
|
||||
export interface ComfyOnExecutedEventProperties extends Record<any, any> {
|
||||
images: GalleryOutput | null,
|
||||
filename: string | null
|
||||
}
|
||||
|
||||
export class ComfyOnExecutedEvent extends ComfyGraphNode {
|
||||
override properties: ComfyOnExecutedEventProperties = {
|
||||
images: null,
|
||||
filename: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "IMAGE" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "images", type: "OUTPUT" },
|
||||
{ name: "onExecuted", type: BuiltInSlotType.EVENT },
|
||||
],
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
if (this.properties.images !== null)
|
||||
this.setOutputData(0, this.properties.images)
|
||||
}
|
||||
|
||||
override receiveOutput(output: any) {
|
||||
if (output && "images" in output) {
|
||||
this.setProperty("images", output as GalleryOutput)
|
||||
this.setOutputData(0, this.properties.images)
|
||||
this.triggerSlot(1, "bang")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyOnExecutedEvent,
|
||||
title: "Comfy.OnExecutedEvent",
|
||||
desc: "Triggers a 'bang' event when a prompt output is received.",
|
||||
type: "actions/on_executed"
|
||||
})
|
||||
|
||||
export interface ComfyCopyActionProperties extends Record<any, any> {
|
||||
@@ -130,3 +180,74 @@ LiteGraph.registerNodeType({
|
||||
desc: "Swaps two inputs when triggered",
|
||||
type: "actions/swap"
|
||||
})
|
||||
|
||||
export interface ComfyNotifyActionProperties extends Record<any, any> {
|
||||
message: string
|
||||
}
|
||||
|
||||
export class ComfyNotifyAction extends ComfyGraphNode {
|
||||
override properties: ComfyNotifyActionProperties = {
|
||||
message: "Nya."
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "message", type: "string" },
|
||||
{ name: "trigger", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
const message = this.getInputData(0);
|
||||
if (message) {
|
||||
toast.push(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyNotifyAction,
|
||||
title: "Comfy.NotifyAction",
|
||||
desc: "Displays a message.",
|
||||
type: "actions/notify"
|
||||
})
|
||||
|
||||
export interface ComfyExecuteSubgraphActionProperties extends Record<any, any> {
|
||||
tag: string | null,
|
||||
}
|
||||
|
||||
export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
|
||||
override properties: ComfyExecuteSubgraphActionProperties = {
|
||||
tag: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "execute", type: BuiltInSlotType.ACTION },
|
||||
{ name: "tag", type: "string" }
|
||||
],
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
const tag = this.getInputData(1)
|
||||
if (tag)
|
||||
this.setProperty("tag", tag)
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
const tag = this.getInputData(1) || this.properties.tag;
|
||||
|
||||
const app = (window as any)?.app;
|
||||
if (!app)
|
||||
return;
|
||||
|
||||
app.queuePrompt(0, 1, tag);
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyExecuteSubgraphAction,
|
||||
title: "Comfy.ExecuteSubgraphAction",
|
||||
desc: "Runs a part of the graph based on a tag",
|
||||
type: "actions/execute_subgraph"
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import ComfyWidgets from "$lib/widgets"
|
||||
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
|
||||
import type { SerializedLGraphNode } from "@litegraph-ts/core";
|
||||
|
||||
/*
|
||||
* Base class for any node with configuration sent by the backend.
|
||||
@@ -25,10 +26,16 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
||||
// It just returns a hash like { "ui": { "images": results } } internally.
|
||||
// So this will need to be hardcoded for now.
|
||||
if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
|
||||
this.addOutput("output", "OUTPUT");
|
||||
this.addOutput("output", "IMAGE");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tags this node belongs to
|
||||
* Allows you to run subsections of the graph
|
||||
*/
|
||||
tags: string[] = []
|
||||
|
||||
private setup(nodeData: any) {
|
||||
var inputs = nodeData["input"]["required"];
|
||||
if (nodeData["input"]["optional"] != undefined) {
|
||||
@@ -74,16 +81,27 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
||||
// app.#invokeExtensionsAsync("nodeCreated", this);
|
||||
}
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
super.onSerialize(o);
|
||||
(o as any).tags = this.tags
|
||||
}
|
||||
|
||||
override onConfigure(o: SerializedLGraphNode) {
|
||||
super.onConfigure(o);
|
||||
this.tags = (o as any).tags || []
|
||||
}
|
||||
|
||||
override onExecuted(outputData: any) {
|
||||
console.warn("onExecuted outputs", outputData)
|
||||
for (let index = 0; index < this.outputs.length; index++) {
|
||||
const output = this.outputs[index]
|
||||
if (output.type === "OUTPUT") {
|
||||
if (output.type === "IMAGE") {
|
||||
this.setOutputData(index, outputData)
|
||||
for (const node of this.getOutputNodes(index)) {
|
||||
console.warn(node)
|
||||
if ("receiveOutput" in node) {
|
||||
const widgetNode = node as ComfyWidgetNode;
|
||||
widgetNode.receiveOutput();
|
||||
const widgetNode = node as ComfyGraphNode;
|
||||
widgetNode.receiveOutput(outputData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
||||
import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||
import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export type DefaultWidgetSpec = {
|
||||
defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
|
||||
@@ -18,11 +20,28 @@ export type DefaultWidgetLayout = {
|
||||
export default class ComfyGraphNode extends LGraphNode {
|
||||
isBackendNode?: boolean;
|
||||
|
||||
beforeQueued?(): void;
|
||||
afterQueued?(prompt: SerializedPrompt): void;
|
||||
onExecuted?(output: any): void;
|
||||
|
||||
defaultWidgets?: DefaultWidgetLayout
|
||||
|
||||
/** Called when a backend node sends a ComfyUI output over a link */
|
||||
receiveOutput(output: any) {
|
||||
}
|
||||
|
||||
override onResize(size: Vector2) {
|
||||
if ((window as any)?.app?.shiftDown) {
|
||||
const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.size[0] / LiteGraph.CANVAS_GRID_SIZE);
|
||||
const h = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.size[1] / LiteGraph.CANVAS_GRID_SIZE);
|
||||
this.size[0] = w;
|
||||
this.size[1] = h;
|
||||
}
|
||||
|
||||
if (super.onResize)
|
||||
super.onResize(size)
|
||||
}
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
for (let index = 0; index < this.inputs.length; index++) {
|
||||
const input = this.inputs[index]
|
||||
@@ -43,7 +62,7 @@ export default class ComfyGraphNode extends LGraphNode {
|
||||
for (let index = 0; index < this.inputs.length; index++) {
|
||||
const input = this.inputs[index]
|
||||
const serInput = o.inputs[index]
|
||||
if ("widgetNodeType" in serInput) {
|
||||
if (serInput && "widgetNodeType" in serInput) {
|
||||
const comfyInput = input as IComfyInputSlot
|
||||
const ty: string = serInput.widgetNodeType as any
|
||||
const widgetNode = Object.values(LiteGraph.registered_node_types)
|
||||
|
||||
208
src/lib/nodes/ComfyImageCacheNode.ts
Normal file
208
src/lib/nodes/ComfyImageCacheNode.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||
|
||||
export interface ComfyImageCacheNodeProperties extends Record<any, any> {
|
||||
images: GalleryOutput | null,
|
||||
index: number,
|
||||
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
|
||||
genNumber: number
|
||||
}
|
||||
|
||||
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
|
||||
|
||||
/*
|
||||
* A node that can act as both an input and output image node by uploading
|
||||
* the output file into ComfyUI's input folder.
|
||||
*/
|
||||
export default class ComfyImageCacheNode extends ComfyGraphNode {
|
||||
override properties: ComfyImageCacheNodeProperties = {
|
||||
images: null,
|
||||
index: 0,
|
||||
filenames: {},
|
||||
genNumber: 0
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "OUTPUT" },
|
||||
{ name: "index", type: "number" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION },
|
||||
{ name: "clear", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "filename", type: "string" },
|
||||
{ name: "state", type: "string" },
|
||||
]
|
||||
}
|
||||
|
||||
private _uploadPromise: Promise<void> | null = null;
|
||||
private _state: ImageCacheState = "none"
|
||||
|
||||
stateWidget: ITextWidget;
|
||||
filenameWidget: ITextWidget;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name)
|
||||
this.stateWidget = this.addWidget<ITextWidget>(
|
||||
"text",
|
||||
"State",
|
||||
"none"
|
||||
);
|
||||
this.stateWidget.disabled = true;
|
||||
|
||||
this.filenameWidget = this.addWidget<ITextWidget>(
|
||||
"text",
|
||||
"File",
|
||||
""
|
||||
);
|
||||
this.filenameWidget.disabled = true;
|
||||
}
|
||||
|
||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||
if (property === "images") {
|
||||
if (value != null)
|
||||
this.properties.index = clamp(this.properties.index, 0, value.length)
|
||||
else
|
||||
this.properties.index = 0
|
||||
}
|
||||
|
||||
if (this.properties.filenames && this.properties.images) {
|
||||
const fileCount = this.properties.images.images.length;
|
||||
const cachedCount = Object.keys(this.properties.filenames).length
|
||||
console.warn(cachedCount, this.properties.filenames)
|
||||
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
|
||||
}
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
const index = this.getInputData(1)
|
||||
if (typeof index === "number")
|
||||
this.setIndex(index)
|
||||
|
||||
const existing = this.properties.filenames[this.properties.index]
|
||||
let state = "none"
|
||||
if (existing)
|
||||
state = existing.status
|
||||
|
||||
this.stateWidget.value = state
|
||||
|
||||
let filename = null
|
||||
if (this.properties.index in this.properties.filenames)
|
||||
filename = this.properties.filenames[this.properties.index].filename
|
||||
|
||||
this.setOutputData(0, filename)
|
||||
this.setOutputData(1, state)
|
||||
}
|
||||
|
||||
private setIndex(newIndex: number, force: boolean = false) {
|
||||
if (newIndex === this.properties.index && !force)
|
||||
return;
|
||||
|
||||
if (!this.properties.images || newIndex < 0 || newIndex >= this.properties.images.images.length) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setProperty("index", newIndex)
|
||||
|
||||
const data = this.properties.images.images[newIndex]
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.properties.filenames ||= {}
|
||||
const existing = this.properties.filenames[newIndex]
|
||||
|
||||
if (existing != null && existing.status === "cached") {
|
||||
return
|
||||
}
|
||||
|
||||
const lastGenNumber = this.properties.genNumber
|
||||
|
||||
// ComfyUI's LoadImage node only operates on files in its input
|
||||
// folder. Usually we're dealing with an image in either the output
|
||||
// folder (SaveImage) or the temp folder (PreviewImage). So we have
|
||||
// to copy the image into ComfyUI's input folder first by using
|
||||
// their upload API.
|
||||
|
||||
if (data.subfolder === "input") {
|
||||
// Already in the correct folder for use by LoadImage
|
||||
this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" }
|
||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
||||
}
|
||||
else {
|
||||
this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
|
||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
||||
const url = "http://localhost:8188" // TODO make configurable
|
||||
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) => {
|
||||
console.debug("Gottem", json)
|
||||
if (lastGenNumber === this.properties.genNumber) {
|
||||
this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" }
|
||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
||||
}
|
||||
else {
|
||||
console.warn("[ComfyImageCacheNode] New generation since index switched!")
|
||||
}
|
||||
this._uploadPromise = null;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Error uploading:", e)
|
||||
if (lastGenNumber === this.properties.genNumber) {
|
||||
this.properties.filenames[newIndex] = { filename: null, status: "failed" }
|
||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
||||
}
|
||||
else {
|
||||
console.warn("[ComfyImageCacheNode] New generation since index switched!")
|
||||
}
|
||||
})
|
||||
|
||||
if (this._uploadPromise)
|
||||
this._uploadPromise.then(() => promise)
|
||||
else
|
||||
this._uploadPromise = promise
|
||||
}
|
||||
}
|
||||
|
||||
override onAction(action: any) {
|
||||
if (action === "clear") {
|
||||
this.setProperty("images", null)
|
||||
this.setProperty("filenames", {})
|
||||
this.setProperty("index", 0)
|
||||
return
|
||||
}
|
||||
|
||||
const link = this.getInputLink(0)
|
||||
|
||||
if (link.data && "images" in link.data) {
|
||||
this.setProperty("genNumber", this.properties.genNumber + 1)
|
||||
this.setProperty("images", link.data as GalleryOutput)
|
||||
this.setProperty("filenames", {})
|
||||
console.debug("[ComfyImageCacheNode] Received output!", link.data)
|
||||
this.setIndex(0, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyImageCacheNode,
|
||||
title: "Comfy.ImageCache",
|
||||
desc: "Allows reusing a previously output image by uploading it into ComfyUI's input folder.",
|
||||
type: "image/cache"
|
||||
})
|
||||
@@ -94,9 +94,9 @@ export class ComfySelectorTwo extends ComfyGraphNode {
|
||||
ctx.fillStyle = "#AFB";
|
||||
var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(50, y);
|
||||
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
|
||||
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
|
||||
ctx.moveTo(65, y);
|
||||
ctx.lineTo(65, y + LiteGraph.NODE_SLOT_HEIGHT);
|
||||
ctx.lineTo(49, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
override isBackendNode = false;
|
||||
override serialize_widgets = true;
|
||||
|
||||
outputIndex: number = 0;
|
||||
outputIndex: number | null = 0;
|
||||
inputIndex: number = 0;
|
||||
changedIndex: number = 1;
|
||||
changedIndex: number | null = 1;
|
||||
|
||||
displayWidget: ITextWidget;
|
||||
|
||||
@@ -91,10 +91,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
console.debug("[Widget] valueUpdated", this, value)
|
||||
this.displayWidget.value = this.formatValue(value)
|
||||
|
||||
if (this.outputs.length >= this.outputIndex) {
|
||||
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
|
||||
this.setOutputData(this.outputIndex, get(this.value))
|
||||
}
|
||||
if (this.outputs.length >= this.changedIndex) {
|
||||
if (this.changedIndex !== null & this.outputs.length >= this.changedIndex) {
|
||||
const changedOutput = this.outputs[this.changedIndex]
|
||||
if (changedOutput.type === BuiltInSlotType.EVENT)
|
||||
this.triggerSlot(this.changedIndex, "changed")
|
||||
@@ -119,7 +119,6 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
if (this.inputs.length >= this.inputIndex) {
|
||||
const data = this.getInputData(this.inputIndex)
|
||||
if (data) { // TODO can "null" be a legitimate value here?
|
||||
console.log(data)
|
||||
this.setValue(data)
|
||||
const input = this.getInputLink(this.inputIndex)
|
||||
input.data = null;
|
||||
@@ -135,10 +134,6 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a backend node sends a ComfyUI output over a link */
|
||||
receiveOutput() {
|
||||
}
|
||||
|
||||
onConnectOutput(
|
||||
outputIndex: number,
|
||||
inputType: INodeInputSlot["type"],
|
||||
@@ -146,7 +141,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
inputNode: LGraphNode,
|
||||
inputIndex: number
|
||||
): boolean {
|
||||
if (this.autoConfig && "config" in input) {
|
||||
if (this.autoConfig && "config" in input && this.outputs.length === 0) {
|
||||
this.doAutoConfig(input as IComfyInputSlot)
|
||||
}
|
||||
|
||||
@@ -402,31 +397,57 @@ export type GalleryOutputEntry = {
|
||||
}
|
||||
|
||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||
index: number
|
||||
}
|
||||
|
||||
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
override properties: ComfyGalleryProperties = {
|
||||
defaultValue: []
|
||||
defaultValue: [],
|
||||
index: 0
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "OUTPUT" }
|
||||
{ name: "images", type: "OUTPUT" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION },
|
||||
{ name: "clear", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "selected_index", type: "number" }
|
||||
]
|
||||
}
|
||||
|
||||
override svelteComponentType = GalleryWidget
|
||||
override copyFromInputLink = false;
|
||||
override outputIndex = null;
|
||||
override changedIndex = null;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, [])
|
||||
}
|
||||
|
||||
override afterQueued() {
|
||||
let queue = get(queueState)
|
||||
if (!(typeof queue.queueRemaining === "number" && queue.queueRemaining > 1)) {
|
||||
override onExecute() {
|
||||
this.setOutputData(0, this.properties.index)
|
||||
}
|
||||
|
||||
override onAction(action: any) {
|
||||
if (action === "clear") {
|
||||
this.setValue([])
|
||||
}
|
||||
else if (action === "store") {
|
||||
const link = this.getInputLink(0)
|
||||
if (link.data && "images" in link.data) {
|
||||
const data = link.data as GalleryOutput
|
||||
console.debug("[ComfyGalleryNode] Received output!", data)
|
||||
|
||||
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||
|
||||
// const currentValue = get(this.value)
|
||||
// this.setValue(currentValue.concat(galleryItems))
|
||||
this.setValue(galleryItems)
|
||||
}
|
||||
this.setProperty("index", 0)
|
||||
}
|
||||
}
|
||||
|
||||
override formatValue(value: GradioFileData[] | null): string {
|
||||
@@ -452,18 +473,10 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
else {
|
||||
super.setValue([])
|
||||
}
|
||||
}
|
||||
|
||||
receiveOutput() {
|
||||
const link = this.getInputLink(0)
|
||||
if (link.data && "images" in link.data) {
|
||||
const data = link.data as GalleryOutput
|
||||
console.debug("[ComfyGalleryNode] Received output!", data)
|
||||
|
||||
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||
|
||||
const currentValue = get(this.value)
|
||||
this.setValue(currentValue.concat(galleryItems))
|
||||
const len = get(this.value).length
|
||||
if (this.properties.index < 0 || this.properties.index >= len) {
|
||||
this.setProperty("index", clamp(this.properties.index, 0, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as ComfyReroute } from "./ComfyReroute"
|
||||
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
|
||||
export { ComfyCopyAction, ComfySwapAction } from "./ComfyActionNodes"
|
||||
export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyOnExecutedEvent, ComfyExecuteSubgraphAction } from "./ComfyActionNodes"
|
||||
export { default as ComfyValueControl } from "./ComfyValueControl"
|
||||
export { default as ComfySelector } from "./ComfySelector"
|
||||
export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode"
|
||||
|
||||
@@ -11,20 +11,45 @@ type DragItemEntry = {
|
||||
parent: IDragItem | null
|
||||
}
|
||||
|
||||
export type LayoutAttributes = {
|
||||
defaultSubgraph: string
|
||||
}
|
||||
|
||||
export type LayoutState = {
|
||||
root: IDragItem | null,
|
||||
allItems: Record<DragItemID, DragItemEntry>,
|
||||
allItemsByNode: Record<number, DragItemEntry>,
|
||||
currentId: number,
|
||||
currentSelection: DragItemID[],
|
||||
currentSelectionNodes: LGraphNode[],
|
||||
isConfiguring: boolean,
|
||||
isMenuOpen: boolean
|
||||
isMenuOpen: boolean,
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
export type Attributes = {
|
||||
direction: "horizontal" | "vertical",
|
||||
title: string,
|
||||
showTitle: boolean,
|
||||
classes: string,
|
||||
blockVariant?: "block" | "hidden",
|
||||
hidden?: boolean,
|
||||
disabled?: boolean,
|
||||
flexGrow?: number
|
||||
}
|
||||
|
||||
export type AttributesSpec = {
|
||||
name: string,
|
||||
type: string,
|
||||
editable: boolean
|
||||
location: "widget" | "nodeProps" | "nodeVars" | "workflow"
|
||||
editable: boolean,
|
||||
|
||||
values?: string[],
|
||||
hidden?: boolean,
|
||||
validNodeTypes?: string[],
|
||||
|
||||
serialize?: (arg: any) => string,
|
||||
deserialize?: (arg: string) => any,
|
||||
}
|
||||
|
||||
export type AttributesCategorySpec = {
|
||||
@@ -41,40 +66,116 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
{
|
||||
name: "title",
|
||||
type: "string",
|
||||
location: "widget",
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "showTitle",
|
||||
name: "hidden",
|
||||
type: "boolean",
|
||||
editable: true,
|
||||
location: "widget",
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
name: "disabled",
|
||||
type: "boolean",
|
||||
location: "widget",
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
name: "direction",
|
||||
type: "string",
|
||||
type: "enum",
|
||||
location: "widget",
|
||||
editable: true,
|
||||
values: ["horizontal", "vertical"]
|
||||
},
|
||||
{
|
||||
name: "flexGrow",
|
||||
type: "number",
|
||||
location: "widget",
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
name: "classes",
|
||||
type: "string",
|
||||
location: "widget",
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "blockVariant",
|
||||
type: "enum",
|
||||
location: "widget",
|
||||
editable: true,
|
||||
values: ["block", "hidden"]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
categoryName: "behavior",
|
||||
specs: [
|
||||
// Node variables
|
||||
{
|
||||
name: "tags",
|
||||
type: "string",
|
||||
location: "nodeVars",
|
||||
editable: true,
|
||||
serialize: (arg: string[]) => arg.join(","),
|
||||
deserialize: (arg: string) => {
|
||||
if (arg === "")
|
||||
return []
|
||||
return arg.split(",").map(s => s.trim())
|
||||
}
|
||||
},
|
||||
|
||||
// Range
|
||||
{
|
||||
name: "min",
|
||||
type: "number",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
type: "number",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
},
|
||||
{
|
||||
name: "step",
|
||||
type: "number",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
},
|
||||
|
||||
// Button
|
||||
{
|
||||
name: "message",
|
||||
type: "string",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/button"],
|
||||
},
|
||||
|
||||
// Workflow
|
||||
{
|
||||
name: "defaultSubgraph",
|
||||
type: "string",
|
||||
location: "workflow",
|
||||
editable: true
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
export { ALL_ATTRIBUTES };
|
||||
|
||||
export type Attributes = {
|
||||
direction: "horizontal" | "vertical",
|
||||
title: string,
|
||||
showTitle: boolean,
|
||||
classes: string
|
||||
}
|
||||
|
||||
export interface IDragItem {
|
||||
type: string,
|
||||
id: DragItemID,
|
||||
isNodeExecuting?: boolean,
|
||||
attrs: Attributes
|
||||
attrs: Attributes,
|
||||
attrsChanged: Writable<boolean>
|
||||
}
|
||||
|
||||
export interface ContainerLayout extends IDragItem {
|
||||
@@ -112,8 +213,12 @@ const store: Writable<LayoutState> = writable({
|
||||
allItemsByNode: {},
|
||||
currentId: 0,
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: true
|
||||
isConfiguring: true,
|
||||
attrs: {
|
||||
defaultSubgraph: ""
|
||||
}
|
||||
})
|
||||
|
||||
function findDefaultContainerForInsertion(): ContainerLayout | null {
|
||||
@@ -141,11 +246,14 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
||||
const dragItem: ContainerLayout = {
|
||||
type: "container",
|
||||
id: `${state.currentId++}`,
|
||||
attrsChanged: writable(false),
|
||||
attrs: {
|
||||
title: "Container",
|
||||
showTitle: true,
|
||||
direction: "vertical",
|
||||
classes: "",
|
||||
blockVariant: "block",
|
||||
flexGrow: 100,
|
||||
...attrs
|
||||
}
|
||||
}
|
||||
@@ -166,11 +274,13 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
||||
type: "widget",
|
||||
id: `${state.currentId++}`,
|
||||
node: node,
|
||||
attrsChanged: writable(false),
|
||||
attrs: {
|
||||
title: widgetName,
|
||||
showTitle: true,
|
||||
direction: "horizontal",
|
||||
classes: "",
|
||||
flexGrow: 100,
|
||||
...attrs
|
||||
}
|
||||
}
|
||||
@@ -310,7 +420,9 @@ function groupItems(dragItems: IDragItem[], attrs: Partial<Attributes> = {}): Co
|
||||
index = indexFound
|
||||
}
|
||||
|
||||
const container = addContainer(parent as ContainerLayout, attrs, index)
|
||||
const title = dragItems.length <= 1 ? "" : "Group";
|
||||
|
||||
const container = addContainer(parent as ContainerLayout, { title, ...attrs }, index)
|
||||
|
||||
for (const item of dragItems) {
|
||||
moveItem(item, container)
|
||||
@@ -366,15 +478,17 @@ function initDefaultLayout() {
|
||||
store.set({
|
||||
root: null,
|
||||
allItems: {},
|
||||
allItemsByNode: {},
|
||||
currentId: 0,
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false
|
||||
})
|
||||
|
||||
const root = addContainer(null, { direction: "horizontal", showTitle: false });
|
||||
const left = addContainer(root, { direction: "vertical", showTitle: false });
|
||||
const right = addContainer(root, { direction: "vertical", showTitle: false });
|
||||
const root = addContainer(null, { direction: "horizontal", title: "" });
|
||||
const left = addContainer(root, { direction: "vertical", title: "" });
|
||||
const right = addContainer(root, { direction: "vertical", title: "" });
|
||||
|
||||
const state = get(store)
|
||||
state.root = root;
|
||||
@@ -387,6 +501,7 @@ export type SerializedLayoutState = {
|
||||
root: DragItemID | null,
|
||||
allItems: Record<DragItemID, SerializedDragEntry>,
|
||||
currentId: number,
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
export type SerializedDragEntry = {
|
||||
@@ -424,6 +539,7 @@ function serialize(): SerializedLayoutState {
|
||||
root: state.root?.id,
|
||||
allItems,
|
||||
currentId: state.currentId,
|
||||
attrs: state.attrs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +552,8 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
const dragItem: IDragItem = {
|
||||
type: entry.dragItem.type,
|
||||
id: entry.dragItem.id,
|
||||
attrs: entry.dragItem.attrs
|
||||
attrs: entry.dragItem.attrs,
|
||||
attrsChanged: writable(false)
|
||||
};
|
||||
|
||||
const dragEntry: DragItemEntry = {
|
||||
@@ -476,8 +593,10 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
allItemsByNode,
|
||||
currentId: data.currentId,
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false
|
||||
isConfiguring: false,
|
||||
attrs: data.attrs
|
||||
}
|
||||
|
||||
console.debug("[layoutState] deserialize", data, state)
|
||||
|
||||
@@ -9,7 +9,7 @@ export type UIState = {
|
||||
nodesLocked: boolean,
|
||||
graphLocked: boolean,
|
||||
autoAddUI: boolean,
|
||||
uiEditMode: UIEditMode
|
||||
uiEditMode: UIEditMode,
|
||||
}
|
||||
|
||||
export type WritableUIStateStore = Writable<UIState>;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import ComfyApp from "./components/ComfyApp";
|
||||
import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
|
||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||
import { get } from "svelte/store"
|
||||
import layoutState from "$lib/stores/layoutState"
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import type { SerializedLGraph } from "@litegraph-ts/core";
|
||||
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(n, min), max)
|
||||
@@ -36,6 +37,8 @@ export function startDrag(evt: MouseEvent) {
|
||||
|
||||
const item = ls.allItems[dragItemId].dragItem
|
||||
|
||||
console.debug("startDrag", item)
|
||||
|
||||
if (evt.ctrlKey) {
|
||||
const index = ls.currentSelection.indexOf(item.id)
|
||||
if (index === -1)
|
||||
@@ -47,8 +50,48 @@ export function startDrag(evt: MouseEvent) {
|
||||
else {
|
||||
ls.currentSelection = [item.id]
|
||||
}
|
||||
ls.currentSelectionNodes = [];
|
||||
|
||||
layoutState.set(ls)
|
||||
};
|
||||
|
||||
export function stopDrag(evt: MouseEvent) {
|
||||
};
|
||||
|
||||
export function workflowToGraphVis(workflow: SerializedLGraph): string {
|
||||
let out = "digraph {\n"
|
||||
|
||||
for (const link of workflow.links) {
|
||||
const nodeA = workflow.nodes.find(n => n.id === link[1])
|
||||
const nodeB = workflow.nodes.find(n => n.id === link[3])
|
||||
out += `"${link[1]}_${nodeA.title}" -> "${link[3]}_${nodeB.title}"\n`;
|
||||
}
|
||||
|
||||
out += "}"
|
||||
return out
|
||||
}
|
||||
|
||||
export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||
let out = "digraph {\n"
|
||||
|
||||
for (const pair of Object.entries(prompt.output)) {
|
||||
const [id, o] = pair;
|
||||
const outNode = prompt.workflow.nodes.find(n => n.id == id)
|
||||
for (const pair2 of Object.entries(o.inputs)) {
|
||||
const [inpName, i] = pair2;
|
||||
|
||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||
// Link
|
||||
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
||||
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
||||
}
|
||||
else {
|
||||
// Value
|
||||
out += `"${id}-${inpName}-${typeof i}" -> "${outNode.title}"\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out += "}"
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -28,15 +28,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-button">
|
||||
<div class="wrapper gradio-button">
|
||||
{#if node !== null}
|
||||
<Button on:click={onClick} variant="primary" {style}>
|
||||
<Button
|
||||
disabled={widget.attrs.disabled}
|
||||
on:click={onClick}
|
||||
variant="primary"
|
||||
{style}>
|
||||
{widget.attrs.title}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
|
||||
@@ -63,16 +63,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-combo" class:updated={werePropsChanged}>
|
||||
<div class="wrapper comfy-combo" class:updated={werePropsChanged}>
|
||||
{#key $propsChanged}
|
||||
{#if node !== null && nodeValue !== null}
|
||||
<label>
|
||||
{#if widget.attrs.title !== ""}
|
||||
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
|
||||
{/if}
|
||||
<Select
|
||||
bind:value={option}
|
||||
bind:items={node.properties.values}
|
||||
disabled={node.properties.values.length === 0}
|
||||
items={node.properties.values}
|
||||
disabled={widget.attrs.disabled || node.properties.values.length === 0}
|
||||
clearable={false}
|
||||
showChevron={true}
|
||||
on:change
|
||||
on:select
|
||||
on:filter
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||
import { clamp } from "$lib/utils";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
let node: ComfyGalleryNode | null = null;
|
||||
@@ -21,6 +23,11 @@
|
||||
node = widget.node as ComfyGalleryNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
|
||||
const len = $nodeValue.length
|
||||
if (node.properties.index < 0 || node.properties.index >= len) {
|
||||
node.setProperty("index", clamp(node.properties.index, 0, len))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,7 +38,8 @@
|
||||
}
|
||||
let element: HTMLDivElement;
|
||||
|
||||
function updateForLightbox() {
|
||||
function onSelect(e: CustomEvent<GradioSelectData>) {
|
||||
// Setup lightbox
|
||||
// Wait for gradio gallery to show the large preview image, if no timeout then
|
||||
// the event might fire too early
|
||||
setTimeout(() => {
|
||||
@@ -41,21 +49,24 @@
|
||||
}
|
||||
ImageViewer.instance.updateOnBackgroundChange();
|
||||
}, 200)
|
||||
|
||||
// Update index
|
||||
node.setProperty("index", e.detail.index as number)
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="wrapper comfy-gallery-widget gr-gallery" bind:this={element}>
|
||||
<div class="wrapper comfy-gallery-widget gradio-gallery" bind:this={element}>
|
||||
{#if widget && node && nodeValue}
|
||||
<Block variant="solid" padding={false}>
|
||||
<div class="padding">
|
||||
<Gallery
|
||||
bind:value={$nodeValue}
|
||||
label={widget.attrs.title}
|
||||
show_label={true}
|
||||
show_label={widget.attrs.title !== ""}
|
||||
{style}
|
||||
root={""}
|
||||
root_url={""}
|
||||
on:select={updateForLightbox}
|
||||
on:select={onSelect}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
@@ -37,12 +37,26 @@
|
||||
$nodeValue = option
|
||||
}
|
||||
}
|
||||
|
||||
let gradient: string = ""
|
||||
let elem: HTMLDivElement = null;
|
||||
|
||||
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
|
||||
const slider = elem.querySelector("input[type='range']") as any
|
||||
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
|
||||
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
|
||||
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
|
||||
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
|
||||
const style = elem.style;
|
||||
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-range">
|
||||
<div class="wrapper gradio-slider" bind:this={elem}>
|
||||
{#if node !== null && option !== null}
|
||||
<Range
|
||||
bind:value={option}
|
||||
disabled={widget.attrs.disabled}
|
||||
minimum={node.properties.min}
|
||||
maximum={node.properties.max}
|
||||
step={node.properties.step}
|
||||
|
||||
@@ -27,11 +27,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-textbox">
|
||||
<div class="wrapper gradio-textbox">
|
||||
{#if node !== null && nodeValue !== null}
|
||||
<TextBox
|
||||
bind:value={$nodeValue}
|
||||
label={widget.attrs.title}
|
||||
disabled={widget.attrs.disabled}
|
||||
lines={node.properties.multiline ? 5 : 1}
|
||||
max_lines={node.properties.multiline ? 5 : 1}
|
||||
show_label={true}
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
@import "gradio"
|
||||
@import "gradio";
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
815
src/scss/ux.scss
Normal file
815
src/scss/ux.scss
Normal file
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
Theme Name: DarkUX
|
||||
Author: anapnoe
|
||||
Author URI: https://github.com/anapnoe/stable-diffusion-webui
|
||||
Version: 1.0
|
||||
License: GNU General Public License
|
||||
*/
|
||||
:root{
|
||||
--ae-extra-networks-card-size: 1;
|
||||
--ae-extra-networks-card-real-size: calc(var(--ae-extra-networks-card-size) * 14vh);
|
||||
--ae-extra-networks-visible-rows: 2;
|
||||
--ae-extra-networks-height: calc((var(--ae-extra-networks-card-real-size) * var(--ae-extra-networks-visible-rows)) + ( var(--ae-inside-padding-size) * 2 ) );
|
||||
--ae-extra-networks-name-size: calc(var(--ae-extra-networks-card-size) * 1em);
|
||||
|
||||
--ae-top-header-padding-top:16px;
|
||||
--ae-top-header-padding-bottom:16px;
|
||||
--ae-top-header-inner-height:38px;
|
||||
--ae-top-header-height: calc( var(--ae-top-header-padding-top) + var(--ae-top-header-inner-height) + var(--ae-top-header-padding-bottom) );
|
||||
|
||||
--ae-container-padding:16px;
|
||||
--ae-footer-height: calc( 32px + (var(--ae-container-padding) * 2) );
|
||||
--ae-gallery-bottom-height: calc(24px + (var(--ae-max-padding) * 2) + 16px + (var(--ae-inside-padding-size) * 2) + (var(--ae-outside-gap-size)* 3 ));
|
||||
|
||||
--ae-subtract-total: calc( var(--ae-top-header-height) + var(--ae-footer-height));
|
||||
--ae-container-height : calc(100vh - var(--ae-subtract-total));
|
||||
--ae-container-total-height : calc( var(--ae-container-height) - (var(--ae-outside-gap-size) * 2) - (var(--ae-inside-padding-size) * 2));
|
||||
--ae-container-height-gap : calc( var(--ae-container-height) - (var(--ae-outside-gap-size) * 2));
|
||||
--ae-container-height-pad : calc( var(--ae-container-height) - (var(--ae-inside-padding-size) * 2));
|
||||
|
||||
|
||||
--ae-processing-border : 2px;
|
||||
--ae-processing-border-double: var(--ae-processing-border) * 2;
|
||||
|
||||
--ae-slider-bg-overlay : transparent;
|
||||
|
||||
|
||||
--ae-border-width: 1px;
|
||||
--ae-accordion-vertical-padding: max(8px, var(--ae-inside-padding-size));
|
||||
--ae-accordion-horizontal-padding: max(4px, var(--ae-inside-padding-size));
|
||||
--ae-accordion-line-height: 24px;
|
||||
--ae-accordion-header-height: calc(var(--ae-accordion-line-height) + var(--ae-accordion-vertical-padding) * 2);
|
||||
|
||||
--ae-results-height: calc(100vh - (var(--ae-top-header-height) + var(--ae-footer-height) + var(--ae-accordion-header-height) + var(--ae-outside-gap-size) * 4 + 38px));
|
||||
|
||||
|
||||
}
|
||||
|
||||
:root{
|
||||
--ae-main-bg-color:hsl(0deg 0% 10%);
|
||||
--ae-primary-color:hsl(168deg 96% 42%);
|
||||
--ae-input-bg-color:hsl(225deg 6% 13%);
|
||||
--ae-input-border-color:hsl(214deg 5% 30%);
|
||||
--ae-panel-bg-color:hsl(225deg 5% 17%);
|
||||
--ae-panel-border-color:hsl(214deg 5% 30%);
|
||||
--ae-panel-border-radius:0px;
|
||||
--ae-panel-border-width:1px;
|
||||
--ae-subgroup-bg-color:hsl(0deg 0% 10%);
|
||||
--ae-subgroup-input-bg-color:hsl(225deg 6% 13%);
|
||||
--ae-subgroup-input-border-color:hsl(214deg 5% 30%);
|
||||
--ae-subpanel-bg-color:hsl(220deg 4% 14%);
|
||||
--ae-subpanel-border-color:hsl(214deg 5% 30%);
|
||||
--ae-subpanel-border-radius:8px;
|
||||
--ae-textarea-focus-color:hsl(210deg 3% 36%);
|
||||
--ae-input-focus-color:hsl(168deg 97% 41%);
|
||||
--ae-outside-gap-size:8px;
|
||||
--ae-inside-padding-size:8px;
|
||||
--ae-tool-button-size:34px;
|
||||
--ae-tool-button-radius:16px;
|
||||
--ae-generate-button-height:70px;
|
||||
--ae-cancel-color:hsl(0deg 84% 60%);
|
||||
--ae-max-padding:max(var(--ae-outside-gap-size),var(--ae-inside-padding-size));
|
||||
--ae-icon-color:hsl(168deg 96% 42%);
|
||||
--ae-icon-hover-color:hsl(0deg 0% 10%);
|
||||
--ae-icon-size:22px;
|
||||
--ae-nav-bg-color:hsl(0deg 0% 4%);
|
||||
--ae-nav-color:hsl(210deg 4% 80%);
|
||||
--ae-nav-hover-color:hsl(0deg 0% 4%);
|
||||
--ae-input-color:hsl(210deg 4% 80%);
|
||||
--ae-label-color:hsl(210deg 4% 80%);
|
||||
--ae-subgroup-input-color:hsl(0deg 100% 100%);
|
||||
--ae-placeholder-color:hsl(214deg 5% 30%);
|
||||
--ae-text-color:hsl(210deg 4% 80%);
|
||||
--ae-mobile-outside-gap-size:3px;
|
||||
--ae-mobile-inside-padding-size:3px;
|
||||
--ae-frame-bg-color:hsl(225deg 6% 13%);
|
||||
--ae-modal-bg-color:hsl(0deg 0% 10%);
|
||||
--ae-modal-icon-color:hsl(168deg 97% 41%);
|
||||
--ae-selected-color:hsl(42deg, 100%, 42%);
|
||||
}/*BREAKPOINT_CSS_CONTENT*/
|
||||
|
||||
:root {
|
||||
--toastColor: var(--ae-text-color);
|
||||
--toastBackground: var(--ae-panel-bg-color);
|
||||
--toastBorder: 1px solid var(--ae-panel-border-color)
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 860px) {
|
||||
:root{
|
||||
--ae-outside-gap-size: var(--ae-mobile-outside-gap-size);
|
||||
--ae-inside-padding-size: var(--ae-mobile-inside-padding-size);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--ae-main-bg-color) !important;
|
||||
}
|
||||
|
||||
#main {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
padding: var(--size-4);
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
min-height: 100vh !important;
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
background-color: var(--ae-main-bg-color);
|
||||
}
|
||||
|
||||
.v-pane {
|
||||
gap: var(--ae-outside-gap-size) !important;
|
||||
|
||||
&.empty {
|
||||
border-color: var(--ae-subpanel-border-color) !important;
|
||||
border-radius: 0 !important;
|
||||
background: var(--ae-subpanel-bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
&.selected {
|
||||
background: var(--ae-primary-color) !important;
|
||||
> .block.padded {
|
||||
background: var(--ae-primary-color) !important;
|
||||
}
|
||||
}
|
||||
> .block {
|
||||
background: var(--ae-panel-bg-color) !important;
|
||||
border-radius: var(--ae-panel-border-radius) !important;
|
||||
}
|
||||
&.z-index0 {
|
||||
> .block {
|
||||
background: var(--ae-main-bg-color) !important;
|
||||
}
|
||||
}
|
||||
&.z-index1, &.z-index2 {
|
||||
// padding: var(--ae-outside-gap-size);
|
||||
// border: 1px solid var(--ae-panel-border-color);
|
||||
> .block {
|
||||
background: var(--ae-frame-bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.edit) {
|
||||
&.z-index1 > .block {
|
||||
padding: calc(var(--ae-outside-gap-size) / 2) !important;
|
||||
border-width: 0px !important;
|
||||
}
|
||||
|
||||
> .block {
|
||||
border: solid var(--ae-panel-border-width) var(--ae-panel-border-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
&.handle-hidden {
|
||||
background-color: hsla(0deg 84% 60% / 70%);
|
||||
}
|
||||
}
|
||||
|
||||
.animation-wrapper > .widget:not(.selected) {
|
||||
background: var(--ae-panel-bg-color) !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
.z-index0, .z-index1, .z-index2 {
|
||||
> .block > .v-pane > .animation-wrapper > .widget:not(.edit) {
|
||||
padding: var(--ae-inside-padding-size) !important;
|
||||
border: 1px solid var(--ae-panel-border-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.widget:has(> .gradio-button) {
|
||||
height: 100%;
|
||||
|
||||
.gradio-button {
|
||||
height: 100%;
|
||||
|
||||
button {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradio-gallery > .block {
|
||||
background: var(--ae-main-bg-color);
|
||||
border-color: var(--ae-panel-border-color);
|
||||
border-radius: 0px;
|
||||
|
||||
.thumbnail-item {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid var(--ae-panel-border-color) !important;
|
||||
border-radius: 0 !important;
|
||||
background: var(--ae-main-bg-color)!important;
|
||||
aspect-ratio: unset !important;
|
||||
overflow: visible !important;
|
||||
object-fit: contain !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--ae-input-color);
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: var(--ae-main-bg-color);
|
||||
|
||||
button {
|
||||
outline: none!important;
|
||||
box-shadow: none!important;
|
||||
border: 1px solid var(--ae-input-border-color)!important;
|
||||
border-radius: var(--ae-panel-border-radius)!important;
|
||||
background: var(--ae-input-bg-color)!important;
|
||||
text-align: left!important;
|
||||
min-width: unset;
|
||||
|
||||
&:hover {
|
||||
color: var(--ae-input-color)!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradio-gallery .preview.fixed-height {
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-height: calc(var(--container-height) - 4px);
|
||||
}
|
||||
|
||||
/* small info upload*/
|
||||
div.float {
|
||||
background: var(--ae-main-bg-color)!important;
|
||||
border: 0 !important;
|
||||
color: var(--ae-primary-color)!important;
|
||||
}
|
||||
|
||||
.widget {
|
||||
&.selected {
|
||||
background: var(--ae-primary-color) !important;
|
||||
}
|
||||
&.edit:not(.selected) {
|
||||
border-width: 2px;
|
||||
border-color: var(--ae-primary-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.target-name {
|
||||
background: var(--ae-subpanel-bg-color) !important;
|
||||
border-color: var(--ae-subpanel-border-color) !important;
|
||||
|
||||
.title, .type {
|
||||
color: var(--ae-label-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
background: var(--ae-panel-bg-color) !important;
|
||||
border-color: var(--ae-panel-border-color) !important;
|
||||
|
||||
.title, .type {
|
||||
color: var(--ae-label-color)
|
||||
}
|
||||
}
|
||||
|
||||
.props-entry {
|
||||
border-width: 1px;
|
||||
border-left: 1px var(--ae-panel-border-color) !important;
|
||||
border-right: 1px var(--ae-panel-border-color) !important;
|
||||
}
|
||||
|
||||
// .container > .block {
|
||||
// box-shadow: none !important;
|
||||
// border-color: var(--ae-panel-border-color) !important;
|
||||
// border-radius: 0 !important;
|
||||
// background: var(--ae-input-bg-color) !important;
|
||||
|
||||
// > .v-pane {
|
||||
// box-shadow: none !important;
|
||||
// border-color: var(--ae-panel-border-color) !important;
|
||||
// border-radius: 0 !important;
|
||||
// background: var(--ae-input-bg-color) !important;
|
||||
// }
|
||||
// }
|
||||
|
||||
.block.gradio-accordion:hover .label-wrap {
|
||||
color: var(--ae-main-bg-color) !important;
|
||||
background-color: var(--ae-primary-color) !important;
|
||||
}
|
||||
|
||||
.block.gradio-accordion > div.wrap {
|
||||
pointer-events: all !important;
|
||||
cursor: pointer;
|
||||
width: auto !important;
|
||||
height: var(--ae-accordion-header-height)!important;
|
||||
z-index: 1;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
opacity: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.form>.gradio-row>.form{
|
||||
border:0 !important;
|
||||
}
|
||||
.padded {
|
||||
padding: var(--ae-inside-padding-size) !important
|
||||
}
|
||||
.gradio-row,
|
||||
.gap {
|
||||
gap: var(--ae-outside-gap-size) !important
|
||||
}
|
||||
button.tool {
|
||||
max-width: 34px;
|
||||
min-height: 34px;
|
||||
min-width: 34px !important;
|
||||
}
|
||||
|
||||
div.block.padded {
|
||||
/*box-shadow: var(--block-shadow);*/
|
||||
border-width: var(--ae-border-width);
|
||||
border-color: var(--ae-panel-border-color);
|
||||
border-radius: var(--ae-panel-border-radius) !important;
|
||||
background: var(--ae-panel-bg-color);
|
||||
/*width: 100%;
|
||||
line-height: var(--line-sm);*/
|
||||
}
|
||||
fieldset.block.padded
|
||||
{
|
||||
background-color: var(--ae-panel-bg-color) !important;
|
||||
/*border-width: var(--ae-border-width) !important;*/
|
||||
/*border-color: var(--ae-panel-border-color) !important;*/
|
||||
border-radius: var(--ae-panel-border-radius) !important;
|
||||
}
|
||||
|
||||
div.svelte-b6y5bg,
|
||||
div.gradio-row>.form{
|
||||
/*box-shadow: var(--block-shadow);*/
|
||||
border-width: var(--ae-border-width) !important;
|
||||
border-color: var(--ae-panel-border-color) !important;
|
||||
border-radius: var(--ae-panel-border-radius) !important;
|
||||
background: var(--ae-panel-border-color) !important;
|
||||
/*width: 100%;
|
||||
line-height: var(--line-sm);*/
|
||||
}
|
||||
|
||||
.block.gradio-dropdown,
|
||||
.block.gradio-slider,
|
||||
.block.gradio-checkbox,
|
||||
.block.gradio-textbox,
|
||||
.block.gradio-radio,
|
||||
.block.gradio-checkboxgroup,
|
||||
.block.gradio-number,
|
||||
.block.gradio-colorpicker
|
||||
{
|
||||
border-width: 0;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.wrapper.gradio-textbox textarea {
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.gradio-dropdown input{
|
||||
margin:0 !important;
|
||||
}
|
||||
|
||||
.block.gradio-dropdown span.single-select{
|
||||
color: var(--ae-input-color)!important;
|
||||
}
|
||||
.dropdown-arrow.svelte-p5edak {
|
||||
fill: var(--ae-input-color)!important;
|
||||
}
|
||||
|
||||
.wrap.svelte-1p9xokt.svelte-1p9xokt.svelte-1p9xokt label,
|
||||
.wrap.svelte-1qxcj04.svelte-1qxcj04.svelte-1qxcj04 label,
|
||||
button.tool.secondary,
|
||||
button.secondary,
|
||||
.gradio-dropdown label .wrap,
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=email],
|
||||
textarea,
|
||||
input[type=number],
|
||||
select {
|
||||
outline: none!important;
|
||||
box-shadow: none!important;
|
||||
border: 1px solid var(--ae-input-border-color)!important;
|
||||
border-radius: var(--ae-panel-border-radius)!important;
|
||||
background: var(--ae-input-bg-color)!important;
|
||||
color: var(--ae-input-color)!important;
|
||||
text-align: left!important;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
select:after{
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #f00;
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
right: 5px;
|
||||
content: "";
|
||||
z-index: 98;
|
||||
}
|
||||
|
||||
button.tool.secondary,
|
||||
button.secondary{
|
||||
text-align: center!important;
|
||||
}
|
||||
|
||||
.gradio-container-3-28-1 .prose * {
|
||||
color: var(--ae-label-color);
|
||||
}
|
||||
|
||||
.gradio-container-3-23-0 .prose code {
|
||||
background-color: var(--ae-panel-bg-color);
|
||||
border-radius: var(--ae-panel-bg-color);
|
||||
border: 1px solid var(--ae-panel-border-color);
|
||||
padding: 0 !important;
|
||||
margin: 0!important;
|
||||
white-space: break-spaces !important;
|
||||
}
|
||||
|
||||
[type=text],
|
||||
[type=email],
|
||||
[type=url],
|
||||
[type=password],
|
||||
[type=number],
|
||||
[type=date],
|
||||
[type=datetime-local],
|
||||
[type=month],
|
||||
[type=search],
|
||||
[type=tel],
|
||||
[type=time],
|
||||
[type=week],
|
||||
[multiple],
|
||||
textarea,
|
||||
select {
|
||||
line-height: 1.5rem;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
button.tool.secondary,
|
||||
button.secondary,
|
||||
.gradio-dropdown label .wrap,
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=email],
|
||||
textarea,
|
||||
input[type=number] {
|
||||
outline: none!important;
|
||||
box-shadow: none!important;
|
||||
border: 1px solid var(--ae-input-border-color)!important;
|
||||
border-radius: var(--ae-panel-border-radius)!important;
|
||||
background: var(--ae-input-bg-color)!important;
|
||||
color: var(--ae-input-color)!important;
|
||||
text-align: left!important;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
input[type=checkbox], input[type=radio] {
|
||||
background-color: var(--ae-input-bg-color) !important;
|
||||
border: 1px solid var(--ae-input-border-color) !important;
|
||||
border-radius: var(--ae-panel-border-radius) !important;
|
||||
}
|
||||
input[type=checkbox]:checked, input[type=radio]:checked {
|
||||
background-color: var(--ae-primary-color) !important;
|
||||
}
|
||||
|
||||
.gradio-slider input[type=number] {
|
||||
padding-right: 2px!important;
|
||||
max-height:24px !important;
|
||||
// width: 64px !important;
|
||||
margin-bottom: var(--ae-inside-padding-size);
|
||||
}
|
||||
|
||||
|
||||
.gradio-dropdown:not(.multiselect) .wrap-inner{
|
||||
padding: 0px 5px !important;
|
||||
height:32px !important;
|
||||
}
|
||||
fieldset span,
|
||||
label > span{
|
||||
color:var(--ae-label-color) !important;
|
||||
}
|
||||
.gradio-radio label > span{
|
||||
color:var(--ae-input-color) !important;
|
||||
}
|
||||
|
||||
input[type=number],
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=email] {
|
||||
height: 34px;
|
||||
}
|
||||
.gradio-slider input[type=range] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
span.svelte-1gfkn6j:not(.has-info) {
|
||||
margin-top: 1px;
|
||||
margin-left: 1px;
|
||||
margin-bottom: var(--ae-inside-padding-size);
|
||||
}
|
||||
|
||||
/* input column alignment */
|
||||
label.block{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
div.block.padded.gradio-slider {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
/***********/
|
||||
/* Buttons */
|
||||
/***********/
|
||||
|
||||
button.secondary,
|
||||
button.primary {
|
||||
border: 1px solid var(--ae-input-border-color) !important;
|
||||
border-radius: var(--ae-panel-border-radius) !important;
|
||||
background: var(--ae-input-bg-color) !important;
|
||||
color: var(--ae-input-color) !important;
|
||||
}
|
||||
|
||||
button.secondary:hover,
|
||||
button.primary:hover {
|
||||
background: var(--ae-primary-color) !important;
|
||||
color: var(--ae-input-bg-color) !important;
|
||||
}
|
||||
|
||||
/**********************/
|
||||
/* Sliders Scrollbars */
|
||||
/**********************/
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
[id$="2img_settings_scroll"]::-webkit-scrollbar
|
||||
{
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 10px 10px var(--ae-main-bg-color);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
box-shadow: inset 0 0 10px 10px var(--ae-panel-bg-color);
|
||||
|
||||
&.horizontal:hover, &.vertical:hover {
|
||||
background: var(--ae-primary-color)
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb,
|
||||
::-webkit-scrollbar-track {
|
||||
border-left: solid 6px var(--ae-main-bg-color);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/**********/
|
||||
/* Ranges */
|
||||
/**********/
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
input[type=range] {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
background-color: var(--ae-input-bg-color);
|
||||
border: 1px solid var(--ae-input-border-color);
|
||||
position:relative;
|
||||
}
|
||||
input[type=range]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 13px;
|
||||
background-image: var(--ae-slider-bg-overlay);
|
||||
opacity: 0.15;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
height: 14px;
|
||||
-webkit-appearance: none;
|
||||
color: var(--ae-primary-color);
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
width: 0px;
|
||||
-webkit-appearance: none;
|
||||
height: 14px;
|
||||
cursor: ew-resize;
|
||||
background-color: var(--ae-primary-color);
|
||||
box-shadow: -1024px 0 0 1024px var(--ae-primary-color);
|
||||
}
|
||||
|
||||
[id$="_sub-group"] input[type=range]
|
||||
{
|
||||
|
||||
background-color: var(--ae-subgroup-input-bg-color);
|
||||
border: 1px solid var(--ae-subgroup-input-border-color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Firefox */
|
||||
|
||||
input[type=range]::-moz-range-progress {
|
||||
background-color: var(--ae-primary-color);
|
||||
height: 14px;
|
||||
border: 1px solid var(--ae-primary-color);
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track {
|
||||
background-color: var(--ae-input-bg-color);
|
||||
}
|
||||
|
||||
input[type=range]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 13px;
|
||||
background-image: var(--ae-slider-bg-overlay);
|
||||
opacity: 0.15;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#quicksettings_overflow_container,
|
||||
#theme_overflow_container,
|
||||
[id$="2img_checkpoints_cards"],
|
||||
[id$="2img_results"],
|
||||
[id$="2img_settings_scroll"]
|
||||
{
|
||||
scrollbar-color: var(--ae-panel-bg-color) var(--ae-main-bg-color) !important;
|
||||
scrollbar-width: thin !important;
|
||||
/*padding: 0 1px;*/
|
||||
}
|
||||
|
||||
|
||||
input[type=range]{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
background-color: var(--ae-input-bg-color);
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-image: var(--ae-slider-bg-overlay);
|
||||
opacity: 0.15;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-thumb {
|
||||
border: 0px solid var(--ae-primary-color);
|
||||
width: 0px;
|
||||
border-radius: 0%;
|
||||
background-color: var(--ae-primary-color);
|
||||
|
||||
}
|
||||
|
||||
/*hide the outline behind the border*/
|
||||
input[type=range]:-moz-focusring{
|
||||
outline: 1px solid var(--ae-primary-color);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
input[type=range]:focus::-moz-range-track {
|
||||
background-color: var(--ae-input-bg-color);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
input[type="number"]:hover,
|
||||
input[type="number"]:focus {
|
||||
-moz-appearance: initial;
|
||||
}
|
||||
|
||||
|
||||
/* IE maybe later */
|
||||
|
||||
input[type=range]::-ms-fill-lower {
|
||||
background-color: var(--ae-primary-color);
|
||||
}
|
||||
|
||||
input[type=range]::-ms-fill-upper {
|
||||
background-color: var(--ae-input-bg-color);
|
||||
}
|
||||
|
||||
@keyframes light-up {
|
||||
from {
|
||||
background-color: var(--ae-selected-color);
|
||||
}
|
||||
to {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
|
||||
.comfy-combo.updated {
|
||||
|
||||
}
|
||||
|
||||
.splitpanes.comfy .splitpanes__splitter {
|
||||
background: var(--ae-panel-bg-color);
|
||||
border: 1px solid var(--ae-panel-border-color);
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
background: var(--ae-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.queue .bottom {
|
||||
color: var(--ae-text-color);
|
||||
> .queue-remaining {
|
||||
background-color: var(--ae-panel-bg-color);
|
||||
border: 1px solid var(--ae-panel-border-color);
|
||||
}
|
||||
> .node-name {
|
||||
background-color: var(--ae-panel-bg-color);
|
||||
border: 1px solid var(--ae-panel-border-color);
|
||||
}
|
||||
.progress {
|
||||
background-color: var(--ae-panel-bg-color);
|
||||
border: 1px solid var(--ae-panel-border-color);
|
||||
> .bar {
|
||||
color: var(--ae-main-bg-color);
|
||||
background-color: var(--ae-primary-color);
|
||||
|
||||
> .label {
|
||||
color: var(--ae-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget > .wrapper.comfy-combo .svelte-select {
|
||||
--font-size: 13px;
|
||||
--height: 24px;
|
||||
--input-padding: 0px;
|
||||
--chevron-width: 24px;
|
||||
--chevron-height: 24px;
|
||||
--padding: 0 0 0 8px;
|
||||
|
||||
background: var(--ae-input-bg-color) !important;
|
||||
border-radius: 0px !important;
|
||||
border-color: var(--ae-input-border-color) !important;
|
||||
> .svelte-select-list {
|
||||
background: var(--ae-panel-bg-color);
|
||||
border-radius: 0px !important;
|
||||
> .list-item > .item {
|
||||
border-radius: 0px !important;
|
||||
color: var(--ae-input-color) !important;
|
||||
transition: none;
|
||||
|
||||
&.active {
|
||||
background: var(--ae-input-focus-color);
|
||||
color: var(--ae-input-bg-color) !important;
|
||||
}
|
||||
|
||||
&.hover:not(.active) {
|
||||
background: var(--ae-textarea-focus-color)
|
||||
}
|
||||
}
|
||||
}
|
||||
> .value-container {
|
||||
> .selected-item {
|
||||
color: var(--ae-input-color) !important;
|
||||
z-index: 100;
|
||||
}
|
||||
> input {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon.chevron {
|
||||
color: var(--ae-input-color) !important;
|
||||
}
|
||||
}
|
||||
BIN
static/screenshot2.png
Normal file
BIN
static/screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -3,15 +3,28 @@ import { defineConfig } from 'vitest/config';
|
||||
import { resolve } from 'path';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import FullReload from 'vite-plugin-full-reload';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
|
||||
export default defineConfig({
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
svelte(),
|
||||
// FullReload(["src/**/*.{js,ts,svelte}"])
|
||||
FullReload(["src/**/*.{js,ts,scss,svelte}"]),
|
||||
svelte(), ,
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: 'bin/run.sh',
|
||||
dest: './'
|
||||
},
|
||||
{
|
||||
src: 'bin/run.bat',
|
||||
dest: './'
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias:{
|
||||
alias: {
|
||||
'$lib': resolve(__dirname, './src/lib'),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user