Toggle nodes/containers on and off by tags

This commit is contained in:
space-nuko
2023-05-07 21:29:36 -05:00
parent e7f4638093
commit 8e363cdd51
27 changed files with 529 additions and 149 deletions

View File

@@ -47,6 +47,7 @@
"@litegraph-ts/core": "workspace:*", "@litegraph-ts/core": "workspace:*",
"@litegraph-ts/nodes-basic": "workspace:*", "@litegraph-ts/nodes-basic": "workspace:*",
"@litegraph-ts/nodes-events": "workspace:*", "@litegraph-ts/nodes-events": "workspace:*",
"@litegraph-ts/nodes-logic": "workspace:*",
"@litegraph-ts/nodes-math": "workspace:*", "@litegraph-ts/nodes-math": "workspace:*",
"@litegraph-ts/nodes-strings": "workspace:*", "@litegraph-ts/nodes-strings": "workspace:*",
"@litegraph-ts/tsconfig": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*",

19
pnpm-lock.yaml generated
View File

@@ -46,6 +46,9 @@ importers:
'@litegraph-ts/nodes-events': '@litegraph-ts/nodes-events':
specifier: workspace:* specifier: workspace:*
version: link:litegraph/packages/nodes-events version: link:litegraph/packages/nodes-events
'@litegraph-ts/nodes-logic':
specifier: workspace:*
version: link:litegraph/packages/nodes-logic
'@litegraph-ts/nodes-math': '@litegraph-ts/nodes-math':
specifier: workspace:* specifier: workspace:*
version: link:litegraph/packages/nodes-math version: link:litegraph/packages/nodes-math
@@ -806,6 +809,22 @@ importers:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.3.1 version: 4.3.1
litegraph/packages/nodes-logic:
dependencies:
'@litegraph-ts/core':
specifier: workspace:*
version: link:../core
devDependencies:
'@litegraph-ts/tsconfig':
specifier: workspace:*
version: link:../tsconfig
typescript:
specifier: ^5.0.3
version: 5.0.3
vite:
specifier: ^4.2.1
version: 4.3.1
litegraph/packages/nodes-math: litegraph/packages/nodes-math:
dependencies: dependencies:
'@litegraph-ts/core': '@litegraph-ts/core':

View File

@@ -17,7 +17,6 @@
import GraphPage from './mobile/routes/graph.svelte'; import GraphPage from './mobile/routes/graph.svelte';
import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte'; import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte';
import SubWorkflowPage from './mobile/routes/subworkflow.svelte'; import SubWorkflowPage from './mobile/routes/subworkflow.svelte';
import HellPage from './mobile/routes/hell.svelte';
import type { Framework7Parameters } from "framework7/types"; import type { Framework7Parameters } from "framework7/types";
export let app: ComfyApp; export let app: ComfyApp;
@@ -83,13 +82,6 @@
props: { app } props: { app }
} }
}, },
{
path: '/hell/',
component: HellPage,
options: {
props: { app }
}
},
], ],
popup: { popup: {
closeOnEscape: true, closeOnEscape: true,

View File

@@ -13,6 +13,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -73,7 +74,7 @@
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
> >
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden} {@const hidden = isHidden(item)}
<div class="animation-wrapper" <div class="animation-wrapper"
class:hidden={hidden} class:hidden={hidden}
animate:flip={{duration:flipDurationMs}} animate:flip={{duration:flipDurationMs}}
@@ -86,7 +87,7 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if container.attrs.hidden && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}

View File

@@ -12,6 +12,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -75,7 +76,7 @@
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
> >
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden} {@const hidden = isHidden(item)}
<div class="animation-wrapper" <div class="animation-wrapper"
class:hidden={hidden} class:hidden={hidden}
animate:flip={{duration:flipDurationMs}} animate:flip={{duration:flipDurationMs}}
@@ -88,7 +89,7 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if container.attrs.hidden && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}
@@ -239,6 +240,10 @@
flex-grow: 100; flex-grow: 100;
} }
.handle-hidden {
background-color: #40404080;
}
.handle-widget:hover { .handle-widget:hover {
background-color: #add8e680; background-color: #add8e680;
} }

View File

@@ -201,7 +201,7 @@
}) })
async function doRefreshCombos() { async function doRefreshCombos() {
await app.refreshComboInNodes() await app.refreshComboInNodes(true)
} }
</script> </script>

View File

