Fix props pane
This commit is contained in:
@@ -42,7 +42,7 @@ import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
|
|||||||
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
|
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
|
||||||
import selectionState from "$lib/stores/selectionState";
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import layoutStates from "$lib/stores/layoutStates";
|
import layoutStates from "$lib/stores/layoutStates";
|
||||||
import { ComfyWorkflow, type WorkflowAttributes } from "$lib/stores/workflowState";
|
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||||
import workflowState from "$lib/stores/workflowState";
|
import workflowState from "$lib/stores/workflowState";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
@@ -181,12 +181,13 @@ export default class ComfyApp {
|
|||||||
this.lCanvas.allow_interaction = uiUnlocked;
|
this.lCanvas.allow_interaction = uiUnlocked;
|
||||||
|
|
||||||
// await this.#invokeExtensionsAsync("init");
|
// await this.#invokeExtensionsAsync("init");
|
||||||
await this.registerNodes();
|
const defs = await this.api.getNodeDefs();
|
||||||
|
await this.registerNodes(defs);
|
||||||
|
|
||||||
// Load previous workflow
|
// Load previous workflow
|
||||||
let restored = false;
|
let restored = false;
|
||||||
try {
|
try {
|
||||||
restored = await this.loadStateFromLocalStorage();
|
restored = await this.loadStateFromLocalStorage(defs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading previous workflow", err);
|
console.error("Error loading previous workflow", err);
|
||||||
notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null })
|
notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null })
|
||||||
@@ -194,7 +195,7 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
// We failed to restore a workflow so load the default
|
// We failed to restore a workflow so load the default
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
await this.initDefaultWorkflow();
|
await this.initDefaultWorkflow(defs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current workflow automatically
|
// Save current workflow automatically
|
||||||
@@ -249,9 +250,11 @@ export default class ComfyApp {
|
|||||||
saveStateToLocalStorage() {
|
saveStateToLocalStorage() {
|
||||||
try {
|
try {
|
||||||
uiState.update(s => { s.isSavingToLocalStorage = true; return s; })
|
uiState.update(s => { s.isSavingToLocalStorage = true; return s; })
|
||||||
const workflows = get(workflowState).openedWorkflows
|
const state = get(workflowState)
|
||||||
|
const workflows = state.openedWorkflows
|
||||||
const savedWorkflows = workflows.map(w => this.serialize(w));
|
const savedWorkflows = workflows.map(w => this.serialize(w));
|
||||||
const json = JSON.stringify(savedWorkflows);
|
const activeWorkflowIndex = workflows.findIndex(w => state.activeWorkflowID === w.id);
|
||||||
|
const json = JSON.stringify({ workflows: savedWorkflows, activeWorkflowIndex });
|
||||||
localStorage.setItem("workflows", json)
|
localStorage.setItem("workflows", json)
|
||||||
for (const workflow of workflows)
|
for (const workflow of workflows)
|
||||||
workflow.isModified = false;
|
workflow.isModified = false;
|
||||||
@@ -266,24 +269,33 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadStateFromLocalStorage(): Promise<boolean> {
|
async loadStateFromLocalStorage(defs: Record<ComfyNodeID, ComfyNodeDef>): Promise<boolean> {
|
||||||
const json = localStorage.getItem("workflows");
|
const json = localStorage.getItem("workflows");
|
||||||
if (!json) {
|
if (!json) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const workflows = JSON.parse(json) as SerializedAppState[];
|
|
||||||
for (const workflow of workflows)
|
const state = JSON.parse(json);
|
||||||
await this.openWorkflow(workflow)
|
if (!("workflows" in state))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const workflows = state.workflows as SerializedAppState[];
|
||||||
|
for (const workflow of workflows) {
|
||||||
|
await this.openWorkflow(workflow, defs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof state.activeWorkflowIndex === "number") {
|
||||||
|
workflowState.setActiveWorkflow(this.lCanvas, state.activeWorkflowIndex);
|
||||||
|
selectionState.clear();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static node_type_overrides: Record<string, typeof ComfyBackendNode> = {}
|
static node_type_overrides: Record<string, typeof ComfyBackendNode> = {}
|
||||||
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
||||||
|
|
||||||
private async registerNodes() {
|
private async registerNodes(defs: Record<ComfyNodeID, ComfyNodeDef>) {
|
||||||
// Load node definitions from the backend
|
|
||||||
const defs = await this.api.getNodeDefs();
|
|
||||||
|
|
||||||
// Register a node for each definition
|
// Register a node for each definition
|
||||||
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
||||||
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
||||||
@@ -504,7 +516,7 @@ export default class ComfyApp {
|
|||||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||||
}
|
}
|
||||||
|
|
||||||
async openWorkflow(data: SerializedAppState): Promise<ComfyWorkflow> {
|
async openWorkflow(data: SerializedAppState, refreshCombos: boolean | Record<string, ComfyNodeDef> = true): Promise<ComfyWorkflow> {
|
||||||
if (data.version !== COMFYBOX_SERIAL_VERSION) {
|
if (data.version !== COMFYBOX_SERIAL_VERSION) {
|
||||||
throw `Invalid ComfyBox saved data format: ${data.version}`
|
throw `Invalid ComfyBox saved data format: ${data.version}`
|
||||||
}
|
}
|
||||||
@@ -515,27 +527,38 @@ export default class ComfyApp {
|
|||||||
// Restore canvas offset/zoom
|
// Restore canvas offset/zoom
|
||||||
this.lCanvas.deserialize(data.canvas)
|
this.lCanvas.deserialize(data.canvas)
|
||||||
|
|
||||||
await this.refreshComboInNodes(workflow);
|
if (refreshCombos) {
|
||||||
|
let defs = null;
|
||||||
|
if (typeof refreshCombos === "object")
|
||||||
|
defs = refreshCombos;
|
||||||
|
await this.refreshComboInNodes(workflow, defs);
|
||||||
|
}
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveWorkflow(index: number) {
|
setActiveWorkflow(id: WorkflowInstID) {
|
||||||
|
const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id)
|
||||||
|
if (index === -1)
|
||||||
|
return;
|
||||||
workflowState.setActiveWorkflow(this.lCanvas, index);
|
workflowState.setActiveWorkflow(this.lCanvas, index);
|
||||||
selectionState.clear();
|
selectionState.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewWorkflow(index: number) {
|
createNewWorkflow() {
|
||||||
workflowState.createNewWorkflow(this.lCanvas, undefined, true);
|
workflowState.createNewWorkflow(this.lCanvas, undefined, true);
|
||||||
selectionState.clear();
|
selectionState.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeWorkflow(index: number) {
|
closeWorkflow(id: WorkflowInstID) {
|
||||||
|
const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id)
|
||||||
|
if (index === -1)
|
||||||
|
return;
|
||||||
workflowState.closeWorkflow(this.lCanvas, index);
|
workflowState.closeWorkflow(this.lCanvas, index);
|
||||||
selectionState.clear();
|
selectionState.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initDefaultWorkflow() {
|
async initDefaultWorkflow(defs?: Record<string, ComfyNodeDef>) {
|
||||||
let state = null;
|
let state = null;
|
||||||
try {
|
try {
|
||||||
const graphResponse = await fetch("/workflows/defaultWorkflow.json");
|
const graphResponse = await fetch("/workflows/defaultWorkflow.json");
|
||||||
@@ -546,7 +569,7 @@ export default class ComfyApp {
|
|||||||
notify(`Failed to load default graph: ${error}`, { type: "error" })
|
notify(`Failed to load default graph: ${error}`, { type: "error" })
|
||||||
state = structuredClone(blankGraph)
|
state = structuredClone(blankGraph)
|
||||||
}
|
}
|
||||||
await this.openWorkflow(state)
|
await this.openWorkflow(state, defs)
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
@@ -783,14 +806,15 @@ export default class ComfyApp {
|
|||||||
/**
|
/**
|
||||||
* Refresh combo list on whole nodes
|
* Refresh combo list on whole nodes
|
||||||
*/
|
*/
|
||||||
async refreshComboInNodes(workflow?: ComfyWorkflow, flashUI: boolean = false) {
|
async refreshComboInNodes(workflow?: ComfyWorkflow, defs?: Record<string, ComfyNodeDef>, flashUI: boolean = false) {
|
||||||
workflow ||= workflowState.getActiveWorkflow();
|
workflow ||= workflowState.getActiveWorkflow();
|
||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
notify("No active workflow!", { type: "error" })
|
notify("No active workflow!", { type: "error" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const defs = await this.api.getNodeDefs();
|
if (defs == null)
|
||||||
|
defs = await this.api.getNodeDefs();
|
||||||
|
|
||||||
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
||||||
return node
|
return node
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
let layoutState: WritableLayoutStateStore | null = null
|
let layoutState: WritableLayoutStateStore | null = null
|
||||||
|
|
||||||
|
$: layoutState = workflow?.layout
|
||||||
|
|
||||||
let target: IDragItem | null = null;
|
let target: IDragItem | null = null;
|
||||||
let node: LGraphNode | null = null;
|
let node: LGraphNode | null = null;
|
||||||
|
|
||||||
@@ -246,7 +248,7 @@
|
|||||||
doRefreshPanel()
|
doRefreshPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
workflow.notifyModified()
|
workflow.notifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWorkflowAttribute(spec: AttributesSpec): any {
|
function getWorkflowAttribute(spec: AttributesSpec): any {
|
||||||
@@ -279,10 +281,10 @@
|
|||||||
if (spec.onChanged)
|
if (spec.onChanged)
|
||||||
spec.onChanged($layoutState, value, prevValue)
|
spec.onChanged($layoutState, value, prevValue)
|
||||||
|
|
||||||
// if (spec.refreshPanelOnChange)
|
if (spec.refreshPanelOnChange)
|
||||||
doRefreshPanel()
|
doRefreshPanel()
|
||||||
|
|
||||||
workflow.notifyModified()
|
workflow.notifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
function doRefreshPanel() {
|
function doRefreshPanel() {
|
||||||
@@ -300,176 +302,178 @@
|
|||||||
<span class="type">({targetType})</span>
|
<span class="type">({targetType})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="props-entries">
|
<div class="props-entries">
|
||||||
{#if workflow != null && layoutState != null}
|
{#if workflow != null && layoutState != null}
|
||||||
{#key $layoutStates.refreshPropsPanel}
|
{#key workflow.id}
|
||||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
{#key $layoutStates.refreshPropsPanel}
|
||||||
<div class="category-name">
|
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||||
<span>
|
<div class="category-name">
|
||||||
<span class="title">{category.categoryName}</span>
|
<span>
|
||||||
</span>
|
<span class="title">{category.categoryName}</span>
|
||||||
</div>
|
</span>
|
||||||
{#each category.specs as spec(spec.id)}
|
</div>
|
||||||
{#if validWidgetAttribute(spec, target)}
|
{#each category.specs as spec(spec.id)}
|
||||||
<div class="props-entry">
|
{#if validWidgetAttribute(spec, target)}
|
||||||
{#if spec.type === "string"}
|
<div class="props-entry">
|
||||||
<TextBox
|
{#if spec.type === "string"}
|
||||||
value={getAttribute(target, spec)}
|
<TextBox
|
||||||
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={spec.multiline ? 5 : 1}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "boolean"}
|
|
||||||
<Checkbox
|
|
||||||
value={getAttribute(target, spec)}
|
value={getAttribute(target, spec)}
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
|
on:input={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
|
max_lines={spec.multiline ? 5 : 1}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "number"}
|
{:else if spec.type === "boolean"}
|
||||||
<ComfyNumberProperty
|
<Checkbox
|
||||||
name={spec.name}
|
|
||||||
value={getAttribute(target, spec)}
|
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)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
label={spec.name}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "enum"}
|
{:else if spec.type === "number"}
|
||||||
<ComfyComboProperty
|
<ComfyNumberProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={getAttribute(target, spec)}
|
value={getAttribute(target, spec)}
|
||||||
values={spec.values}
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
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={getProperty(node, spec)}
|
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
|
||||||
on:input={(e) => updateProperty(spec, e.detail)}
|
|
||||||
label={spec.name}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
max_lines={spec.multiline ? 5 : 1}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "boolean"}
|
|
||||||
<Checkbox
|
|
||||||
value={getProperty(node, spec)}
|
|
||||||
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={getProperty(node, spec)}
|
|
||||||
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"}
|
{:else if spec.type === "enum"}
|
||||||
<ComfyComboProperty
|
<ComfyComboProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={getProperty(node, spec)}
|
value={getAttribute(target, 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) => updateAttribute(spec, target, e.detail)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if validNodeVar(spec, node)}
|
{:else if node}
|
||||||
|
{#if validNodeProperty(spec, node)}
|
||||||
|
<div class="props-entry">
|
||||||
|
{#if spec.type === "string"}
|
||||||
|
<TextBox
|
||||||
|
value={getProperty(node, spec)}
|
||||||
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
|
on:input={(e) => updateProperty(spec, e.detail)}
|
||||||
|
label={spec.name}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
max_lines={spec.multiline ? 5 : 1}
|
||||||
|
/>
|
||||||
|
{:else if spec.type === "boolean"}
|
||||||
|
<Checkbox
|
||||||
|
value={getProperty(node, spec)}
|
||||||
|
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={getProperty(node, spec)}
|
||||||
|
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={getProperty(node, spec)}
|
||||||
|
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={spec.multiline ? 5 : 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">
|
<div class="props-entry">
|
||||||
{#if spec.type === "string"}
|
{#if spec.type === "string"}
|
||||||
<TextBox
|
<TextBox
|
||||||
value={getVar(node, spec)}
|
value={getWorkflowAttribute(spec)}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
on:input={(e) => updateVar(spec, e.detail)}
|
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
max_lines={spec.multiline ? 5 : 1}
|
max_lines={spec.multiline ? 5 : 1}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "boolean"}
|
{:else if spec.type === "boolean"}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={getVar(node, spec)}
|
value={getWorkflowAttribute(spec)}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
label={spec.name}
|
label={spec.name}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "number"}
|
{:else if spec.type === "number"}
|
||||||
<ComfyNumberProperty
|
<ComfyNumberProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={getVar(node, spec)}
|
value={getWorkflowAttribute(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}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "enum"}
|
{:else if spec.type === "enum"}
|
||||||
<ComfyComboProperty
|
<ComfyComboProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
value={getVar(node, spec)}
|
value={getWorkflowAttribute(spec)}
|
||||||
values={spec.values}
|
values={spec.values}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if !node && !target && validWorkflowAttribute(spec)}
|
{/if}
|
||||||
<div class="props-entry">
|
|
||||||
{#if spec.type === "string"}
|
|
||||||
<TextBox
|
|
||||||
value={getWorkflowAttribute(spec)}
|
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
|
||||||
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
|
||||||
label={spec.name}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
max_lines={spec.multiline ? 5 : 1}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "boolean"}
|
|
||||||
<Checkbox
|
|
||||||
value={getWorkflowAttribute(spec)}
|
|
||||||
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={getWorkflowAttribute(spec)}
|
|
||||||
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={getWorkflowAttribute(spec)}
|
|
||||||
values={spec.values}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
|
||||||
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/each}
|
{/each}
|
||||||
{/key}
|
{/key}
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -14,12 +14,15 @@
|
|||||||
import selectionState from "$lib/stores/selectionState";
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import type ComfyApp from './ComfyApp';
|
import type ComfyApp from './ComfyApp';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import type { WritableLayoutStateStore } from '$lib/stores/layoutStates';
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { cubicIn } from 'svelte/easing';
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
export let uiTheme: string = "gradio-dark" // TODO config
|
export let uiTheme: string = "gradio-dark" // TODO config
|
||||||
|
|
||||||
let workflow: ComfyWorkflow | null = null;
|
let workflow: ComfyWorkflow | null = null;
|
||||||
|
let openedWorkflows = []
|
||||||
|
|
||||||
let containerElem: HTMLDivElement;
|
let containerElem: HTMLDivElement;
|
||||||
let resizeTimeout: NodeJS.Timeout | null;
|
let resizeTimeout: NodeJS.Timeout | null;
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
let appSetupPromise: Promise<void> = null;
|
let appSetupPromise: Promise<void> = null;
|
||||||
|
|
||||||
$: workflow = $workflowState.activeWorkflow;
|
$: workflow = $workflowState.activeWorkflow;
|
||||||
|
$: openedWorkflows = $workflowState.openedWorkflows.map(w => { return { id: w.id } })
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
appSetupPromise = app.setup().then(() => {
|
appSetupPromise = app.setup().then(() => {
|
||||||
@@ -154,7 +158,7 @@
|
|||||||
app.createNewWorkflow();
|
app.createNewWorkflow();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeWorkflow(event: Event, index: number, workflow: ComfyWorkflow) {
|
function closeWorkflow(event: Event, workflow: ComfyWorkflow) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
@@ -163,15 +167,33 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.closeWorkflow(index);
|
app.closeWorkflow(workflow.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleConsider(evt: any) {
|
||||||
|
console.warn(openedWorkflows.length, openedWorkflows, evt.detail.items.length, evt.detail.items)
|
||||||
|
openedWorkflows = evt.detail.items;
|
||||||
|
// openedWorkflows = evt.detail.items.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID);
|
||||||
|
// workflowState.update(s => {
|
||||||
|
// s.openedWorkflows = openedWorkflows.map(w => workflowState.getWorkflow(w.id));
|
||||||
|
// return s;
|
||||||
|
// })
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleFinalize(evt: any) {
|
||||||
|
openedWorkflows = evt.detail.items;
|
||||||
|
workflowState.update(s => {
|
||||||
|
s.openedWorkflows = openedWorkflows.filter(w => w.id !== SHADOW_PLACEHOLDER_ITEM_ID).map(w => workflowState.getWorkflow(w.id));
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="comfy-content" bind:this={containerElem} class:loading>
|
<div id="comfy-content" bind:this={containerElem} class:loading>
|
||||||
<Splitpanes theme="comfy" on:resize={refreshView}>
|
<Splitpanes theme="comfy" on:resize={refreshView}>
|
||||||
<Pane bind:size={propsSidebarSize}>
|
<Pane bind:size={propsSidebarSize}>
|
||||||
<div class="sidebar-wrapper pane-wrapper">
|
<div class="sidebar-wrapper pane-wrapper">
|
||||||
<ComfyProperties layoutState={$workflowState.activeWorkflow} />
|
<ComfyProperties workflow={$workflowState.activeWorkflow} />
|
||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane>
|
<Pane>
|
||||||
@@ -195,22 +217,38 @@
|
|||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
<div id="workflow-tabs">
|
<div id="workflow-tabs">
|
||||||
{#each $workflowState.openedWorkflows as workflow, index}
|
<div class="workflow-tab-items"
|
||||||
<button class="workflow-tab"
|
use:dndzone="{{
|
||||||
class:selected={index === $workflowState.activeWorkflowIdx}
|
items: openedWorkflows,
|
||||||
on:click={() => app.setActiveWorkflow(index)}>
|
flipDurationMs: 200,
|
||||||
<span class="workflow-tab-title">
|
type: "workflow-tab",
|
||||||
{workflow.attrs.title}
|
morphDisabled: true,
|
||||||
{#if workflow.isModified}
|
dropFromOthersDisabled: true,
|
||||||
*
|
dropTargetStyle: {outline: "none"},
|
||||||
|
}}"
|
||||||
|
on:consider={handleConsider}
|
||||||
|
on:finalize={handleFinalize}>
|
||||||
|
{#each openedWorkflows.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||||
|
{@const workflow = workflowState.getWorkflow(item.id)}
|
||||||
|
<button class="workflow-tab"
|
||||||
|
class:selected={item.id === $workflowState.activeWorkflowID}
|
||||||
|
on:click={() => app.setActiveWorkflow(item.id)}>
|
||||||
|
<span class="workflow-tab-title">
|
||||||
|
{workflow.attrs.title}
|
||||||
|
{#if workflow.isModified}
|
||||||
|
*
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<button class="workflow-close-button"
|
||||||
|
on:click={(e) => closeWorkflow(e, workflow)}>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
{#if workflow[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||||
|
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
|
||||||
<button class="workflow-close-button"
|
|
||||||
on:click={(e) => closeWorkflow(e, index, workflow)}>
|
|
||||||
✕
|
|
||||||
</button>
|
</button>
|
||||||
</button>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
<button class="workflow-add-new-button"
|
<button class="workflow-add-new-button"
|
||||||
on:click={createNewWorkflow}>
|
on:click={createNewWorkflow}>
|
||||||
➕
|
➕
|
||||||
@@ -336,12 +374,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#workflow-tabs {
|
#workflow-tabs, .workflow-tab-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-right: 1em;
|
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#workflow-tabs {
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#topbar {
|
#topbar {
|
||||||
background: var(--neutral-900);
|
background: var(--neutral-900);
|
||||||
@@ -393,6 +435,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--size-4);
|
gap: var(--size-4);
|
||||||
|
cursor: pointer !important;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-right: 1px solid var(--neutral-600);
|
border-right: 1px solid var(--neutral-600);
|
||||||
@@ -407,6 +450,7 @@
|
|||||||
background: var(--neutral-700);
|
background: var(--neutral-700);
|
||||||
color: var(--neutral-300);
|
color: var(--neutral-300);
|
||||||
border-top-color: var(--primary-500);
|
border-top-color: var(--primary-500);
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .workflow-close-button {
|
> .workflow-close-button {
|
||||||
@@ -433,7 +477,6 @@
|
|||||||
color: var(--neutral-500);
|
color: var(--neutral-500);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-top: 3px solid var(--neutral-600);
|
border-top: 3px solid var(--neutral-600);
|
||||||
border-left: 1px solid var(--neutral-600);
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -536,4 +579,12 @@
|
|||||||
#comfy-file-input {
|
#comfy-file-input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drag-item-shadow {
|
||||||
|
visibility: visible;
|
||||||
|
border: 1px dashed grey;
|
||||||
|
background: lightblue;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -548,6 +548,13 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Workflow
|
// Workflow
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "string",
|
||||||
|
location: "workflow",
|
||||||
|
editable: true,
|
||||||
|
defaultValue: "New Workflow"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "queuePromptButtonName",
|
name: "queuePromptButtonName",
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export class ComfyWorkflow {
|
|||||||
export type WorkflowState = {
|
export type WorkflowState = {
|
||||||
openedWorkflows: ComfyWorkflow[],
|
openedWorkflows: ComfyWorkflow[],
|
||||||
openedWorkflowsByID: Record<WorkflowInstID, ComfyWorkflow>,
|
openedWorkflowsByID: Record<WorkflowInstID, ComfyWorkflow>,
|
||||||
activeWorkflowIdx: number,
|
activeWorkflowID: WorkflowInstID | null,
|
||||||
activeWorkflow: ComfyWorkflow | null,
|
activeWorkflow: ComfyWorkflow | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ const store: Writable<WorkflowState> = writable(
|
|||||||
{
|
{
|
||||||
openedWorkflows: [],
|
openedWorkflows: [],
|
||||||
openedWorkflowsByID: {},
|
openedWorkflowsByID: {},
|
||||||
activeWorkflowIdx: -1,
|
activeWorkflowID: null,
|
||||||
activeWorkflow: null
|
activeWorkflow: null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -245,9 +245,9 @@ function getWorkflowByNodeID(id: NodeID): ComfyWorkflow | null {
|
|||||||
|
|
||||||
function getActiveWorkflow(): ComfyWorkflow | null {
|
function getActiveWorkflow(): ComfyWorkflow | null {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
if (state.activeWorkflowIdx === -1)
|
if (state.activeWorkflowID == null)
|
||||||
return null;
|
return null;
|
||||||
return state.openedWorkflows[state.activeWorkflowIdx];
|
return state.openedWorkflowsByID[state.activeWorkflowID];
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workflow", setActive: boolean = false): ComfyWorkflow {
|
function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workflow", setActive: boolean = false): ComfyWorkflow {
|
||||||
@@ -292,9 +292,10 @@ function closeWorkflow(canvas: ComfyGraphCanvas, index: number) {
|
|||||||
|
|
||||||
layoutStates.remove(workflow.id)
|
layoutStates.remove(workflow.id)
|
||||||
|
|
||||||
|
|
||||||
state.openedWorkflows.splice(index, 1)
|
state.openedWorkflows.splice(index, 1)
|
||||||
delete state.openedWorkflowsByID[workflow.id]
|
delete state.openedWorkflowsByID[workflow.id]
|
||||||
const newIndex = clamp(state.activeWorkflowIdx, 0, state.openedWorkflows.length - 1);
|
let newIndex = clamp(index, 0, state.openedWorkflows.length - 1)
|
||||||
setActiveWorkflow(canvas, newIndex);
|
setActiveWorkflow(canvas, newIndex);
|
||||||
|
|
||||||
store.set(state);
|
store.set(state);
|
||||||
@@ -310,19 +311,22 @@ function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkfl
|
|||||||
const state = get(store);
|
const state = get(store);
|
||||||
|
|
||||||
if (state.openedWorkflows.length === 0) {
|
if (state.openedWorkflows.length === 0) {
|
||||||
state.activeWorkflowIdx = -1;
|
state.activeWorkflowID = null;
|
||||||
state.activeWorkflow = null
|
state.activeWorkflow = null
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0 || index >= state.openedWorkflows.length || state.activeWorkflowIdx === index)
|
if (index < 0 || index >= state.openedWorkflows.length)
|
||||||
return state.activeWorkflow;
|
return state.activeWorkflow;
|
||||||
|
|
||||||
|
const workflow = state.openedWorkflows[index]
|
||||||
|
if (workflow.id === state.activeWorkflowID)
|
||||||
|
return;
|
||||||
|
|
||||||
if (state.activeWorkflow != null)
|
if (state.activeWorkflow != null)
|
||||||
state.activeWorkflow.stop("app")
|
state.activeWorkflow.stop("app")
|
||||||
|
|
||||||
const workflow = state.openedWorkflows[index]
|
state.activeWorkflowID = workflow.id;
|
||||||
state.activeWorkflowIdx = index;
|
|
||||||
state.activeWorkflow = workflow;
|
state.activeWorkflow = workflow;
|
||||||
|
|
||||||
workflow.start("app", canvas);
|
workflow.start("app", canvas);
|
||||||
|
|||||||
Reference in New Issue
Block a user