@@ -14,7 +14,7 @@
|
|||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
"format": "prettier --plugin-search-dir . --write .",
|
"format": "prettier --plugin-search-dir . --write .",
|
||||||
"svelte-check": "svelte-check",
|
"svelte-check": "svelte-check",
|
||||||
"prebuild": "pnpm run build:css && pnpm --filter=klecks lang:build",
|
"prebuild": "pnpm run build:css",
|
||||||
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
|
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -85,7 +85,6 @@
|
|||||||
"framework7": "^8.0.3",
|
"framework7": "^8.0.3",
|
||||||
"framework7-svelte": "^8.0.3",
|
"framework7-svelte": "^8.0.3",
|
||||||
"img-comparison-slider": "^8.0.0",
|
"img-comparison-slider": "^8.0.0",
|
||||||
"klecks": "workspace:*",
|
|
||||||
"pollen-css": "^4.6.2",
|
"pollen-css": "^4.6.2",
|
||||||
"radix-icons-svelte": "^1.2.1",
|
"radix-icons-svelte": "^1.2.1",
|
||||||
"style-mod": "^4.0.3",
|
"style-mod": "^4.0.3",
|
||||||
|
|||||||
1894
pnpm-lock.yaml
generated
1894
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,4 +2,3 @@ packages:
|
|||||||
- 'gradio/js/*'
|
- 'gradio/js/*'
|
||||||
- 'gradio/client/js'
|
- 'gradio/client/js'
|
||||||
- 'litegraph/packages/*'
|
- 'litegraph/packages/*'
|
||||||
- 'klecks'
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"comfyUIHostname": "localhost",
|
|
||||||
"comfyUIPort": 8188,
|
|
||||||
"alwaysStripUserState": false,
|
|
||||||
"promptForWorkflowName": false,
|
|
||||||
"confirmWhenUnloadingUnsavedChanges": false,
|
|
||||||
"builtInTemplates": ["ControlNet", "LoRA x5", "Model Loader", "Positive_Negative", "Seed Randomizer"],
|
|
||||||
"cacheBuiltInResources": true
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ import queueState from "./stores/queueState";
|
|||||||
import selectionState from "./stores/selectionState";
|
import selectionState from "./stores/selectionState";
|
||||||
import templateState from "./stores/templateState";
|
import templateState from "./stores/templateState";
|
||||||
import { calcNodesBoundingBox } from "./utils";
|
import { calcNodesBoundingBox } from "./utils";
|
||||||
|
import interfaceState from "./stores/interfaceState";
|
||||||
|
|
||||||
export type SerializedGraphCanvasState = {
|
export type SerializedGraphCanvasState = {
|
||||||
offset: Vector2,
|
offset: Vector2,
|
||||||
@@ -118,13 +119,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
// color = "yellow";
|
// color = "yellow";
|
||||||
// thickness = 5;
|
// thickness = 5;
|
||||||
// }
|
// }
|
||||||
if (ss.currentHoveredNodes.has(node.id)) {
|
if (nodeErrors) {
|
||||||
color = "lightblue";
|
|
||||||
}
|
|
||||||
else if (isRunningNode) {
|
|
||||||
color = "#0f0";
|
|
||||||
}
|
|
||||||
else if (nodeErrors) {
|
|
||||||
const hasExecutionError = nodeErrors.find(e => e.errorType === "execution");
|
const hasExecutionError = nodeErrors.find(e => e.errorType === "execution");
|
||||||
if (hasExecutionError) {
|
if (hasExecutionError) {
|
||||||
blink = true;
|
blink = true;
|
||||||
@@ -139,6 +134,12 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
color = "cyan";
|
color = "cyan";
|
||||||
thickness = 2
|
thickness = 2
|
||||||
}
|
}
|
||||||
|
else if (ss.currentHoveredNodes.has(node.id)) {
|
||||||
|
color = "lightblue";
|
||||||
|
}
|
||||||
|
else if (isRunningNode) {
|
||||||
|
color = "#0f0";
|
||||||
|
}
|
||||||
|
|
||||||
if (blink) {
|
if (blink) {
|
||||||
if (nodeErrors && nodeErrors.includes(this.blinkError) && this.blinkErrorTime > 0) {
|
if (nodeErrors && nodeErrors.includes(this.blinkError) && this.blinkErrorTime > 0) {
|
||||||
@@ -193,6 +194,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CONNECTION_POS: Vector2 = [0, 0];
|
||||||
|
|
||||||
private highlightNodeInput(node: LGraphNode, inputSlot: SlotNameOrIndex, ctx: CanvasRenderingContext2D) {
|
private highlightNodeInput(node: LGraphNode, inputSlot: SlotNameOrIndex, ctx: CanvasRenderingContext2D) {
|
||||||
let inputIndex: number;
|
let inputIndex: number;
|
||||||
if (typeof inputSlot === "number")
|
if (typeof inputSlot === "number")
|
||||||
@@ -200,7 +203,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
else
|
else
|
||||||
inputIndex = node.findInputSlotIndexByName(inputSlot)
|
inputIndex = node.findInputSlotIndexByName(inputSlot)
|
||||||
if (inputIndex !== -1) {
|
if (inputIndex !== -1) {
|
||||||
let pos = node.getConnectionPos(true, inputIndex);
|
let pos = node.getConnectionPos(true, inputIndex, ComfyGraphCanvas.CONNECTION_POS);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false)
|
ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false)
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
@@ -700,6 +703,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jumpToNode(node: LGraphNode) {
|
jumpToNode(node: LGraphNode) {
|
||||||
|
interfaceState.update(s => { s.isJumpingToNode = true; return s; })
|
||||||
|
|
||||||
this.closeAllSubgraphs();
|
this.closeAllSubgraphs();
|
||||||
|
|
||||||
const subgraphs = Array.from(node.iterateParentSubgraphNodes()).reverse();
|
const subgraphs = Array.from(node.iterateParentSubgraphNodes()).reverse();
|
||||||
@@ -709,6 +714,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.centerOnNode(node);
|
this.centerOnNode(node);
|
||||||
|
this.selectNode(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpToNodeAndInput(node: LGraphNode, slotIndex: number) {
|
jumpToNodeAndInput(node: LGraphNode, slotIndex: number) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import ComfyBoxWorkflowsView from "./ComfyBoxWorkflowsView.svelte";
|
import ComfyBoxWorkflowsView from "./ComfyBoxWorkflowsView.svelte";
|
||||||
import GlobalModal from "./GlobalModal.svelte";
|
import GlobalModal from "./GlobalModal.svelte";
|
||||||
|
import ComfySettingsView from "./ComfySettingsView.svelte";
|
||||||
|
|
||||||
export let app: ComfyApp = undefined;
|
export let app: ComfyApp = undefined;
|
||||||
let hasShownUIHelpToast: boolean = false;
|
let hasShownUIHelpToast: boolean = false;
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
<ComfyBoxWorkflowsView {app} {uiTheme} />
|
<ComfyBoxWorkflowsView {app} {uiTheme} />
|
||||||
</SidebarItem>
|
</SidebarItem>
|
||||||
<SidebarItem id="settings" name="Settings" icon={Gear}>
|
<SidebarItem id="settings" name="Settings" icon={Gear}>
|
||||||
|
<ComfySettingsView {app} />
|
||||||
</SidebarItem>
|
</SidebarItem>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -261,18 +261,29 @@ export default class ComfyApp {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
try {
|
try {
|
||||||
const config = await fetch(`/config.json`, { cache: "no-store" });
|
console.log("Loading config.json...")
|
||||||
const newConfig = await config.json() as ConfigState;
|
const config = localStorage.getItem("config")
|
||||||
configState.set({ ...get(configState), ...newConfig });
|
if (config == null)
|
||||||
|
configState.loadDefault();
|
||||||
|
else
|
||||||
|
configState.load(JSON.parse(config));
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(`Failed to load config`, error)
|
console.error(`Failed to load config, falling back to defaults`, error)
|
||||||
|
configState.loadDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configState.onChange("linkDisplayType", (newValue) => {
|
||||||
|
// if (!this.lCanvas)
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// this.lCanvas.links_render_mode = newValue;
|
||||||
|
// this.lCanvas.setDirty(true, true);
|
||||||
|
// })
|
||||||
|
|
||||||
|
configState.runOnChangedEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBuiltInTemplates(): Promise<SerializedComfyBoxTemplate[]> {
|
async loadBuiltInTemplates(): Promise<SerializedComfyBoxTemplate[]> {
|
||||||
|
|||||||
@@ -220,6 +220,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openGraph(cb: () => void) {
|
||||||
|
const newGraphSize = Math.max(50, graphSize);
|
||||||
|
const willOpenPane = newGraphSize != graphSize
|
||||||
|
graphSize = newGraphSize
|
||||||
|
|
||||||
|
if (willOpenPane) {
|
||||||
|
const graphPane = getGraphPane();
|
||||||
|
if (graphPane) {
|
||||||
|
graphPane.addEventListener("transitionend", cb, { once: true })
|
||||||
|
await tick()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function showError(promptIDWithError: PromptID) {
|
async function showError(promptIDWithError: PromptID) {
|
||||||
hideError();
|
hideError();
|
||||||
|
|
||||||
@@ -247,23 +267,7 @@
|
|||||||
app.lCanvas.jumpToFirstError();
|
app.lCanvas.jumpToFirstError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGraphSize = Math.max(50, graphSize);
|
await openGraph(jumpToError)
|
||||||
const willOpenPane = newGraphSize != graphSize
|
|
||||||
graphSize = newGraphSize
|
|
||||||
|
|
||||||
if (willOpenPane) {
|
|
||||||
const graphPane = getGraphPane();
|
|
||||||
if (graphPane) {
|
|
||||||
graphPane.addEventListener("transitionend", jumpToError, { once: true })
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
jumpToError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
jumpToError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideError() {
|
function hideError() {
|
||||||
@@ -273,7 +277,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContext(WORKFLOWS_VIEW, {
|
setContext(WORKFLOWS_VIEW, {
|
||||||
showError
|
showError,
|
||||||
|
openGraph
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,11 @@
|
|||||||
<div class="error-list-header">
|
<div class="error-list-header">
|
||||||
<button class="error-list-close" on:click={closeList}>✕</button>
|
<button class="error-list-close" on:click={closeList}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
{#each Object.entries(errors.errorsByID) as [nodeID, nodeErrors]}
|
<div class="error-list-scroll-container">
|
||||||
|
{#each Object.entries(errors.errorsByID) as [nodeID, nodeErrors], i}
|
||||||
{@const first = nodeErrors[0]}
|
{@const first = nodeErrors[0]}
|
||||||
{@const parent = getParentNode(first)}
|
{@const parent = getParentNode(first)}
|
||||||
|
{@const last = i === Object.keys(errors.errorsByID).length - 1}
|
||||||
<div class="error-group">
|
<div class="error-group">
|
||||||
<div class="error-node-details">
|
<div class="error-node-details">
|
||||||
<span class="error-node-type">{first.comfyNodeType}</span>
|
<span class="error-node-type">{first.comfyNodeType}</span>
|
||||||
@@ -78,7 +80,7 @@
|
|||||||
<span class="error-node-parent">({parent.title})</span>
|
<span class="error-node-parent">({parent.title})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="error-entries">
|
<div class="error-entries" class:last>
|
||||||
{#each nodeErrors as error}
|
{#each nodeErrors as error}
|
||||||
{@const isExecutionError = error.errorType === "execution"}
|
{@const isExecutionError = error.errorType === "execution"}
|
||||||
<div class="error-entry">
|
<div class="error-entry">
|
||||||
@@ -150,6 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -158,7 +161,6 @@
|
|||||||
width: 30%;
|
width: 30%;
|
||||||
height: 70%;
|
height: 70%;
|
||||||
margin: 1.0rem;
|
margin: 1.0rem;
|
||||||
overflow-y: auto;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -186,6 +188,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-list-scroll-container {
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.error-node-details {
|
.error-node-details {
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
@@ -200,7 +207,7 @@
|
|||||||
font-weight: initial;
|
font-weight: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-entries:last-child {
|
.error-entries:not(.last):last-child {
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import ComfyQueue from "./ComfyQueue.svelte";
|
import ComfyQueue from "./ComfyQueue.svelte";
|
||||||
import ComfyTemplates from "./ComfyTemplates.svelte";
|
import ComfyTemplates from "./ComfyTemplates.svelte";
|
||||||
import { SvelteComponent } from "svelte";
|
import { SvelteComponent } from "svelte";
|
||||||
|
import { capitalize } from "$lib/utils";
|
||||||
|
|
||||||
export let app: ComfyApp
|
export let app: ComfyApp
|
||||||
export let mode: ComfyPaneMode = "none";
|
export let mode: ComfyPaneMode = "none";
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
{:else if mode === "graph"}
|
{:else if mode === "graph"}
|
||||||
<ComfyGraphView {app} />
|
<ComfyGraphView {app} />
|
||||||
{:else if mode === "properties"}
|
{:else if mode === "properties"}
|
||||||
<ComfyProperties workflow={$workflowState.activeWorkflow} />
|
<ComfyProperties {app} workflow={$workflowState.activeWorkflow} />
|
||||||
{:else if mode === "templates"}
|
{:else if mode === "templates"}
|
||||||
<ComfyTemplates {app} />
|
<ComfyTemplates {app} />
|
||||||
{:else if mode === "queue"}
|
{:else if mode === "queue"}
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<button class="mode-button ternary"
|
<button class="mode-button ternary"
|
||||||
disabled={mode === theMode}
|
disabled={mode === theMode}
|
||||||
|
title={capitalize(theMode)}
|
||||||
class:selected={mode === theMode}
|
class:selected={mode === theMode}
|
||||||
on:click={() => switchMode(theMode)}>
|
on:click={() => switchMode(theMode)}>
|
||||||
<svelte:component this={icon} width="100%" height="100%" />
|
<svelte:component this={icon} width="100%" height="100%" />
|
||||||
@@ -99,7 +101,6 @@
|
|||||||
color: var(--body-text-color);
|
color: var(--body-text-color);
|
||||||
}
|
}
|
||||||
&.selected {
|
&.selected {
|
||||||
color: var(--body-text-color);
|
|
||||||
background-color: var(--panel-background-fill);
|
background-color: var(--panel-background-fill);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { LGraphNode } from "@litegraph-ts/core"
|
import { LGraphNode } from "@litegraph-ts/core"
|
||||||
import { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
|
import { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
|
||||||
import uiState from "$lib/stores/uiState"
|
import uiState from "$lib/stores/uiState"
|
||||||
|
import interfaceState from "$lib/stores/interfaceState"
|
||||||
import workflowState from "$lib/stores/workflowState"
|
import workflowState from "$lib/stores/workflowState"
|
||||||
import layoutStates from "$lib/stores/layoutStates"
|
import layoutStates from "$lib/stores/layoutStates"
|
||||||
import selectionState from "$lib/stores/selectionState"
|
import selectionState from "$lib/stores/selectionState"
|
||||||
@@ -12,7 +13,11 @@
|
|||||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import type { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
import type { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||||
|
import { Diagram3 } from "svelte-bootstrap-icons";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
|
||||||
|
|
||||||
|
export let app: ComfyApp
|
||||||
export let workflow: ComfyBoxWorkflow | null;
|
export let workflow: ComfyBoxWorkflow | null;
|
||||||
|
|
||||||
let layoutState: WritableLayoutStateStore | null = null
|
let layoutState: WritableLayoutStateStore | null = null
|
||||||
@@ -22,7 +27,12 @@
|
|||||||
let target: IDragItem | null = null;
|
let target: IDragItem | null = null;
|
||||||
let node: LGraphNode | null = null;
|
let node: LGraphNode | null = null;
|
||||||
|
|
||||||
$: if (layoutState) {
|
$: {
|
||||||
|
if ($interfaceState.isJumpingToNode) {
|
||||||
|
$interfaceState.isJumpingToNode = false;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
if (layoutState) {
|
||||||
if ($selectionState.currentSelection.length > 0) {
|
if ($selectionState.currentSelection.length > 0) {
|
||||||
node = null;
|
node = null;
|
||||||
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
||||||
@@ -37,6 +47,13 @@
|
|||||||
else if ($selectionState.currentSelectionNodes.length > 0) {
|
else if ($selectionState.currentSelectionNodes.length > 0) {
|
||||||
target = null;
|
target = null;
|
||||||
node = $selectionState.currentSelectionNodes[0]
|
node = $selectionState.currentSelectionNodes[0]
|
||||||
|
|
||||||
|
if (node != null && layoutState != null) {
|
||||||
|
const dragItem = layoutState.findLayoutForNode(node.id);
|
||||||
|
if (dragItem != null) {
|
||||||
|
target = dragItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
target = null
|
target = null
|
||||||
@@ -47,6 +64,8 @@
|
|||||||
target = null;
|
target = null;
|
||||||
node = null;
|
node = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: if (target) {
|
$: if (target) {
|
||||||
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||||
@@ -291,18 +310,52 @@
|
|||||||
console.warn("[ComfyProperties] doRefreshPanel")
|
console.warn("[ComfyProperties] doRefreshPanel")
|
||||||
$layoutStates.refreshPropsPanel += 1;
|
$layoutStates.refreshPropsPanel += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workflowsViewContext = getContext(WORKFLOWS_VIEW) as any;
|
||||||
|
|
||||||
|
async function jumpToNode() {
|
||||||
|
if (!workflowsViewContext) {
|
||||||
|
// strange svelte bug caused by HMR
|
||||||
|
// https://github.com/sveltejs/svelte/issues/8655
|
||||||
|
console.error("[ComfyProperties] No workflows view context!")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app?.lCanvas == null || workflow == null || node == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const activeWorkflow = workflowState.setActiveWorkflow(app.lCanvas, workflow.id);
|
||||||
|
|
||||||
|
if (activeWorkflow == null || !activeWorkflow.graph.getNodeByIdRecursive(node.id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await workflowsViewContext.openGraph(() => {
|
||||||
|
app.lCanvas.jumpToNode(node);
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="props">
|
<div class="props">
|
||||||
|
<div class="props-scroller">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<div class="target-name">
|
<div class="target-name">
|
||||||
<span>
|
<div class="target-title-wrapper">
|
||||||
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
|
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}</span>
|
||||||
{#if targetType !== ""}
|
{#if targetType !== ""}
|
||||||
<span class="type">({targetType})</span>
|
<span class="type">({targetType})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</div>
|
||||||
</span>
|
{#if node != null}
|
||||||
|
<div class="target-name-button">
|
||||||
|
<button class="mode-button ternary"
|
||||||
|
disabled={node == null}
|
||||||
|
title="View in Graph"
|
||||||
|
on:click={jumpToNode}
|
||||||
|
>
|
||||||
|
<Diagram3 width="100%" height="100%" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="props-entries">
|
<div class="props-entries">
|
||||||
@@ -477,9 +530,24 @@
|
|||||||
{/key}
|
{/key}
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$bottom-bar-height: 2.5rem;
|
||||||
|
|
||||||
|
.props {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.props-scroller {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - $bottom-bar-height);
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.props-entry {
|
.props-entry {
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
@@ -491,7 +559,7 @@
|
|||||||
|
|
||||||
.target-name {
|
.target-name {
|
||||||
background: var(--input-background-fill);
|
background: var(--input-background-fill);
|
||||||
border-color: var(--input-border-color);
|
border-color: var(--input-border-color);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -501,6 +569,48 @@
|
|||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> .target-title-wrapper {
|
||||||
|
padding: 0.8rem 0 0.8rem 1.0rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .target-name-button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
.mode-button {
|
||||||
|
color: var(--comfy-accent-soft);
|
||||||
|
height: $bottom-bar-height;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin: 1.0rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
@include square-button;
|
||||||
|
|
||||||
|
color: var(--neutral-300);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
filter: brightness(120%) !important;
|
||||||
|
}
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
filter: brightness(50%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,13 +628,5 @@
|
|||||||
color: var(--neutral-500);
|
color: var(--neutral-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
|
||||||
/* width: 100%;
|
|
||||||
height: auto;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
padding: 0.5em; */
|
|
||||||
}
|
|
||||||
|
|
||||||
@include disable-inputs;
|
@include disable-inputs;
|
||||||
|
|||||||
260
src/lib/components/ComfySettingsView.svelte
Normal file
260
src/lib/components/ComfySettingsView.svelte
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { CONFIG_CATEGORIES, CONFIG_DEFS_BY_CATEGORY, CONFIG_DEFS_BY_NAME, type ConfigDefAny, type ConfigDefEnum, type ConfigState } from "$lib/stores/configDefs";
|
||||||
|
import { capitalize } from "$lib/utils";
|
||||||
|
import { Checkbox } from "@gradio/form";
|
||||||
|
import configState from "$lib/stores/configState";
|
||||||
|
import type ComfyApp from "./ComfyApp";
|
||||||
|
import NumberInput from "./NumberInput.svelte";
|
||||||
|
import Textbox from "@gradio/form/src/Textbox.svelte";
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import { SvelteToast } from "@zerodevx/svelte-toast";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
|
||||||
|
export let app: ComfyApp
|
||||||
|
|
||||||
|
let selectedCategory = CONFIG_CATEGORIES[0];
|
||||||
|
let changes: Partial<Record<keyof ConfigState, any>> = {}
|
||||||
|
|
||||||
|
const toastOptions = {
|
||||||
|
intro: { duration: 200 },
|
||||||
|
theme: {
|
||||||
|
'--toastBarHeight': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCategory(category: string) {
|
||||||
|
selectedCategory = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOption(def: ConfigDefAny, value: any) {
|
||||||
|
if (!configState.validateConfigOption(def, value)) {
|
||||||
|
console.warn(`[configState] Invalid value for option ${def.name} (${value}), setting to default (${def.defaultValue})`);
|
||||||
|
value = def.defaultValue
|
||||||
|
}
|
||||||
|
changes[def.name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnumOption(def: ConfigDefEnum<any, any>, e: Event): void {
|
||||||
|
const select = e.target as HTMLSelectElement;
|
||||||
|
const index = select.selectedIndex
|
||||||
|
setOption(def, def.options.values[index].value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSave() {
|
||||||
|
for (const [k, v] of Object.entries(changes)) {
|
||||||
|
const def = CONFIG_DEFS_BY_NAME[k]
|
||||||
|
configState.setConfigOption(def, v, true);
|
||||||
|
}
|
||||||
|
changes = {};
|
||||||
|
const json = JSON.stringify($configState);
|
||||||
|
localStorage.setItem("config", json);
|
||||||
|
notify("Config applied!", { type: "success" })
|
||||||
|
}
|
||||||
|
|
||||||
|
function doReset() {
|
||||||
|
if (!confirm("Are you sure you want to reset the config to the defaults?"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
configState.loadDefault(true);
|
||||||
|
notify("Config reset!")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="comfy-settings">
|
||||||
|
<div class="comfy-settings-categories">
|
||||||
|
{#each CONFIG_CATEGORIES as category}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="comfy-settings-category" class:selected={selectedCategory === category} on:click={() => selectCategory(category)}>
|
||||||
|
{capitalize(category)}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="comfy-settings-main">
|
||||||
|
{#if selectedCategory}
|
||||||
|
{@const categoryDefs = CONFIG_DEFS_BY_CATEGORY[selectedCategory]}
|
||||||
|
{#key $configState}
|
||||||
|
<div class="comfy-settings-entries">
|
||||||
|
{#each categoryDefs as def}
|
||||||
|
{@const value = $configState[def.name]}
|
||||||
|
<div class="comfy-settings-entry">
|
||||||
|
<div class="name">{def.name}</div>
|
||||||
|
{#if def.type === "boolean"}
|
||||||
|
<span class="ctrl checkbox">
|
||||||
|
<Checkbox label={def.description} {value} on:change={(e) => setOption(def, e.detail)} />
|
||||||
|
</span>
|
||||||
|
{:else if def.type === "number"}
|
||||||
|
<div class="description">{def.description}</div>
|
||||||
|
<span class="ctrl number">
|
||||||
|
<NumberInput label="" min={def.options.min} max={def.options.max} step={def.options.step} {value} on:release={(e) => setOption(def, e.detail)} />
|
||||||
|
</span>
|
||||||
|
{:else if def.type === "string"}
|
||||||
|
<div class="description">{def.description}</div>
|
||||||
|
<span class="ctrl textbox">
|
||||||
|
<Textbox label="" lines={1} max_lines={1} {value} on:change={(e) => setOption(def, e.detail)} />
|
||||||
|
</span>
|
||||||
|
{:else if def.type === "string[]"}
|
||||||
|
<div class="description">{def.description}</div>
|
||||||
|
<span class="ctrl string-array">
|
||||||
|
{value.join(",")}
|
||||||
|
</span>
|
||||||
|
{:else if def.type === "enum"}
|
||||||
|
<div class="description">{def.description}</div>
|
||||||
|
<span class="ctrl enum">
|
||||||
|
<select id="ui-theme" name="ui-theme" on:change={(e) => setEnumOption(def, e)}>
|
||||||
|
{#each def.options.values as option, i}
|
||||||
|
{@const selected = def.options.values[i].value === value}
|
||||||
|
<option value={option.value} {selected}>{option.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
(Unknown config type {def.type})
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{:else}
|
||||||
|
Please select a category.
|
||||||
|
{/if}
|
||||||
|
<div class="comfy-settings-bottom-bar">
|
||||||
|
<div>
|
||||||
|
<div class="left">
|
||||||
|
<Button variant="secondary" on:click={doReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<Button variant="primary" on:click={doSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SvelteToast options={toastOptions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$bottom-bar-height: 5rem;
|
||||||
|
|
||||||
|
.comfy-settings {
|
||||||
|
color: var(--body-text-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-settings-categories {
|
||||||
|
width: 20rem;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--neutral-500);
|
||||||
|
background: var(--neutral-800);
|
||||||
|
border-left: 2px solid var(--comfy-splitpanes-background-fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-settings-category {
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
font-size: 14pt;
|
||||||
|
|
||||||
|
border-bottom: 1px solid grey;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
color: var(--body-text-color);
|
||||||
|
background: var(--neutral-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-settings-main {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - $bottom-bar-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-settings-entries {
|
||||||
|
padding: 3rem 3rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-settings-entry {
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 11pt;
|
||||||
|
color: var(--neutral-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
min-width: 5rem;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:not(.checkbox) {
|
||||||
|
width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.textbox {
|
||||||
|
:global(span) {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checkbox {
|
||||||
|
display: inline-flex !important;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
|
||||||
|
:global(label) {
|
||||||
|
color: var(--neutral-400);
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.enum {
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position-x: 100%;
|
||||||
|
background-position-y: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-settings-bottom-bar {
|
||||||
|
background: var(--neutral-900);
|
||||||
|
width: 100%;
|
||||||
|
border-top: 2px solid var(--neutral-800);
|
||||||
|
gap: var(--layout-gap);
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: $bottom-bar-height;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 2rem;
|
||||||
|
margin: auto;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--layout-gap);
|
||||||
|
margin: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
#lightboxModal{
|
#lightboxModal{
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1001;
|
z-index: var(--layer-top);
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let value: number = 0;
|
export let value: number = 0;
|
||||||
export let min: number = -1024
|
export let min: number | null = null
|
||||||
export let max: number = 1024
|
export let max: number | null = null
|
||||||
export let step: number = 1;
|
export let step: number = 1;
|
||||||
export let label: string = "";
|
export let label: string = "";
|
||||||
export let disabled: boolean = false;
|
export let disabled: boolean = false;
|
||||||
@@ -41,9 +41,11 @@
|
|||||||
|
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
|
{#if label}
|
||||||
<label>
|
<label>
|
||||||
<BlockTitle>{label}</BlockTitle>
|
<BlockTitle>{label}</BlockTitle>
|
||||||
</label>
|
</label>
|
||||||
|
{/if}
|
||||||
<input
|
<input
|
||||||
data-testid="number-input"
|
data-testid="number-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -83,11 +85,11 @@
|
|||||||
border: var(--input-border-width) solid var(--input-border-color);
|
border: var(--input-border-width) solid var(--input-border-color);
|
||||||
border-radius: var(--input-radius);
|
border-radius: var(--input-radius);
|
||||||
background: var(--input-background-fill);
|
background: var(--input-background-fill);
|
||||||
padding: var(--size-2) var(--size-2);
|
padding: var(--input-padding);
|
||||||
color: var(--body-text-color);
|
color: var(--body-text-color);
|
||||||
font-size: var(--input-text-size);
|
font-size: var(--input-text-size);
|
||||||
line-height: var(--line-sm);
|
line-height: var(--line-sm);
|
||||||
text-align: center;
|
// text-align: center;
|
||||||
}
|
}
|
||||||
input:disabled {
|
input:disabled {
|
||||||
-webkit-text-fill-color: var(--body-text-color);
|
-webkit-text-fill-color: var(--body-text-color);
|
||||||
@@ -95,11 +97,18 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"]:focus {
|
input[type="number"] {
|
||||||
|
&:focus {
|
||||||
box-shadow: var(--input-shadow-focus);
|
box-shadow: var(--input-shadow-focus);
|
||||||
border-color: var(--input-border-color-focus);
|
border-color: var(--input-border-color-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::-webkit-inner-spin-button,
|
||||||
|
&::-webkit-outer-spin-button {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input::placeholder {
|
input::placeholder {
|
||||||
color: var(--input-placeholder-color);
|
color: var(--input-placeholder-color);
|
||||||
}
|
}
|
||||||
@@ -107,4 +116,5 @@
|
|||||||
input[disabled] {
|
input[disabled] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,3 +16,9 @@
|
|||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div on:click={onClick}>{message}</div>
|
<div on:click={onClick}>{message}</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
div {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||||
import ComfyWidgets from "$lib/widgets"
|
import ComfyWidgets from "$lib/widgets"
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import { BuiltInSlotShape, BuiltInSlotType, LiteGraph, type SerializedLGraphNode } from "@litegraph-ts/core";
|
import { BuiltInSlotShape, BuiltInSlotType, LiteGraph, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||||
@@ -8,10 +8,19 @@ import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
|||||||
import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef";
|
import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef";
|
||||||
import type { SerializedPromptOutput } from "$lib/utils";
|
import type { SerializedPromptOutput } from "$lib/utils";
|
||||||
|
|
||||||
|
export interface ComfyBackendNodeProperties extends ComfyGraphNodeProperties {
|
||||||
|
noOutputDisplay: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Base class for any node with configuration sent by the backend.
|
* Base class for any node with configuration sent by the backend.
|
||||||
*/
|
*/
|
||||||
export class ComfyBackendNode extends ComfyGraphNode {
|
export class ComfyBackendNode extends ComfyGraphNode {
|
||||||
|
override properties: ComfyBackendNodeProperties = {
|
||||||
|
tags: [],
|
||||||
|
noOutputDisplay: false
|
||||||
|
}
|
||||||
|
|
||||||
comfyClass: string;
|
comfyClass: string;
|
||||||
comfyNodeDef: ComfyNodeDef;
|
comfyNodeDef: ComfyNodeDef;
|
||||||
displayName: string | null;
|
displayName: string | null;
|
||||||
@@ -37,6 +46,10 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isOutputNode(): boolean {
|
||||||
|
return this.comfyNodeDef.output_node;
|
||||||
|
}
|
||||||
|
|
||||||
// comfy class -> input name -> input config
|
// comfy class -> input name -> input config
|
||||||
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
|
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
|
||||||
|
|
||||||
|
|||||||
200
src/lib/stores/configDefs.ts
Normal file
200
src/lib/stores/configDefs.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import { LinkRenderMode } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Supported config option types.
|
||||||
|
*/
|
||||||
|
type ConfigDefType = "boolean" | "number" | "string" | "string[]" | "enum";
|
||||||
|
|
||||||
|
// A simple parameter description interface
|
||||||
|
export interface ConfigDef<IdType extends string, TypeType extends ConfigDefType, ValueType, OptionsType = any> {
|
||||||
|
// This generic `IdType` is what makes the "array of keys and values to new
|
||||||
|
// interface definition" thing work
|
||||||
|
name: IdType;
|
||||||
|
|
||||||
|
type: TypeType,
|
||||||
|
|
||||||
|
description?: string,
|
||||||
|
|
||||||
|
category: string,
|
||||||
|
|
||||||
|
defaultValue: ValueType,
|
||||||
|
|
||||||
|
options: OptionsType,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfigDefAny = ConfigDef<string, any, any>
|
||||||
|
export type ConfigDefBoolean<IdType extends string> = ConfigDef<IdType, "boolean", boolean>;
|
||||||
|
|
||||||
|
export type NumberOptions = {
|
||||||
|
min?: number,
|
||||||
|
max?: number,
|
||||||
|
step: number
|
||||||
|
}
|
||||||
|
export type ConfigDefNumber<IdType extends string> = ConfigDef<IdType, "number", number, NumberOptions>;
|
||||||
|
|
||||||
|
export type ConfigDefString<IdType extends string> = ConfigDef<IdType, "string", string>;
|
||||||
|
export type ConfigDefStringArray<IdType extends string> = ConfigDef<IdType, "string[]", string[]>;
|
||||||
|
|
||||||
|
export interface EnumValue<T> {
|
||||||
|
label: string,
|
||||||
|
value: T
|
||||||
|
}
|
||||||
|
export interface EnumOptions<T> {
|
||||||
|
values: EnumValue<T>[]
|
||||||
|
}
|
||||||
|
export type ConfigDefEnum<IdType extends string, T> = ConfigDef<IdType, "enum", T, EnumOptions<T>>;
|
||||||
|
|
||||||
|
export function validateConfigOption(def: ConfigDefAny, v: any): boolean {
|
||||||
|
switch (def.type) {
|
||||||
|
case "boolean":
|
||||||
|
return typeof v === "boolean";
|
||||||
|
case "number":
|
||||||
|
return typeof v === "number";
|
||||||
|
case "string":
|
||||||
|
return typeof v === "string";
|
||||||
|
case "string[]":
|
||||||
|
return Array.isArray(v) && v.every(vs => typeof vs === "string");
|
||||||
|
case "enum":
|
||||||
|
return Boolean(def.options.values.find((o: EnumValue<any>) => o.value === v));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration parameters ------------------------------------
|
||||||
|
|
||||||
|
const defComfyUIHostname: ConfigDefString<"comfyUIHostname"> = {
|
||||||
|
name: "comfyUIHostname",
|
||||||
|
type: "string",
|
||||||
|
defaultValue: "localhost",
|
||||||
|
category: "backend",
|
||||||
|
description: "Backend domain for ComfyUI",
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defComfyUIPort: ConfigDefNumber<"comfyUIPort"> = {
|
||||||
|
name: "comfyUIPort",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 8188,
|
||||||
|
category: "backend",
|
||||||
|
description: "Backend port for ComfyUI",
|
||||||
|
options: {
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
step: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defAlwaysStripUserState: ConfigDefBoolean<"alwaysStripUserState"> = {
|
||||||
|
name: "alwaysStripUserState",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: false,
|
||||||
|
category: "behavior",
|
||||||
|
description: "Strip user state even if saving to local storage",
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defPromptForWorkflowName: ConfigDefBoolean<"promptForWorkflowName"> = {
|
||||||
|
name: "promptForWorkflowName",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: false,
|
||||||
|
category: "behavior",
|
||||||
|
description: "When saving, always prompt for a name to save the workflow as",
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defConfirmWhenUnloadingUnsavedChanges: ConfigDefBoolean<"confirmWhenUnloadingUnsavedChanges"> = {
|
||||||
|
name: "confirmWhenUnloadingUnsavedChanges",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true,
|
||||||
|
category: "behavior",
|
||||||
|
description: "When closing the tab, open the confirmation window if there's unsaved changes",
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defCacheBuiltInResources: ConfigDefBoolean<"cacheBuiltInResources"> = {
|
||||||
|
name: "cacheBuiltInResources",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true,
|
||||||
|
category: "behavior",
|
||||||
|
description: "Cache loading of built-in resources to save network use",
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defBuiltInTemplates: ConfigDefStringArray<"builtInTemplates"> = {
|
||||||
|
name: "builtInTemplates",
|
||||||
|
type: "string[]",
|
||||||
|
defaultValue: ["ControlNet", "LoRA x5", "Model Loader", "Positive_Negative", "Seed Randomizer"],
|
||||||
|
category: "templates",
|
||||||
|
description: "Basenames of templates that can be loaded from public/templates. Saves LocalStorage space.",
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const defLinkDisplayType: ConfigDefEnum<"linkDisplayType", LinkRenderMode> = {
|
||||||
|
// name: "linkDisplayType",
|
||||||
|
// type: "enum",
|
||||||
|
// defaultValue: LinkRenderMode.SPLINE_LINK,
|
||||||
|
// category: "graph",
|
||||||
|
// description: "How to display links in the graph",
|
||||||
|
// options: {
|
||||||
|
// values: [
|
||||||
|
// {
|
||||||
|
// value: LinkRenderMode.STRAIGHT_LINK,
|
||||||
|
// label: "Straight"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: LinkRenderMode.LINEAR_LINK,
|
||||||
|
// label: "Linear"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// value: LinkRenderMode.SPLINE_LINK,
|
||||||
|
// label: "Spline"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Configuration exports ------------------------------------
|
||||||
|
|
||||||
|
export const CONFIG_DEFS = [
|
||||||
|
defComfyUIHostname,
|
||||||
|
defComfyUIPort,
|
||||||
|
defAlwaysStripUserState,
|
||||||
|
defPromptForWorkflowName,
|
||||||
|
defConfirmWhenUnloadingUnsavedChanges,
|
||||||
|
defCacheBuiltInResources,
|
||||||
|
defBuiltInTemplates,
|
||||||
|
// defLinkDisplayType
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const CONFIG_DEFS_BY_NAME: Record<string, ConfigDefAny>
|
||||||
|
= CONFIG_DEFS.reduce((dict, def) => {
|
||||||
|
if (def.name in dict)
|
||||||
|
throw new Error(`Duplicate named config definition: ${def.name}`)
|
||||||
|
dict[def.name] = def;
|
||||||
|
return dict
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
export const CONFIG_DEFS_BY_CATEGORY: Record<string, ConfigDefAny[]>
|
||||||
|
= CONFIG_DEFS.reduce((dict, def) => {
|
||||||
|
dict[def.category] ||= []
|
||||||
|
dict[def.category].push(def)
|
||||||
|
return dict
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
export const CONFIG_CATEGORIES: string[]
|
||||||
|
= CONFIG_DEFS.reduce((arr, def) => {
|
||||||
|
if (!arr.includes(def.category))
|
||||||
|
arr.push(def.category)
|
||||||
|
return arr
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
type Config<T extends ReadonlyArray<Readonly<ConfigDef<string, ConfigDefType, any>>>> = {
|
||||||
|
[K in T[number]["name"]]: Extract<T[number], { name: K }>["defaultValue"]
|
||||||
|
} extends infer O
|
||||||
|
? { [P in keyof O]: O[P] }
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type ConfigState = Config<typeof CONFIG_DEFS>
|
||||||
|
|
||||||
|
const pairs: [string, any][] = CONFIG_DEFS.map(item => { return [item.name, structuredClone(item.defaultValue)] })
|
||||||
|
export const defaultConfig: ConfigState = pairs.reduce((dict, v) => { dict[v[0]] = v[1]; return dict; }, {}) as any;
|
||||||
@@ -1,54 +1,122 @@
|
|||||||
import { debounce } from '$lib/utils';
|
import { debounce } from '$lib/utils';
|
||||||
|
import { toHashMap } from '@litegraph-ts/core';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
|
import { defaultConfig, type ConfigState, type ConfigDefAny, CONFIG_DEFS_BY_NAME, validateConfigOption } from './configDefs';
|
||||||
export type ConfigState = {
|
|
||||||
/** Backend domain for ComfyUI */
|
|
||||||
comfyUIHostname: string,
|
|
||||||
|
|
||||||
/** Backend port for ComfyUI */
|
|
||||||
comfyUIPort: number,
|
|
||||||
|
|
||||||
/** Strip user state even if saving to local storage */
|
|
||||||
alwaysStripUserState: boolean,
|
|
||||||
|
|
||||||
/** When saving, always prompt for a name to save the workflow as */
|
|
||||||
promptForWorkflowName: boolean,
|
|
||||||
|
|
||||||
/** When closing the tab, open the confirmation window if there's unsaved changes */
|
|
||||||
confirmWhenUnloadingUnsavedChanges: boolean,
|
|
||||||
|
|
||||||
/** Basenames of templates that can be loaded from public/templates. Saves LocalStorage space. */
|
|
||||||
builtInTemplates: string[],
|
|
||||||
|
|
||||||
/** Cache loading of built-in resources to save network use */
|
|
||||||
cacheBuiltInResources: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigStateOps = {
|
type ConfigStateOps = {
|
||||||
getBackendURL: () => string
|
getBackendURL: () => string,
|
||||||
|
|
||||||
|
load: (data: any, runOnChanged?: boolean) => ConfigState
|
||||||
|
loadDefault: (runOnChanged?: boolean) => ConfigState
|
||||||
|
setConfigOption: (def: ConfigDefAny, v: any, runOnChanged: boolean) => boolean
|
||||||
|
validateConfigOption: (def: ConfigDefAny, v: any) => boolean
|
||||||
|
onChange: <K extends keyof ConfigState>(optionName: K, callback: ConfigOnChangeCallback<ConfigState[K]>) => void
|
||||||
|
runOnChangedEvents: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
|
export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
|
||||||
const store: Writable<ConfigState> = writable(
|
const store: Writable<ConfigState> = writable({ ...defaultConfig })
|
||||||
{
|
const callbacks: Record<string, ConfigOnChangeCallback<any>[]> = {}
|
||||||
comfyUIHostname: "localhost",
|
let changedOptions: Partial<Record<keyof ConfigState, [any, any]>> = {}
|
||||||
comfyUIPort: 8188,
|
|
||||||
alwaysStripUserState: false,
|
|
||||||
promptForWorkflowName: false,
|
|
||||||
confirmWhenUnloadingUnsavedChanges: true,
|
|
||||||
builtInTemplates: [],
|
|
||||||
cacheBuiltInResources: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
function getBackendURL(): string {
|
function getBackendURL(): string {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}`
|
return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setConfigOption(def: ConfigDefAny, v: any, runOnChanged: boolean): boolean {
|
||||||
|
let valid = false;
|
||||||
|
store.update(state => {
|
||||||
|
const oldValue = state[def.name]
|
||||||
|
|
||||||
|
valid = validateConfigOption(def, v);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
console.warn(`[configState] Invalid value for option ${def.name} (${v}), setting to default (${def.defaultValue})`);
|
||||||
|
state[def.name] = structuredClone(def.defaultValue);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state[def.name] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed = oldValue != state[def.name];
|
||||||
|
if (changed) {
|
||||||
|
if (runOnChanged) {
|
||||||
|
if (callbacks[def.name]) {
|
||||||
|
for (const callback of callbacks[def.name]) {
|
||||||
|
callback(state[def.name], oldValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (changedOptions[def.name] == null) {
|
||||||
|
changedOptions[def.name] = [oldValue, state[def.name]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
changedOptions[def.name][1] = state[def.name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
})
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(data: any, runOnChanged: boolean = false): ConfigState {
|
||||||
|
changedOptions = {}
|
||||||
|
|
||||||
|
store.set({ ...defaultConfig })
|
||||||
|
if (data != null && typeof data === "object") {
|
||||||
|
for (const [k, v] of Object.entries(data)) {
|
||||||
|
const def = CONFIG_DEFS_BY_NAME[k]
|
||||||
|
if (def == null) {
|
||||||
|
delete data[k]
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfigOption(def, v, runOnChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDefault(runOnChanged: boolean = false) {
|
||||||
|
return load(null, runOnChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfigOnChangeCallback<V> = (value: V, oldValue?: V) => void;
|
||||||
|
|
||||||
|
function onChange<K extends keyof ConfigState>(optionName: K, callback: ConfigOnChangeCallback<ConfigState[K]>) {
|
||||||
|
callbacks[optionName] ||= []
|
||||||
|
callbacks[optionName].push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function runOnChangedEvents() {
|
||||||
|
console.debug("Running changed events for config...")
|
||||||
|
for (const [optionName, [oldValue, newValue]] of Object.entries(changedOptions)) {
|
||||||
|
const def = CONFIG_DEFS_BY_NAME[optionName]
|
||||||
|
if (callbacks[optionName]) {
|
||||||
|
console.debug("Running callback!", optionName, oldValue, newValue)
|
||||||
|
for (const callback of callbacks[def.name]) {
|
||||||
|
callback(newValue, oldValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changedOptions = {}
|
||||||
|
}
|
||||||
|
|
||||||
const configStateStore: WritableConfigStateStore =
|
const configStateStore: WritableConfigStateStore =
|
||||||
{
|
{
|
||||||
...store,
|
...store,
|
||||||
getBackendURL
|
getBackendURL,
|
||||||
|
validateConfigOption,
|
||||||
|
setConfigOption,
|
||||||
|
load,
|
||||||
|
loadDefault,
|
||||||
|
onChange,
|
||||||
|
runOnChangedEvents
|
||||||
}
|
}
|
||||||
export default configStateStore;
|
export default configStateStore;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type InterfaceState = {
|
|||||||
indicatorValue: any,
|
indicatorValue: any,
|
||||||
|
|
||||||
graphTransitioning: boolean
|
graphTransitioning: boolean
|
||||||
|
isJumpingToNode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceStateOps = {
|
type InterfaceStateOps = {
|
||||||
@@ -25,7 +26,8 @@ const store: Writable<InterfaceState> = writable(
|
|||||||
showIndicator: false,
|
showIndicator: false,
|
||||||
indicatorValue: null,
|
indicatorValue: null,
|
||||||
|
|
||||||
graphTransitioning: false
|
graphTransitioning: false,
|
||||||
|
isJumpingToNode: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000)
|
const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000)
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -812,8 +812,8 @@ type LayoutStateOps = {
|
|||||||
moveItem: (target: IDragItem, to: ContainerLayout, index?: number) => void,
|
moveItem: (target: IDragItem, to: ContainerLayout, index?: number) => void,
|
||||||
groupItems: (dragItemIDs: DragItemID[], attrs?: Partial<Attributes>) => ContainerLayout,
|
groupItems: (dragItemIDs: DragItemID[], attrs?: Partial<Attributes>) => ContainerLayout,
|
||||||
ungroup: (container: ContainerLayout) => void,
|
ungroup: (container: ContainerLayout) => void,
|
||||||
findLayoutEntryForNode: (nodeId: ComfyNodeID) => DragItemEntry | null,
|
findLayoutEntryForNode: (nodeId: NodeID) => DragItemEntry | null,
|
||||||
findLayoutForNode: (nodeId: ComfyNodeID) => IDragItem | null,
|
findLayoutForNode: (nodeId: NodeID) => IDragItem | null,
|
||||||
iterateBreadthFirst: (id?: DragItemID | null) => Iterable<DragItemEntry>,
|
iterateBreadthFirst: (id?: DragItemID | null) => Iterable<DragItemEntry>,
|
||||||
serialize: () => SerializedLayoutState,
|
serialize: () => SerializedLayoutState,
|
||||||
serializeAtRoot: (rootID: DragItemID) => SerializedLayoutState,
|
serializeAtRoot: (rootID: DragItemID) => SerializedLayoutState,
|
||||||
@@ -1216,7 +1216,7 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
store.set(state)
|
store.set(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLayoutEntryForNode(nodeId: ComfyNodeID): DragItemEntry | null {
|
function findLayoutEntryForNode(nodeId: NodeID): 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"
|
||||||
@@ -1226,7 +1226,7 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLayoutForNode(nodeId: ComfyNodeID): WidgetLayout | null {
|
function findLayoutForNode(nodeId: NodeID): WidgetLayout | null {
|
||||||
const found = findLayoutEntryForNode(nodeId);
|
const found = findLayoutEntryForNode(nodeId);
|
||||||
if (!found)
|
if (!found)
|
||||||
return null;
|
return null;
|
||||||
@@ -1511,16 +1511,12 @@ function getLayoutByDragItemID(dragItemID: DragItemID): WritableLayoutStateStore
|
|||||||
return Object.values(get(layoutStates).all).find(l => get(l).allItems[dragItemID] != null)
|
return Object.values(get(layoutStates).all).find(l => get(l).allItems[dragItemID] != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDragItemByNode(node: LGraphNode): WidgetLayout | null {
|
function getDragItemByNode(node: LGraphNode): IDragItem | null {
|
||||||
const layout = getLayoutByNode(node);
|
const layout = getLayoutByNode(node);
|
||||||
if (layout == null)
|
if (layout == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const entry = get(layout).allItemsByNode[node.id]
|
return layout.findLayoutForNode(node.id);
|
||||||
if (entry && entry.dragItem.type === "widget")
|
|
||||||
return entry.dragItem as WidgetLayout;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LayoutStateStores = {
|
export type LayoutStateStores = {
|
||||||
@@ -1543,7 +1539,7 @@ export type LayoutStateStoresOps = {
|
|||||||
getLayoutByGraph: (graph: LGraph) => WritableLayoutStateStore | null,
|
getLayoutByGraph: (graph: LGraph) => WritableLayoutStateStore | null,
|
||||||
getLayoutByNode: (node: LGraphNode) => WritableLayoutStateStore | null,
|
getLayoutByNode: (node: LGraphNode) => WritableLayoutStateStore | null,
|
||||||
getLayoutByDragItemID: (dragItemID: DragItemID) => WritableLayoutStateStore | null,
|
getLayoutByDragItemID: (dragItemID: DragItemID) => WritableLayoutStateStore | null,
|
||||||
getDragItemByNode: (node: LGraphNode) => WidgetLayout | null,
|
getDragItemByNode: (node: LGraphNode) => IDragItem | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WritableLayoutStateStores = Writable<LayoutStateStores> & LayoutStateStoresOps;
|
export type WritableLayoutStateStores = Writable<LayoutStateStores> & LayoutStateStoresOps;
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number | WorkflowIns
|
|||||||
|
|
||||||
const workflow = state.openedWorkflows[index]
|
const workflow = state.openedWorkflows[index]
|
||||||
if (workflow.id === state.activeWorkflowID)
|
if (workflow.id === state.activeWorkflowID)
|
||||||
return;
|
return state.activeWorkflow;
|
||||||
|
|
||||||
if (state.activeWorkflow != null)
|
if (state.activeWorkflow != null)
|
||||||
state.activeWorkflow.stop("app")
|
state.activeWorkflow.stop("app")
|
||||||
|
|||||||
@@ -92,33 +92,43 @@ body {
|
|||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background: var(--button-primary-background-fill);
|
background: var(--button-primary-background-fill);
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
background: var(--button-primary-background-fill-hover);
|
background: var(--button-primary-background-fill-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.secondary {
|
&.secondary {
|
||||||
background: var(--button-secondary-background-fill);
|
background: var(--button-secondary-background-fill);
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
background: var(--button-secondary-background-fill-hover);
|
background: var(--button-secondary-background-fill-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ternary {
|
&.ternary {
|
||||||
background: var(--panel-background-fill);
|
background: var(--panel-background-fill);
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
background: var(--block-background-fill);
|
background: var(--block-background-fill);
|
||||||
}
|
}
|
||||||
|
&.selected {
|
||||||
|
background: var(--panel-background-fill);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
filter: brightness(85%);
|
filter: brightness(85%);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active:not(:disabled) {
|
||||||
filter: brightness(50%)
|
filter: brightness(50%)
|
||||||
}
|
}
|
||||||
&.selected {
|
&.selected {
|
||||||
filter: brightness(80%)
|
color: var(--body-text-color);
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled:not(.selected) {
|
||||||
|
background: var(--neutral-700);
|
||||||
|
color: var(--neutral-400);
|
||||||
|
opacity: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
src/tests/stores/configStateTests.ts
Normal file
33
src/tests/stores/configStateTests.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { get } from "svelte/store";
|
||||||
|
import configState, { type ConfigState } from "$lib/stores/configState"
|
||||||
|
import { expect } from 'vitest';
|
||||||
|
import UnitTest from "../UnitTest";
|
||||||
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
|
|
||||||
|
export default class configStateTests extends UnitTest {
|
||||||
|
test__loadsDefaultsFromInvalid() {
|
||||||
|
const saved = "foo"
|
||||||
|
|
||||||
|
const config = configState.load(saved)
|
||||||
|
expect(config).toBeInstanceOf(Object)
|
||||||
|
expect(config.comfyUIHostname).toEqual("localhost")
|
||||||
|
}
|
||||||
|
|
||||||
|
test__loadsDefaultsFromBlank() {
|
||||||
|
const saved = {}
|
||||||
|
|
||||||
|
const config = configState.load(saved)
|
||||||
|
expect(config).toBeInstanceOf(Object)
|
||||||
|
expect(config.comfyUIHostname).toEqual("localhost")
|
||||||
|
}
|
||||||
|
|
||||||
|
test__loadsDefaultsFromInvalidValues() {
|
||||||
|
const saved = {
|
||||||
|
comfyUIHostname: 1234 as any
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = configState.load(saved)
|
||||||
|
expect(config).toBeInstanceOf(Object)
|
||||||
|
expect(config.comfyUIHostname).toEqual("localhost")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ export { default as ComfyGraphTests } from "./ComfyGraphTests"
|
|||||||
export { default as parseA1111Tests } from "./parseA1111Tests"
|
export { default as parseA1111Tests } from "./parseA1111Tests"
|
||||||
export { default as convertA1111ToStdPromptTests } from "./convertA1111ToStdPromptTests"
|
export { default as convertA1111ToStdPromptTests } from "./convertA1111ToStdPromptTests"
|
||||||
export { default as convertVanillaWorkflowTest } from "./convertVanillaWorkflowTests"
|
export { default as convertVanillaWorkflowTest } from "./convertVanillaWorkflowTests"
|
||||||
|
export { default as configStateTests } from "./stores/configStateTests"
|
||||||
|
|||||||
Reference in New Issue
Block a user