@@ -9,6 +9,7 @@ import type TypedEmitter from "typed-emitter";
// Import nodes // Import nodes
import "@litegraph-ts/nodes-basic" import "@litegraph-ts/nodes-basic"
import "@litegraph-ts/nodes-events" import "@litegraph-ts/nodes-events"
import "@litegraph-ts/nodes-logic"
import "@litegraph-ts/nodes-math" import "@litegraph-ts/nodes-math"
import "@litegraph-ts/nodes-strings" import "@litegraph-ts/nodes-strings"
import "$lib/nodes/index" import "$lib/nodes/index"
@@ -70,6 +71,27 @@ export type Progress = {
max: number max: number
} }
function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): boolean {
if (!node.isBackendNode)
return false;
if (tag && !hasTag(node, tag)) {
console.debug("Skipping tagged node", tag, node.properties.tags, node)
return false;
}
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
return false;
}
return true;
}
function hasTag(node: LGraphNode, tag: string): boolean {
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
}
export default class ComfyApp { export default class ComfyApp {
api: ComfyAPI; api: ComfyAPI;
rootEl: HTMLDivElement | null = null; rootEl: HTMLDivElement | null = null;
@@ -443,23 +465,12 @@ export default class ComfyApp {
for (const node_ of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) { for (const node_ of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) {
const n = workflow.nodes.find((n) => n.id === node_.id); const n = workflow.nodes.find((n) => n.id === node_.id);
if (!node_.isBackendNode) { if (!isActiveBackendNode(node_, tag)) {
// console.debug("Not serializing node: ", node_.type)
continue; continue;
} }
const node = node_ as ComfyBackendNode; const node = node_ as ComfyBackendNode;
if (tag && node.tags.indexOf(tag) === -1) {
console.debug("Skipping tagged node", tag, node.tags)
continue;
}
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
continue;
}
const inputs = {}; const inputs = {};
// Store all link values // Store all link values
@@ -469,7 +480,11 @@ export default class ComfyApp {
const inputLink = node.getInputLink(i) const inputLink = node.getInputLink(i)
const inputNode = node.getInputNode(i) const inputNode = node.getInputNode(i)
if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) { // We don't check tags for non-backend nodes.
// Just check for node inactivity (so you can toggle groups of
// tagged frontend nodes on/off)
if (inputNode && inputNode.mode === NodeMode.NEVER) {
console.debug("Skipping inactive node", inputNode)
continue; continue;
} }
@@ -515,7 +530,7 @@ export default class ComfyApp {
const isValidParent = (parent: ComfyGraphNode) => { const isValidParent = (parent: ComfyGraphNode) => {
if (!parent || parent.isBackendNode) if (!parent || parent.isBackendNode)
return false; return false;
if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1) if (tag && !hasTag(parent, tag))
return false; return false;
return true; return true;
} }
@@ -525,8 +540,8 @@ export default class ComfyApp {
if (link && !seen[link.id]) { if (link && !seen[link.id]) {
seen[link.id] = true seen[link.id] = true
const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode; const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) { if (inputNode && tag && !hasTag(inputNode, tag)) {
console.debug("Skipping tagged parent node", tag, node.tags) console.debug("Skipping tagged parent node", tag, node.properties.tags)
parent = null; parent = null;
} }
else { else {
@@ -538,7 +553,7 @@ export default class ComfyApp {
} }
if (link && parent && parent.isBackendNode) { if (link && parent && parent.isBackendNode) {
if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1) if (tag && !hasTag(parent, tag))
continue; continue;
const input = node.inputs[i] const input = node.inputs[i]
@@ -669,7 +684,7 @@ export default class ComfyApp {
/** /**
* Refresh combo list on whole nodes * Refresh combo list on whole nodes
*/ */
async refreshComboInNodes() { async refreshComboInNodes(flashUI: boolean = false) {
const defs = await this.api.getNodeDefs(); const defs = await this.api.getNodeDefs();
for (let nodeNum in this.lGraph._nodes) { for (let nodeNum in this.lGraph._nodes) {
@@ -687,11 +702,13 @@ export default class ComfyApp {
const inputNode = node.getInputNode(index) const inputNode = node.getInputNode(index)
if (inputNode && "doAutoConfig" in inputNode) { if (inputNode && "doAutoConfig" in inputNode) {
const comfyInputNode = inputNode as nodes.ComfyWidgetNode; const comfyComboNode = inputNode as nodes.ComfyComboNode;
comfyInputNode.doAutoConfig(comfyInput) comfyComboNode.doAutoConfig(comfyInput)
if (!comfyInput.config.values.includes(get(comfyInputNode.value))) { if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {
comfyInputNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0]) comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0])
} }
if (flashUI)
comfyComboNode.comboRefreshed.set(true)
} }
} }
} }

View File

@@ -142,7 +142,7 @@
value = spec.deserialize(value) value = spec.deserialize(value)
target.attrs[name] = value target.attrs[name] = value
target.attrsChanged.set(!get(target.attrsChanged)) target.attrsChanged.set(get(target.attrsChanged) + 1)
if (node && "propsChanged" in node) { if (node && "propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode const comfyNode = node as ComfyWidgetNode
@@ -151,27 +151,39 @@
console.warn(spec) console.warn(spec)
if (spec.refreshPanelOnChange) { if (spec.refreshPanelOnChange) {
console.error("A! refresh") doRefreshPanel()
$refreshPanel += 1;
} }
} }
function getProperty(node: LGraphNode, spec: AttributesSpec) {
let value = node.properties[spec.name]
if (value == null)
value = spec.defaultValue
else if (spec.serialize)
value = spec.serialize(value)
console.debug("[ComfyProperties] getProperty", spec, value, node)
return value
}
function updateProperty(spec: AttributesSpec, value: any) { function updateProperty(spec: AttributesSpec, value: any) {
if (node == null || !spec.editable) if (node == null || !spec.editable)
return return
const name = spec.name const name = spec.name
console.warn("updateProperty", name, value) console.warn("[ComfyProperties] updateProperty", name, value)
if (spec.deserialize)
value = spec.deserialize(value)
node.properties[name] = value; node.properties[name] = value;
if ("propsChanged" in node) { if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) comfyNode.notifyPropsChanged();
} }
if (spec.refreshPanelOnChange) if (spec.refreshPanelOnChange)
$refreshPanel += 1; doRefreshPanel()
} }
function getVar(node: LGraphNode, spec: AttributesSpec) { function getVar(node: LGraphNode, spec: AttributesSpec) {
@@ -201,7 +213,13 @@
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
} }
if (spec.refreshPanelOnChange) if (spec.refreshPanelOnChange) {
doRefreshPanel()
}
}
function doRefreshPanel() {
console.warn("[ComfyProperties] doRefreshPanel")
$refreshPanel += 1; $refreshPanel += 1;
} }
@@ -214,6 +232,9 @@
$layoutState.attrs[name] = value $layoutState.attrs[name] = value
$layoutState = $layoutState $layoutState = $layoutState
if (spec.refreshPanelOnChange)
doRefreshPanel()
} }
</script> </script>
@@ -281,7 +302,7 @@
<div class="props-entry"> <div class="props-entry">
{#if spec.type === "string"} {#if spec.type === "string"}
<TextBox <TextBox
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)} on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name} label={spec.name}
@@ -290,7 +311,7 @@
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
label={spec.name} label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
@@ -298,7 +319,7 @@
{:else if spec.type === "number"} {:else if spec.type === "number"}
<ComfyNumberProperty <ComfyNumberProperty
name={spec.name} name={spec.name}
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
step={spec.step || 1} step={spec.step || 1}
min={spec.min || -1024} min={spec.min || -1024}
max={spec.max || 1024} max={spec.max || 1024}
@@ -308,7 +329,7 @@
{:else if spec.type === "enum"} {:else if spec.type === "enum"}
<ComfyComboProperty <ComfyComboProperty
name={spec.name} name={spec.name}
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
values={spec.values} values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}

