Merge pull request #26 from space-nuko/container-variants
Container variants
This commit is contained in:
Submodule litegraph updated: 6cbae97b3c...39b040a0b1
@@ -33,12 +33,14 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gradio/accordion": "workspace:*",
|
||||||
"@gradio/atoms": "workspace:*",
|
"@gradio/atoms": "workspace:*",
|
||||||
"@gradio/button": "workspace:*",
|
"@gradio/button": "workspace:*",
|
||||||
"@gradio/client": "workspace:*",
|
"@gradio/client": "workspace:*",
|
||||||
"@gradio/form": "workspace:*",
|
"@gradio/form": "workspace:*",
|
||||||
"@gradio/gallery": "workspace:*",
|
"@gradio/gallery": "workspace:*",
|
||||||
"@gradio/icons": "workspace:*",
|
"@gradio/icons": "workspace:*",
|
||||||
|
"@gradio/tabs": "workspace:*",
|
||||||
"@gradio/theme": "workspace:*",
|
"@gradio/theme": "workspace:*",
|
||||||
"@gradio/upload": "workspace:*",
|
"@gradio/upload": "workspace:*",
|
||||||
"@gradio/utils": "workspace:*",
|
"@gradio/utils": "workspace:*",
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -4,6 +4,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@gradio/accordion':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:gradio/js/accordion
|
||||||
'@gradio/atoms':
|
'@gradio/atoms':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:gradio/js/atoms
|
version: link:gradio/js/atoms
|
||||||
@@ -22,6 +25,9 @@ importers:
|
|||||||
'@gradio/icons':
|
'@gradio/icons':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:gradio/js/icons
|
version: link:gradio/js/icons
|
||||||
|
'@gradio/tabs':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:gradio/js/tabs
|
||||||
'@gradio/theme':
|
'@gradio/theme':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:gradio/js/theme
|
version: link:gradio/js/theme
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export class ImageViewer {
|
|||||||
showModal(event: Event) {
|
showModal(event: Event) {
|
||||||
const source = (event.target || event.srcElement) as HTMLImageElement;
|
const source = (event.target || event.srcElement) as HTMLImageElement;
|
||||||
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
||||||
|
console.debug("[ImageViewer] showModal", event, source, galleryElem);
|
||||||
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
|
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
|
||||||
console.error("No buttons found on gallery element!", galleryElem)
|
console.error("No buttons found on gallery element!", galleryElem)
|
||||||
return;
|
return;
|
||||||
|
|||||||
202
src/lib/components/AccordionContainer.svelte
Normal file
202
src/lib/components/AccordionContainer.svelte
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
|
import { Accordion } from "@gradio/accordion";
|
||||||
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import WidgetContainer from "./WidgetContainer.svelte"
|
||||||
|
|
||||||
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
|
|
||||||
|
import {fade} from 'svelte/transition';
|
||||||
|
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||||
|
import {cubicIn} from 'svelte/easing';
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||||
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let container: ContainerLayout | null = null;
|
||||||
|
export let zIndex: number = 0;
|
||||||
|
export let classes: string[] = [];
|
||||||
|
export let showHandles: boolean = false;
|
||||||
|
export let edit: boolean = false;
|
||||||
|
export let dragDisabled: boolean = false;
|
||||||
|
|
||||||
|
let attrsChanged: Writable<boolean> | null = null;
|
||||||
|
let children: IDragItem[] | null = null;
|
||||||
|
const flipDurationMs = 100;
|
||||||
|
|
||||||
|
let selectedIndex: number = 0;
|
||||||
|
|
||||||
|
$: if (container) {
|
||||||
|
children = $layoutState.allItems[container.id].children;
|
||||||
|
attrsChanged = container.attrsChanged
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
children = null;
|
||||||
|
attrsChanged = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConsider(evt: any) {
|
||||||
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
|
// console.log(dragItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleFinalize(evt: any) {
|
||||||
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
|
// Ensure dragging is stopped on drag finish
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if container && children}
|
||||||
|
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
||||||
|
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||||
|
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
||||||
|
class:root-container={zIndex === 0}
|
||||||
|
class:is-executing={container.isNodeExecuting}
|
||||||
|
class:edit={edit}>
|
||||||
|
{#if edit}
|
||||||
|
<Block elem_classes={["gradio-accordion"]}>
|
||||||
|
<Accordion label={container.attrs.title} open={true}>
|
||||||
|
<div class="v-pane"
|
||||||
|
class:empty={children.length === 0}
|
||||||
|
class:edit={edit}
|
||||||
|
use:dndzone="{{
|
||||||
|
items: children,
|
||||||
|
flipDurationMs,
|
||||||
|
centreDraggedOnCursor: true,
|
||||||
|
morphDisabled: true,
|
||||||
|
dropFromOthersDisabled: zIndex === 0,
|
||||||
|
dragDisabled
|
||||||
|
}}"
|
||||||
|
on:consider="{handleConsider}"
|
||||||
|
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"
|
||||||
|
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'/>
|
||||||
|
{/if}
|
||||||
|
</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}
|
||||||
|
</Accordion>
|
||||||
|
</Block>
|
||||||
|
{:else}
|
||||||
|
<Block elem_classes={["gradio-accordion"]}>
|
||||||
|
<Accordion label={container.attrs.title} open={container.attrs.openOnStartup}>
|
||||||
|
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||||
|
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
|
||||||
|
{/each}
|
||||||
|
</Accordion>
|
||||||
|
</Block>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.container {
|
||||||
|
> :global(*) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.v-pane > .block) {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit > :global(.v-pane > .block) {
|
||||||
|
border-color: var(--color-pink-500);
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: dashed !important;
|
||||||
|
margin: 0.2em;
|
||||||
|
padding: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* :global(.hide-block > .v-pane > .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);
|
||||||
|
width: var(--size-full);
|
||||||
|
|
||||||
|
> :global(.block > .v-pane) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :global(*), > :global(.form > *) {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-width: min(160px, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> :global(.block > .v-pane) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :global(*), > :global(.form > *), .v-pane {
|
||||||
|
width: var(--size-full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.label-wrap > span:not(.icon)) {
|
||||||
|
color: var(--block-title-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
cursor: grab;
|
||||||
|
z-index: 99999;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-widget:hover {
|
||||||
|
background-color: #add8e680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-container:hover {
|
||||||
|
background-color: #d8ade680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.selected > :global(.block) {
|
||||||
|
background: var(--color-yellow-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradio-accordion {
|
||||||
|
.widget, .container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,11 +11,15 @@
|
|||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
export let zIndex: number = 0;
|
export let zIndex: number = 0;
|
||||||
export let classes: string[] = [];
|
export let classes: string[] = [];
|
||||||
export let showHandles: boolean = false;
|
export let showHandles: boolean = false;
|
||||||
|
export let edit: boolean = false;
|
||||||
|
export let dragDisabled: boolean = false;
|
||||||
|
|
||||||
let attrsChanged: Writable<boolean> | null = null;
|
let attrsChanged: Writable<boolean> | null = null;
|
||||||
let children: IDragItem[] | null = null;
|
let children: IDragItem[] | null = null;
|
||||||
const flipDurationMs = 100;
|
const flipDurationMs = 100;
|
||||||
@@ -38,20 +42,20 @@
|
|||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
// Ensure dragging is stopped on drag finish
|
// Ensure dragging is stopped on drag finish
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tt = "asd\nasdlkj"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container && children}
|
{#if container && children}
|
||||||
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
|
|
||||||
{#key $attrsChanged}
|
|
||||||
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
||||||
class:hide-block={container.attrs.blockVariant === "hidden"}
|
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)}
|
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
||||||
class:root-container={zIndex === 0}
|
class:root-container={zIndex === 0}
|
||||||
class:is-executing={container.isNodeExecuting}
|
class:is-executing={container.isNodeExecuting}
|
||||||
class:edit={edit}>
|
class:edit={edit}>
|
||||||
<Block>
|
<Block>
|
||||||
{#if container.attrs.title !== ""}
|
{#if container.attrs.title && container.attrs.title !== ""}
|
||||||
<label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}>
|
<label for={String(container.id)} class={($uiState.uiUnlocked && $uiState.uiEditMode === "widgets") ? "edit-title-label" : ""}>
|
||||||
<BlockTitle>{container.attrs.title}</BlockTitle>
|
<BlockTitle>{container.attrs.title}</BlockTitle>
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -64,7 +68,7 @@
|
|||||||
centreDraggedOnCursor: true,
|
centreDraggedOnCursor: true,
|
||||||
morphDisabled: true,
|
morphDisabled: true,
|
||||||
dropFromOthersDisabled: zIndex === 0,
|
dropFromOthersDisabled: zIndex === 0,
|
||||||
dragDisabled: zIndex === 0 || $layoutState.currentSelection.length > 2 || $uiState.uiEditMode === "disabled"
|
dragDisabled
|
||||||
}}"
|
}}"
|
||||||
on:consider="{handleConsider}"
|
on:consider="{handleConsider}"
|
||||||
on:finalize="{handleFinalize}"
|
on:finalize="{handleFinalize}"
|
||||||
@@ -91,7 +95,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Block>
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -158,7 +161,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.block) {
|
> :global(.block) {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +173,7 @@
|
|||||||
padding: 1.4em;
|
padding: 1.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.hide-block > .block) {
|
> :global(.hide-block > .block) {
|
||||||
padding: 0.5em 0.25em;
|
padding: 0.5em 0.25em;
|
||||||
box-shadow: unset;
|
box-shadow: unset;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
@@ -289,12 +292,6 @@
|
|||||||
color: var(--input-placeholder-color);
|
color: var(--input-placeholder-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-edit-outline {
|
|
||||||
border: 2px dashed var(--color-blue-400);
|
|
||||||
margin: 0.2em;
|
|
||||||
padding: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-container > :global(.block) {
|
.root-container > :global(.block) {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
import ComfyQueue from "./ComfyQueue.svelte";
|
import ComfyQueue from "./ComfyQueue.svelte";
|
||||||
import ComfyProperties from "./ComfyProperties.svelte";
|
import ComfyProperties from "./ComfyProperties.svelte";
|
||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
|
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
|
||||||
|
import ComfyGraphView from "./ComfyGraphView.svelte";
|
||||||
|
|
||||||
export let app: ComfyApp = undefined;
|
export let app: ComfyApp = undefined;
|
||||||
let imageViewer: ImageViewer;
|
let imageViewer: ImageViewer;
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
let containerElem: HTMLDivElement;
|
let containerElem: HTMLDivElement;
|
||||||
let resizeTimeout: NodeJS.Timeout | null;
|
let resizeTimeout: NodeJS.Timeout | null;
|
||||||
let hasShownUIHelpToast: boolean = false;
|
let hasShownUIHelpToast: boolean = false;
|
||||||
let uiTheme: string = "anapnoe";
|
let uiTheme: string = "";
|
||||||
|
|
||||||
let debugLayout: boolean = false;
|
let debugLayout: boolean = false;
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
$layoutState.currentSelection = []
|
$layoutState.currentSelection = []
|
||||||
|
|
||||||
let graphSize = 0;
|
let graphSize = 0;
|
||||||
|
let graphTransitioning = false;
|
||||||
|
|
||||||
function toggleGraph() {
|
function toggleGraph() {
|
||||||
if (graphSize == 0) {
|
if (graphSize == 0) {
|
||||||
@@ -69,7 +72,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let propsSidebarSize = 0; //15;
|
let propsSidebarSize = 15; //15;
|
||||||
|
|
||||||
function toggleProps() {
|
function toggleProps() {
|
||||||
if (propsSidebarSize == 0) {
|
if (propsSidebarSize == 0) {
|
||||||
@@ -120,11 +123,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doRecenter(): void {
|
$: if ($uiState.uiUnlocked && !hasShownUIHelpToast) {
|
||||||
app.lCanvas.recenter();
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if ($uiState.uiEditMode !== "disabled" && !hasShownUIHelpToast) {
|
|
||||||
hasShownUIHelpToast = true;
|
hasShownUIHelpToast = true;
|
||||||
toast.push("Right-click to open context menu.")
|
toast.push("Right-click to open context menu.")
|
||||||
}
|
}
|
||||||
@@ -144,21 +143,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: if (containerElem) {
|
$: if (containerElem) {
|
||||||
let wrappers = containerElem.querySelectorAll<HTMLDivElement>(".pane-wrapper")
|
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
|
||||||
for (const wrapper of wrappers) {
|
if (canvas) {
|
||||||
const paneNode = wrapper.parentNode as HTMLElement; // get the node inside the <Pane/>
|
const paneNode = canvas.closest(".splitpanes__pane")
|
||||||
paneNode.ontransitionend = () => {
|
if (paneNode) {
|
||||||
|
(paneNode as HTMLElement).ontransitionstart = () => {
|
||||||
|
graphTransitioning = true
|
||||||
|
}
|
||||||
|
(paneNode as HTMLElement).ontransitionend = () => {
|
||||||
|
graphTransitioning = false
|
||||||
app.resizeCanvas()
|
app.resizeCanvas()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await app.setup();
|
await app.setup();
|
||||||
(window as any).app = app;
|
(window as any).app = app;
|
||||||
(window as any).appPane = uiPane;
|
(window as any).appPane = uiPane;
|
||||||
|
|
||||||
await import('../../scss/ux.scss');
|
// await import('../../scss/ux.scss');
|
||||||
|
|
||||||
refreshView();
|
refreshView();
|
||||||
})
|
})
|
||||||
@@ -189,9 +194,7 @@
|
|||||||
<ComfyUIPane bind:this={uiPane} {app} />
|
<ComfyUIPane bind:this={uiPane} {app} />
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane bind:size={graphSize}>
|
<Pane bind:size={graphSize}>
|
||||||
<div class="canvas-wrapper pane-wrapper">
|
<ComfyGraphView {app} transitioning={graphTransitioning} />
|
||||||
<canvas id="graph-canvas" />
|
|
||||||
</div>
|
|
||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
</Pane>
|
</Pane>
|
||||||
@@ -203,6 +206,7 @@
|
|||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
</div>
|
</div>
|
||||||
<div id="bottombar">
|
<div id="bottombar">
|
||||||
|
<div class="left">
|
||||||
<Button variant="primary" on:click={queuePrompt}>
|
<Button variant="primary" on:click={queuePrompt}>
|
||||||
Queue Prompt
|
Queue Prompt
|
||||||
</Button>
|
</Button>
|
||||||
@@ -224,29 +228,31 @@
|
|||||||
<Button variant="secondary" on:click={doLoadDefault}>
|
<Button variant="secondary" on:click={doLoadDefault}>
|
||||||
Load Default
|
Load Default
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" on:click={doRecenter}>
|
|
||||||
Recenter
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" on:click={doRefreshCombos}>
|
<Button variant="secondary" on:click={doRefreshCombos}>
|
||||||
🔄
|
🔄
|
||||||
</Button>
|
</Button>
|
||||||
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
||||||
|
<span style="display: inline-flex !important">
|
||||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||||
<label class="label" for="enable-ui-editing">
|
</span>
|
||||||
<BlockTitle>Enable UI Editing</BlockTitle>
|
<span class="label" for="ui-edit-mode">
|
||||||
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
|
<BlockTitle>UI Edit mode</BlockTitle>
|
||||||
<option value="disabled">Disabled</option>
|
<select id="ui-edit-mode" name="ui-edit-mode" bind:value={$uiState.uiEditMode}>
|
||||||
<option value="widgets">Widgets</option>
|
<option value="widgets">Widgets</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</span>
|
||||||
<label class="label" for="ui-theme">
|
<span class="label" for="ui-theme">
|
||||||
<BlockTitle>Theme</BlockTitle>
|
<BlockTitle>Theme</BlockTitle>
|
||||||
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
||||||
<option value="">None</option>
|
<option value="">None</option>
|
||||||
<option value="anapnoe">Anapnoe</option>
|
<option value="anapnoe">Anapnoe</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<ComfyUnlockUIButton bind:toggled={$uiState.uiUnlocked} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LightboxModal />
|
<LightboxModal />
|
||||||
</div>
|
</div>
|
||||||
@@ -266,26 +272,24 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#comfy-ui {
|
|
||||||
}
|
|
||||||
|
|
||||||
#comfy-graph {
|
|
||||||
}
|
|
||||||
|
|
||||||
#graph-canvas {
|
|
||||||
}
|
|
||||||
|
|
||||||
#bottombar {
|
#bottombar {
|
||||||
|
padding-top: 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
gap: var(--layout-gap);
|
gap: var(--layout-gap);
|
||||||
margin: 10px;
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
margin-top: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
> .left {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-wrapper {
|
> .right {
|
||||||
width: 100%;
|
margin-left: auto
|
||||||
height: 100%;
|
}
|
||||||
background-color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-wrapper {
|
.sidebar-wrapper {
|
||||||
@@ -350,4 +354,8 @@
|
|||||||
label.label > :global(span) {
|
label.label > :global(span) {
|
||||||
top: 20%;
|
top: 20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.left {
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export default class ComfyApp {
|
|||||||
LiteGraph.release_link_on_empty_shows_menu = true;
|
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||||
LiteGraph.alt_drag_do_clone_nodes = true;
|
LiteGraph.alt_drag_do_clone_nodes = true;
|
||||||
|
|
||||||
|
(window as any).LiteGraph = LiteGraph;
|
||||||
|
|
||||||
// await this.#invokeExtensionsAsync("init");
|
// await this.#invokeExtensionsAsync("init");
|
||||||
await this.registerNodes();
|
await this.registerNodes();
|
||||||
|
|
||||||
@@ -230,17 +232,17 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addDropHandler() {
|
private addDropHandler() {
|
||||||
this.dropZone = document.getElementById("dropzone");
|
// this.dropZone = document.getElementById("dropzone");
|
||||||
|
|
||||||
if (this.dropZone) {
|
// if (this.dropZone) {
|
||||||
window.addEventListener('dragenter', this.allowDrag.bind(this));
|
// window.addEventListener('dragenter', this.allowDrag.bind(this));
|
||||||
this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
|
// this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
|
||||||
this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
|
// this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
|
||||||
this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
|
// this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
|
||||||
}
|
// }
|
||||||
else {
|
// else {
|
||||||
console.warn("No dropzone detected (probably on mobile).")
|
// console.warn("No dropzone detected (probably on mobile).")
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,7 +439,7 @@ export default class ComfyApp {
|
|||||||
const n = workflow.nodes.find((n) => n.id === node_.id);
|
const n = workflow.nodes.find((n) => n.id === node_.id);
|
||||||
|
|
||||||
if (!node_.isBackendNode) {
|
if (!node_.isBackendNode) {
|
||||||
console.debug("Not serializing node: ", node_.type)
|
// console.debug("Not serializing node: ", node_.type)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,8 +564,8 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn({ workflow, output })
|
// console.debug({ workflow, output })
|
||||||
console.warn(promptToGraphVis({ workflow, output }))
|
// console.debug(promptToGraphVis({ workflow, output }))
|
||||||
|
|
||||||
return { workflow, output };
|
return { workflow, output };
|
||||||
}
|
}
|
||||||
@@ -588,7 +590,7 @@ export default class ComfyApp {
|
|||||||
for (let i = 0; i < batchCount; i++) {
|
for (let i = 0; i < batchCount; i++) {
|
||||||
for (const node of this.lGraph._nodes_in_order) {
|
for (const node of this.lGraph._nodes_in_order) {
|
||||||
if ("beforeQueued" in node) {
|
if ("beforeQueued" in node) {
|
||||||
(node as ComfyGraphNode).beforeQueued();
|
(node as ComfyGraphNode).beforeQueued(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +613,7 @@ export default class ComfyApp {
|
|||||||
for (const n of p.workflow.nodes) {
|
for (const n of p.workflow.nodes) {
|
||||||
const node = this.lGraph.getNodeById(n.id);
|
const node = this.lGraph.getNodeById(n.id);
|
||||||
if ("afterQueued" in node) {
|
if ("afterQueued" in node) {
|
||||||
(node as ComfyGraphNode).afterQueued(p);
|
(node as ComfyGraphNode).afterQueued(p, tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
export let value: string = "";
|
export let value: string = "";
|
||||||
export let values: string[] = [""];
|
export let values: string[] = [""];
|
||||||
export let name: string = "";
|
export let name: string = "";
|
||||||
|
export let disabled: boolean = false;
|
||||||
let value_: string = ""
|
let value_: string = ""
|
||||||
|
|
||||||
$: value;
|
$: value;
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
<label class="select-wrapper">
|
<label class="select-wrapper">
|
||||||
<BlockTitle>{name}</BlockTitle>
|
<BlockTitle>{name}</BlockTitle>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<select on:blur bind:value>
|
<select on:blur bind:value {disabled}>
|
||||||
{#each values as value}
|
{#each values as value}
|
||||||
<option {value}>
|
<option {value}>
|
||||||
{value}
|
{value}
|
||||||
@@ -53,4 +54,8 @@
|
|||||||
.select-title {
|
.select-title {
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
76
src/lib/components/ComfyGraphView.svelte
Normal file
76
src/lib/components/ComfyGraphView.svelte
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import type ComfyApp from "./ComfyApp";
|
||||||
|
|
||||||
|
export let app: ComfyApp;
|
||||||
|
export let transitioning: boolean = false;
|
||||||
|
|
||||||
|
function doRecenter(): void {
|
||||||
|
app?.lCanvas?.recenter();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="canvas-wrapper pane-wrapper">
|
||||||
|
<canvas id="graph-canvas" />
|
||||||
|
</div>
|
||||||
|
<div class="bar">
|
||||||
|
{#if !transitioning}
|
||||||
|
<span class="left">
|
||||||
|
<button on:click={doRecenter}>Recenter</button>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$bar-height: 3em;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - $bar-height);
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: $bar-height;
|
||||||
|
background-color: #3A3A3A;
|
||||||
|
border: 2px solid #2A2A2A;
|
||||||
|
gap: var(--layout-gap);
|
||||||
|
margin-top: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
> .left {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .right {
|
||||||
|
margin-left: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: #EEE;
|
||||||
|
border: 1px solid #888;
|
||||||
|
padding: 2px 5px;
|
||||||
|
background-color: #666;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #999;
|
||||||
|
border-color: #AAA;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: #555;
|
||||||
|
border-color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,8 +3,11 @@
|
|||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let value: number = 0;
|
export let value: number = 0;
|
||||||
|
export let min: number = -1024
|
||||||
|
export let max: number = 1024
|
||||||
export let step: number = 1;
|
export let step: number = 1;
|
||||||
export let name: string = "";
|
export let name: string = "";
|
||||||
|
export let disabled: boolean = false;
|
||||||
let value_: number = 0;
|
let value_: number = 0;
|
||||||
|
|
||||||
$: value;
|
$: value;
|
||||||
@@ -26,7 +29,7 @@
|
|||||||
<label class="number-wrapper">
|
<label class="number-wrapper">
|
||||||
<BlockTitle>{name}</BlockTitle>
|
<BlockTitle>{name}</BlockTitle>
|
||||||
<div class="number">
|
<div class="number">
|
||||||
<input type="number" bind:value {step}>
|
<input type="number" bind:value {min} {max} {step} {disabled}>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -42,4 +45,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
input[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
import { TextBox, Checkbox } from "@gradio/form";
|
import { TextBox, Checkbox } from "@gradio/form";
|
||||||
import { LGraphNode } from "@litegraph-ts/core"
|
import { LGraphNode } from "@litegraph-ts/core"
|
||||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||||
import { get } from "svelte/store"
|
import uiState from "$lib/stores/uiState"
|
||||||
|
import { get, type Writable, writable } from "svelte/store"
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||||
|
|
||||||
let target: IDragItem | null = null;
|
let target: IDragItem | null = null;
|
||||||
let node: LGraphNode | null = null;
|
let node: LGraphNode | null = null;
|
||||||
|
let attrsChanged: Writable<boolean> | null = null;
|
||||||
|
let refreshPanel: Writable<number> = writable(0);
|
||||||
|
|
||||||
$: if ($layoutState.currentSelection.length > 0) {
|
$: if ($layoutState.currentSelection.length > 0) {
|
||||||
const targetId = $layoutState.currentSelection.slice(-1)[0]
|
const targetId = $layoutState.currentSelection.slice(-1)[0]
|
||||||
target = $layoutState.allItems[targetId].dragItem
|
target = $layoutState.allItems[targetId].dragItem
|
||||||
|
attrsChanged = target.attrsChanged;
|
||||||
if (target.type === "widget") {
|
if (target.type === "widget") {
|
||||||
node = (target as WidgetLayout).node
|
node = (target as WidgetLayout).node
|
||||||
}
|
}
|
||||||
@@ -24,10 +28,32 @@
|
|||||||
else if ($layoutState.currentSelectionNodes.length > 0) {
|
else if ($layoutState.currentSelectionNodes.length > 0) {
|
||||||
target = null;
|
target = null;
|
||||||
node = $layoutState.currentSelectionNodes[0]
|
node = $layoutState.currentSelectionNodes[0]
|
||||||
|
attrsChanged = null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
target = null
|
target = null
|
||||||
node = null;
|
node = null;
|
||||||
|
attrsChanged = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (target) {
|
||||||
|
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||||
|
for (const spec of Object.values(cat.specs)) {
|
||||||
|
if (spec.location === "widget" && target.attrs[spec.name] == null) {
|
||||||
|
if (!spec.editable)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (spec.canShow && !spec.canShow(target))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
console.warn("Set default widget attr", spec.name, spec.defaultValue, target)
|
||||||
|
let value = spec.defaultValue;
|
||||||
|
target.attrs[spec.name] = value;
|
||||||
|
if (spec.refreshPanelOnChange)
|
||||||
|
$refreshPanel += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetType: string = "???"
|
let targetType: string = "???"
|
||||||
@@ -41,17 +67,79 @@
|
|||||||
targetType = ""
|
targetType = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function validNodeProperty(spec: AttributesSpec, node: LGraphNode): boolean {
|
function validNodeProperty(spec: AttributesSpec, node: LGraphNode | null): boolean {
|
||||||
|
if (node == null || spec.location !== "nodeProps")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (spec.canShow && !spec.canShow(node))
|
||||||
|
return false;
|
||||||
|
|
||||||
if (spec.validNodeTypes) {
|
if (spec.validNodeTypes) {
|
||||||
return spec.validNodeTypes.indexOf(node.type) !== -1;
|
return spec.validNodeTypes.indexOf(node.type) !== -1;
|
||||||
}
|
}
|
||||||
return spec.name in node.properties
|
return spec.name in node.properties
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAttribute(entry: AttributesSpec, target: IDragItem, value: any) {
|
function validNodeVar(spec: AttributesSpec, node: LGraphNode | null): boolean {
|
||||||
if (target) {
|
if (node == null || spec.location !== "nodeVars")
|
||||||
const name = entry.name
|
return false;
|
||||||
console.warn("updateAttribute", name, value)
|
|
||||||
|
if (spec.canShow && !spec.canShow(node))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (spec.validNodeTypes) {
|
||||||
|
return spec.validNodeTypes.indexOf(node.type) !== -1;
|
||||||
|
}
|
||||||
|
return spec.name in node
|
||||||
|
}
|
||||||
|
|
||||||
|
function validWidgetAttribute(spec: AttributesSpec, widget: IDragItem | null): boolean {
|
||||||
|
if (widget == null || spec.location !== "widget")
|
||||||
|
return false;
|
||||||
|
if (spec.canShow)
|
||||||
|
return spec.canShow(widget);
|
||||||
|
|
||||||
|
if (spec.validNodeTypes) {
|
||||||
|
if (widget.type === "widget") {
|
||||||
|
const node = (widget as WidgetLayout).node
|
||||||
|
if (!node)
|
||||||
|
return false;
|
||||||
|
return spec.validNodeTypes.indexOf(node.type) !== -1;
|
||||||
|
}
|
||||||
|
else if (widget.type === "container") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec.name in widget.attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
function validWorkflowAttribute(spec: AttributesSpec): boolean {
|
||||||
|
if (spec.location !== "workflow")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return spec.name in $layoutState.attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttribute(target: IDragItem, spec: AttributesSpec): any {
|
||||||
|
let value = target.attrs[spec.name]
|
||||||
|
if (value == null)
|
||||||
|
value = spec.defaultValue
|
||||||
|
else if (spec.serialize)
|
||||||
|
value = spec.serialize(value)
|
||||||
|
console.debug("[ComfyProperties] getAttribute", spec.name, value, target, spec)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAttribute(spec: AttributesSpec, target: IDragItem | null, value: any) {
|
||||||
|
if (target == null || !spec.editable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const name = spec.name
|
||||||
|
|
||||||
|
console.debug("[ComfyProperties] updateAttribute", spec, value, name, node)
|
||||||
|
if (spec.deserialize)
|
||||||
|
value = spec.deserialize(value)
|
||||||
|
|
||||||
target.attrs[name] = value
|
target.attrs[name] = value
|
||||||
target.attrsChanged.set(!get(target.attrsChanged))
|
target.attrsChanged.set(!get(target.attrsChanged))
|
||||||
@@ -60,12 +148,19 @@
|
|||||||
const comfyNode = node as ComfyWidgetNode
|
const comfyNode = node as ComfyWidgetNode
|
||||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn(spec)
|
||||||
|
if (spec.refreshPanelOnChange) {
|
||||||
|
console.error("A! refresh")
|
||||||
|
$refreshPanel += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProperty(entry: AttributesSpec, value: any) {
|
function updateProperty(spec: AttributesSpec, value: any) {
|
||||||
if (node) {
|
if (node == null || !spec.editable)
|
||||||
const name = entry.name
|
return
|
||||||
|
|
||||||
|
const name = spec.name
|
||||||
console.warn("updateProperty", name, value)
|
console.warn("updateProperty", name, value)
|
||||||
|
|
||||||
node.properties[name] = value;
|
node.properties[name] = value;
|
||||||
@@ -74,37 +169,47 @@
|
|||||||
const comfyNode = node as ComfyWidgetNode
|
const comfyNode = node as ComfyWidgetNode
|
||||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (spec.refreshPanelOnChange)
|
||||||
|
$refreshPanel += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVar(node: LGraphNode, entry: AttributesSpec) {
|
function getVar(node: LGraphNode, spec: AttributesSpec) {
|
||||||
let value = node[entry.name]
|
let value = node[spec.name]
|
||||||
if (entry.serialize)
|
if (value == null)
|
||||||
value = entry.serialize(value)
|
value = spec.defaultValue
|
||||||
console.debug("[ComfyProperties] getVar", entry, value, node)
|
else if (spec.serialize)
|
||||||
|
value = spec.serialize(value)
|
||||||
|
console.debug("[ComfyProperties] getVar", spec, value, node)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVar(entry: any, value: any) {
|
function updateVar(spec: AttributesSpec, value: any) {
|
||||||
if (node) {
|
if (node == null || !spec.editable)
|
||||||
const name = entry.name
|
return;
|
||||||
console.warn("updateProperty", name, value)
|
|
||||||
|
|
||||||
if (entry.deserialize)
|
const name = spec.name
|
||||||
value = entry.deserialize(value)
|
|
||||||
|
console.debug("[ComfyProperties] updateVar", spec, value, name, node)
|
||||||
|
if (spec.deserialize)
|
||||||
|
value = spec.deserialize(value)
|
||||||
|
|
||||||
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
|
|
||||||
node[name] = value;
|
node[name] = value;
|
||||||
|
|
||||||
if ("propsChanged" in node) {
|
if ("propsChanged" in node) {
|
||||||
const comfyNode = node as ComfyWidgetNode
|
const comfyNode = node as ComfyWidgetNode
|
||||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (spec.refreshPanelOnChange)
|
||||||
|
$refreshPanel += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWorkflowAttribute(entry: AttributesSpec, value: any) {
|
function updateWorkflowAttribute(spec: AttributesSpec, value: any) {
|
||||||
const name = entry.name
|
if (!spec.editable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const name = spec.name
|
||||||
console.warn("updateWorkflowAttribute", name, value)
|
console.warn("updateWorkflowAttribute", name, value)
|
||||||
|
|
||||||
$layoutState.attrs[name] = value
|
$layoutState.attrs[name] = value
|
||||||
@@ -121,82 +226,96 @@
|
|||||||
<span class="type">({targetType})</span>
|
<span class="type">({targetType})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="props-entries">
|
<div class="props-entries">
|
||||||
|
{#key $refreshPanel}
|
||||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||||
<div class="category-name">
|
<div class="category-name">
|
||||||
<span>
|
<span>
|
||||||
<span class="title">{category.categoryName}</span>
|
<span class="title">{category.categoryName}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#each category.specs as spec(spec.name)}
|
{#each category.specs as spec(spec.id)}
|
||||||
{#if spec.location === "widget" && target && spec.name in target.attrs}
|
{#if validWidgetAttribute(spec, target)}
|
||||||
<div class="props-entry">
|
<div class="props-entry">
|
||||||
{#if spec.type === "string"}
|
{#if spec.type === "string"}
|
||||||
<TextBox
|
<TextBox
|
||||||
value={target.attrs[spec.name]}
|
value={getAttribute(target, spec)}
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
on:input={(e) => updateAttribute(spec, target, e.detail)}
|
on:input={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
max_lines={1}
|
max_lines={1}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "boolean"}
|
{:else if spec.type === "boolean"}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={target.attrs[spec.name]}
|
value={getAttribute(target, spec)}
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "number"}
|
{:else if spec.type === "number"}
|
||||||
<ComfyNumberProperty
|
<ComfyNumberProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={target.attrs[spec.name]}
|
value={getAttribute(target, spec)}
|
||||||
step={1}
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "enum"}
|
{:else if spec.type === "enum"}
|
||||||
<ComfyComboProperty
|
<ComfyComboProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={target.attrs[spec.name]}
|
value={getAttribute(target, spec)}
|
||||||
values={spec.values}
|
values={spec.values}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if node}
|
{:else if node}
|
||||||
{#if spec.location === "nodeProps" && validNodeProperty(spec, node)}
|
{#if validNodeProperty(spec, node)}
|
||||||
<div class="props-entry">
|
<div class="props-entry">
|
||||||
{#if spec.type === "string"}
|
{#if spec.type === "string"}
|
||||||
<TextBox
|
<TextBox
|
||||||
value={node.properties[spec.name]}
|
value={node.properties[spec.name] || spec.defaultValue}
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
on:input={(e) => updateProperty(spec, e.detail)}
|
on:input={(e) => updateProperty(spec, e.detail)}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
max_lines={1}
|
max_lines={1}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "boolean"}
|
{:else if spec.type === "boolean"}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={node.properties[spec.name]}
|
value={node.properties[spec.name] || spec.defaultValue}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "number"}
|
{:else if spec.type === "number"}
|
||||||
<ComfyNumberProperty
|
<ComfyNumberProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={node.properties[spec.name]}
|
value={node.properties[spec.name] || spec.defaultValue}
|
||||||
step={1}
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "enum"}
|
{:else if spec.type === "enum"}
|
||||||
<ComfyComboProperty
|
<ComfyComboProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={node.properties[spec.name]}
|
value={node.properties[spec.name] || spec.defaultValue}
|
||||||
values={spec.values}
|
values={spec.values}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if spec.location === "nodeVars" && spec.name in node}
|
{:else if validNodeVar(spec, node)}
|
||||||
<div class="props-entry">
|
<div class="props-entry">
|
||||||
{#if spec.type === "string"}
|
{#if spec.type === "string"}
|
||||||
<TextBox
|
<TextBox
|
||||||
@@ -204,19 +323,24 @@
|
|||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
on:input={(e) => updateVar(spec, e.detail)}
|
on:input={(e) => updateVar(spec, e.detail)}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
max_lines={1}
|
max_lines={1}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "boolean"}
|
{:else if spec.type === "boolean"}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={getVar(node, spec)}
|
value={getVar(node, spec)}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "number"}
|
{:else if spec.type === "number"}
|
||||||
<ComfyNumberProperty
|
<ComfyNumberProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={getVar(node, spec)}
|
value={getVar(node, spec)}
|
||||||
step={1}
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "enum"}
|
{:else if spec.type === "enum"}
|
||||||
@@ -224,39 +348,46 @@
|
|||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={getVar(node, spec)}
|
value={getVar(node, spec)}
|
||||||
values={spec.values}
|
values={spec.values}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs}
|
{:else if !node && !target && validWorkflowAttribute(spec)}
|
||||||
<div class="props-entry">
|
<div class="props-entry">
|
||||||
{#if spec.type === "string"}
|
{#if spec.type === "string"}
|
||||||
<TextBox
|
<TextBox
|
||||||
value={$layoutState.attrs[spec.name]}
|
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
max_lines={1}
|
max_lines={1}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "boolean"}
|
{:else if spec.type === "boolean"}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={$layoutState.attrs[spec.name]}
|
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "number"}
|
{:else if spec.type === "number"}
|
||||||
<ComfyNumberProperty
|
<ComfyNumberProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={$layoutState.attrs[spec.name]}
|
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||||
step={1}
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "enum"}
|
{:else if spec.type === "enum"}
|
||||||
<ComfyComboProperty
|
<ComfyComboProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={$layoutState.attrs[spec.name]}
|
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||||
values={spec.values}
|
values={spec.values}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
@@ -264,6 +395,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{/each}
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,6 +415,11 @@
|
|||||||
padding: 0.8rem 1.0rem;
|
padding: 0.8rem 1.0rem;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.type {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
function ungroup() {
|
function ungroup() {
|
||||||
const item = layoutState.getCurrentSelection()[0]
|
const item = layoutState.getCurrentSelection()[0]
|
||||||
if (item.type !== "container")
|
if (!item || item.type !== "container")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$layoutState.currentSelection = []
|
$layoutState.currentSelection = []
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onRightClick(e) {
|
async function onRightClick(e) {
|
||||||
if ($uiState.uiEditMode === "disabled")
|
if (!$uiState.uiUnlocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
46
src/lib/components/ComfyUnlockUIButton.svelte
Normal file
46
src/lib/components/ComfyUnlockUIButton.svelte
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "@gradio/button"
|
||||||
|
import { LockOpen2, LockClosed } from "radix-icons-svelte"
|
||||||
|
|
||||||
|
export let toggled: boolean = false;
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
toggled = !toggled;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="comfy-toggle-button" class:toggled>
|
||||||
|
<Button on:click={toggle} variant={toggled ? "primary" : "secondary"}>
|
||||||
|
{#if toggled}
|
||||||
|
<LockOpen2 />
|
||||||
|
{:else}
|
||||||
|
<LockClosed />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.comfy-toggle-button {
|
||||||
|
display: inline-flex;
|
||||||
|
width: var(--size-12);
|
||||||
|
height: var(--size-12);
|
||||||
|
|
||||||
|
> :global(.secondary.lg) {
|
||||||
|
border: var(--button-border-width) solid var(--neutral-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
> :global(.primary.lg) {
|
||||||
|
border: var(--button-border-width) solid var(--primary-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
:global(svg) {
|
||||||
|
color: var(--button-primary-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(svg) {
|
||||||
|
color: var(--button-secondary-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
src/lib/components/Container.svelte
Normal file
44
src/lib/components/Container.svelte
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import WidgetContainer from "./WidgetContainer.svelte"
|
||||||
|
import BlockContainer from "./BlockContainer.svelte"
|
||||||
|
import AccordionContainer from "./AccordionContainer.svelte"
|
||||||
|
import TabsContainer from "./TabsContainer.svelte"
|
||||||
|
|
||||||
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
|
|
||||||
|
import {fade} from 'svelte/transition';
|
||||||
|
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||||
|
import {cubicIn} from 'svelte/easing';
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||||
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
|
|
||||||
|
export let container: ContainerLayout | null = null;
|
||||||
|
export let zIndex: number = 0;
|
||||||
|
export let classes: string[] = [];
|
||||||
|
export let showHandles: boolean = false;
|
||||||
|
let attrsChanged: Writable<boolean> | null = null;
|
||||||
|
|
||||||
|
$: if (container) {
|
||||||
|
attrsChanged = container.attrsChanged
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
attrsChanged = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if container}
|
||||||
|
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||||
|
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked}
|
||||||
|
{#key $attrsChanged}
|
||||||
|
{#if container.attrs.variant === "tabs"}
|
||||||
|
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} />
|
||||||
|
{:else if container.attrs.variant === "accordion"}
|
||||||
|
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} />
|
||||||
|
{:else}
|
||||||
|
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} />
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
212
src/lib/components/TabsContainer.svelte
Normal file
212
src/lib/components/TabsContainer.svelte
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
|
import { Tabs, TabItem } from "@gradio/tabs";
|
||||||
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import WidgetContainer from "./WidgetContainer.svelte"
|
||||||
|
|
||||||
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
|
|
||||||
|
import {fade} from 'svelte/transition';
|
||||||
|
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||||
|
import {cubicIn} from 'svelte/easing';
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||||
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let container: ContainerLayout | null = null;
|
||||||
|
export let zIndex: number = 0;
|
||||||
|
export let classes: string[] = [];
|
||||||
|
export let showHandles: boolean = false;
|
||||||
|
export let edit: boolean = false;
|
||||||
|
export let dragDisabled: boolean = false;
|
||||||
|
|
||||||
|
let attrsChanged: Writable<boolean> | null = null;
|
||||||
|
let children: IDragItem[] | null = null;
|
||||||
|
const flipDurationMs = 100;
|
||||||
|
|
||||||
|
let selectedIndex: number = 0;
|
||||||
|
|
||||||
|
$: if (container) {
|
||||||
|
children = $layoutState.allItems[container.id].children;
|
||||||
|
attrsChanged = container.attrsChanged
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
children = null;
|
||||||
|
attrsChanged = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConsider(evt: any) {
|
||||||
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
|
// console.log(dragItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleFinalize(evt: any) {
|
||||||
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
|
// Ensure dragging is stopped on drag finish
|
||||||
|
};
|
||||||
|
|
||||||
|
function getTabName(container: ContainerLayout, i: number): string {
|
||||||
|
const title = container.attrs.title
|
||||||
|
if (!title)
|
||||||
|
return `Tab ${i+1}`
|
||||||
|
|
||||||
|
const tabNames = title.split(",").map(s => s.trim());
|
||||||
|
|
||||||
|
const tabName = tabNames[i]
|
||||||
|
if (tabName == null || tabName === "")
|
||||||
|
return `Tab ${i+1}`
|
||||||
|
|
||||||
|
return tabName
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if container && children}
|
||||||
|
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
||||||
|
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||||
|
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
||||||
|
class:root-container={zIndex === 0}
|
||||||
|
class:is-executing={container.isNodeExecuting}
|
||||||
|
class:edit={edit}>
|
||||||
|
{#if edit}
|
||||||
|
<Block>
|
||||||
|
<div class="v-pane"
|
||||||
|
class:empty={children.length === 0}
|
||||||
|
class:edit={edit}
|
||||||
|
use:dndzone="{{
|
||||||
|
items: children,
|
||||||
|
flipDurationMs,
|
||||||
|
centreDraggedOnCursor: true,
|
||||||
|
morphDisabled: true,
|
||||||
|
dropFromOthersDisabled: zIndex === 0,
|
||||||
|
dragDisabled
|
||||||
|
}}"
|
||||||
|
on:consider="{handleConsider}"
|
||||||
|
on:finalize="{handleFinalize}"
|
||||||
|
>
|
||||||
|
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
|
||||||
|
{@const hidden = item?.attrs?.hidden}
|
||||||
|
{@const tabName = getTabName(container, i)}
|
||||||
|
<div class="animation-wrapper"
|
||||||
|
class:hidden={hidden}
|
||||||
|
animate:flip={{duration:flipDurationMs}}
|
||||||
|
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}>
|
||||||
|
<Block>
|
||||||
|
<label for={String(item.id)}>
|
||||||
|
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
|
||||||
|
</label>
|
||||||
|
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
|
||||||
|
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||||
|
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||||
|
{/if}
|
||||||
|
</Block>
|
||||||
|
</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>
|
||||||
|
{:else}
|
||||||
|
<Tabs elem_classes={["gradio-tabs"]}>
|
||||||
|
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
|
||||||
|
{@const tabName = getTabName(container, i)}
|
||||||
|
<TabItem name={tabName} on:select={() => console.log("tab " + i)}>
|
||||||
|
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
|
||||||
|
</TabItem>
|
||||||
|
{/each}
|
||||||
|
</Tabs>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> :global(*) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.v-pane > .block) {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit > :global(.v-pane > .block) {
|
||||||
|
border-color: var(--color-pink-500);
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: dashed !important;
|
||||||
|
margin: 0.2em;
|
||||||
|
padding: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* :global(.hide-block > .v-pane > .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);
|
||||||
|
width: var(--size-full);
|
||||||
|
|
||||||
|
> :global(.block > .v-pane) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :global(*), > :global(.form > *) {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-width: min(160px, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> :global(.block > .v-pane) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :global(*), > :global(.form > *), .v-pane {
|
||||||
|
width: var(--size-full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
cursor: grab;
|
||||||
|
z-index: 99999;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-widget:hover {
|
||||||
|
background-color: #add8e680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-container:hover {
|
||||||
|
background-color: #d8ade680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.selected > :global(.block) {
|
||||||
|
background: var(--color-yellow-300);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
import BlockContainer from "./BlockContainer.svelte"
|
import Container from "./Container.svelte"
|
||||||
import { type Writable } from "svelte/store"
|
import { type Writable } from "svelte/store"
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||||
|
|
||||||
@@ -40,7 +40,8 @@
|
|||||||
propsChanged = null;
|
propsChanged = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: showHandles = $uiState.uiEditMode === "widgets" // TODO
|
$: showHandles = $uiState.uiUnlocked
|
||||||
|
&& $uiState.uiEditMode === "widgets" // TODO
|
||||||
&& zIndex > 1
|
&& zIndex > 1
|
||||||
&& !$layoutState.isMenuOpen
|
&& !$layoutState.isMenuOpen
|
||||||
|
|
||||||
@@ -58,15 +59,15 @@
|
|||||||
|
|
||||||
{#if container}
|
{#if container}
|
||||||
{#key $attrsChanged}
|
{#key $attrsChanged}
|
||||||
<BlockContainer {container} {classes} {zIndex} {showHandles} />
|
<Container {container} {classes} {zIndex} {showHandles} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else if widget && widget.node}
|
{:else if widget && widget.node}
|
||||||
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
|
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||||
{#key $attrsChanged}
|
{#key $attrsChanged}
|
||||||
{#key $propsChanged}
|
{#key $propsChanged}
|
||||||
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
|
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
|
||||||
class:edit={edit}
|
class:edit={edit}
|
||||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
|
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
|
||||||
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
|
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
|
||||||
class:hidden={widget.attrs.hidden}
|
class:hidden={widget.attrs.hidden}
|
||||||
>
|
>
|
||||||
@@ -83,9 +84,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.widget.selected {
|
.widget {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
background: var(--color-yellow-200);
|
background: var(--color-yellow-200);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.container.selected {
|
.container.selected {
|
||||||
background: var(--color-yellow-400);
|
background: var(--color-yellow-400);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,14 @@ import { Watch } from "@litegraph-ts/nodes-basic";
|
|||||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||||
import { toast } from '@zerodevx/svelte-toast'
|
import { toast } from '@zerodevx/svelte-toast'
|
||||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import queueState from "$lib/stores/queueState";
|
||||||
|
|
||||||
export interface ComfyQueueEventsProperties extends Record<any, any> {
|
export interface ComfyQueueEventsProperties extends Record<any, any> {
|
||||||
prompt: SerializedPrompt | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||||
override properties: ComfyQueueEventsProperties = {
|
override properties: ComfyQueueEventsProperties = {
|
||||||
prompt: null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -22,29 +22,29 @@ export class ComfyQueueEvents extends ComfyGraphNode {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
private getActionParams(subgraph: string | null): any {
|
||||||
if (property === "value") {
|
let queue = get(queueState)
|
||||||
this.setOutputData(2, this.properties.prompt)
|
let remaining = 0;
|
||||||
|
|
||||||
|
if (typeof queue.queueRemaining === "number")
|
||||||
|
remaining = queue.queueRemaining
|
||||||
|
|
||||||
|
return {
|
||||||
|
queueRemaining: remaining,
|
||||||
|
subgraph
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExecute() {
|
override beforeQueued(subgraph: string | null) {
|
||||||
this.setOutputData(2, this.properties.prompt)
|
this.triggerSlot(0, this.getActionParams(subgraph))
|
||||||
}
|
}
|
||||||
|
|
||||||
override beforeQueued() {
|
override afterQueued(p: SerializedPrompt, subgraph: string | null) {
|
||||||
this.setProperty("value", null)
|
this.triggerSlot(1, this.getActionParams(subgraph))
|
||||||
this.triggerSlot(0, "bang")
|
|
||||||
}
|
|
||||||
|
|
||||||
override afterQueued(p: SerializedPrompt) {
|
|
||||||
this.setProperty("value", p)
|
|
||||||
this.triggerSlot(1, "bang")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override onSerialize(o: SerializedLGraphNode) {
|
override onSerialize(o: SerializedLGraphNode) {
|
||||||
super.onSerialize(o)
|
super.onSerialize(o)
|
||||||
o.properties = { prompt: null }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export class ComfyCopyAction extends ComfyGraphNode {
|
|||||||
{ name: "copy", type: BuiltInSlotType.ACTION }
|
{ name: "copy", type: BuiltInSlotType.ACTION }
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
{ name: "out", type: "*" }
|
{ name: "out", type: BuiltInSlotType.EVENT }
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +130,15 @@ export class ComfyCopyAction extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override onExecute() {
|
override onExecute() {
|
||||||
|
if (this.getInputLink(0))
|
||||||
this.setProperty("value", this.getInputData(0))
|
this.setProperty("value", this.getInputData(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
override onAction(action: any, param: any) {
|
override onAction(action: any, param: any) {
|
||||||
|
if (action === "copy") {
|
||||||
this.setProperty("value", this.getInputData(0))
|
this.setProperty("value", this.getInputData(0))
|
||||||
this.setOutputData(0, this.properties.value)
|
this.triggerSlot(0, this.properties.value)
|
||||||
console.log("setData", this.properties.value)
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export type DefaultWidgetLayout = {
|
|||||||
export default class ComfyGraphNode extends LGraphNode {
|
export default class ComfyGraphNode extends LGraphNode {
|
||||||
isBackendNode?: boolean;
|
isBackendNode?: boolean;
|
||||||
|
|
||||||
beforeQueued?(): void;
|
beforeQueued?(subgraph: string | null): void;
|
||||||
afterQueued?(prompt: SerializedPrompt): void;
|
afterQueued?(prompt: SerializedPrompt, subgraph: string | null): void;
|
||||||
onExecuted?(output: any): void;
|
onExecuted?(output: any): void;
|
||||||
|
|
||||||
defaultWidgets?: DefaultWidgetLayout
|
defaultWidgets?: DefaultWidgetLayout
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp } from "@litegraph-ts/core";
|
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core";
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||||
|
|
||||||
@@ -6,7 +6,8 @@ export interface ComfyImageCacheNodeProperties extends Record<any, any> {
|
|||||||
images: GalleryOutput | null,
|
images: GalleryOutput | null,
|
||||||
index: number,
|
index: number,
|
||||||
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
|
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
|
||||||
genNumber: number
|
genNumber: number,
|
||||||
|
updateMode: "replace" | "append"
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
|
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
|
||||||
@@ -20,7 +21,8 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
images: null,
|
images: null,
|
||||||
index: 0,
|
index: 0,
|
||||||
filenames: {},
|
filenames: {},
|
||||||
genNumber: 0
|
genNumber: 0,
|
||||||
|
updateMode: "replace"
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -36,11 +38,15 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propertyLayout: PropertyLayout = [
|
||||||
|
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
||||||
|
]
|
||||||
|
|
||||||
private _uploadPromise: Promise<void> | null = null;
|
private _uploadPromise: Promise<void> | null = null;
|
||||||
private _state: ImageCacheState = "none"
|
|
||||||
|
|
||||||
stateWidget: ITextWidget;
|
stateWidget: ITextWidget;
|
||||||
filenameWidget: ITextWidget;
|
filenameWidget: ITextWidget;
|
||||||
|
modeWidget: IComboWidget;
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
super(name)
|
super(name)
|
||||||
@@ -57,6 +63,14 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
""
|
""
|
||||||
);
|
);
|
||||||
this.filenameWidget.disabled = true;
|
this.filenameWidget.disabled = true;
|
||||||
|
|
||||||
|
this.modeWidget = this.addWidget<IComboWidget>(
|
||||||
|
"combo",
|
||||||
|
"Mode",
|
||||||
|
this.properties.updateMode,
|
||||||
|
null,
|
||||||
|
{ property: "updateMode", values: ["replace", "append"] }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||||
@@ -66,13 +80,23 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
else
|
else
|
||||||
this.properties.index = 0
|
this.properties.index = 0
|
||||||
}
|
}
|
||||||
|
else if (property === "updateMode") {
|
||||||
|
this.modeWidget.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateWidgets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateWidgets() {
|
||||||
if (this.properties.filenames && this.properties.images) {
|
if (this.properties.filenames && this.properties.images) {
|
||||||
const fileCount = this.properties.images.images.length;
|
const fileCount = this.properties.images.images.length;
|
||||||
const cachedCount = Object.keys(this.properties.filenames).length
|
const cachedCount = Object.keys(this.properties.filenames).length
|
||||||
console.warn(cachedCount, this.properties.filenames)
|
console.warn(cachedCount, this.properties.filenames)
|
||||||
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
|
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.filenameWidget.value = `No files cached`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExecute() {
|
override onExecute() {
|
||||||
@@ -185,6 +209,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
this.setProperty("images", null)
|
this.setProperty("images", null)
|
||||||
this.setProperty("filenames", {})
|
this.setProperty("filenames", {})
|
||||||
this.setProperty("index", 0)
|
this.setProperty("index", 0)
|
||||||
|
this.updateWidgets();
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,11 +217,24 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
|
|
||||||
if (link.data && "images" in link.data) {
|
if (link.data && "images" in link.data) {
|
||||||
this.setProperty("genNumber", this.properties.genNumber + 1)
|
this.setProperty("genNumber", this.properties.genNumber + 1)
|
||||||
|
|
||||||
|
const output = link.data as GalleryOutput;
|
||||||
|
|
||||||
|
if (this.properties.updateMode === "append" && this.properties.images != null) {
|
||||||
|
const newImages = this.properties.images.images.concat(output.images)
|
||||||
|
this.properties.images.images = newImages
|
||||||
|
this.setProperty("images", this.properties.images)
|
||||||
|
}
|
||||||
|
else {
|
||||||
this.setProperty("images", link.data as GalleryOutput)
|
this.setProperty("images", link.data as GalleryOutput)
|
||||||
this.setProperty("filenames", {})
|
this.setProperty("filenames", {})
|
||||||
console.debug("[ComfyImageCacheNode] Received output!", link.data)
|
}
|
||||||
|
|
||||||
|
console.debug("[ComfyImageCacheNode] Received output!", output, this.properties.updateMode, this.properties.images)
|
||||||
this.setIndex(0, true)
|
this.setIndex(0, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateWidgets();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType } from "@litegraph-ts/core";
|
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core";
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||||
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||||
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
||||||
|
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
@@ -48,8 +49,11 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
override isBackendNode = false;
|
override isBackendNode = false;
|
||||||
override serialize_widgets = true;
|
override serialize_widgets = true;
|
||||||
|
|
||||||
outputIndex: number | null = 0;
|
// input slots
|
||||||
inputIndex: number = 0;
|
inputIndex: number = 0;
|
||||||
|
|
||||||
|
// output slots
|
||||||
|
outputIndex: number | null = 0;
|
||||||
changedIndex: number | null = 1;
|
changedIndex: number | null = 1;
|
||||||
|
|
||||||
displayWidget: ITextWidget;
|
displayWidget: ITextWidget;
|
||||||
@@ -94,7 +98,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
|
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
|
||||||
this.setOutputData(this.outputIndex, get(this.value))
|
this.setOutputData(this.outputIndex, get(this.value))
|
||||||
}
|
}
|
||||||
if (this.changedIndex !== null & this.outputs.length >= this.changedIndex) {
|
if (this.changedIndex !== null && this.outputs.length >= this.changedIndex) {
|
||||||
const changedOutput = this.outputs[this.changedIndex]
|
const changedOutput = this.outputs[this.changedIndex]
|
||||||
if (changedOutput.type === BuiltInSlotType.EVENT)
|
if (changedOutput.type === BuiltInSlotType.EVENT)
|
||||||
this.triggerSlot(this.changedIndex, "changed")
|
this.triggerSlot(this.changedIndex, "changed")
|
||||||
@@ -118,10 +122,8 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
if (this.copyFromInputLink) {
|
if (this.copyFromInputLink) {
|
||||||
if (this.inputs.length >= this.inputIndex) {
|
if (this.inputs.length >= this.inputIndex) {
|
||||||
const data = this.getInputData(this.inputIndex)
|
const data = this.getInputData(this.inputIndex)
|
||||||
if (data) { // TODO can "null" be a legitimate value here?
|
if (data != null) { // TODO can "null" be a legitimate value here?
|
||||||
this.setValue(data)
|
this.setValue(data)
|
||||||
const input = this.getInputLink(this.inputIndex)
|
|
||||||
input.data = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +233,8 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
|
|||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "value", type: "number" }
|
{ name: "value", type: "number" },
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
{ name: "value", type: "number" },
|
{ name: "value", type: "number" },
|
||||||
@@ -250,6 +253,11 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
|
|||||||
super(name, 0)
|
super(name, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onAction(action: any, param: any) {
|
||||||
|
if (action === "store" && typeof param === "number")
|
||||||
|
this.setValue(param)
|
||||||
|
}
|
||||||
|
|
||||||
override setValue(value: any) {
|
override setValue(value: any) {
|
||||||
if (typeof value !== "number")
|
if (typeof value !== "number")
|
||||||
return;
|
return;
|
||||||
@@ -283,7 +291,8 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
|
|||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "value", type: "string" }
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
{ name: "value", type: "string" },
|
{ name: "value", type: "string" },
|
||||||
@@ -323,6 +332,11 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onAction(action: any, param: any) {
|
||||||
|
if (action === "store" && typeof param === "string")
|
||||||
|
this.setValue(param)
|
||||||
|
}
|
||||||
|
|
||||||
override setValue(value: any) {
|
override setValue(value: any) {
|
||||||
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
||||||
return;
|
return;
|
||||||
@@ -358,7 +372,8 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
|
|||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "value", type: "string" }
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
{ name: "value", type: "string" },
|
{ name: "value", type: "string" },
|
||||||
@@ -372,6 +387,11 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
|
|||||||
super(name, "")
|
super(name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onAction(action: any, param: any) {
|
||||||
|
if (action === "store")
|
||||||
|
this.setValue(param)
|
||||||
|
}
|
||||||
|
|
||||||
override setValue(value: any) {
|
override setValue(value: any) {
|
||||||
super.setValue(`${value}`)
|
super.setValue(`${value}`)
|
||||||
}
|
}
|
||||||
@@ -397,13 +417,15 @@ export type GalleryOutputEntry = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||||
index: number
|
index: number,
|
||||||
|
updateMode: "replace" | "append"
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||||
override properties: ComfyGalleryProperties = {
|
override properties: ComfyGalleryProperties = {
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
index: 0
|
index: 0,
|
||||||
|
updateMode: "replace"
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -417,20 +439,33 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propertyLayout: PropertyLayout = [
|
||||||
|
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
||||||
|
]
|
||||||
|
|
||||||
override svelteComponentType = GalleryWidget
|
override svelteComponentType = GalleryWidget
|
||||||
override copyFromInputLink = false;
|
override copyFromInputLink = false;
|
||||||
override outputIndex = null;
|
override outputIndex = null;
|
||||||
override changedIndex = null;
|
override changedIndex = null;
|
||||||
|
|
||||||
|
modeWidget: IComboWidget;
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
super(name, [])
|
super(name, [])
|
||||||
|
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropertyChanged(property: any, value: any) {
|
||||||
|
if (property === "updateMode") {
|
||||||
|
this.modeWidget.value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExecute() {
|
override onExecute() {
|
||||||
this.setOutputData(0, this.properties.index)
|
this.setOutputData(0, this.properties.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
override onAction(action: any) {
|
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||||
if (action === "clear") {
|
if (action === "clear") {
|
||||||
this.setValue([])
|
this.setValue([])
|
||||||
}
|
}
|
||||||
@@ -442,10 +477,14 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
|||||||
|
|
||||||
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||||
|
|
||||||
// const currentValue = get(this.value)
|
if (this.properties.updateMode === "append") {
|
||||||
// this.setValue(currentValue.concat(galleryItems))
|
const currentValue = get(this.value)
|
||||||
|
this.setValue(currentValue.concat(galleryItems))
|
||||||
|
}
|
||||||
|
else {
|
||||||
this.setValue(galleryItems)
|
this.setValue(galleryItems)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.setProperty("index", 0)
|
this.setProperty("index", 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,13 +528,13 @@ LiteGraph.registerNodeType({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
||||||
message: string
|
param: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||||
override properties: ComfyButtonProperties = {
|
override properties: ComfyButtonProperties = {
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
message: "bang"
|
param: "bang"
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -514,8 +553,8 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
|||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
this.setValue(true)
|
this.setValue(true)
|
||||||
this.triggerSlot(0, this.properties.message);
|
this.triggerSlot(0, this.properties.param);
|
||||||
this.setValue(false)
|
this.setValue(false) // TODO onRelease
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
@@ -529,3 +568,40 @@ LiteGraph.registerNodeType({
|
|||||||
desc: "Button that triggers an event when clicked",
|
desc: "Button that triggers an event when clicked",
|
||||||
type: "ui/button"
|
type: "ui/button"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export interface ComfyCheckboxProperties extends ComfyWidgetProperties {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
||||||
|
override properties: ComfyCheckboxProperties = {
|
||||||
|
defaultValue: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "boolean" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = CheckboxWidget;
|
||||||
|
|
||||||
|
override setValue(value: any) {
|
||||||
|
value = Boolean(value)
|
||||||
|
const changed = value != get(this.value);
|
||||||
|
super.setValue(Boolean(value))
|
||||||
|
if (changed)
|
||||||
|
this.triggerSlot(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyCheckboxNode,
|
||||||
|
title: "UI.Checkbox",
|
||||||
|
desc: "Checkbox that stores a boolean value",
|
||||||
|
type: "ui/checkbox"
|
||||||
|
})
|
||||||
|
|||||||
@@ -6,52 +6,249 @@ import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
|||||||
import type { ComfyWidgetNode } from '$lib/nodes';
|
import type { ComfyWidgetNode } from '$lib/nodes';
|
||||||
|
|
||||||
type DragItemEntry = {
|
type DragItemEntry = {
|
||||||
|
/*
|
||||||
|
* Drag item.
|
||||||
|
*/
|
||||||
dragItem: IDragItem,
|
dragItem: IDragItem,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Children of this drag item.
|
||||||
|
* Only applies if the drag item's type is "container"
|
||||||
|
*/
|
||||||
children: IDragItem[] | null,
|
children: IDragItem[] | null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parent of this drag item.
|
||||||
|
*/
|
||||||
parent: IDragItem | null
|
parent: IDragItem | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global workflow attributes
|
||||||
|
*/
|
||||||
export type LayoutAttributes = {
|
export type LayoutAttributes = {
|
||||||
|
/*
|
||||||
|
* Default subgraph to run when the "Queue Prompt" button in the bottom bar
|
||||||
|
* is pressed.
|
||||||
|
*
|
||||||
|
* If it's an empty string, all backend nodes will be included in the prompt
|
||||||
|
* instead.
|
||||||
|
*/
|
||||||
defaultSubgraph: string
|
defaultSubgraph: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keeps track of the tree of UI components - widgets and the containers that
|
||||||
|
* group them together.
|
||||||
|
*/
|
||||||
export type LayoutState = {
|
export type LayoutState = {
|
||||||
|
/*
|
||||||
|
* Root of the UI tree
|
||||||
|
*/
|
||||||
root: IDragItem | null,
|
root: IDragItem | null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All items indexed by their own ID
|
||||||
|
*/
|
||||||
allItems: Record<DragItemID, DragItemEntry>,
|
allItems: Record<DragItemID, DragItemEntry>,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Items indexed by the litegraph node they're bound to
|
||||||
|
* Only contains drag items of type "widget"
|
||||||
|
*/
|
||||||
allItemsByNode: Record<number, DragItemEntry>,
|
allItemsByNode: Record<number, DragItemEntry>,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Next ID to use for instantiating a new drag item
|
||||||
|
*/
|
||||||
currentId: number,
|
currentId: number,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Selected drag items.
|
||||||
|
*/
|
||||||
currentSelection: DragItemID[],
|
currentSelection: DragItemID[],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Selected LGraphNodes inside the litegraph canvas.
|
||||||
|
*/
|
||||||
currentSelectionNodes: LGraphNode[],
|
currentSelectionNodes: LGraphNode[],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If true, a saved workflow is being deserialized, so ignore any
|
||||||
|
* nodeAdded/nodeRemoved events.
|
||||||
|
*
|
||||||
|
* TODO: instead use LGraphAddNodeOptions.addedByDeserialize
|
||||||
|
*/
|
||||||
isConfiguring: boolean,
|
isConfiguring: boolean,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If true, the right-click context menu is open
|
||||||
|
*/
|
||||||
isMenuOpen: boolean,
|
isMenuOpen: boolean,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global workflow attributes
|
||||||
|
*/
|
||||||
attrs: LayoutAttributes
|
attrs: LayoutAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes for both containers and nodes.
|
||||||
|
**/
|
||||||
export type Attributes = {
|
export type Attributes = {
|
||||||
|
/*
|
||||||
|
* Flex direction for containers.
|
||||||
|
*/
|
||||||
direction: "horizontal" | "vertical",
|
direction: "horizontal" | "vertical",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Display name of this item.
|
||||||
|
*/
|
||||||
title: string,
|
title: string,
|
||||||
showTitle: boolean,
|
|
||||||
|
/*
|
||||||
|
* List of classes to apply to the component.
|
||||||
|
*/
|
||||||
classes: string,
|
classes: string,
|
||||||
blockVariant?: "block" | "hidden",
|
|
||||||
|
/*
|
||||||
|
* Variant for containers. "hidden" hides margin/borders.
|
||||||
|
*/
|
||||||
|
containerVariant?: "block" | "hidden",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If true, don't show this component in the UI
|
||||||
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If true, grey out this component in the UI
|
||||||
|
*/
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
flexGrow?: number
|
|
||||||
|
/*
|
||||||
|
* CSS height
|
||||||
|
*/
|
||||||
|
height?: string,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CSS Flex grow
|
||||||
|
*/
|
||||||
|
flexGrow?: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display variant for widgets/containers (e.g. number widget can act as slider/knob/dial)
|
||||||
|
* Valid values depend on the widget in question.
|
||||||
|
*/
|
||||||
|
variant?: string,
|
||||||
|
|
||||||
|
/*********************************************/
|
||||||
|
/* Special attributes for widgets/containers */
|
||||||
|
/*********************************************/
|
||||||
|
|
||||||
|
// Accordion
|
||||||
|
openOnStartup?: boolean
|
||||||
|
|
||||||
|
// Button
|
||||||
|
buttonVariant?: "primary" | "secondary",
|
||||||
|
buttonSize?: "large" | "small"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AttributesSpec = {
|
export type AttributesSpec = {
|
||||||
|
/*
|
||||||
|
* ID necessary for svelte's keyed each, autoset at the top level in this source file.
|
||||||
|
*/
|
||||||
|
id?: number,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Attribute name. Corresponds to the name of the instance variable in the
|
||||||
|
* hashmap/class instance, which depends on `location`.
|
||||||
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
type: string,
|
|
||||||
|
/*
|
||||||
|
* Type of this attribute.
|
||||||
|
* If you want to support a custom type, use "string" combined with
|
||||||
|
* `serialize` and `deserialize`.
|
||||||
|
*/
|
||||||
|
type: "string" | "enum" | "number" | "boolean",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Location of this attribute.
|
||||||
|
* - "widget": inside IDragNode.attrs
|
||||||
|
* - "nodeProps": inside LGraphNode.properties
|
||||||
|
* - "nodeVars": an instance variable directly on an LGraphNode
|
||||||
|
* - "workflow": inside $layoutState.attrs
|
||||||
|
*/
|
||||||
location: "widget" | "nodeProps" | "nodeVars" | "workflow"
|
location: "widget" | "nodeProps" | "nodeVars" | "workflow"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Can this attribute be edited in the properties pane.
|
||||||
|
*/
|
||||||
editable: boolean,
|
editable: boolean,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Default value to supply to this attribute if it is null when the properties pane is opened.
|
||||||
|
* NOTE: This means that any attribute can't have a default null value!
|
||||||
|
*/
|
||||||
|
defaultValue: any,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If `type` is "enum", the valid values for the combo widget.
|
||||||
|
*/
|
||||||
values?: string[],
|
values?: string[],
|
||||||
hidden?: boolean,
|
|
||||||
|
/*
|
||||||
|
* If `type` is "number", step for the slider
|
||||||
|
*/
|
||||||
|
step?: number,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If `type` is "number", min for the slider
|
||||||
|
*/
|
||||||
|
min?: number,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If `type` is "number", max for the slider
|
||||||
|
*/
|
||||||
|
max?: number,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Valid `LGraphNode.type`s this property applies to if it's located in a node.
|
||||||
|
* These are like "ui/button", "ui/slider".
|
||||||
|
*/
|
||||||
validNodeTypes?: string[],
|
validNodeTypes?: string[],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback: if false, don't show the property in the pane.
|
||||||
|
* Useful if you need to show the property based on another property.
|
||||||
|
* Example: If the IDragItem is a container (not a widget), show its flex `direction`.
|
||||||
|
*/
|
||||||
|
canShow?: (arg: IDragItem | LGraphNode) => boolean,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the type of this spec is "string", but the underlying type is something else,
|
||||||
|
* convert the value to a string here so it can be edited in the textbox.
|
||||||
|
*/
|
||||||
serialize?: (arg: any) => string,
|
serialize?: (arg: any) => string,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the type of this spec is "string", but the underlying type is something else,
|
||||||
|
* convert the textbox value into the underlying value.
|
||||||
|
*/
|
||||||
deserialize?: (arg: string) => any,
|
deserialize?: (arg: string) => any,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If true, when this property is changed the properties pane will be rebuilt.
|
||||||
|
* This should be used if there's a canShow dependent on this property so
|
||||||
|
* the pane can be updated with the new list of valid properties.
|
||||||
|
*/
|
||||||
|
refreshPanelOnChange?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A list of `AttributesSpec`s grouped under a category.
|
||||||
|
*/
|
||||||
export type AttributesCategorySpec = {
|
export type AttributesCategorySpec = {
|
||||||
categoryName: string,
|
categoryName: string,
|
||||||
specs: AttributesSpec[]
|
specs: AttributesSpec[]
|
||||||
@@ -59,6 +256,17 @@ export type AttributesCategorySpec = {
|
|||||||
|
|
||||||
export type AttributesSpecList = AttributesCategorySpec[]
|
export type AttributesSpecList = AttributesCategorySpec[]
|
||||||
|
|
||||||
|
const serializeStringArray = (arg: string[]) => arg.join(",")
|
||||||
|
const deserializeStringArray = (arg: string) => {
|
||||||
|
if (arg === "")
|
||||||
|
return []
|
||||||
|
return arg.split(",").map(s => s.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Attributes that will show up in the properties panel.
|
||||||
|
* Their order in the list is the order they'll appear in the panel.
|
||||||
|
*/
|
||||||
const ALL_ATTRIBUTES: AttributesSpecList = [
|
const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||||
{
|
{
|
||||||
categoryName: "appearance",
|
categoryName: "appearance",
|
||||||
@@ -67,18 +275,21 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
name: "title",
|
name: "title",
|
||||||
type: "string",
|
type: "string",
|
||||||
location: "widget",
|
location: "widget",
|
||||||
|
defaultValue: "",
|
||||||
editable: true,
|
editable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "hidden",
|
name: "hidden",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
location: "widget",
|
location: "widget",
|
||||||
|
defaultValue: false,
|
||||||
editable: true
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disabled",
|
name: "disabled",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
location: "widget",
|
location: "widget",
|
||||||
|
defaultValue: false,
|
||||||
editable: true
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,26 +297,74 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
type: "enum",
|
type: "enum",
|
||||||
location: "widget",
|
location: "widget",
|
||||||
editable: true,
|
editable: true,
|
||||||
values: ["horizontal", "vertical"]
|
values: ["horizontal", "vertical"],
|
||||||
|
defaultValue: "vertical",
|
||||||
|
canShow: (di: IDragItem) => di.type === "container"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "flexGrow",
|
name: "flexGrow",
|
||||||
type: "number",
|
type: "number",
|
||||||
location: "widget",
|
location: "widget",
|
||||||
|
defaultValue: 100,
|
||||||
editable: true
|
editable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "classes",
|
name: "classes",
|
||||||
type: "string",
|
type: "string",
|
||||||
location: "widget",
|
location: "widget",
|
||||||
|
defaultValue: "",
|
||||||
editable: true,
|
editable: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Container variants
|
||||||
{
|
{
|
||||||
name: "blockVariant",
|
name: "variant",
|
||||||
type: "enum",
|
type: "enum",
|
||||||
location: "widget",
|
location: "widget",
|
||||||
editable: true,
|
editable: true,
|
||||||
values: ["block", "hidden"]
|
values: ["block", "accordion", "tabs"],
|
||||||
|
defaultValue: "block",
|
||||||
|
canShow: (di: IDragItem) => di.type === "container",
|
||||||
|
refreshPanelOnChange: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "containerVariant",
|
||||||
|
type: "enum",
|
||||||
|
location: "widget",
|
||||||
|
editable: true,
|
||||||
|
values: ["block", "hidden"],
|
||||||
|
defaultValue: "block",
|
||||||
|
canShow: (di: IDragItem) => di.type === "container"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Accordion
|
||||||
|
{
|
||||||
|
name: "openOnStartup",
|
||||||
|
type: "boolean",
|
||||||
|
location: "widget",
|
||||||
|
editable: true,
|
||||||
|
defaultValue: false,
|
||||||
|
canShow: (di: IDragItem) => di.type === "container" && di.attrs.variant === "accordion"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Button
|
||||||
|
{
|
||||||
|
name: "buttonVariant",
|
||||||
|
type: "enum",
|
||||||
|
location: "widget",
|
||||||
|
editable: true,
|
||||||
|
validNodeTypes: ["ui/button"],
|
||||||
|
values: ["primary", "secondary"],
|
||||||
|
defaultValue: "primary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "buttonSize",
|
||||||
|
type: "enum",
|
||||||
|
location: "widget",
|
||||||
|
editable: true,
|
||||||
|
validNodeTypes: ["ui/button"],
|
||||||
|
values: ["large", "small"],
|
||||||
|
defaultValue: "large"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -118,12 +377,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
type: "string",
|
type: "string",
|
||||||
location: "nodeVars",
|
location: "nodeVars",
|
||||||
editable: true,
|
editable: true,
|
||||||
serialize: (arg: string[]) => arg.join(","),
|
defaultValue: [],
|
||||||
deserialize: (arg: string) => {
|
serialize: serializeStringArray,
|
||||||
if (arg === "")
|
deserialize: deserializeStringArray
|
||||||
return []
|
|
||||||
return arg.split(",").map(s => s.trim())
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Range
|
// Range
|
||||||
@@ -132,6 +388,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
type: "number",
|
type: "number",
|
||||||
location: "nodeProps",
|
location: "nodeProps",
|
||||||
editable: true,
|
editable: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
min: -2 ^ 16,
|
||||||
|
max: 2 ^ 16,
|
||||||
validNodeTypes: ["ui/slider"],
|
validNodeTypes: ["ui/slider"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -139,6 +398,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
type: "number",
|
type: "number",
|
||||||
location: "nodeProps",
|
location: "nodeProps",
|
||||||
editable: true,
|
editable: true,
|
||||||
|
defaultValue: 10,
|
||||||
|
min: -2 ^ 16,
|
||||||
|
max: 2 ^ 16,
|
||||||
validNodeTypes: ["ui/slider"],
|
validNodeTypes: ["ui/slider"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,16 +408,31 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
type: "number",
|
type: "number",
|
||||||
location: "nodeProps",
|
location: "nodeProps",
|
||||||
editable: true,
|
editable: true,
|
||||||
|
defaultValue: 1,
|
||||||
|
min: -2 ^ 16,
|
||||||
|
max: 2 ^ 16,
|
||||||
validNodeTypes: ["ui/slider"],
|
validNodeTypes: ["ui/slider"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Button
|
// Button
|
||||||
{
|
{
|
||||||
name: "message",
|
name: "param",
|
||||||
type: "string",
|
type: "string",
|
||||||
location: "nodeProps",
|
location: "nodeProps",
|
||||||
editable: true,
|
editable: true,
|
||||||
validNodeTypes: ["ui/button"],
|
validNodeTypes: ["ui/button"],
|
||||||
|
defaultValue: "bang"
|
||||||
|
},
|
||||||
|
|
||||||
|
// gallery
|
||||||
|
{
|
||||||
|
name: "updateMode",
|
||||||
|
type: "enum",
|
||||||
|
location: "nodeProps",
|
||||||
|
editable: true,
|
||||||
|
validNodeTypes: ["ui/gallery"],
|
||||||
|
values: ["replace", "append"],
|
||||||
|
defaultValue: "replace"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Workflow
|
// Workflow
|
||||||
@@ -163,35 +440,80 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
name: "defaultSubgraph",
|
name: "defaultSubgraph",
|
||||||
type: "string",
|
type: "string",
|
||||||
location: "workflow",
|
location: "workflow",
|
||||||
editable: true
|
editable: true,
|
||||||
|
defaultValue: ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// This is needed so the specs can be iterated with svelte's keyed #each.
|
||||||
|
let i = 0;
|
||||||
|
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||||
|
for (const val of Object.values(cat.specs)) {
|
||||||
|
val.id = i;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { ALL_ATTRIBUTES };
|
export { ALL_ATTRIBUTES };
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Something that can be dragged around in the frontend - a widget or a container.
|
||||||
|
*/
|
||||||
export interface IDragItem {
|
export interface IDragItem {
|
||||||
type: string,
|
/*
|
||||||
|
* Type of the item.
|
||||||
|
*/
|
||||||
|
type: "container" | "widget",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Unique ID of the item.
|
||||||
|
*/
|
||||||
id: DragItemID,
|
id: DragItemID,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If true, the node associated with this drag item is executing.
|
||||||
|
* Used to show an indicator on the widget/container.
|
||||||
|
*/
|
||||||
isNodeExecuting?: boolean,
|
isNodeExecuting?: boolean,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Attributes for this drag item.
|
||||||
|
*/
|
||||||
attrs: Attributes,
|
attrs: Attributes,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hackish thing to indicate to Svelte that an attribute changed.
|
||||||
|
* TODO Use Writeable<Attributes> instead!
|
||||||
|
*/
|
||||||
attrsChanged: Writable<boolean>
|
attrsChanged: Writable<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A container (block, accordion, tabs). Has child drag items.
|
||||||
|
*/
|
||||||
export interface ContainerLayout extends IDragItem {
|
export interface ContainerLayout extends IDragItem {
|
||||||
type: "container",
|
type: "container",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A widget (slider, dropdown, textbox...)
|
||||||
|
*/
|
||||||
export interface WidgetLayout extends IDragItem {
|
export interface WidgetLayout extends IDragItem {
|
||||||
type: "widget",
|
type: "widget",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* litegraph node this widget is bound to.
|
||||||
|
*/
|
||||||
node: ComfyWidgetNode
|
node: ComfyWidgetNode
|
||||||
}
|
}
|
||||||
|
|
||||||
type DragItemID = string;
|
type DragItemID = string;
|
||||||
|
|
||||||
type LayoutStateOps = {
|
type LayoutStateOps = {
|
||||||
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index: number) => ContainerLayout,
|
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
|
||||||
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index: number) => WidgetLayout,
|
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
|
||||||
findDefaultContainerForInsertion: () => ContainerLayout | null,
|
findDefaultContainerForInsertion: () => ContainerLayout | null,
|
||||||
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
||||||
nodeAdded: (node: LGraphNode) => void,
|
nodeAdded: (node: LGraphNode) => void,
|
||||||
@@ -241,7 +563,7 @@ function findDefaultContainerForInsertion(): ContainerLayout | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index: number = -1): ContainerLayout {
|
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index?: number): ContainerLayout {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
const dragItem: ContainerLayout = {
|
const dragItem: ContainerLayout = {
|
||||||
type: "container",
|
type: "container",
|
||||||
@@ -249,10 +571,9 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
|||||||
attrsChanged: writable(false),
|
attrsChanged: writable(false),
|
||||||
attrs: {
|
attrs: {
|
||||||
title: "Container",
|
title: "Container",
|
||||||
showTitle: true,
|
|
||||||
direction: "vertical",
|
direction: "vertical",
|
||||||
classes: "",
|
classes: "",
|
||||||
blockVariant: "block",
|
containerVariant: "block",
|
||||||
flexGrow: 100,
|
flexGrow: 100,
|
||||||
...attrs
|
...attrs
|
||||||
}
|
}
|
||||||
@@ -260,14 +581,14 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
|||||||
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
||||||
state.allItems[dragItem.id] = entry;
|
state.allItems[dragItem.id] = entry;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
moveItem(dragItem, parent)
|
moveItem(dragItem, parent, index)
|
||||||
}
|
}
|
||||||
console.debug("[layoutState] addContainer", state)
|
console.debug("[layoutState] addContainer", state)
|
||||||
store.set(state)
|
store.set(state)
|
||||||
return dragItem;
|
return dragItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index: number = -1): WidgetLayout {
|
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index?: number): WidgetLayout {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
const widgetName = "Widget"
|
const widgetName = "Widget"
|
||||||
const dragItem: WidgetLayout = {
|
const dragItem: WidgetLayout = {
|
||||||
@@ -277,7 +598,6 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
|||||||
attrsChanged: writable(false),
|
attrsChanged: writable(false),
|
||||||
attrs: {
|
attrs: {
|
||||||
title: widgetName,
|
title: widgetName,
|
||||||
showTitle: true,
|
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
classes: "",
|
classes: "",
|
||||||
flexGrow: 100,
|
flexGrow: 100,
|
||||||
@@ -289,7 +609,7 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
|||||||
state.allItems[dragItem.id] = entry;
|
state.allItems[dragItem.id] = entry;
|
||||||
state.allItemsByNode[node.id] = entry;
|
state.allItemsByNode[node.id] = entry;
|
||||||
console.debug("[layoutState] addWidget", state)
|
console.debug("[layoutState] addWidget", state)
|
||||||
moveItem(dragItem, parent)
|
moveItem(dragItem, parent, index)
|
||||||
return dragItem;
|
return dragItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,23 +633,10 @@ function nodeAdded(node: LGraphNode) {
|
|||||||
|
|
||||||
const parent = findDefaultContainerForInsertion();
|
const parent = findDefaultContainerForInsertion();
|
||||||
|
|
||||||
// Two cases where we want to add nodes:
|
console.debug("[layoutState] nodeAdded", node)
|
||||||
// 1. User adds a new UI node, so we should instantiate its widget in the frontend.
|
|
||||||
// 2. User adds a node with inputs that can be filled by frontend widgets.
|
|
||||||
// Depending on config, this means we should instantiate default UI nodes connected to those inputs.
|
|
||||||
|
|
||||||
console.debug(node)
|
|
||||||
if ("svelteComponentType" in node) {
|
if ("svelteComponentType" in node) {
|
||||||
addWidget(parent, node as ComfyWidgetNode);
|
addWidget(parent, node as ComfyWidgetNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add default node panel with all widgets autoinstantiated
|
|
||||||
// if (node.widgets && node.widgets.length > 0) {
|
|
||||||
// const container = addContainer(parent.id, { title: node.title, direction: "vertical", associatedNode: node.id });
|
|
||||||
// for (const widget of node.widgets) {
|
|
||||||
// addWidget(container.id, node, widget, { associatedNode: node.id });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEntry(state: LayoutState, id: DragItemID) {
|
function removeEntry(state: LayoutState, id: DragItemID) {
|
||||||
@@ -367,7 +674,7 @@ function nodeRemoved(node: LGraphNode) {
|
|||||||
store.set(state)
|
store.set(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
|
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const entry = state.allItems[target.id]
|
const entry = state.allItems[target.id]
|
||||||
if (entry.parent && entry.parent.id === to.id)
|
if (entry.parent && entry.parent.id === to.id)
|
||||||
@@ -375,9 +682,9 @@ function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
|
|||||||
|
|
||||||
if (entry.parent) {
|
if (entry.parent) {
|
||||||
const parentEntry = state.allItems[entry.parent.id];
|
const parentEntry = state.allItems[entry.parent.id];
|
||||||
const index = parentEntry.children.findIndex(c => c.id === target.id)
|
const parentIndex = parentEntry.children.findIndex(c => c.id === target.id)
|
||||||
if (index !== -1) {
|
if (parentIndex !== -1) {
|
||||||
parentEntry.children.splice(index, 1)
|
parentEntry.children.splice(parentIndex, 1)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.error(parentEntry)
|
console.error(parentEntry)
|
||||||
@@ -387,7 +694,7 @@ function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toEntry = state.allItems[to.id];
|
const toEntry = state.allItems[to.id];
|
||||||
if (index !== -1)
|
if (index != null && index >= 0)
|
||||||
toEntry.children.splice(index, 0, target)
|
toEntry.children.splice(index, 0, target)
|
||||||
else
|
else
|
||||||
toEntry.children.push(target)
|
toEntry.children.push(target)
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { writable } from 'svelte/store';
|
|||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
import type ComfyApp from "$lib/components/ComfyApp"
|
import type ComfyApp from "$lib/components/ComfyApp"
|
||||||
|
|
||||||
export type UIEditMode = "disabled" | "widgets" | "containers" | "layout";
|
export type UIEditMode = "widgets" | "containers" | "layout";
|
||||||
|
|
||||||
export type UIState = {
|
export type UIState = {
|
||||||
app: ComfyApp,
|
app: ComfyApp,
|
||||||
nodesLocked: boolean,
|
nodesLocked: boolean,
|
||||||
graphLocked: boolean,
|
graphLocked: boolean,
|
||||||
autoAddUI: boolean,
|
autoAddUI: boolean,
|
||||||
|
uiUnlocked: boolean,
|
||||||
uiEditMode: UIEditMode,
|
uiEditMode: UIEditMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +20,8 @@ const store: WritableUIStateStore = writable(
|
|||||||
graphLocked: false,
|
graphLocked: false,
|
||||||
nodesLocked: false,
|
nodesLocked: false,
|
||||||
autoAddUI: true,
|
autoAddUI: true,
|
||||||
uiEditMode: "disabled",
|
uiUnlocked: false,
|
||||||
|
uiEditMode: "widgets"
|
||||||
})
|
})
|
||||||
|
|
||||||
const uiStateStore: WritableUIStateStore =
|
const uiStateStore: WritableUIStateStore =
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
import type { ComfySliderNode } from "$lib/nodes/index";
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Button } from "@gradio/button";
|
import { Button } from "@gradio/button";
|
||||||
import { get, type Writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
let node: ComfyButtonNode | null = null;
|
let node: ComfyButtonNode | null = null;
|
||||||
let nodeValue: Writable<boolean> | null = null;
|
let nodeValue: Writable<boolean> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let attrsChanged: Writable<boolean> | null = null;
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
if (widget) {
|
if (widget) {
|
||||||
node = widget.node as ComfyButtonNode
|
node = widget.node as ComfyButtonNode
|
||||||
nodeValue = node.value;
|
nodeValue = node.value;
|
||||||
propsChanged = node.propsChanged;
|
attrsChanged = widget.attrsChanged;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,25 +24,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
full_width: "100%"
|
full_width: "100%",
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper gradio-button">
|
<div class="wrapper gradio-button">
|
||||||
|
{#key $attrsChanged}
|
||||||
{#if node !== null}
|
{#if node !== null}
|
||||||
<Button
|
<Button
|
||||||
disabled={widget.attrs.disabled}
|
disabled={widget.attrs.disabled}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
variant="primary"
|
variant={widget.attrs.buttonVariant || "primary"}
|
||||||
|
size={widget.attrs.buttonSize === "small" ? "sm" : "lg"}
|
||||||
{style}>
|
{style}>
|
||||||
{widget.attrs.title}
|
{widget.attrs.title}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.wrapper {
|
.wrapper {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
:global(> button) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
55
src/lib/widgets/CheckboxWidget.svelte
Normal file
55
src/lib/widgets/CheckboxWidget.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import { Block } from "@gradio/atoms";
|
||||||
|
import { Checkbox } from "@gradio/form";
|
||||||
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
let node: ComfyCheckboxNode | null = null;
|
||||||
|
let nodeValue: Writable<boolean> | null = null;
|
||||||
|
let attrsChanged: Writable<boolean> | null = null;
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
|
if (widget) {
|
||||||
|
node = widget.node as ComfyCheckboxNode
|
||||||
|
nodeValue = node.value;
|
||||||
|
attrsChanged = widget.attrsChanged;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper gradio-checkbox">
|
||||||
|
<div class="inner">
|
||||||
|
{#key $attrsChanged}
|
||||||
|
{#if node !== null}
|
||||||
|
<Block>
|
||||||
|
<Checkbox disabled={widget.attrs.disabled} label={widget.attrs.title} bind:value={$nodeValue} />
|
||||||
|
</Block>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> .inner {
|
||||||
|
padding: 2px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: min-content;
|
||||||
|
|
||||||
|
:global(> label) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -114,6 +114,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.svelte-select) {
|
||||||
|
width: auto;
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.svelte-select-list) {
|
:global(.svelte-select-list) {
|
||||||
z-index: var(--layer-top) !important;
|
z-index: var(--layer-top) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,10 +73,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.wrapper {
|
.wrapper {
|
||||||
padding: 2px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
:global(> .block) {
|
||||||
|
border-radius: 0px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.padding {
|
.padding {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
disabled={widget.attrs.disabled}
|
disabled={widget.attrs.disabled}
|
||||||
lines={node.properties.multiline ? 5 : 1}
|
lines={node.properties.multiline ? 5 : 1}
|
||||||
max_lines={node.properties.multiline ? 5 : 1}
|
max_lines={node.properties.multiline ? 5 : 1}
|
||||||
show_label={true}
|
show_label={widget.attrs.title !== ""}
|
||||||
on:change
|
on:change
|
||||||
on:submit
|
on:submit
|
||||||
on:blur
|
on:blur
|
||||||
@@ -49,4 +49,8 @@
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(span.hide) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
127
src/scss/ux.scss
127
src/scss/ux.scss
@@ -265,36 +265,66 @@ div.float {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-name {
|
.category-name {
|
||||||
background: var(--ae-panel-bg-color) !important;
|
background: var(--ae-panel-bg-color) !important;
|
||||||
border-color: var(--ae-panel-border-color) !important;
|
border-color: var(--ae-panel-border-color) !important;
|
||||||
|
|
||||||
.title, .type {
|
.title, .type {
|
||||||
color: var(--ae-label-color)
|
color: var(--ae-label-color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.props-entry {
|
.props-entry {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-left: 1px var(--ae-panel-border-color) !important;
|
border-left: 1px var(--ae-panel-border-color) !important;
|
||||||
border-right: 1px var(--ae-panel-border-color) !important;
|
border-right: 1px var(--ae-panel-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**************/
|
||||||
|
/* Accordions */
|
||||||
|
/**************/
|
||||||
|
|
||||||
|
.block.gradio-accordion {
|
||||||
|
background-color: var(--ae-main-bg-color) !important;
|
||||||
|
/*padding-bottom: 0 !important;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.gradio-accordion:has(.label-wrap:hover) {
|
||||||
|
border-color: var(--ae-primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.gradio-accordion .label-wrap {
|
||||||
|
margin: calc(-1px + var(--ae-inside-padding-size) * -1);
|
||||||
|
width: auto;
|
||||||
|
padding: var(--ae-accordion-vertical-padding) var(--ae-accordion-horizontal-padding);
|
||||||
|
border-radius: var(--ae-panel-border-radius);
|
||||||
|
line-height: var(--ae-accordion-line-height);
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: var(--ae-label-color) !important;
|
||||||
}
|
}
|
||||||
|
&:hover {
|
||||||
|
> span {
|
||||||
|
color: var(--ae-main-bg-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*pointer-events: none !important;*/
|
||||||
|
}
|
||||||
|
.block.gradio-accordion .hide + .open.label-wrap {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.block.gradio-accordion .label-wrap.open {
|
||||||
|
margin-bottom: calc(var(--ae-inside-padding-size) / 2);
|
||||||
|
}
|
||||||
|
.edit > .block.gradio-accordion .label-wrap.open {
|
||||||
|
margin-bottom: var(--ae-inside-padding-size);
|
||||||
|
}
|
||||||
|
.block.gradio-accordion > .gap.svelte-vt1mxs > div:first-child {
|
||||||
|
margin-top: calc(var(--ae-inside-padding-size) * 2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
// .container > .block {
|
.block.gradio-accordion .label-wrap:hover {
|
||||||
// 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;
|
color: var(--ae-main-bg-color) !important;
|
||||||
background-color: var(--ae-primary-color) !important;
|
background-color: var(--ae-primary-color) !important;
|
||||||
}
|
}
|
||||||
@@ -308,7 +338,28 @@ div.float {
|
|||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradio-tabs.tabs {
|
||||||
|
> .tab-nav {
|
||||||
|
border-bottom: 1px solid var(--ae-subpanel-border-color);
|
||||||
|
> button {
|
||||||
|
border-radius: 0;
|
||||||
|
border-width: var(--ae-border-width);
|
||||||
|
color: var(--ae-text-color);
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--ae-subpanel-border-color);
|
||||||
|
background: var(--ae-subpanel-bg-color);
|
||||||
|
color: var(--ae-primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .tabitem {
|
||||||
|
border: 1px solid var(--ae-subpanel-border-color);
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form>.gradio-row>.form{
|
.form>.gradio-row>.form{
|
||||||
@@ -368,6 +419,10 @@ div.gradio-row>.form{
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradio-checkbox > .inner > .block {
|
||||||
|
background-color: var(--ae-input-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper.gradio-textbox textarea {
|
.wrapper.gradio-textbox textarea {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -545,12 +600,16 @@ button.primary {
|
|||||||
border-radius: var(--ae-panel-border-radius) !important;
|
border-radius: var(--ae-panel-border-radius) !important;
|
||||||
background: var(--ae-input-bg-color) !important;
|
background: var(--ae-input-bg-color) !important;
|
||||||
color: var(--ae-input-color) !important;
|
color: var(--ae-input-color) !important;
|
||||||
}
|
|
||||||
|
|
||||||
button.secondary:hover,
|
&:hover {
|
||||||
button.primary:hover {
|
|
||||||
background: var(--ae-primary-color) !important;
|
background: var(--ae-primary-color) !important;
|
||||||
color: var(--ae-input-bg-color) !important;
|
color: var(--ae-input-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--ae-input-bg-color) !important;
|
||||||
|
color: var(--ae-input-color) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**********************/
|
/**********************/
|
||||||
@@ -813,3 +872,29 @@ input[type=range]::-ms-fill-upper {
|
|||||||
color: var(--ae-input-color) !important;
|
color: var(--ae-input-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comfy-toggle-button {
|
||||||
|
> .lg {
|
||||||
|
border-color: var(--ae-subpanel-border-color) !important;
|
||||||
|
}
|
||||||
|
&:hover svg {
|
||||||
|
color: var(--ae-main-bg-color) !important;
|
||||||
|
}
|
||||||
|
&:active svg {
|
||||||
|
color: var(--ae-placeholder-color) !important;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
color: var(--ae-input-color) !important;
|
||||||
|
}
|
||||||
|
&.toggled {
|
||||||
|
svg {
|
||||||
|
color: var(--ae-icon-color) !important;
|
||||||
|
}
|
||||||
|
&:hover svg {
|
||||||
|
color: var(--ae-main-bg-color) !important;
|
||||||
|
}
|
||||||
|
&:active svg {
|
||||||
|
color: var(--ae-placeholder-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
FullReload(["src/**/*.{js,ts,scss,svelte}"]),
|
// FullReload([
|
||||||
svelte(), ,
|
// // "src/**/*.{js,ts,scss,svelte}"
|
||||||
|
// "src/**/*.{scss}",
|
||||||
|
// "src/lib/stores/*.*",
|
||||||
|
// "src/**/ComfyApp.{ts,svelte}"
|
||||||
|
// ]),
|
||||||
|
svelte(),
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user