Fix props pane

This commit is contained in:
space-nuko
2023-05-20 23:13:31 -05:00
parent ee97bd43bc
commit 3aad259869
5 changed files with 261 additions and 171 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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>

View File

@@ -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",

View File

@@ -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);