View File

@@ -81,7 +81,7 @@
<span>Node: {getNodeInfo($queueState.runningNodeId)}</span> <span>Node: {getNodeInfo($queueState.runningNodeId)}</span>
</div> </div>
<div> <div>
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} /> <ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} styles="height: 30px;" />
</div> </div>
{/if} {/if}
{#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0} {#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0}

View File

@@ -14,6 +14,8 @@
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -34,6 +36,7 @@
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked} {@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked}
{#key $attrsChanged} {#key $attrsChanged}
{#if edit || !isHidden(container)}
{#if container.attrs.variant === "tabs"} {#if container.attrs.variant === "tabs"}
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} /> <TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{:else if container.attrs.variant === "accordion"} {:else if container.attrs.variant === "accordion"}
@@ -41,5 +44,6 @@
{:else} {:else}
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} /> <BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{/if} {/if}
{/if}
{/key} {/key}
{/if} {/if}

View File

@@ -13,6 +13,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -86,7 +87,7 @@
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
> >
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
{@const hidden = item?.attrs?.hidden} {@const hidden = isHidden(item)}
{@const tabName = getTabName(container, i)} {@const tabName = getTabName(container, i)}
<div class="animation-wrapper" <div class="animation-wrapper"
class:hidden={hidden} class:hidden={hidden}
@@ -104,7 +105,7 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if container.attrs.hidden && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}

View File

