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",
|
||||
"dependencies": {
|
||||
"@gradio/accordion": "workspace:*",
|
||||
"@gradio/atoms": "workspace:*",
|
||||
"@gradio/button": "workspace:*",
|
||||
"@gradio/client": "workspace:*",
|
||||
"@gradio/form": "workspace:*",
|
||||
"@gradio/gallery": "workspace:*",
|
||||
"@gradio/icons": "workspace:*",
|
||||
"@gradio/tabs": "workspace:*",
|
||||
"@gradio/theme": "workspace:*",
|
||||
"@gradio/upload": "workspace:*",
|
||||
"@gradio/utils": "workspace:*",
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -4,6 +4,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@gradio/accordion':
|
||||
specifier: workspace:*
|
||||
version: link:gradio/js/accordion
|
||||
'@gradio/atoms':
|
||||
specifier: workspace:*
|
||||
version: link:gradio/js/atoms
|
||||
@@ -22,6 +25,9 @@ importers:
|
||||
'@gradio/icons':
|
||||
specifier: workspace:*
|
||||
version: link:gradio/js/icons
|
||||
'@gradio/tabs':
|
||||
specifier: workspace:*
|
||||
version: link:gradio/js/tabs
|
||||
'@gradio/theme':
|
||||
specifier: workspace:*
|
||||
version: link:gradio/js/theme
|
||||
|
||||
@@ -50,6 +50,7 @@ export class ImageViewer {
|
||||
showModal(event: Event) {
|
||||
const source = (event.target || event.srcElement) as HTMLImageElement;
|
||||
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
||||
console.debug("[ImageViewer] showModal", event, source, galleryElem);
|
||||
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
|
||||
console.error("No buttons found on gallery element!", galleryElem)
|
||||
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 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;
|
||||
@@ -38,60 +42,59 @@
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
};
|
||||
|
||||
const tt = "asd\nasdlkj"
|
||||
</script>
|
||||
|
||||
{#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}"
|
||||
class:hide-block={container.attrs.blockVariant === "hidden"}
|
||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)}
|
||||
class:root-container={zIndex === 0}
|
||||
class:is-executing={container.isNodeExecuting}
|
||||
class:edit={edit}>
|
||||
<Block>
|
||||
{#if container.attrs.title !== ""}
|
||||
<label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}>
|
||||
<BlockTitle>{container.attrs.title}</BlockTitle>
|
||||
</label>
|
||||
{/if}
|
||||
<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: zIndex === 0 || $layoutState.currentSelection.length > 2 || $uiState.uiEditMode === "disabled"
|
||||
}}"
|
||||
on:consider="{handleConsider}"
|
||||
on:finalize="{handleFinalize}"
|
||||
<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}>
|
||||
<Block>
|
||||
{#if container.attrs.title && container.attrs.title !== ""}
|
||||
<label for={String(container.id)} class={($uiState.uiUnlocked && $uiState.uiEditMode === "widgets") ? "edit-title-label" : ""}>
|
||||
<BlockTitle>{container.attrs.title}</BlockTitle>
|
||||
</label>
|
||||
{/if}
|
||||
<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}` : ""}
|
||||
>
|
||||
{#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}
|
||||
</Block>
|
||||
</div>
|
||||
{/key}
|
||||
<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}
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@@ -158,7 +161,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
:global(.block) {
|
||||
> :global(.block) {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@@ -170,7 +173,7 @@
|
||||
padding: 1.4em;
|
||||
}
|
||||
|
||||
:global(.hide-block > .block) {
|
||||
> :global(.hide-block > .block) {
|
||||
padding: 0.5em 0.25em;
|
||||
box-shadow: unset;
|
||||
border-width: 0;
|
||||
@@ -289,12 +292,6 @@
|
||||
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) {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
import ComfyProperties from "./ComfyProperties.svelte";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
|
||||
import ComfyGraphView from "./ComfyGraphView.svelte";
|
||||
|
||||
export let app: ComfyApp = undefined;
|
||||
let imageViewer: ImageViewer;
|
||||
@@ -29,7 +31,7 @@
|
||||
let containerElem: HTMLDivElement;
|
||||
let resizeTimeout: NodeJS.Timeout | null;
|
||||
let hasShownUIHelpToast: boolean = false;
|
||||
let uiTheme: string = "anapnoe";
|
||||
let uiTheme: string = "";
|
||||
|
||||
let debugLayout: boolean = false;
|
||||
|
||||
@@ -58,6 +60,7 @@
|
||||
$layoutState.currentSelection = []
|
||||
|
||||
let graphSize = 0;
|
||||
let graphTransitioning = false;
|
||||
|
||||
function toggleGraph() {
|
||||
if (graphSize == 0) {
|
||||
@@ -69,7 +72,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let propsSidebarSize = 0; //15;
|
||||
let propsSidebarSize = 15; //15;
|
||||
|
||||
function toggleProps() {
|
||||
if (propsSidebarSize == 0) {
|
||||
@@ -120,11 +123,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function doRecenter(): void {
|
||||
app.lCanvas.recenter();
|
||||
}
|
||||
|
||||
$: if ($uiState.uiEditMode !== "disabled" && !hasShownUIHelpToast) {
|
||||
$: if ($uiState.uiUnlocked && !hasShownUIHelpToast) {
|
||||
hasShownUIHelpToast = true;
|
||||
toast.push("Right-click to open context menu.")
|
||||
}
|
||||
@@ -144,11 +143,17 @@
|
||||
}
|
||||
|
||||
$: if (containerElem) {
|
||||
let wrappers = containerElem.querySelectorAll<HTMLDivElement>(".pane-wrapper")
|
||||
for (const wrapper of wrappers) {
|
||||
const paneNode = wrapper.parentNode as HTMLElement; // get the node inside the <Pane/>
|
||||
paneNode.ontransitionend = () => {
|
||||
app.resizeCanvas()
|
||||
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
|
||||
if (canvas) {
|
||||
const paneNode = canvas.closest(".splitpanes__pane")
|
||||
if (paneNode) {
|
||||
(paneNode as HTMLElement).ontransitionstart = () => {
|
||||
graphTransitioning = true
|
||||
}
|
||||
(paneNode as HTMLElement).ontransitionend = () => {
|
||||
graphTransitioning = false
|
||||
app.resizeCanvas()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +163,7 @@
|
||||
(window as any).app = app;
|
||||
(window as any).appPane = uiPane;
|
||||
|
||||
await import('../../scss/ux.scss');
|
||||
// await import('../../scss/ux.scss');
|
||||
|
||||
refreshView();
|
||||
})
|
||||
@@ -189,9 +194,7 @@
|
||||
<ComfyUIPane bind:this={uiPane} {app} />
|
||||
</Pane>
|
||||
<Pane bind:size={graphSize}>
|
||||
<div class="canvas-wrapper pane-wrapper">
|
||||
<canvas id="graph-canvas" />
|
||||
</div>
|
||||
<ComfyGraphView {app} transitioning={graphTransitioning} />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
@@ -203,50 +206,53 @@
|
||||
</Splitpanes>
|
||||
</div>
|
||||
<div id="bottombar">
|
||||
<Button variant="primary" on:click={queuePrompt}>
|
||||
Queue Prompt
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleGraph}>
|
||||
Toggle Graph
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleProps}>
|
||||
Toggle Props
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleQueue}>
|
||||
Toggle Queue
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doLoadDefault}>
|
||||
Load Default
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doRecenter}>
|
||||
Recenter
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doRefreshCombos}>
|
||||
🔄
|
||||
</Button>
|
||||
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||
<label class="label" for="enable-ui-editing">
|
||||
<BlockTitle>Enable UI Editing</BlockTitle>
|
||||
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="widgets">Widgets</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label" for="ui-theme">
|
||||
<BlockTitle>Theme</BlockTitle>
|
||||
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
||||
<option value="">None</option>
|
||||
<option value="anapnoe">Anapnoe</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="left">
|
||||
<Button variant="primary" on:click={queuePrompt}>
|
||||
Queue Prompt
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleGraph}>
|
||||
Toggle Graph
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleProps}>
|
||||
Toggle Props
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={toggleQueue}>
|
||||
Toggle Queue
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doLoadDefault}>
|
||||
Load Default
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doRefreshCombos}>
|
||||
🔄
|
||||
</Button>
|
||||
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
||||
<span style="display: inline-flex !important">
|
||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||
</span>
|
||||
<span class="label" for="ui-edit-mode">
|
||||
<BlockTitle>UI Edit mode</BlockTitle>
|
||||
<select id="ui-edit-mode" name="ui-edit-mode" bind:value={$uiState.uiEditMode}>
|
||||
<option value="widgets">Widgets</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="label" for="ui-theme">
|
||||
<BlockTitle>Theme</BlockTitle>
|
||||
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
||||
<option value="">None</option>
|
||||
<option value="anapnoe">Anapnoe</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ComfyUnlockUIButton bind:toggled={$uiState.uiUnlocked} />
|
||||
</div>
|
||||
</div>
|
||||
<LightboxModal />
|
||||
</div>
|
||||
@@ -266,26 +272,24 @@
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#comfy-ui {
|
||||
}
|
||||
|
||||
#comfy-graph {
|
||||
}
|
||||
|
||||
#graph-canvas {
|
||||
}
|
||||
|
||||
#bottombar {
|
||||
padding-top: 0.5em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--layout-gap);
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #333;
|
||||
gap: var(--layout-gap);
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
margin-top: auto;
|
||||
overflow-x: auto;
|
||||
|
||||
> .left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> .right {
|
||||
margin-left: auto
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
@@ -350,4 +354,8 @@
|
||||
label.label > :global(span) {
|
||||
top: 20%;
|
||||
}
|
||||
|
||||
span.left {
|
||||
right: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -106,6 +106,8 @@ export default class ComfyApp {
|
||||
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||
LiteGraph.alt_drag_do_clone_nodes = true;
|
||||
|
||||
(window as any).LiteGraph = LiteGraph;
|
||||
|
||||
// await this.#invokeExtensionsAsync("init");
|
||||
await this.registerNodes();
|
||||
|
||||
@@ -230,17 +232,17 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
private addDropHandler() {
|
||||
this.dropZone = document.getElementById("dropzone");
|
||||
// this.dropZone = document.getElementById("dropzone");
|
||||
|
||||
if (this.dropZone) {
|
||||
window.addEventListener('dragenter', this.allowDrag.bind(this));
|
||||
this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
|
||||
this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
|
||||
this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
|
||||
}
|
||||
else {
|
||||
console.warn("No dropzone detected (probably on mobile).")
|
||||
}
|
||||
// if (this.dropZone) {
|
||||
// window.addEventListener('dragenter', this.allowDrag.bind(this));
|
||||
// this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
|
||||
// this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
|
||||
// this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
|
||||
// }
|
||||
// else {
|
||||
// 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);
|
||||
|
||||
if (!node_.isBackendNode) {
|
||||
console.debug("Not serializing node: ", node_.type)
|
||||
// console.debug("Not serializing node: ", node_.type)
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -562,8 +564,8 @@ export default class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
console.warn({ workflow, output })
|
||||
console.warn(promptToGraphVis({ workflow, output }))
|
||||
// console.debug({ workflow, output })
|
||||
// console.debug(promptToGraphVis({ workflow, output }))
|
||||
|
||||
return { workflow, output };
|
||||
}
|
||||
@@ -588,7 +590,7 @@ export default class ComfyApp {
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
for (const node of this.lGraph._nodes_in_order) {
|
||||
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) {
|
||||
const node = this.lGraph.getNodeById(n.id);
|
||||
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 values: string[] = [""];
|
||||
export let name: string = "";
|
||||
export let disabled: boolean = false;
|
||||
let value_: string = ""
|
||||
|
||||
$: value;
|
||||
@@ -27,7 +28,7 @@
|
||||
<label class="select-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="select">
|
||||
<select on:blur bind:value>
|
||||
<select on:blur bind:value {disabled}>
|
||||
{#each values as value}
|
||||
<option {value}>
|
||||
{value}
|
||||
@@ -53,4 +54,8 @@
|
||||
.select-title {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</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";
|
||||
|
||||
export let value: number = 0;
|
||||
export let min: number = -1024
|
||||
export let max: number = 1024
|
||||
export let step: number = 1;
|
||||
export let name: string = "";
|
||||
export let disabled: boolean = false;
|
||||
let value_: number = 0;
|
||||
|
||||
$: value;
|
||||
@@ -26,7 +29,7 @@
|
||||
<label class="number-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="number">
|
||||
<input type="number" bind:value {step}>
|
||||
<input type="number" bind:value {min} {max} {step} {disabled}>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -42,4 +45,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
input[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,310 +1,447 @@
|
||||
<script lang="ts">
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { TextBox, Checkbox } from "@gradio/form";
|
||||
import { LGraphNode } from "@litegraph-ts/core"
|
||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||
import { get } from "svelte/store"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||
|
||||
let target: IDragItem | null = null;
|
||||
let node: LGraphNode | null = null;
|
||||
|
||||
$: if ($layoutState.currentSelection.length > 0) {
|
||||
const targetId = $layoutState.currentSelection.slice(-1)[0]
|
||||
target = $layoutState.allItems[targetId].dragItem
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
}
|
||||
else {
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
else if ($layoutState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $layoutState.currentSelectionNodes[0]
|
||||
}
|
||||
else {
|
||||
target = null
|
||||
node = null;
|
||||
}
|
||||
|
||||
let targetType: string = "???"
|
||||
|
||||
$: {
|
||||
if (node != null)
|
||||
targetType = node.type || "Node"
|
||||
else if (target)
|
||||
targetType = "Group"
|
||||
else
|
||||
targetType = ""
|
||||
}
|
||||
|
||||
function validNodeProperty(spec: AttributesSpec, node: LGraphNode): boolean {
|
||||
if (spec.validNodeTypes) {
|
||||
return spec.validNodeTypes.indexOf(node.type) !== -1;
|
||||
}
|
||||
return spec.name in node.properties
|
||||
}
|
||||
|
||||
function updateAttribute(entry: AttributesSpec, target: IDragItem, value: any) {
|
||||
if (target) {
|
||||
const name = entry.name
|
||||
console.warn("updateAttribute", name, value)
|
||||
|
||||
target.attrs[name] = value
|
||||
target.attrsChanged.set(!get(target.attrsChanged))
|
||||
|
||||
if (node && "propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateProperty(entry: AttributesSpec, value: any) {
|
||||
if (node) {
|
||||
const name = entry.name
|
||||
console.warn("updateProperty", name, value)
|
||||
|
||||
node.properties[name] = value;
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getVar(node: LGraphNode, entry: AttributesSpec) {
|
||||
let value = node[entry.name]
|
||||
if (entry.serialize)
|
||||
value = entry.serialize(value)
|
||||
console.debug("[ComfyProperties] getVar", entry, value, node)
|
||||
return value
|
||||
}
|
||||
|
||||
function updateVar(entry: any, value: any) {
|
||||
if (node) {
|
||||
const name = entry.name
|
||||
console.warn("updateProperty", name, value)
|
||||
|
||||
if (entry.deserialize)
|
||||
value = entry.deserialize(value)
|
||||
|
||||
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
|
||||
node[name] = value;
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateWorkflowAttribute(entry: AttributesSpec, value: any) {
|
||||
const name = entry.name
|
||||
console.warn("updateWorkflowAttribute", name, value)
|
||||
|
||||
$layoutState.attrs[name] = value
|
||||
$layoutState = $layoutState
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="props">
|
||||
<div class="top">
|
||||
<div class="target-name">
|
||||
<span>
|
||||
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
|
||||
{#if targetType !== ""}
|
||||
<span class="type">({targetType})</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="props-entries">
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
<span class="title">{category.categoryName}</span>
|
||||
</span>
|
||||
</div>
|
||||
{#each category.specs as spec(spec.name)}
|
||||
{#if spec.location === "widget" && target && spec.name in target.attrs}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
on:input={(e) => updateAttribute(spec, target, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={target.attrs[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={target.attrs[spec.name]}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if node}
|
||||
{#if spec.location === "nodeProps" && validNodeProperty(spec, node)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={node.properties[spec.name]}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
on:input={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={node.properties[spec.name]}
|
||||
label={spec.name}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name]}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if spec.location === "nodeVars" && spec.name in node}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
on:input={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
step={1}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
values={spec.values}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.props-entry {
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.target-name {
|
||||
border-color: var(--neutral-400);
|
||||
background: var(--neutral-300);
|
||||
padding: 0.8rem 1.0rem;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
padding: 0.4rem 1.0rem;
|
||||
border-color: var(--neutral-300);
|
||||
background: var(--neutral-200);
|
||||
}
|
||||
|
||||
.target-name, .category-name {
|
||||
border-width: var(--block-border-width);
|
||||
|
||||
.type {
|
||||
color: var(--neutral-500);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
/* width: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0.5em; */
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { TextBox, Checkbox } from "@gradio/form";
|
||||
import { LGraphNode } from "@litegraph-ts/core"
|
||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||
import uiState from "$lib/stores/uiState"
|
||||
import { get, type Writable, writable } from "svelte/store"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||
|
||||
let target: IDragItem | null = null;
|
||||
let node: LGraphNode | null = null;
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
let refreshPanel: Writable<number> = writable(0);
|
||||
|
||||
$: if ($layoutState.currentSelection.length > 0) {
|
||||
const targetId = $layoutState.currentSelection.slice(-1)[0]
|
||||
target = $layoutState.allItems[targetId].dragItem
|
||||
attrsChanged = target.attrsChanged;
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
}
|
||||
else {
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
else if ($layoutState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $layoutState.currentSelectionNodes[0]
|
||||
attrsChanged = null;
|
||||
}
|
||||
else {
|
||||
target = 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 = "???"
|
||||
|
||||
$: {
|
||||
if (node != null)
|
||||
targetType = node.type || "Node"
|
||||
else if (target)
|
||||
targetType = "Group"
|
||||
else
|
||||
targetType = ""
|
||||
}
|
||||
|
||||
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) {
|
||||
return spec.validNodeTypes.indexOf(node.type) !== -1;
|
||||
}
|
||||
return spec.name in node.properties
|
||||
}
|
||||
|
||||
function validNodeVar(spec: AttributesSpec, node: LGraphNode | null): boolean {
|
||||
if (node == null || spec.location !== "nodeVars")
|
||||
return false;
|
||||
|
||||
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.attrsChanged.set(!get(target.attrsChanged))
|
||||
|
||||
if (node && "propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
|
||||
console.warn(spec)
|
||||
if (spec.refreshPanelOnChange) {
|
||||
console.error("A! refresh")
|
||||
$refreshPanel += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function updateProperty(spec: AttributesSpec, value: any) {
|
||||
if (node == null || !spec.editable)
|
||||
return
|
||||
|
||||
const name = spec.name
|
||||
console.warn("updateProperty", name, value)
|
||||
|
||||
node.properties[name] = value;
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
|
||||
if (spec.refreshPanelOnChange)
|
||||
$refreshPanel += 1;
|
||||
}
|
||||
|
||||
function getVar(node: LGraphNode, spec: AttributesSpec) {
|
||||
let value = node[spec.name]
|
||||
if (value == null)
|
||||
value = spec.defaultValue
|
||||
else if (spec.serialize)
|
||||
value = spec.serialize(value)
|
||||
console.debug("[ComfyProperties] getVar", spec, value, node)
|
||||
return value
|
||||
}
|
||||
|
||||
function updateVar(spec: AttributesSpec, value: any) {
|
||||
if (node == null || !spec.editable)
|
||||
return;
|
||||
|
||||
const name = spec.name
|
||||
|
||||
console.debug("[ComfyProperties] updateVar", spec, value, name, node)
|
||||
if (spec.deserialize)
|
||||
value = spec.deserialize(value)
|
||||
|
||||
node[name] = value;
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
|
||||
if (spec.refreshPanelOnChange)
|
||||
$refreshPanel += 1;
|
||||
}
|
||||
|
||||
function updateWorkflowAttribute(spec: AttributesSpec, value: any) {
|
||||
if (!spec.editable)
|
||||
return;
|
||||
|
||||
const name = spec.name
|
||||
console.warn("updateWorkflowAttribute", name, value)
|
||||
|
||||
$layoutState.attrs[name] = value
|
||||
$layoutState = $layoutState
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="props">
|
||||
<div class="top">
|
||||
<div class="target-name">
|
||||
<span>
|
||||
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
|
||||
{#if targetType !== ""}
|
||||
<span class="type">({targetType})</span>
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="props-entries">
|
||||
{#key $refreshPanel}
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
<span class="title">{category.categoryName}</span>
|
||||
</span>
|
||||
</div>
|
||||
{#each category.specs as spec(spec.id)}
|
||||
{#if validWidgetAttribute(spec, target)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={getAttribute(target, spec)}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
on:input={(e) => updateAttribute(spec, target, e.detail)}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={getAttribute(target, spec)}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={getAttribute(target, spec)}
|
||||
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)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={getAttribute(target, spec)}
|
||||
values={spec.values}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if node}
|
||||
{#if validNodeProperty(spec, node)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={node.properties[spec.name] || spec.defaultValue}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
on:input={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={node.properties[spec.name] || spec.defaultValue}
|
||||
label={spec.name}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name] || spec.defaultValue}
|
||||
step={spec.step || 1}
|
||||
min={spec.min || -1024}
|
||||
max={spec.max || 1024}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name] || spec.defaultValue}
|
||||
values={spec.values}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if validNodeVar(spec, node)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
on:input={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
step={spec.step || 1}
|
||||
min={spec.min || -1024}
|
||||
max={spec.max || 1024}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
values={spec.values}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if !node && !target && validWorkflowAttribute(spec)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||
step={spec.step || 1}
|
||||
min={spec.min || -1024}
|
||||
max={spec.max || 1024}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name] || spec.defaultValue}
|
||||
values={spec.values}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.props-entry {
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.target-name {
|
||||
border-color: var(--neutral-400);
|
||||
background: var(--neutral-300);
|
||||
padding: 0.8rem 1.0rem;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
|
||||
.type {
|
||||
padding-left: 0.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
padding: 0.4rem 1.0rem;
|
||||
border-color: var(--neutral-300);
|
||||
background: var(--neutral-200);
|
||||
}
|
||||
|
||||
.target-name, .category-name {
|
||||
border-width: var(--block-border-width);
|
||||
|
||||
.type {
|
||||
color: var(--neutral-500);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
/* width: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 0.5em; */
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
function ungroup() {
|
||||
const item = layoutState.getCurrentSelection()[0]
|
||||
if (item.type !== "container")
|
||||
if (!item || item.type !== "container")
|
||||
return;
|
||||
|
||||
$layoutState.currentSelection = []
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
|
||||
async function onRightClick(e) {
|
||||
if ($uiState.uiEditMode === "disabled")
|
||||
if (!$uiState.uiUnlocked)
|
||||
return;
|
||||
|
||||
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 { startDrag, stopDrag } from "$lib/utils"
|
||||
import BlockContainer from "./BlockContainer.svelte"
|
||||
import Container from "./Container.svelte"
|
||||
import { type Writable } from "svelte/store"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
propsChanged = null;
|
||||
}
|
||||
|
||||
$: showHandles = $uiState.uiEditMode === "widgets" // TODO
|
||||
$: showHandles = $uiState.uiUnlocked
|
||||
&& $uiState.uiEditMode === "widgets" // TODO
|
||||
&& zIndex > 1
|
||||
&& !$layoutState.isMenuOpen
|
||||
|
||||
@@ -58,15 +59,15 @@
|
||||
|
||||
{#if container}
|
||||
{#key $attrsChanged}
|
||||
<BlockContainer {container} {classes} {zIndex} {showHandles} />
|
||||
<Container {container} {classes} {zIndex} {showHandles} />
|
||||
{/key}
|
||||
{:else if widget && widget.node}
|
||||
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
{#key $attrsChanged}
|
||||
{#key $propsChanged}
|
||||
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
|
||||
class:edit={edit}
|
||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
|
||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
|
||||
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
|
||||
class:hidden={widget.attrs.hidden}
|
||||
>
|
||||
@@ -83,8 +84,12 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.widget.selected {
|
||||
background: var(--color-yellow-200);
|
||||
.widget {
|
||||
height: 100%;
|
||||
|
||||
&.selected {
|
||||
background: var(--color-yellow-200);
|
||||
}
|
||||
}
|
||||
.container.selected {
|
||||
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 { toast } from '@zerodevx/svelte-toast'
|
||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||
import { get } from "svelte/store";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
|
||||
export interface ComfyQueueEventsProperties extends Record<any, any> {
|
||||
prompt: SerializedPrompt | null
|
||||
}
|
||||
|
||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||
override properties: ComfyQueueEventsProperties = {
|
||||
prompt: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
@@ -22,29 +22,29 @@ export class ComfyQueueEvents extends ComfyGraphNode {
|
||||
],
|
||||
}
|
||||
|
||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||
if (property === "value") {
|
||||
this.setOutputData(2, this.properties.prompt)
|
||||
private getActionParams(subgraph: string | null): any {
|
||||
let queue = get(queueState)
|
||||
let remaining = 0;
|
||||
|
||||
if (typeof queue.queueRemaining === "number")
|
||||
remaining = queue.queueRemaining
|
||||
|
||||
return {
|
||||
queueRemaining: remaining,
|
||||
subgraph
|
||||
}
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
this.setOutputData(2, this.properties.prompt)
|
||||
override beforeQueued(subgraph: string | null) {
|
||||
this.triggerSlot(0, this.getActionParams(subgraph))
|
||||
}
|
||||
|
||||
override beforeQueued() {
|
||||
this.setProperty("value", null)
|
||||
this.triggerSlot(0, "bang")
|
||||
}
|
||||
|
||||
override afterQueued(p: SerializedPrompt) {
|
||||
this.setProperty("value", p)
|
||||
this.triggerSlot(1, "bang")
|
||||
override afterQueued(p: SerializedPrompt, subgraph: string | null) {
|
||||
this.triggerSlot(1, this.getActionParams(subgraph))
|
||||
}
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
super.onSerialize(o)
|
||||
o.properties = { prompt: null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export class ComfyCopyAction extends ComfyGraphNode {
|
||||
{ name: "copy", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "out", type: "*" }
|
||||
{ name: "out", type: BuiltInSlotType.EVENT }
|
||||
],
|
||||
}
|
||||
|
||||
@@ -130,13 +130,15 @@ export class ComfyCopyAction extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
this.setProperty("value", this.getInputData(0))
|
||||
if (this.getInputLink(0))
|
||||
this.setProperty("value", this.getInputData(0))
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
this.setProperty("value", this.getInputData(0))
|
||||
this.setOutputData(0, this.properties.value)
|
||||
console.log("setData", this.properties.value)
|
||||
if (action === "copy") {
|
||||
this.setProperty("value", this.getInputData(0))
|
||||
this.triggerSlot(0, this.properties.value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ export type DefaultWidgetLayout = {
|
||||
export default class ComfyGraphNode extends LGraphNode {
|
||||
isBackendNode?: boolean;
|
||||
|
||||
beforeQueued?(): void;
|
||||
afterQueued?(prompt: SerializedPrompt): void;
|
||||
beforeQueued?(subgraph: string | null): void;
|
||||
afterQueued?(prompt: SerializedPrompt, subgraph: string | null): void;
|
||||
onExecuted?(output: any): void;
|
||||
|
||||
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 type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||
|
||||
@@ -6,7 +6,8 @@ export interface ComfyImageCacheNodeProperties extends Record<any, any> {
|
||||
images: GalleryOutput | null,
|
||||
index: number,
|
||||
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
|
||||
genNumber: number
|
||||
genNumber: number,
|
||||
updateMode: "replace" | "append"
|
||||
}
|
||||
|
||||
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
|
||||
@@ -20,7 +21,8 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
||||
images: null,
|
||||
index: 0,
|
||||
filenames: {},
|
||||
genNumber: 0
|
||||
genNumber: 0,
|
||||
updateMode: "replace"
|
||||
}
|
||||
|
||||
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 _state: ImageCacheState = "none"
|
||||
|
||||
stateWidget: ITextWidget;
|
||||
filenameWidget: ITextWidget;
|
||||
modeWidget: IComboWidget;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name)
|
||||
@@ -57,6 +63,14 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
||||
""
|
||||
);
|
||||
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) {
|
||||
@@ -66,13 +80,23 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
||||
else
|
||||
this.properties.index = 0
|
||||
}
|
||||
else if (property === "updateMode") {
|
||||
this.modeWidget.value = value;
|
||||
}
|
||||
|
||||
this.updateWidgets()
|
||||
}
|
||||
|
||||
private updateWidgets() {
|
||||
if (this.properties.filenames && this.properties.images) {
|
||||
const fileCount = this.properties.images.images.length;
|
||||
const cachedCount = Object.keys(this.properties.filenames).length
|
||||
console.warn(cachedCount, this.properties.filenames)
|
||||
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
|
||||
}
|
||||
else {
|
||||
this.filenameWidget.value = `No files cached`
|
||||
}
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
@@ -185,6 +209,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
||||
this.setProperty("images", null)
|
||||
this.setProperty("filenames", {})
|
||||
this.setProperty("index", 0)
|
||||
this.updateWidgets();
|
||||
return
|
||||
}
|
||||
|
||||
@@ -192,11 +217,24 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
||||
|
||||
if (link.data && "images" in link.data) {
|
||||
this.setProperty("genNumber", this.properties.genNumber + 1)
|
||||
this.setProperty("images", link.data as GalleryOutput)
|
||||
this.setProperty("filenames", {})
|
||||
console.debug("[ComfyImageCacheNode] Received output!", link.data)
|
||||
|
||||
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("filenames", {})
|
||||
}
|
||||
|
||||
console.debug("[ComfyImageCacheNode] Received output!", output, this.properties.updateMode, this.properties.images)
|
||||
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 ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
||||
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
@@ -48,8 +49,11 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
override isBackendNode = false;
|
||||
override serialize_widgets = true;
|
||||
|
||||
outputIndex: number | null = 0;
|
||||
// input slots
|
||||
inputIndex: number = 0;
|
||||
|
||||
// output slots
|
||||
outputIndex: number | null = 0;
|
||||
changedIndex: number | null = 1;
|
||||
|
||||
displayWidget: ITextWidget;
|
||||
@@ -94,7 +98,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
|
||||
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]
|
||||
if (changedOutput.type === BuiltInSlotType.EVENT)
|
||||
this.triggerSlot(this.changedIndex, "changed")
|
||||
@@ -118,10 +122,8 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
if (this.copyFromInputLink) {
|
||||
if (this.inputs.length >= 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)
|
||||
const input = this.getInputLink(this.inputIndex)
|
||||
input.data = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +233,8 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "number" }
|
||||
{ name: "value", type: "number" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "number" },
|
||||
@@ -250,6 +253,11 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
|
||||
super(name, 0)
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
if (action === "store" && typeof param === "number")
|
||||
this.setValue(param)
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
if (typeof value !== "number")
|
||||
return;
|
||||
@@ -283,7 +291,8 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "string" }
|
||||
{ name: "value", type: "string" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "string" },
|
||||
@@ -323,6 +332,11 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||
return true;
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
if (action === "store" && typeof param === "string")
|
||||
this.setValue(param)
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
||||
return;
|
||||
@@ -358,7 +372,8 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "string" }
|
||||
{ name: "value", type: "string" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "string" },
|
||||
@@ -372,6 +387,11 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||
super(name, "")
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
if (action === "store")
|
||||
this.setValue(param)
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
super.setValue(`${value}`)
|
||||
}
|
||||
@@ -397,13 +417,15 @@ export type GalleryOutputEntry = {
|
||||
}
|
||||
|
||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||
index: number
|
||||
index: number,
|
||||
updateMode: "replace" | "append"
|
||||
}
|
||||
|
||||
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
override properties: ComfyGalleryProperties = {
|
||||
defaultValue: [],
|
||||
index: 0
|
||||
index: 0,
|
||||
updateMode: "replace"
|
||||
}
|
||||
|
||||
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 copyFromInputLink = false;
|
||||
override outputIndex = null;
|
||||
override changedIndex = null;
|
||||
|
||||
modeWidget: IComboWidget;
|
||||
|
||||
constructor(name?: string) {
|
||||
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() {
|
||||
this.setOutputData(0, this.properties.index)
|
||||
}
|
||||
|
||||
override onAction(action: any) {
|
||||
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||
if (action === "clear") {
|
||||
this.setValue([])
|
||||
}
|
||||
@@ -442,9 +477,13 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
|
||||
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||
|
||||
// const currentValue = get(this.value)
|
||||
// this.setValue(currentValue.concat(galleryItems))
|
||||
this.setValue(galleryItems)
|
||||
if (this.properties.updateMode === "append") {
|
||||
const currentValue = get(this.value)
|
||||
this.setValue(currentValue.concat(galleryItems))
|
||||
}
|
||||
else {
|
||||
this.setValue(galleryItems)
|
||||
}
|
||||
}
|
||||
this.setProperty("index", 0)
|
||||
}
|
||||
@@ -489,13 +528,13 @@ LiteGraph.registerNodeType({
|
||||
})
|
||||
|
||||
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
||||
message: string
|
||||
param: string
|
||||
}
|
||||
|
||||
export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||
override properties: ComfyButtonProperties = {
|
||||
defaultValue: false,
|
||||
message: "bang"
|
||||
param: "bang"
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
@@ -514,8 +553,8 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||
|
||||
onClick() {
|
||||
this.setValue(true)
|
||||
this.triggerSlot(0, this.properties.message);
|
||||
this.setValue(false)
|
||||
this.triggerSlot(0, this.properties.param);
|
||||
this.setValue(false) // TODO onRelease
|
||||
}
|
||||
|
||||
constructor(name?: string) {
|
||||
@@ -529,3 +568,40 @@ LiteGraph.registerNodeType({
|
||||
desc: "Button that triggers an event when clicked",
|
||||
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';
|
||||
|
||||
type DragItemEntry = {
|
||||
/*
|
||||
* Drag item.
|
||||
*/
|
||||
dragItem: IDragItem,
|
||||
|
||||
/*
|
||||
* Children of this drag item.
|
||||
* Only applies if the drag item's type is "container"
|
||||
*/
|
||||
children: IDragItem[] | null,
|
||||
|
||||
/*
|
||||
* Parent of this drag item.
|
||||
*/
|
||||
parent: IDragItem | null
|
||||
}
|
||||
|
||||
/*
|
||||
* Global workflow attributes
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
* Keeps track of the tree of UI components - widgets and the containers that
|
||||
* group them together.
|
||||
*/
|
||||
export type LayoutState = {
|
||||
/*
|
||||
* Root of the UI tree
|
||||
*/
|
||||
root: IDragItem | null,
|
||||
|
||||
/*
|
||||
* All items indexed by their own ID
|
||||
*/
|
||||
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>,
|
||||
|
||||
/*
|
||||
* Next ID to use for instantiating a new drag item
|
||||
*/
|
||||
currentId: number,
|
||||
|
||||
/*
|
||||
* Selected drag items.
|
||||
*/
|
||||
currentSelection: DragItemID[],
|
||||
|
||||
/*
|
||||
* Selected LGraphNodes inside the litegraph canvas.
|
||||
*/
|
||||
currentSelectionNodes: LGraphNode[],
|
||||
|
||||
/*
|
||||
* If true, a saved workflow is being deserialized, so ignore any
|
||||
* nodeAdded/nodeRemoved events.
|
||||
*
|
||||
* TODO: instead use LGraphAddNodeOptions.addedByDeserialize
|
||||
*/
|
||||
isConfiguring: boolean,
|
||||
|
||||
/*
|
||||
* If true, the right-click context menu is open
|
||||
*/
|
||||
isMenuOpen: boolean,
|
||||
|
||||
/*
|
||||
* Global workflow attributes
|
||||
*/
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
/**
|
||||
* Attributes for both containers and nodes.
|
||||
**/
|
||||
export type Attributes = {
|
||||
/*
|
||||
* Flex direction for containers.
|
||||
*/
|
||||
direction: "horizontal" | "vertical",
|
||||
|
||||
/*
|
||||
* Display name of this item.
|
||||
*/
|
||||
title: string,
|
||||
showTitle: boolean,
|
||||
|
||||
/*
|
||||
* List of classes to apply to the component.
|
||||
*/
|
||||
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,
|
||||
|
||||
/*
|
||||
* If true, grey out this component in the UI
|
||||
*/
|
||||
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 = {
|
||||
/*
|
||||
* 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,
|
||||
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"
|
||||
|
||||
/*
|
||||
* Can this attribute be edited in the properties pane.
|
||||
*/
|
||||
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[],
|
||||
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[],
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* 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 = {
|
||||
categoryName: string,
|
||||
specs: AttributesSpec[]
|
||||
@@ -59,6 +256,17 @@ export type 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 = [
|
||||
{
|
||||
categoryName: "appearance",
|
||||
@@ -67,18 +275,21 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
name: "title",
|
||||
type: "string",
|
||||
location: "widget",
|
||||
defaultValue: "",
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "hidden",
|
||||
type: "boolean",
|
||||
location: "widget",
|
||||
defaultValue: false,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
name: "disabled",
|
||||
type: "boolean",
|
||||
location: "widget",
|
||||
defaultValue: false,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
@@ -86,26 +297,74 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
type: "enum",
|
||||
location: "widget",
|
||||
editable: true,
|
||||
values: ["horizontal", "vertical"]
|
||||
values: ["horizontal", "vertical"],
|
||||
defaultValue: "vertical",
|
||||
canShow: (di: IDragItem) => di.type === "container"
|
||||
},
|
||||
{
|
||||
name: "flexGrow",
|
||||
type: "number",
|
||||
location: "widget",
|
||||
defaultValue: 100,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
name: "classes",
|
||||
type: "string",
|
||||
location: "widget",
|
||||
defaultValue: "",
|
||||
editable: true,
|
||||
},
|
||||
|
||||
// Container variants
|
||||
{
|
||||
name: "blockVariant",
|
||||
name: "variant",
|
||||
type: "enum",
|
||||
location: "widget",
|
||||
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",
|
||||
location: "nodeVars",
|
||||
editable: true,
|
||||
serialize: (arg: string[]) => arg.join(","),
|
||||
deserialize: (arg: string) => {
|
||||
if (arg === "")
|
||||
return []
|
||||
return arg.split(",").map(s => s.trim())
|
||||
}
|
||||
defaultValue: [],
|
||||
serialize: serializeStringArray,
|
||||
deserialize: deserializeStringArray
|
||||
},
|
||||
|
||||
// Range
|
||||
@@ -132,6 +388,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
type: "number",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: 0,
|
||||
min: -2 ^ 16,
|
||||
max: 2 ^ 16,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
},
|
||||
{
|
||||
@@ -139,6 +398,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
type: "number",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: 10,
|
||||
min: -2 ^ 16,
|
||||
max: 2 ^ 16,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
},
|
||||
{
|
||||
@@ -146,16 +408,31 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
type: "number",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: 1,
|
||||
min: -2 ^ 16,
|
||||
max: 2 ^ 16,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
},
|
||||
|
||||
// Button
|
||||
{
|
||||
name: "message",
|
||||
name: "param",
|
||||
type: "string",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/button"],
|
||||
defaultValue: "bang"
|
||||
},
|
||||
|
||||
// gallery
|
||||
{
|
||||
name: "updateMode",
|
||||
type: "enum",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/gallery"],
|
||||
values: ["replace", "append"],
|
||||
defaultValue: "replace"
|
||||
},
|
||||
|
||||
// Workflow
|
||||
@@ -163,35 +440,80 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
name: "defaultSubgraph",
|
||||
type: "string",
|
||||
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 };
|
||||
|
||||
/*
|
||||
* Something that can be dragged around in the frontend - a widget or a container.
|
||||
*/
|
||||
export interface IDragItem {
|
||||
type: string,
|
||||
/*
|
||||
* Type of the item.
|
||||
*/
|
||||
type: "container" | "widget",
|
||||
|
||||
/*
|
||||
* Unique ID of the item.
|
||||
*/
|
||||
id: DragItemID,
|
||||
|
||||
/*
|
||||
* If true, the node associated with this drag item is executing.
|
||||
* Used to show an indicator on the widget/container.
|
||||
*/
|
||||
isNodeExecuting?: boolean,
|
||||
|
||||
/*
|
||||
* Attributes for this drag item.
|
||||
*/
|
||||
attrs: Attributes,
|
||||
|
||||
/*
|
||||
* Hackish thing to indicate to Svelte that an attribute changed.
|
||||
* TODO Use Writeable<Attributes> instead!
|
||||
*/
|
||||
attrsChanged: Writable<boolean>
|
||||
}
|
||||
|
||||
/*
|
||||
* A container (block, accordion, tabs). Has child drag items.
|
||||
*/
|
||||
export interface ContainerLayout extends IDragItem {
|
||||
type: "container",
|
||||
}
|
||||
|
||||
/*
|
||||
* A widget (slider, dropdown, textbox...)
|
||||
*/
|
||||
export interface WidgetLayout extends IDragItem {
|
||||
type: "widget",
|
||||
|
||||
/*
|
||||
* litegraph node this widget is bound to.
|
||||
*/
|
||||
node: ComfyWidgetNode
|
||||
}
|
||||
|
||||
type DragItemID = string;
|
||||
|
||||
type LayoutStateOps = {
|
||||
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index: number) => ContainerLayout,
|
||||
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index: number) => WidgetLayout,
|
||||
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
|
||||
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
|
||||
findDefaultContainerForInsertion: () => ContainerLayout | null,
|
||||
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
||||
nodeAdded: (node: LGraphNode) => void,
|
||||
@@ -241,7 +563,7 @@ function findDefaultContainerForInsertion(): ContainerLayout | 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 dragItem: ContainerLayout = {
|
||||
type: "container",
|
||||
@@ -249,10 +571,9 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
||||
attrsChanged: writable(false),
|
||||
attrs: {
|
||||
title: "Container",
|
||||
showTitle: true,
|
||||
direction: "vertical",
|
||||
classes: "",
|
||||
blockVariant: "block",
|
||||
containerVariant: "block",
|
||||
flexGrow: 100,
|
||||
...attrs
|
||||
}
|
||||
@@ -260,14 +581,14 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
||||
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
||||
state.allItems[dragItem.id] = entry;
|
||||
if (parent) {
|
||||
moveItem(dragItem, parent)
|
||||
moveItem(dragItem, parent, index)
|
||||
}
|
||||
console.debug("[layoutState] addContainer", state)
|
||||
store.set(state)
|
||||
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 widgetName = "Widget"
|
||||
const dragItem: WidgetLayout = {
|
||||
@@ -277,7 +598,6 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
||||
attrsChanged: writable(false),
|
||||
attrs: {
|
||||
title: widgetName,
|
||||
showTitle: true,
|
||||
direction: "horizontal",
|
||||
classes: "",
|
||||
flexGrow: 100,
|
||||
@@ -289,7 +609,7 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
||||
state.allItems[dragItem.id] = entry;
|
||||
state.allItemsByNode[node.id] = entry;
|
||||
console.debug("[layoutState] addWidget", state)
|
||||
moveItem(dragItem, parent)
|
||||
moveItem(dragItem, parent, index)
|
||||
return dragItem;
|
||||
}
|
||||
|
||||
@@ -313,23 +633,10 @@ function nodeAdded(node: LGraphNode) {
|
||||
|
||||
const parent = findDefaultContainerForInsertion();
|
||||
|
||||
// Two cases where we want to add nodes:
|
||||
// 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)
|
||||
console.debug("[layoutState] nodeAdded", node)
|
||||
if ("svelteComponentType" in node) {
|
||||
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) {
|
||||
@@ -367,7 +674,7 @@ function nodeRemoved(node: LGraphNode) {
|
||||
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 entry = state.allItems[target.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) {
|
||||
const parentEntry = state.allItems[entry.parent.id];
|
||||
const index = parentEntry.children.findIndex(c => c.id === target.id)
|
||||
if (index !== -1) {
|
||||
parentEntry.children.splice(index, 1)
|
||||
const parentIndex = parentEntry.children.findIndex(c => c.id === target.id)
|
||||
if (parentIndex !== -1) {
|
||||
parentEntry.children.splice(parentIndex, 1)
|
||||
}
|
||||
else {
|
||||
console.error(parentEntry)
|
||||
@@ -387,7 +694,7 @@ function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
|
||||
}
|
||||
|
||||
const toEntry = state.allItems[to.id];
|
||||
if (index !== -1)
|
||||
if (index != null && index >= 0)
|
||||
toEntry.children.splice(index, 0, target)
|
||||
else
|
||||
toEntry.children.push(target)
|
||||
|
||||
@@ -2,13 +2,14 @@ import { writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type ComfyApp from "$lib/components/ComfyApp"
|
||||
|
||||
export type UIEditMode = "disabled" | "widgets" | "containers" | "layout";
|
||||
export type UIEditMode = "widgets" | "containers" | "layout";
|
||||
|
||||
export type UIState = {
|
||||
app: ComfyApp,
|
||||
nodesLocked: boolean,
|
||||
graphLocked: boolean,
|
||||
autoAddUI: boolean,
|
||||
uiUnlocked: boolean,
|
||||
uiEditMode: UIEditMode,
|
||||
}
|
||||
|
||||
@@ -19,7 +20,8 @@ const store: WritableUIStateStore = writable(
|
||||
graphLocked: false,
|
||||
nodesLocked: false,
|
||||
autoAddUI: true,
|
||||
uiEditMode: "disabled",
|
||||
uiUnlocked: false,
|
||||
uiEditMode: "widgets"
|
||||
})
|
||||
|
||||
const uiStateStore: WritableUIStateStore =
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
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;
|
||||
let node: ComfyButtonNode | null = null;
|
||||
let nodeValue: Writable<boolean> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
if (widget) {
|
||||
node = widget.node as ComfyButtonNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
attrsChanged = widget.attrsChanged;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,25 +24,33 @@
|
||||
}
|
||||
|
||||
const style = {
|
||||
full_width: "100%"
|
||||
full_width: "100%",
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gradio-button">
|
||||
{#if node !== null}
|
||||
<Button
|
||||
disabled={widget.attrs.disabled}
|
||||
on:click={onClick}
|
||||
variant="primary"
|
||||
{style}>
|
||||
{widget.attrs.title}
|
||||
</Button>
|
||||
{/if}
|
||||
{#key $attrsChanged}
|
||||
{#if node !== null}
|
||||
<Button
|
||||
disabled={widget.attrs.disabled}
|
||||
on:click={onClick}
|
||||
variant={widget.attrs.buttonVariant || "primary"}
|
||||
size={widget.attrs.buttonSize === "small" ? "sm" : "lg"}
|
||||
{style}>
|
||||
{widget.attrs.title}
|
||||
</Button>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:global(> button) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</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) {
|
||||
z-index: var(--layer-top) !important;
|
||||
}
|
||||
|
||||
@@ -73,10 +73,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
|
||||
:global(> .block) {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.padding {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
disabled={widget.attrs.disabled}
|
||||
lines={node.properties.multiline ? 5 : 1}
|
||||
max_lines={node.properties.multiline ? 5 : 1}
|
||||
show_label={true}
|
||||
show_label={widget.attrs.title !== ""}
|
||||
on:change
|
||||
on:submit
|
||||
on:blur
|
||||
@@ -49,4 +49,8 @@
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(span.hide) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
135
src/scss/ux.scss
135
src/scss/ux.scss
@@ -265,36 +265,66 @@ div.float {
|
||||
}
|
||||
}
|
||||
|
||||
.category-name {
|
||||
.category-name {
|
||||
background: var(--ae-panel-bg-color) !important;
|
||||
border-color: var(--ae-panel-border-color) !important;
|
||||
|
||||
.title, .type {
|
||||
color: var(--ae-label-color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.props-entry {
|
||||
border-width: 1px;
|
||||
border-left: 1px var(--ae-panel-border-color) !important;
|
||||
border-right: 1px var(--ae-panel-border-color) !important;
|
||||
}
|
||||
.props-entry {
|
||||
border-width: 1px;
|
||||
border-left: 1px var(--ae-panel-border-color) !important;
|
||||
border-right: 1px var(--ae-panel-border-color) !important;
|
||||
}
|
||||
|
||||
// .container > .block {
|
||||
// box-shadow: none !important;
|
||||
// border-color: var(--ae-panel-border-color) !important;
|
||||
// border-radius: 0 !important;
|
||||
// background: var(--ae-input-bg-color) !important;
|
||||
/**************/
|
||||
/* Accordions */
|
||||
/**************/
|
||||
|
||||
// > .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 {
|
||||
background-color: var(--ae-main-bg-color) !important;
|
||||
/*padding-bottom: 0 !important;*/
|
||||
}
|
||||
|
||||
.block.gradio-accordion:hover .label-wrap {
|
||||
.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;
|
||||
}
|
||||
|
||||
.block.gradio-accordion .label-wrap:hover {
|
||||
color: var(--ae-main-bg-color) !important;
|
||||
background-color: var(--ae-primary-color) !important;
|
||||
}
|
||||
@@ -308,7 +338,28 @@ div.float {
|
||||
left: 0 !important;
|
||||
top: 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{
|
||||
@@ -368,6 +419,10 @@ div.gradio-row>.form{
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.gradio-checkbox > .inner > .block {
|
||||
background-color: var(--ae-input-bg-color) !important;
|
||||
}
|
||||
|
||||
.wrapper.gradio-textbox textarea {
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
@@ -545,12 +600,16 @@ button.primary {
|
||||
border-radius: var(--ae-panel-border-radius) !important;
|
||||
background: var(--ae-input-bg-color) !important;
|
||||
color: var(--ae-input-color) !important;
|
||||
}
|
||||
|
||||
button.secondary:hover,
|
||||
button.primary:hover {
|
||||
background: var(--ae-primary-color) !important;
|
||||
color: var(--ae-input-bg-color) !important;
|
||||
&:hover {
|
||||
background: var(--ae-primary-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;
|
||||
}
|
||||
}
|
||||
|
||||
.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({
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
FullReload(["src/**/*.{js,ts,scss,svelte}"]),
|
||||
svelte(), ,
|
||||
// FullReload([
|
||||
// // "src/**/*.{js,ts,scss,svelte}"
|
||||
// "src/**/*.{scss}",
|
||||
// "src/lib/stores/*.*",
|
||||
// "src/**/ComfyApp.{ts,svelte}"
|
||||
// ]),
|
||||
svelte(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user