@@ -7,6 +7,8 @@
import Container from "./Container.svelte" import Container from "./Container.svelte"
import { type Writable } from "svelte/store" import { type Writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes"; import type { ComfyWidgetNode } from "$lib/nodes";
import { NodeMode } from "@litegraph-ts/core";
import { isHidden } from "$lib/widgets/utils";
export let dragItem: IDragItem | null = null; export let dragItem: IDragItem | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -64,17 +66,18 @@
{/key} {/key}
{:else if widget && widget.node} {:else if widget && widget.node}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const hidden = isHidden(widget)}
{#key $attrsChanged} {#key $attrsChanged}
{#key $propsChanged} {#key $propsChanged}
<div class="widget {widget.attrs.classes} {getWidgetClass()}" <div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit} class:edit={edit}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)} class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id} class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
class:hidden={widget.attrs.hidden} class:hidden={hidden}
> >
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} /> <svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
</div> </div>
{#if widget.attrs.hidden && edit} {#if hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}

View File

@@ -1,4 +1,4 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget, type SerializedLGraphNode } from "@litegraph-ts/core"; import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget, type SerializedLGraphNode, NodeMode, type IToggleWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic"; import { Watch } from "@litegraph-ts/nodes-basic";
import type { SerializedPrompt } from "$lib/components/ComfyApp"; import type { SerializedPrompt } from "$lib/components/ComfyApp";
@@ -7,6 +7,7 @@ import type { GalleryOutput } from "./ComfyWidgetNodes";
import { get } from "svelte/store"; import { get } from "svelte/store";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import notify from "$lib/notify"; import notify from "$lib/notify";
import layoutState from "$lib/stores/layoutState";
export interface ComfyQueueEventsProperties extends Record<any, any> { export interface ComfyQueueEventsProperties extends Record<any, any> {
} }
@@ -251,3 +252,86 @@ LiteGraph.registerNodeType({
desc: "Runs a part of the graph based on a tag", desc: "Runs a part of the graph based on a tag",
type: "actions/execute_subgraph" type: "actions/execute_subgraph"
}) })
export interface ComfySetNodeModeActionProperties extends Record<any, any> {
targetTags: string,
enable: boolean,
}
export class ComfySetNodeModeAction extends ComfyGraphNode {
override properties: ComfySetNodeModeActionProperties = {
targetTags: "",
enable: false
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "enabled", type: "boolean" },
{ name: "set", type: BuiltInSlotType.ACTION },
],
}
displayWidget: ITextWidget;
enableWidget: IToggleWidget;
constructor(title?: string) {
super(title)
this.displayWidget = this.addWidget("text", "Tags", this.properties.targetTags, "targetTags")
this.enableWidget = this.addWidget("toggle", "Enable", this.properties.enable, "enable")
}
override onPropertyChanged(property: any, value: any) {
if (property === "enabled") {
this.enableWidget.value = value
}
}
override onExecute() {
const enabled = this.getInputData(0)
if (typeof enabled === "boolean")
this.setProperty("enabled", enabled)
}
override onAction(action: any, param: any) {
let enabled = this.properties.enabled
if (typeof param === "object" && "enabled" in param)
enabled = param["enabled"]
const tags = this.properties.targetTags.split(",").map(s => s.trim());
for (const node of this.graph._nodes) {
if ("tags" in node.properties) {
const comfyNode = node as ComfyGraphNode;
const hasTag = tags.some(t => comfyNode.properties.tags.indexOf(t) != -1);
if (hasTag) {
let newMode: NodeMode;
if (enabled) {
newMode = NodeMode.ALWAYS;
} else {
newMode = NodeMode.NEVER;
}
node.changeMode(newMode);
}
}
}
for (const entry of Object.values(get(layoutState).allItems)) {
if (entry.dragItem.type === "container") {
const container = entry.dragItem;
const hasTag = tags.some(t => container.attrs.tags.indexOf(t) != -1);
if (hasTag) {
container.attrs.hidden = !enabled;
console.warn("Cont", container.attrs.tags, tags, hasTag, container.attrs, enabled)
}
container.attrsChanged.set(get(container.attrsChanged) + 1)
}
}
}
}
LiteGraph.registerNodeType({
class: ComfySetNodeModeAction,
title: "Comfy.SetNodeModeAction",
desc: "Sets a group of nodes/UI containers as enabled/disabled based on their tags (comma-separated)",
type: "actions/set_node_mode"
})

View File

@@ -32,12 +32,6 @@ export class ComfyBackendNode extends ComfyGraphNode {
} }
} }
/*
* Tags this node belongs to
* Allows you to run subsections of the graph
*/
tags: string[] = []
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {} private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
private setup(nodeData: any) { private setup(nodeData: any) {
@@ -88,7 +82,6 @@ export class ComfyBackendNode extends ComfyGraphNode {
override onSerialize(o: SerializedLGraphNode) { override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o); super.onSerialize(o);
(o as any).tags = this.tags
for (const input of o.inputs) { for (const input of o.inputs) {
// strip user-identifying data, it will be reinstantiated later // strip user-identifying data, it will be reinstantiated later
if ((input as any).config != null) { if ((input as any).config != null) {
@@ -100,8 +93,6 @@ export class ComfyBackendNode extends ComfyGraphNode {
override onConfigure(o: SerializedLGraphNode) { override onConfigure(o: SerializedLGraphNode) {
super.onConfigure(o); super.onConfigure(o);
this.tags = (o as any).tags || []
const configs = ComfyBackendNode.defaultInputConfigs[o.type] const configs = ComfyBackendNode.defaultInputConfigs[o.type]
for (let index = 0; index < this.inputs.length; index++) { for (let index = 0; index < this.inputs.length; index++) {
const input = this.inputs[index] as IComfyInputSlot const input = this.inputs[index] as IComfyInputSlot

View File

@@ -1,7 +1,7 @@
import type { ComfyInputConfig } from "$lib/IComfyInputSlot"; import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
import type { SerializedPrompt } from "$lib/components/ComfyApp"; import type { SerializedPrompt } from "$lib/components/ComfyApp";
import type ComfyWidget from "$lib/components/widgets/ComfyWidget"; import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core"; import { LGraph, LGraphNode, LiteGraph, NodeMode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import type { ComfyWidgetNode } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type IComfyInputSlot from "$lib/IComfyInputSlot"; import type IComfyInputSlot from "$lib/IComfyInputSlot";

View File

@@ -0,0 +1,124 @@
import { BuiltInSlotType, LiteGraph, NodeMode, type INodeInputSlot, type SlotLayout, type INodeOutputSlot, LLink, LConnectionKind, type ITextWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic";
export interface ComfyPickFirstNodeProperties extends Record<any, any> {
acceptNullLinkData: boolean
}
function nextLetter(s: string): string {
return s.replace(/([a-zA-Z])[^a-zA-Z]*$/, function(a) {
var c = a.charCodeAt(0);
switch (c) {
case 90: return 'A';
case 122: return 'a';
default: return String.fromCharCode(++c);
}
});
}
export default class ComfyPickFirstNode extends ComfyGraphNode {
override properties: ComfyPickFirstNodeProperties = {
acceptNullLinkData: false
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "A", type: "*" },
{ name: "B", type: "*" },
],
outputs: [
{ name: "out", type: "*" }
],
}
private selected: number = -1;
displayWidget: ITextWidget;
constructor(title?: string) {
super(title);
this.displayWidget = this.addWidget("text", "Value", "")
this.displayWidget.disabled = true;
}
override onDrawBackground(ctx: CanvasRenderingContext2D) {
if (this.flags.collapsed || this.selected === -1) {
return;
}
ctx.fillStyle = "#AFB";
var y = (this.selected) * LiteGraph.NODE_SLOT_HEIGHT + 6;
ctx.beginPath();
ctx.moveTo(50, y);
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
ctx.fill();
};
override onConnectionsChange(
type: LConnectionKind,
slotIndex: number,
isConnected: boolean,
link: LLink,
ioSlot: (INodeInputSlot | INodeOutputSlot)
) {
if (type !== LConnectionKind.INPUT)
return;
if (isConnected) {
if (link != null && slotIndex === this.inputs.length - 1) {
// Add a new input
const lastInputName = this.inputs[this.inputs.length - 1].name
const inputName = nextLetter(lastInputName);
this.addInput(inputName, "*")
}
}
else {
if (this.getInputLink(this.inputs.length - 1) != null)
return;
// Remove empty inputs
for (let i = this.inputs.length - 2; i > 0; i--) {
if (i <= 0)
break;
if (this.getInputLink(i) == null)
this.removeInput(i)
else
break;
}
let name = "A"
for (let i = 0; i < this.inputs.length; i++) {
this.inputs[i].name = name;
name = nextLetter(name);
}
}
}
override onExecute() {
for (let index = 0; index < this.inputs.length; index++) {
const link = this.getInputLink(index);
if (link != null && (link.data != null || this.properties.acceptNullLinkData)) {
const node = this.getInputNode(index);
if (node != null && node.mode === NodeMode.ALWAYS) {
this.selected = index;
this.displayWidget.value = Watch.toString(link.data)
this.setOutputData(0, link.data)
return
}
}
}
this.selected = -1;
this.setOutputData(0, null)
}
}
LiteGraph.registerNodeType({
class: ComfyPickFirstNode,
title: "Comfy.PickFirst",
desc: "Picks the first active input connected to this node (top to bottom)",
type: "utils/pick_first"
})

View File

@@ -1,4 +1,4 @@
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 { 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, NodeMode } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode from "./ComfyGraphNode";
import ComboWidget from "$lib/widgets/ComboWidget.svelte"; import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte"; import RangeWidget from "$lib/widgets/RangeWidget.svelte";
@@ -93,6 +93,17 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
return Watch.toString(value) return Watch.toString(value)
} }
override changeMode(modeTo: NodeMode): boolean {
const result = super.changeMode(modeTo);
this.notifyPropsChanged();
// Also need to notify the parent container since it's what controls the
// hidden state of the widget
const layoutEntry = layoutState.findLayoutEntryForNode(this.id)
if (layoutEntry && layoutEntry.parent)
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
return result;
}
private onValueUpdated(value: any) { private onValueUpdated(value: any) {
console.debug("[Widget] valueUpdated", this, value) console.debug("[Widget] valueUpdated", this, value)
this.displayWidget.value = this.formatValue(value) this.displayWidget.value = this.formatValue(value)
@@ -171,6 +182,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
console.debug("Property copy", input, this.properties) console.debug("Property copy", input, this.properties)
this.setValue(get(this.value)) this.setValue(get(this.value))
this.notifyPropsChanged();
}
notifyPropsChanged() {
this.propsChanged.set(get(this.propsChanged) + 1) this.propsChanged.set(get(this.propsChanged) + 1)
} }
@@ -200,7 +215,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
} }
// Force reactivity change so the frontend can be updated with the new props // Force reactivity change so the frontend can be updated with the new props
this.propsChanged.set(get(this.propsChanged) + 1) this.notifyPropsChanged();
} }
clampOneConfig(input: IComfyInputSlot) { } clampOneConfig(input: IComfyInputSlot) { }
@@ -314,8 +329,11 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
override svelteComponentType = ComboWidget override svelteComponentType = ComboWidget
override defaultValue = "A"; override defaultValue = "A";
comboRefreshed: Writable<boolean>;
constructor(name?: string) { constructor(name?: string) {
super(name, "A") super(name, "A")
this.comboRefreshed = writable(false)
} }
onConnectOutput( onConnectOutput(
@@ -613,7 +631,7 @@ export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
const changed = value != get(this.value); const changed = value != get(this.value);
super.setValue(Boolean(value)) super.setValue(Boolean(value))
if (changed) if (changed)
this.triggerSlot(1) this.triggerSlot(1, value)
} }
constructor(name?: string) { constructor(name?: string) {

View File

@@ -1,6 +1,7 @@
export { default as ComfyReroute } from "./ComfyReroute" export { default as ComfyReroute } from "./ComfyReroute"
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes" export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyStoreImagesAction, ComfyExecuteSubgraphAction } from "./ComfyActionNodes" export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyStoreImagesAction, ComfyExecuteSubgraphAction } from "./ComfyActionNodes"
export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode"
export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfyValueControl } from "./ComfyValueControl"
export { default as ComfySelector } from "./ComfySelector" export { default as ComfySelector } from "./ComfySelector"
export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode" export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode"

View File

@@ -1,7 +1,7 @@
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp" import type ComfyApp from "$lib/components/ComfyApp"
import type { LGraphNode, IWidget, LGraph } from "@litegraph-ts/core" import { type LGraphNode, type IWidget, type LGraph, NodeMode } from "@litegraph-ts/core"
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes'; import type { ComfyWidgetNode } from '$lib/nodes';
@@ -116,6 +116,12 @@ export type Attributes = {
*/ */
containerVariant?: "block" | "hidden", containerVariant?: "block" | "hidden",
/*
* Tags for hiding containers with
* For WidgetLayouts this will be ignored, it will use node.properties.tags instead
*/
tags: string[],
/* /*
* If true, don't show this component in the UI * If true, don't show this component in the UI
*/ */
@@ -142,6 +148,12 @@ export type Attributes = {
*/ */
variant?: string, variant?: string,
/*
* What state to set this widget to in the frontend if its corresponding
* node is disabled in the graph.
*/
nodeDisabledState: "visible" | "disabled" | "hidden",
/*********************************************/ /*********************************************/
/* Special attributes for widgets/containers */ /* Special attributes for widgets/containers */
/*********************************************/ /*********************************************/
@@ -154,6 +166,9 @@ export type Attributes = {
buttonSize?: "large" | "small" buttonSize?: "large" | "small"
} }
/*
* Defines something that can be edited in the properties side panel.
*/
export type AttributesSpec = { export type AttributesSpec = {
/* /*
* ID necessary for svelte's keyed each, autoset at the top level in this source file. * ID necessary for svelte's keyed each, autoset at the top level in this source file.
@@ -256,9 +271,13 @@ export type AttributesCategorySpec = {
export type AttributesSpecList = AttributesCategorySpec[] export type AttributesSpecList = AttributesCategorySpec[]
const serializeStringArray = (arg: string[]) => arg.join(",") const serializeStringArray = (arg: string[]) => {
if (arg == null)
arg = []
return arg.join(",")
}
const deserializeStringArray = (arg: string) => { const deserializeStringArray = (arg: string) => {
if (arg === "") if (arg === "" || arg == null)
return [] return []
return arg.split(",").map(s => s.trim()) return arg.split(",").map(s => s.trim())
} }
@@ -315,6 +334,15 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "", defaultValue: "",
editable: true, editable: true,
}, },
{
name: "nodeDisabledState",
type: "enum",
location: "widget",
editable: true,
values: ["visible", "disabled", "hidden"],
defaultValue: "disabled",
canShow: (di: IDragItem) => di.type === "widget"
},
// Container variants // Container variants
{ {
@@ -372,15 +400,6 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
categoryName: "behavior", categoryName: "behavior",
specs: [ specs: [
// Node variables // Node variables
{
name: "tags",
type: "string",
location: "nodeVars",
editable: true,
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray
},
{ {
name: "saveUserState", name: "saveUserState",
type: "boolean", type: "boolean",
@@ -388,6 +407,39 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
editable: true, editable: true,
defaultValue: true, defaultValue: true,
}, },
{
name: "mode",
type: "enum",
location: "nodeVars",
editable: true,
values: ["ALWAYS", "NEVER"],
defaultValue: "ALWAYS",
serialize: (s) => s === NodeMode.ALWAYS ? "ALWAYS" : "NEVER",
deserialize: (m) => m === "ALWAYS" ? NodeMode.ALWAYS : NodeMode.NEVER
},
// Node properties
{
name: "tags",
type: "string",
location: "nodeProps",
editable: true,
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray
},
// Container tags are contained in the widget attributes
{
name: "tags",
type: "string",
location: "widget",
editable: true,
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray,
canShow: (di: IDragItem) => di.type === "container"
},
// Range // Range
{ {
@@ -457,14 +509,23 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
// This is needed so the specs can be iterated with svelte's keyed #each. // This is needed so the specs can be iterated with svelte's keyed #each.
let i = 0; let i = 0;
for (const cat of Object.values(ALL_ATTRIBUTES)) { for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const val of Object.values(cat.specs)) { for (const spec of Object.values(cat.specs)) {
val.id = i; spec.id = i;
i += 1; i += 1;
} }
} }
export { ALL_ATTRIBUTES }; export { ALL_ATTRIBUTES };
const defaultWidgetAttributes: Attributes = {} as any
for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const spec of Object.values(cat.specs)) {
if (spec.location === "widget" && spec.defaultValue != null) {
defaultWidgetAttributes[spec.name] = spec.defaultValue;
}
}
}
/* /*
* Something that can be dragged around in the frontend - a widget or a container. * Something that can be dragged around in the frontend - a widget or a container.
*/ */
@@ -494,7 +555,7 @@ export interface IDragItem {
* Hackish thing to indicate to Svelte that an attribute changed. * Hackish thing to indicate to Svelte that an attribute changed.
* TODO Use Writeable<Attributes> instead! * TODO Use Writeable<Attributes> instead!
*/ */
attrsChanged: Writable<boolean> attrsChanged: Writable<number>
} }
/* /*
@@ -528,7 +589,8 @@ type LayoutStateOps = {
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout, groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout,
ungroup: (container: ContainerLayout) => void, ungroup: (container: ContainerLayout) => void,
getCurrentSelection: () => IDragItem[], getCurrentSelection: () => IDragItem[],
findLayoutForNode: (nodeId: number) => IDragItem | null; findLayoutEntryForNode: (nodeId: number) => DragItemEntry | null,
findLayoutForNode: (nodeId: number) => IDragItem | null,
serialize: () => SerializedLayoutState, serialize: () => SerializedLayoutState,
deserialize: (data: SerializedLayoutState, graph: LGraph) => void, deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
initDefaultLayout: () => void, initDefaultLayout: () => void,
@@ -575,13 +637,10 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
const dragItem: ContainerLayout = { const dragItem: ContainerLayout = {
type: "container", type: "container",
id: `${state.currentId++}`, id: `${state.currentId++}`,
attrsChanged: writable(false), attrsChanged: writable(0),
attrs: { attrs: {
...defaultWidgetAttributes,
title: "Container", title: "Container",
direction: "vertical",
classes: "",
containerVariant: "block",
flexGrow: 100,
...attrs ...attrs
} }
} }
@@ -602,12 +661,11 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
type: "widget", type: "widget",
id: `${state.currentId++}`, id: `${state.currentId++}`,
node: node, node: node,
attrsChanged: writable(false), attrsChanged: writable(0),
attrs: { attrs: {
...defaultWidgetAttributes,
title: widgetName, title: widgetName,
direction: "horizontal", nodeDisabledState: "disabled",
classes: "",
flexGrow: 100,
...attrs ...attrs
} }
} }
@@ -778,16 +836,23 @@ function ungroup(container: ContainerLayout) {
store.set(state) store.set(state)
} }
function findLayoutForNode(nodeId: number): WidgetLayout | null { function findLayoutEntryForNode(nodeId: number): DragItemEntry | null {
const state = get(store) const state = get(store)
const found = Object.entries(state.allItems).find(pair => const found = Object.entries(state.allItems).find(pair =>
pair[1].dragItem.type === "widget" pair[1].dragItem.type === "widget"
&& (pair[1].dragItem as WidgetLayout).node.id === nodeId) && (pair[1].dragItem as WidgetLayout).node.id === nodeId)
if (found) if (found)
return found[1].dragItem as WidgetLayout return found[1]
return null; return null;
} }
function findLayoutForNode(nodeId: number): WidgetLayout | null {
const found = findLayoutEntryForNode(nodeId);
if (!found)
return null;
return found.dragItem as WidgetLayout
}
function initDefaultLayout() { function initDefaultLayout() {
store.set({ store.set({
root: null, root: null,
@@ -869,8 +934,8 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
const dragItem: IDragItem = { const dragItem: IDragItem = {
type: entry.dragItem.type, type: entry.dragItem.type,
id: entry.dragItem.id, id: entry.dragItem.id,
attrs: entry.dragItem.attrs, attrs: { ...defaultWidgetAttributes, ...entry.dragItem.attrs },
attrsChanged: writable(false) attrsChanged: writable(0)
}; };
const dragEntry: DragItemEntry = { const dragEntry: DragItemEntry = {
@@ -939,6 +1004,7 @@ const layoutStateStore: WritableLayoutStateStore =
nodeRemoved, nodeRemoved,
getCurrentSelection, getCurrentSelection,
groupItems, groupItems,
findLayoutEntryForNode,
findLayoutForNode, findLayoutForNode,
ungroup, ungroup,
initDefaultLayout, initDefaultLayout,

View File

@@ -4,6 +4,7 @@
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import { get, type Writable, writable } from "svelte/store"; import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
let node: ComfyButtonNode | null = null; let node: ComfyButtonNode | null = null;
@@ -32,9 +33,9 @@
<div class="wrapper gradio-button"> <div class="wrapper gradio-button">
{#key $attrsChanged} {#key $attrsChanged}
{#if node !== null} {#if widget !== null && node !== null}
<Button <Button
disabled={widget.attrs.disabled} disabled={isDisabled(widget)}
on:click={onClick} on:click={onClick}
variant={widget.attrs.buttonVariant || "primary"} variant={widget.attrs.buttonVariant || "primary"}
size={widget.attrs.buttonSize === "small" ? "sm" : "lg"} size={widget.attrs.buttonSize === "small" ? "sm" : "lg"}

View File

@@ -4,6 +4,7 @@
import { Block } from "@gradio/atoms"; import { Block } from "@gradio/atoms";
import { Checkbox } from "@gradio/form"; import { Checkbox } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store"; import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
@@ -31,7 +32,7 @@
{#key $attrsChanged} {#key $attrsChanged}
{#if node !== null} {#if node !== null}
<Block> <Block>
<Checkbox disabled={widget.attrs.disabled} label={widget.attrs.title} bind:value={$nodeValue} on:select={onSelect} /> <Checkbox disabled={isDisabled(widget)} label={widget.attrs.title} bind:value={$nodeValue} on:select={onSelect} />
</Block> </Block>
{/if} {/if}
{/key} {/key}

View File

@@ -4,11 +4,14 @@
import type { ComfyComboNode } from "$lib/nodes/index"; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
let node: ComfyComboNode | null = null; let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null; let nodeValue: Writable<string> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
let comboRefreshed: Writable<boolean> | null = null;
let wasComboRefreshed: boolean = false;
let option: any let option: any
export let debug: boolean = false; export let debug: boolean = false;
@@ -34,6 +37,9 @@
node = widget.node as ComfyComboNode node = widget.node as ComfyComboNode
nodeValue = node.value; nodeValue = node.value;
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
comboRefreshed = node.comboRefreshed;
if ($comboRefreshed)
flashOnRefreshed();
setOption($nodeValue) // don't react on option setOption($nodeValue) // don't react on option
} }
} }
@@ -46,6 +52,12 @@
$nodeValue = option.value; $nodeValue = option.value;
} }
$: $comboRefreshed && flashOnRefreshed();
function flashOnRefreshed() {
setTimeout(() => ($comboRefreshed = false), 1000);
}
function getLinkValue() { function getLinkValue() {
if (!node) if (!node)
return "???"; return "???";
@@ -64,19 +76,11 @@
input.blur(); input.blur();
navigator.vibrate(20) navigator.vibrate(20)
} }
let lastPropsChanged: number = 0;
let werePropsChanged: boolean = false;
$: if ($propsChanged !== lastPropsChanged) {
werePropsChanged = true;
lastPropsChanged = $propsChanged;
setTimeout(() => (werePropsChanged = false), 2000);
}
</script> </script>
<div class="wrapper comfy-combo" class:updated={werePropsChanged}> <div class="wrapper comfy-combo" class:updated={$comboRefreshed}>
{#key $propsChanged} {#key $propsChanged}
{#key $comboRefreshed}
{#if node !== null && nodeValue !== null} {#if node !== null && nodeValue !== null}
<label> <label>
{#if widget.attrs.title !== ""} {#if widget.attrs.title !== ""}
@@ -85,7 +89,7 @@
<Select <Select
bind:value={option} bind:value={option}
items={node.properties.values} items={node.properties.values}
disabled={widget.attrs.disabled || node.properties.values.length === 0} disabled={isDisabled(widget) || node.properties.values.length === 0}
clearable={false} clearable={false}
showChevron={true} showChevron={true}
inputAttributes={{ autocomplete: 'off' }} inputAttributes={{ autocomplete: 'off' }}
@@ -105,6 +109,7 @@
</label> </label>
{/if} {/if}
{/key} {/key}
{/key}
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -5,6 +5,7 @@
import { get, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { debounce } from "$lib/utils"; import { debounce } from "$lib/utils";
import interfaceState from "$lib/stores/interfaceState"; import interfaceState from "$lib/stores/interfaceState";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
let node: ComfySliderNode | null = null; let node: ComfySliderNode | null = null;
@@ -104,7 +105,7 @@
{#if node !== null && option !== null} {#if node !== null && option !== null}
<Range <Range
bind:value={option} bind:value={option}
disabled={widget.attrs.disabled} disabled={isDisabled(widget)}
minimum={node.properties.min} minimum={node.properties.min}
maximum={node.properties.max} maximum={node.properties.max}
step={node.properties.step} step={node.properties.step}

View File

@@ -3,6 +3,7 @@
import type { ComfyComboNode } from "$lib/nodes/index"; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
let node: ComfyComboNode | null = null; let node: ComfyComboNode | null = null;
@@ -33,7 +34,7 @@
<TextBox <TextBox
bind:value={$nodeValue} bind:value={$nodeValue}
label={widget.attrs.title} label={widget.attrs.title}
disabled={widget.attrs.disabled} disabled={isDisabled(widget)}
lines={node.properties.multiline ? 5 : 1} lines={node.properties.multiline ? 5 : 1}
max_lines={node.properties.multiline ? 5 : 1} max_lines={node.properties.multiline ? 5 : 1}
show_label={widget.attrs.title !== ""} show_label={widget.attrs.title !== ""}

26
src/lib/widgets/utils.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { IDragItem } from "$lib/stores/layoutState";
import layoutState from "$lib/stores/layoutState";
import { NodeMode } from "@litegraph-ts/core";
import { get } from "svelte/store";
export function isDisabled(widget: IDragItem) {
if (widget.attrs.disabled)
return true;
if (widget.type === "widget") {
return widget.attrs.nodeDisabledState === "disabled" && widget.node.mode === NodeMode.NEVER
}
return false;
}
export function isHidden(widget: IDragItem) {
if (widget.attrs.hidden)
return true;
if (widget.type === "widget") {
return widget.attrs.nodeDisabledState === "hidden" && widget.node.mode === NodeMode.NEVER
}
return false;
}

View File

@@ -33,8 +33,5 @@
<ListItem link="/graph/" title="Show Node Graph"> <ListItem link="/graph/" title="Show Node Graph">
<i class="icon icon-f7" slot="media" /> <i class="icon icon-f7" slot="media" />
</ListItem> </ListItem>
<ListItem link="/hell/" title="🔥 HELL 🔥">
<i class="icon icon-f7" slot="media" />
</ListItem>
</List> </List>
</Page> </Page>