Merge pull request #72 from space-nuko/builtin-templates

Builtin templates
This commit is contained in:
space-nuko
2023-05-25 21:19:25 -05:00
committed by GitHub
36 changed files with 3875 additions and 3460 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ node_modules
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/dist
/stats.html

View File

@@ -5059,14 +5059,15 @@
"\n",
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
"%cd $WORKSPACE\n",
"![ ! -d \"ComfyBox\" ] && echo -= Initial setup ComfyBox =- && git clone --recursive https://github.com/space-nuko/ComfyBox -b subgraph-templates3 && cd ComfyBox && pnpm install && pnpm prebuild && pnpm build && cd ..\n",
"![ ! -d \"ComfyBox\" ] && echo -= Initial setup ComfyBox =- && git clone --recursive https://github.com/space-nuko/ComfyBox && cd ComfyBox && pnpm install && pnpm prebuild && pnpm build && cd ..\n",
"\n",
"if OPTIONS['UPDATE_COMFY_UI']:\n",
" !echo -= Updating ComfyUI =-\n",
" !git pull\n",
" !echo -= Updating ComfyBox =-\n",
" %cd ComfyBox\n",
" !git pull\n",
" !git fetch -a\n",
" !git reset --hard origin/master\n",
" !pnpm install\n",
" !pnpm prebuild\n",
" !pnpm build\n",
@@ -5585,4 +5586,4 @@
},
"nbformat": 4,
"nbformat_minor": 0
}
}

View File

@@ -28,6 +28,7 @@
"jsdom": "^22.0.0",
"prettier": "^2.8.7",
"prettier-plugin-svelte": "^2.10.0",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.61.0",
"svelte": "^3.58.0",
"svelte-check": "^3.2.0",
@@ -73,7 +74,6 @@
"@sveltejs/vite-plugin-svelte": "^2.1.1",
"@tsconfig/svelte": "^4.0.1",
"@types/dompurify": "^3.0.2",
"@zerodevx/svelte-json-view": "^1.0.5",
"canvas-to-svg": "^1.0.3",
"cm6-theme-basic-dark": "^0.2.0",
"cm6-theme-basic-light": "^0.2.0",

57
pnpm-lock.yaml generated
View File

@@ -97,9 +97,6 @@ importers:
'@types/dompurify':
specifier: ^3.0.2
version: 3.0.2
'@zerodevx/svelte-json-view':
specifier: ^1.0.5
version: 1.0.5(svelte@3.58.0)
canvas-to-svg:
specifier: ^1.0.3
version: 1.0.3
@@ -212,6 +209,9 @@ importers:
prettier-plugin-svelte:
specifier: ^2.10.0
version: 2.10.0(prettier@2.8.7)(svelte@3.58.0)
rollup-plugin-visualizer:
specifier: ^5.9.0
version: 5.9.0
sass:
specifier: ^1.61.0
version: 1.61.0
@@ -3455,14 +3455,6 @@ packages:
pretty-format: 27.5.1
dev: false
/@zerodevx/svelte-json-view@1.0.5(svelte@3.58.0):
resolution: {integrity: sha512-oQDI9v0dJEte6PYVDVjLOjU58AOoWLYRXjghKggFpZXrglWJJqoMeDe14Jrd0cs6NPcPogT/aR/LtkuW2Z1GkQ==}
peerDependencies:
svelte: ^3.55.1
dependencies:
svelte: 3.58.0
dev: false
/@zerodevx/svelte-toast@0.9.3(svelte@3.58.0):
resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==}
peerDependencies:
@@ -4487,6 +4479,11 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
/define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
dev: true
/delaunator@5.0.0:
resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==}
dependencies:
@@ -5806,6 +5803,12 @@ packages:
dependencies:
has: 1.0.3
/is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
dev: true
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -5852,6 +5855,13 @@ packages:
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
dev: false
/is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
dependencies:
is-docker: 2.2.1
dev: true
/isarray@0.0.1:
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
dev: false
@@ -7065,6 +7075,15 @@ packages:
mimic-fn: 2.1.0
dev: true
/open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
dependencies:
define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
dev: true
/optionator@0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
@@ -7656,6 +7675,22 @@ packages:
resolution: {integrity: sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==}
dev: false
/rollup-plugin-visualizer@5.9.0:
resolution: {integrity: sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==}
engines: {node: '>=14'}
hasBin: true
peerDependencies:
rollup: 2.x || 3.x
peerDependenciesMeta:
rollup:
optional: true
dependencies:
open: 8.4.2
picomatch: 2.3.1
source-map: 0.7.4
yargs: 17.7.1
dev: true
/rollup@2.79.1:
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
engines: {node: '>=10.0.0'}

View File

@@ -1,7 +1,9 @@
{
"comfyUIHostname": "localhost",
"comfyUIPort": 8188,
"alwaysStripUserState": false,
"promptForWorkflowName": false,
"confirmWhenUnloadingUnsavedChanges": true
"comfyUIHostname": "localhost",
"comfyUIPort": 8188,
"alwaysStripUserState": false,
"promptForWorkflowName": false,
"confirmWhenUnloadingUnsavedChanges": true,
"builtInTemplates": ["ControlNet", "LoRA x5", "Model Loader", "Positive_Negative", "Seed Randomizer"],
"cacheBuiltInResources": true
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 319 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -707,7 +707,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [
@@ -733,7 +733,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [
@@ -760,7 +760,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [
@@ -788,7 +788,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [],
@@ -813,7 +813,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [],
@@ -837,7 +837,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [
@@ -864,7 +864,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [],
@@ -888,7 +888,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [
@@ -915,7 +915,7 @@
"buttonVariant": "primary",
"buttonSize": "large",
"tags": [],
"destroyChildOnCLose": false
"destroyChildOnClose": false
}
},
"children": [],
@@ -930,4 +930,4 @@
],
"scale": 1
}
}
}

View File

@@ -42,6 +42,7 @@ export type SerializedComfyBoxTemplate = {
version: 1,
id: UUID,
commitHash: string,
isBuiltIn?: boolean,
/*
* Serialized metadata
@@ -356,35 +357,40 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
return serTemplate;
}
export function deserializeTemplateFromSVG(file: File): Promise<SerializedComfyBoxTemplate> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async () => {
const svg = reader.result as string;
let template = null;
// Extract embedded workflow from desc tags
const descEnd = svg.lastIndexOf("</desc>");
if (descEnd !== -1) {
const descStart = svg.lastIndexOf("<desc>", descEnd);
if (descStart !== -1) {
const json = svg.substring(descStart + 6, descEnd);
template = JSON.parse(unescapeXml(json));
}
}
/*
* Extract embedded workflow from desc tags
*/
export function extractTemplateJSONFromSVG(svg: string): string | null {
const descEnd = svg.lastIndexOf("</desc>");
if (descEnd !== -1) {
const descStart = svg.lastIndexOf("<desc>", descEnd);
if (descStart !== -1) {
const json = svg.substring(descStart + 6, descEnd);
return unescapeXml(json);
}
}
if (!isSerializedComfyBoxTemplate(template)) {
reject("Invalid template format!")
}
else {
template.svg = svg;
resolve(template)
}
};
reader.readAsText(file);
});
return null;
}
/*
* Credit goes to pythongosssss for this format
*/
export function deserializeTemplateFromSVG(svg: string): SerializedComfyBoxTemplate | null {
let template = null;
let templateJSON = extractTemplateJSONFromSVG(svg);
if (templateJSON)
template = JSON.parse(templateJSON);
if (!isSerializedComfyBoxTemplate(template)) {
return null;
}
else {
template.svg = svg;
return template;
}
}
export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
if (nodes.length === 0) {

View File

@@ -489,7 +489,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
const serialized = serializeTemplate(this, template);
try {
if (templateState.add(serialized)) {
if (templateState.addTemplate(serialized)) {
notify("Template saved!", { type: "success" })
}
else {

View File

@@ -85,10 +85,8 @@ export default class DanbooruTags {
}
async load(force: boolean = false) {
console.log("Parsing danbooru tags CSV...")
if (this.tags.length > 0 && !force) {
console.info("Danbooru tags already parsed")
console.warn("Danbooru tags already parsed")
return;
}

View File

@@ -29,14 +29,14 @@ import queueState from "$lib/stores/queueState";
import selectionState from "$lib/stores/selectionState";
import uiState from "$lib/stores/uiState";
import workflowState, { ComfyBoxWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
import type { SerializedPromptOutput } from "$lib/utils";
import { readFileToText, type SerializedPromptOutput } from "$lib/utils";
import { basename, capitalize, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range } from "$lib/utils";
import { tick } from "svelte";
import { type SvelteComponentDev } from "svelte/internal";
import { get, writable, type Writable } from "svelte/store";
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
import ComfyPromptSerializer, { isActiveBackendNode, nodeHasTag, UpstreamNodeLocator } from "./ComfyPromptSerializer";
import DanbooruTags from "$lib/DanbooruTags";
import { deserializeTemplateFromSVG } from "$lib/ComfyBoxTemplate";
import { deserializeTemplateFromSVG, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
import templateState from "$lib/stores/templateState";
export const COMFYBOX_SERIAL_VERSION = 1;
@@ -240,7 +240,9 @@ export default class ComfyApp {
this.addKeyboardHandler();
await this.updateHistoryAndQueue();
templateState.load();
const builtInTemplates = await this.loadBuiltInTemplates();
templateState.load(builtInTemplates);
await this.initFrontendFeatures();
@@ -262,15 +264,60 @@ export default class ComfyApp {
*/
async loadConfig() {
try {
const config = await fetch(`/config.json`);
const state = await config.json() as ConfigState;
configState.set(state);
const config = await fetch(`/config.json`, { cache: "no-store" });
const newConfig = await config.json() as ConfigState;
configState.set({ ...get(configState), ...newConfig });
}
catch (error) {
console.error(`Failed to load config`, error)
}
}
async loadBuiltInTemplates(): Promise<SerializedComfyBoxTemplate[]> {
const builtInTemplates = get(configState).builtInTemplates
const options: RequestInit = get(configState).cacheBuiltInResources ? {} : { cache: "no-store" }
const promises = builtInTemplates.map(basename => {
return fetch(`/templates/${basename}.svg`, options)
.then(res => res.text())
.catch(error => error)
})
const [templates, error] = await Promise.all(promises).then((results) => {
const templates: SerializedComfyBoxTemplate[] = []
const errors: string[] = []
for (const r of results) {
if (r instanceof Error) {
errors.push(r.toString())
}
else {
// bare filename of image
const svg = r as string;
const templateAndSvg = deserializeTemplateFromSVG(svg)
if (templateAndSvg == null) {
errors.push("Invalid SVG template format")
}
else {
templates.push(templateAndSvg)
}
}
}
let error = null;
if (errors && errors.length > 0)
error = "Error(s) loading builtin templates:\n" + errors.join("\n");
console.log(`Loaded {templates.length} builtin templates.`);
return [templates, error]
})
if (error)
notify(error, { type: "error" })
return templates;
}
resizeCanvas() {
if (!this.canvasEl)
return;
@@ -606,10 +653,12 @@ export default class ComfyApp {
// Queue prompt using ctrl or command + enter
if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.code === "Enter" || e.keyCode === 10)) {
e.preventDefault();
e.stopImmediatePropagation();
this.runDefaultQueueAction();
}
else if ((e.ctrlKey) && (e.key === "s" || e.code === "KeyS")) {
e.preventDefault();
e.stopImmediatePropagation();
this.saveStateToLocalStorage();
}
});
@@ -780,7 +829,8 @@ export default class ComfyApp {
async initDefaultWorkflow(name: string = "defaultWorkflow", options?: OpenWorkflowOptions) {
let state = null;
try {
const graphResponse = await fetch(`/workflows/${name}.json`);
const options: RequestInit = get(configState).cacheBuiltInResources ? {} : { cache: "no-store" }
const graphResponse = await fetch(`/workflows/${name}.json`, options);
state = await graphResponse.json() as SerializedAppState;
}
catch (error) {
@@ -918,10 +968,7 @@ export default class ComfyApp {
const thumbnails = []
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
if (node.mode !== NodeMode.ALWAYS
|| (tag != null
&& Array.isArray(node.properties.tags)
&& node.properties.tags.indexOf(tag) === -1))
if (node.mode !== NodeMode.ALWAYS || (tag != null && !nodeHasTag(node, tag)))
continue;
if ("getPromptThumbnails" in node) {
@@ -1058,11 +1105,16 @@ export default class ComfyApp {
};
reader.readAsText(file);
} else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
const templateAndSvg = await deserializeTemplateFromSVG(file);
const svg = await readFileToText(file);
const templateAndSvg = deserializeTemplateFromSVG(svg);
if (templateAndSvg == null) {
notify("Invalid SVG template format!", { type: "error" })
return;
}
const importTemplate = () => {
try {
if (templateState.add(templateAndSvg)) {
if (templateState.addTemplate(templateAndSvg)) {
notify("Template imported successfully!", { type: "success" })
}
else {

View File

@@ -4,21 +4,42 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph, type SlotIndex } from "@litegraph-ts/core";
import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptInputs } from "./ComfyApp";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import { Reroute } from "@litegraph-ts/nodes-basic";
import { ComfyReroute } from "$lib/nodes";
function hasTag(node: LGraphNode, tag: string): boolean {
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
function isReroute(node: LGraphNode): boolean {
return node.is(Reroute) || node.is(ComfyReroute)
}
function isGraphInputOutput(node: LGraphNode): boolean {
return node.is(GraphInput) || node.is(GraphOutput)
}
export function nodeHasTag(node: LGraphNode, tag: string): boolean {
// Ignore tags on reroutes since they're just movable wires and it defeats
// the convenience gains to have to set tags for all them
if (isReroute(node))
return true;
while (node != null) {
if ("tags" in node.properties) {
if (node.properties.tags.indexOf(tag) !== -1)
return true;
}
// Count parent subgraphs having the tag also.
node = node.graph?._subgraph_node;
}
return false;
}
export function isActiveNode(node: LGraphNode, tag: string | null = null): boolean {
if (!node)
return false;
// Check tags but not on graph inputs/outputs
if (!isGraphInputOutput(node) && (tag && !hasTag(node, tag))) {
if (!isGraphInputOutput(node) && (tag && !nodeHasTag(node, tag))) {
console.debug("Skipping tagged node", tag, node.properties.tags, node)
return false;
}

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import { embedTemplateInSvg, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
import templateState from "$lib/stores/templateState";
import templateState, { type TemplateState, type WritableTemplateStateStore } from "$lib/stores/templateState";
import { BoxSeam, Hdd } from "svelte-bootstrap-icons";
import uiState from "$lib/stores/uiState";
import { download, truncateString } from "$lib/utils";
import { download, getLocalStorageUsedMB, MAX_LOCAL_STORAGE_MB, truncateString } from "$lib/utils";
import type ComfyApp from "./ComfyApp";
import { flip } from 'svelte/animate';
import {fade} from 'svelte/transition';
@@ -26,12 +27,26 @@
source?: string
}
let storageUsedPercent = 0;
let storageUsedMB = 0;
$: {
if ($templateState) {
storageUsedMB = getLocalStorageUsedMB()
storageUsedPercent = (storageUsedMB / MAX_LOCAL_STORAGE_MB) * 100;
}
else {
storageUsedPercent = 0;
storageUsedMB = 0;
}
}
let _sorted: TemplateLayout[] = []
$: rebuildTemplates($templateState.templates);
$: $templateState && rebuildTemplates(templateState.getAllTemplates());
function rebuildTemplates(templates: SerializedComfyBoxTemplate[]) {
_sorted = Array.from(templates).map(t => {
function rebuildTemplates(allTemplates: SerializedComfyBoxTemplate[]): void {
_sorted = Array.from(allTemplates).map(t => {
return {
type: "template", id: uuidv4(), template: t, attrs: {...defaultWidgetAttributes}, attrsChanged: writable(0)
}
@@ -88,7 +103,7 @@
const saveTemplate = (modal: ModalData) => {
updateTemplate(modal);
try {
templateState.update(layout.template);
templateState.updateTemplate(layout.template);
notify("Saved template!", { type: "success" })
}
catch (error) {
@@ -101,7 +116,7 @@
return false;
try {
if (templateState.remove(layout.template.id)) {
if (templateState.removeTemplate(layout.template.id)) {
notify("Template deleted!", { type: "success" })
}
else {
@@ -131,7 +146,8 @@
{
name: "Save",
variant: "primary",
onClick: saveTemplate
onClick: saveTemplate,
hidden: layout.template.isBuiltIn
},
{
name: "Download",
@@ -142,13 +158,13 @@
{
name: "Delete",
variant: "secondary",
onClick: deleteTemplate
onClick: deleteTemplate,
hidden: layout.template.isBuiltIn
},
{
name: "Close",
variant: "secondary",
onClick: () => {
}
onClick: () => {}
},
]
})
@@ -159,35 +175,52 @@
<div class="template-entries">
{#if _sorted.length > 0}
{@const draggable = $uiState.uiUnlocked}
<div class="template-category-group">
<div class="template-category-header">
General
<div class="template-list">
<div class="template-category-group">
<div class="template-category-header">
General
</div>
<div class="template-entries-wrapper"
use:dndzone={{
type: "layout",
items: _sorted,
flipDurationMs,
dragDisabled: !draggable,
dropFromOthersDisabled: true
}}
on:consider={handleDndConsider}
on:finalize={handleDndFinalize}>
{#each _sorted.filter(i => i.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="template-entry" class:built-in={item.template.isBuiltIn} class:draggable on:click={() => handleClick(item)}>
<div class="template-name">{item.template.metadata.title}</div>
<div class="template-desc">{item.template.metadata.description}</div>
</div>
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='template-drag-item-shadow'/>
{/if}
{/each}
</div>
</div>
<div class="template-entries-wrapper"
use:dndzone={{
type: "layout",
items: _sorted,
flipDurationMs,
dragDisabled: !draggable,
dropFromOthersDisabled: true
}}
on:consider={handleDndConsider}
on:finalize={handleDndFinalize}>
{#each _sorted.filter(i => i.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="template-entry" class:draggable on:click={() => handleClick(item)}>
<div class="template-name">{item.template.metadata.title}</div>
<div class="template-desc">{item.template.metadata.description}</div>
</div>
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='template-drag-item-shadow'/>
{/if}
{/each}
</div>
<div class="storage-used">
<div class="storage-used-icon">
<Hdd width="100%" height="2rem" />
</div>
<div class="storage-used-bar-wrapper">
<div class="storage-used-bar" style="width: {storageUsedPercent}%;">
<span class="storage-used-label">{storageUsedMB.toFixed(2)} / {MAX_LOCAL_STORAGE_MB} MB</span>
</div>
</div>
</div>
{:else}
<div class="no-templates">
<span>(No templates)</span>
<div class="no-templates-container">
<div class="no-templates-icon">
<BoxSeam width="100%" height="8rem" />
</div>
<div class="no-templates-message">
(No templates)
</div>
</div>
{/if}
</div>
@@ -200,9 +233,13 @@
.template-entries {
height: 100%;
overflow-y: auto;
display: flex;
flex-flow: column nowrap;
margin: auto;
}
.template-list {
overflow-y: auto;
}
.template-category-header {
@@ -214,6 +251,7 @@
}
.template-entry {
min-width: 12rem;
padding: 1.0rem;
display: flex;
flex-direction: column;
@@ -227,6 +265,16 @@
text-overflow: ellipsis;
overflow: hidden;
&.built-in {
background: repeating-linear-gradient(
45deg,
var(--neutral-800),
var(--neutral-800) 15px,
#1f293780 15px,
#1f293780 30px,
);
}
font-size: 13pt;
.template-desc {
opacity: 65%;
@@ -253,15 +301,19 @@
}
}
.no-templates {
.no-templates-container {
display: flex;
color: var(--comfy-accent-soft);
flex-direction: row;
flex-direction: column;
margin: auto;
height: 100%;
color: var(--comfy-accent-soft);
span {
.no-templates-icon {
margin: auto;
margin-bottom: 1rem;
}
.no-templates-message {
margin: auto;
font-size: 32px;
font-weight: bolder;
@@ -277,4 +329,45 @@
opacity: 0.5;
margin: 0;
}
.storage-used {
height: 2.5rem;
margin: 5px;
text-align: center;
position: relative;
padding: 0.1rem 0.5rem;
display: flex;
flex-direction: row;
gap: var(--spacing-lg);
.storage-used-icon {
color: var(--neutral-500);
margin: auto;
}
.storage-used-bar-wrapper {
width: 100%;
background: var(--panel-background-fill);
border: 1px solid var(--neutral-700);
position: relative;
.storage-used-bar {
height: 100%;
background: var(--neutral-700);
.storage-used-label {
width: 100%;
color: var(--neutral-200);
font-size: 12pt;
position: absolute;
margin: 0;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
}
</style>

View File

@@ -13,6 +13,9 @@
}
function onButtonClicked(modal: ModalData, button: ModalButton, closeDialog: Function) {
if (button.disabled || button.hidden)
return;
if (button.onClick(modal) === false)
return
@@ -39,9 +42,11 @@
<div slot="buttons" class="buttons" let:closeDialog>
{#if modal != null && modal.buttons?.length > 0}
{#each modal.buttons as button}
<Button variant={button.variant} on:click={() => onButtonClicked(modal, button, closeDialog)}>
{button.name}
</Button>
{#if !button.hidden}
<Button variant={button.variant} disabled={button.disabled} on:click={() => onButtonClicked(modal, button, closeDialog)}>
{button.name}
</Button>
{/if}
{/each}
{/if}
{#if modal.showCloseButton}

View File

@@ -89,7 +89,7 @@
for (const r of results) {
if (r instanceof Error) {
errors.push(r.cause)
errors.push(r.toString())
}
else {
// bare filename of image

View File

@@ -0,0 +1,141 @@
<script lang="ts">
/*
* Modified from @zerodevx/svelte-json-view
*/
/** @type {*} - object or array to display */
export let json: any
/** @type {number} - initial expansion depth */
export let depth: number = Infinity
export let collapseByDefault: boolean | ((v: any) => boolean) | null = false;
export let _cur: number = 0
export let _last: boolean = true
/** @type {*[]} */
let items: any[]
let isArray: boolean = false
let brackets: [string, string] = ['', '']
export let collapsed: boolean | null = null
/**
* @param {*} i
* @returns {string}
*/
function getType(i: any): string {
if (i === null) return 'null'
return typeof i
}
/**
* @param {*} i
* @returns {string}
*/
function format(i: any): string {
const t = getType(i)
if (t === 'string') return `"${i}"`
if (t === 'function') return 'f () {...}'
if (t === 'symbol') return i.toString()
return i
}
function clicked() {
collapsed = !collapsed
}
/**
* @param {Event} e
*/
function pressed(e) {
if (e instanceof KeyboardEvent && ['Enter', ' '].includes(e.key)) clicked()
}
$: {
items = getType(json) === 'object' ? Object.keys(json) : []
isArray = Array.isArray(json)
brackets = isArray ? ['[', ']'] : ['{', '}']
}
$: {
if (collapsed === null)
collapsed = depth < _cur;
}
function calcCollapsed(json: any): boolean | null {
if (typeof collapseByDefault === "function")
return collapseByDefault(json)
else if (typeof collapseByDefault === "boolean")
return collapseByDefault
return null;
}
</script>
{#if !items.length}
<span class="_jsonBkt empty">{brackets[0]}{brackets[1]}</span>{#if !_last}<span class="_jsonSep"
>,</span
>{/if}
{:else if collapsed}
<span class="_jsonBkt _jsonCollapsed" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
>{brackets[0]}...{brackets[1]}</span
>{#if !_last && collapsed}<span class="_jsonSep">,</span>{/if}
{:else}
<span class="_jsonBkt" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
>{brackets[0]}</span
>
<ul class="_jsonList">
{#each items as i, idx}
<li>
{#if !isArray}
<span class="_jsonKey">"{i}"</span><span class="_jsonSep">:</span>
{/if}
{#if getType(json[i]) === 'object'}
<svelte:self json={json[i]} {collapseByDefault} collapsed={calcCollapsed(json[i])} {depth} _cur={_cur + 1} _last={idx === items.length - 1} />
{:else}
<span class="_jsonVal {getType(json[i])}">{format(json[i])}</span
>{#if idx < items.length - 1}<span class="_jsonSep">,</span>{/if}
{/if}
</li>
{/each}
</ul>
<span class="_jsonBkt" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
>{brackets[1]}</span
>{#if !_last}<span class="_jsonSep">,</span>{/if}
{/if}
<style>
._jsonList {
list-style: none;
margin: 0;
padding: 0;
padding-left: var(--jsonPaddingLeft, 1rem);
border-left: var(--jsonBorderLeft, 1px dotted);
}
._jsonBkt {
color: var(--jsonBracketColor, currentcolor);
}
._jsonBkt:not(.empty):hover {
cursor: pointer;
background: var(--jsonBracketHoverBackground, #e5e7eb);
}
._jsonSep {
color: var(--jsonSeparatorColor, currentcolor);
}
._jsonKey {
color: var(--jsonKeyColor, currentcolor);
}
._jsonVal {
color: var(--jsonValColor, #9ca3af);
}
._jsonVal.string {
color: var(--jsonValStringColor, #059669);
}
._jsonVal.number {
color: var(--jsonValNumberColor, #d97706);
}
._jsonVal.boolean {
color: var(--jsonValBooleanColor, #2563eb);
}
._jsonCollapsed {
color: var(--jsonCollapsedColor, currentcolor);
background: var(--jsonCollapsedBackground, currentcolor);
}
</style>

View File

@@ -2,7 +2,7 @@
import type { A1111ParsedInfotext } from "$lib/parseA1111";
import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox } from "@gradio/form";
import { JsonView } from '@zerodevx/svelte-json-view'
import JsonView from '$lib/components/JsonView.svelte'
import type { A1111PromptAndInfo } from "$lib/components/ComfyApp";
import { StaticImage } from "$lib/components/gradio/image";
@@ -65,17 +65,6 @@
height: 70vh;
color: none;
--jsonPaddingLeft: 1rem;
--jsonBorderLeft: 1px dotted var(--neutral-600);
--jsonBracketColor: currentcolor;
--jsonBracketHoverBackground: var(--neutral-100);
--jsonSeparatorColor: currentcolor;
--jsonKeyColor: var(--body-text-color);
--jsonValColor: var(--body-text-color-subdued);
--jsonValStringColor: var(--color-green-500);
--jsonValNumberColor: var(--color-blue-500);
--jsonValBooleanColor: var(--color-red-500);
display: flex;
flex-wrap: nowrap;
overflow-y: none;
@@ -87,7 +76,7 @@
overflow: auto;
.json {
font-family: monospace;
@include json-view;
}
.scroll-container {

View File

@@ -2,14 +2,18 @@
import type { ComfyBoxTemplate, SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
import type { SerializedDragEntry, SerializedLayoutState } from "$lib/stores/layoutStates";
import { Block, BlockTitle } from "@gradio/atoms";
import { Tabs, TabItem } from "@gradio/tabs";
import { JSON as JSONIcon } from "@gradio/icons";
import JsonView from '$lib/components/JsonView.svelte'
import SerializedLayoutPreviewNode from "./SerializedLayoutPreviewNode.svelte";
import Row from "../gradio/app/Row.svelte";
import createDOMPurify from "dompurify"
import Column from "../gradio/app/Column.svelte";
import Accordion from "../gradio/app/Accordion.svelte";
import Textbox from "@gradio/form/src/Textbox.svelte";
import type { ModalData } from "$lib/stores/modalState";
import { writable, type Writable } from "svelte/store";
import Column from "../gradio/app/Column.svelte";
import Accordion from "../gradio/app/Accordion.svelte";
import Textbox from "@gradio/form/src/Textbox.svelte";
import type { ModalData } from "$lib/stores/modalState";
import { writable, type Writable } from "svelte/store";
import { negmod } from "$lib/utils";
const DOMPurify = createDOMPurify(window);
export let templateAndSvg: SerializedComfyBoxTemplate;
@@ -18,6 +22,49 @@
let layout: SerializedLayoutState | null
let root: SerializedDragEntry | null
let state: Writable<any> = writable({})
let rawTemplate: SerializedComfyBoxTemplate | null
let showJSON = false;
let showAllJSON: number = 0;
let createdAt = "";
let isEditable = true;
$: isEditable = editable && templateAndSvg && !templateAndSvg.isBuiltIn;
$: {
rawTemplate = { ...templateAndSvg };
rawTemplate.svg = undefined;
}
function collapseByDefault(json: any): boolean {
switch (showAllJSON) {
case 0:
return typeof json["id"] === "string";
case 1:
return typeof json["nodes"] === "object"
case 2:
default:
return false;
}
}
function expandJSON() {
showAllJSON = negmod(showAllJSON + 1, 3)
}
$: {
let options: Intl.DateTimeFormatOptions = {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
};
const date = new Date(templateAndSvg.metadata.createdAt);
createdAt = date.toLocaleString('en-US', options);
}
$: {
state = _modal.state;
@@ -31,9 +78,9 @@
let saneSvg: string = "";
$: saneSvg = templateAndSvg
? DOMPurify.sanitize(templateAndSvg.svg, { USE_PROFILES: { svg: true, svgFilters: true } })
.replace("<svg", "<svg style='background: url(\"image/graph-bg.png\")'")
: "";
? DOMPurify.sanitize(templateAndSvg.svg, { USE_PROFILES: { svg: true, svgFilters: true } })
.replace("<svg", "<svg style='background: url(\"image/graph-bg.png\")'")
: "";
$: if (templateAndSvg) {
layout = templateAndSvg.layout;
@@ -57,9 +104,13 @@
<Block>
<BlockTitle>Metadata</BlockTitle>
<div>
<Textbox label="Name" disabled={!editable} bind:value={$state.name} lines={1} max_lines={1} />
<Textbox label="Author" disabled={!editable} bind:value={$state.author} lines={1} max_lines={1} />
<Textbox label="Description" disabled={!editable} bind:value={$state.description} lines={5} max_lines={5} />
<Textbox label="Name" disabled={!isEditable} bind:value={$state.name} lines={1} max_lines={1} />
<Textbox label="Author" disabled={!isEditable} bind:value={$state.author} lines={1} max_lines={1} />
<Textbox label="Description" disabled={!isEditable} bind:value={$state.description} lines={5} max_lines={5} />
<Row>
<Textbox label="Created At" disabled={true} bind:value={createdAt} lines={1} max_lines={1} />
<Textbox label="Size" disabled={true} value="{(templateAndSvg.svg.length/1024).toFixed(2)} KB" lines={1} max_lines={1} />
</Row>
</div>
</Block>
</div>
@@ -76,15 +127,31 @@
{/if}
</Row>
<div class="template-graph-preview">
<Block>
<Accordion label="Graph">
<Tabs selected="graph">
<TabItem name="Graph" id="graph">
<Block>
<div class="template-graph-wrapper">
{@html saneSvg}
</div>
<Accordion label="Graph">
<Block>
<div class="template-graph-wrapper">
{@html saneSvg}
</div>
</Block>
</Accordion>
</Block>
</Accordion>
</Block>
</TabItem>
<TabItem name="Raw JSON" id="json" on:select={() => (showJSON = true)}>
{#key showAllJSON}
{#if showJSON}
<button class="json-button" on:click={expandJSON}>
<JSONIcon />
</button>
<div class="json">
<JsonView json={rawTemplate} {collapseByDefault} />
</div>
{/if}
{/key}
</TabItem>
</Tabs>
</div>
</div>
@@ -120,10 +187,34 @@
:global(> .block) {
background: var(--panel-background-fill);
}
.json {
@include json-view;
}
}
.template-graph-wrapper {
overflow: auto;
margin: auto;
}
.json-button {
display: flex;
position: absolute;
top: var(--block-label-margin);
right: var(--block-label-margin);
align-items: center;
box-shadow: var(--shadow-drop);
border: 1px solid var(--border-color-primary);
border-radius: var(--block-label-right-radius);
background: var(--block-label-background-fill);
padding: 5px;
width: 30px;
height: 30px;
overflow: hidden;
color: var(--block-label-text-color);
font: var(--font);
font-size: var(--button-small-text-size);
}
</style>

View File

@@ -2,6 +2,7 @@ import type { ComfyWidgetNode } from "$lib/nodes/widgets";
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SlotLayout } from "@litegraph-ts/core";
import { get } from "svelte/store";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode";
import { nodeHasTag } from "$lib/components/ComfyPromptSerializer";
export interface ComfySetNodeModeActionProperties extends ComfyGraphNodeProperties {
targetTags: string,
@@ -52,7 +53,7 @@ export default class ComfySetNodeModeAction extends ComfyGraphNode {
for (const node of this.graph._nodes) {
if ("tags" in node.properties) {
const comfyNode = node as ComfyGraphNode;
const hasTag = tags.some(t => comfyNode.properties.tags.indexOf(t) != -1);
const hasTag = tags.some(t => nodeHasTag(comfyNode, t));
if (hasTag) {
let newMode: NodeMode;
if (enabled) {

View File

@@ -2,6 +2,7 @@ import { type DragItemID } from "$lib/stores/layoutStates";
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
import { get } from "svelte/store";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode";
import { nodeHasTag } from "$lib/components/ComfyPromptSerializer";
export type TagAction = {
tag: string,
@@ -68,7 +69,7 @@ export default class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
for (const node of this.graph.iterateNodesInOrderRecursive()) {
if ("tags" in node.properties) {
const comfyNode = node as ComfyGraphNode;
const hasTag = comfyNode.properties.tags.indexOf(action.tag) != -1;
const hasTag = nodeHasTag(comfyNode, action.tag);
if (hasTag) {
let newMode: NodeMode;

View File

@@ -1,5 +1,5 @@
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils";
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout, type INumberWidget, clamp } from "@litegraph-ts/core";
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout, type INumberWidget, clamp, type SerializedLGraphNode } from "@litegraph-ts/core";
import { get, writable, type Writable } from "svelte/store";
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
@@ -58,6 +58,7 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
this.selectedIndexWidget = this.addWidget("text", "Selected", String(get(this.selectedImage)))
this.selectedIndexWidget.disabled = true;
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
this.defaultValue = []
}
override onPropertyChanged(property: any, value: any) {
@@ -103,6 +104,12 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
private _newSelectedIndex: number | null = null;
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
o.properties.defaultValue = [];
(o as any).comfyValue = [];
}
override parseValue(param: any): ComfyBoxImageMetadata[] {
if (param == null)
return []

View File

@@ -1,6 +1,6 @@
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata } from "$lib/utils";
import type { FileData as GradioFileData } from "@gradio/upload";
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
import { BuiltInSlotType, LiteGraph, type SerializedLGraphNode, type SlotLayout } from "@litegraph-ts/core";
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
@@ -55,6 +55,12 @@ export default class ComfyImageUploadNode extends ComfyWidgetNode<ComfyBoxImageM
override formatValue(value: GradioFileData[]): string {
return `Images: ${value?.length || 0}`
}
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
o.properties.defaultValue = [];
(o as any).comfyValue = [];
}
}
LiteGraph.registerNodeType({

View File

@@ -355,6 +355,6 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
(o as any).comfyValue = this.properties.defaultValue;
(o as any).comfyValue = LiteGraph.cloneObject(this.properties.defaultValue);
}
}

View File

@@ -17,6 +17,12 @@ export type ConfigState = {
/** 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 = {
@@ -30,7 +36,9 @@ const store: Writable<ConfigState> = writable(
comfyUIPort: 8188,
alwaysStripUserState: false,
promptForWorkflowName: false,
confirmWhenUnloadingUnsavedChanges: true
confirmWhenUnloadingUnsavedChanges: true,
builtInTemplates: [],
cacheBuiltInResources: true,
})
function getBackendURL(): string {

View File

@@ -8,6 +8,8 @@ export type ModalButton = {
name: string,
variant: "primary" | "secondary",
onClick: (state: ModalData) => boolean | void,
disabled?: boolean,
hidden?: boolean,
closeOnClick?: boolean
}
export interface ModalData {

View File

@@ -5,33 +5,48 @@ import type { Readable, Writable } from 'svelte/store';
import { v4 as uuidv4 } from "uuid";
export type TemplateState = {
templates: SerializedComfyBoxTemplate[]
builtInTemplates: SerializedComfyBoxTemplate[]
userTemplates: SerializedComfyBoxTemplate[]
templatesByID: Record<UUID, SerializedComfyBoxTemplate>
}
type TemplateStateOps = {
getAllTemplates: () => SerializedComfyBoxTemplate[],
addTemplate: (template: SerializedComfyBoxTemplate) => boolean,
updateTemplate: (template: SerializedComfyBoxTemplate) => boolean,
removeTemplate: (templateID: UUID) => boolean,
save: () => void,
load: () => void,
add: (template: SerializedComfyBoxTemplate) => boolean,
update: (template: SerializedComfyBoxTemplate) => boolean,
remove: (templateID: UUID) => boolean,
load: (builtInTemplates: SerializedComfyBoxTemplate[]) => void,
}
export type WritableTemplateStateStore = Writable<TemplateState> & TemplateStateOps;
const store: Writable<TemplateState> = writable(
{
templates: [],
builtInTemplates: [],
userTemplates: [],
templatesByID: {}
})
function add(template: SerializedComfyBoxTemplate): boolean {
function getTemplateList(template: SerializedComfyBoxTemplate, state: TemplateState): SerializedComfyBoxTemplate[] {
if (template.isBuiltIn)
return state.builtInTemplates
return state.userTemplates;
}
function getAllTemplates(): SerializedComfyBoxTemplate[] {
const state = get(store);
return state.builtInTemplates.concat(state.userTemplates);
}
function addTemplate(template: SerializedComfyBoxTemplate): boolean {
const state = get(store);
if (state.templatesByID[template.id]) {
return false;
}
store.update(s => {
s.templates.push(template);
const templateList = getTemplateList(template, s)
templateList.push(template)
s.templatesByID[template.id] = template;
return s;
})
@@ -41,15 +56,16 @@ function add(template: SerializedComfyBoxTemplate): boolean {
return true;
}
function remove(templateID: UUID): boolean {
function removeTemplate(templateID: UUID): boolean {
const state = get(store);
if (!state.templatesByID[templateID]) {
return false;
}
store.update(s => {
const index = s.templates.findIndex(t => t.id === templateID)
s.templates.splice(index, 1);
const templateList = getTemplateList(s.templatesByID[templateID], s)
const index = templateList.findIndex(t => t.id === templateID)
templateList.splice(index, 1);
delete s.templatesByID[templateID];
return s;
})
@@ -59,7 +75,7 @@ function remove(templateID: UUID): boolean {
return true;
}
function update(template: SerializedComfyBoxTemplate): boolean {
function updateTemplate(template: SerializedComfyBoxTemplate): boolean {
const state = get(store);
if (!state.templatesByID[template.id]) {
return false;
@@ -67,13 +83,14 @@ function update(template: SerializedComfyBoxTemplate): boolean {
store.update(s => {
const oldId = template.id
const index = s.templates.findIndex(t => t.id === oldId)
s.templates.splice(index, 1);
const templateList = getTemplateList(template, s)
const index = templateList.findIndex(t => t.id === oldId)
templateList.splice(index, 1);
delete s.templatesByID[oldId];
template.id = uuidv4();
s.templates.push(template);
templateList.push(template);
s.templatesByID[template.id] = template;
return s;
})
@@ -84,12 +101,26 @@ function update(template: SerializedComfyBoxTemplate): boolean {
}
function save() {
const json = JSON.stringify(get(store).templates)
const json = JSON.stringify(get(store).userTemplates)
localStorage.setItem("templates", json)
store.set(get(store))
}
function load() {
function load(builtInTemplates: SerializedComfyBoxTemplate[]) {
store.update(s => {
s.userTemplates = []
s.templatesByID = {}
for (const t of builtInTemplates) {
t.isBuiltIn = true;
s.templatesByID[t.id] = t;
}
s.builtInTemplates = builtInTemplates;
return s
})
const json = localStorage.getItem("templates")
if (!json) {
console.info("No templates in local storage, creating store")
@@ -100,18 +131,15 @@ function load() {
const data = JSON.parse(json) as SerializedComfyBoxTemplate[];
if (Array.isArray(data)) {
const templatesByID: Record<UUID, SerializedComfyBoxTemplate> =
data.map(d => [d.id, d])
.reduce((dict, el: [UUID, SerializedComfyBoxTemplate]) => (dict[el[0]] = el[1], dict), {})
data.map(t => {
t.isBuiltIn = false;
return [t.id, t]
}).reduce((dict, el: [UUID, SerializedComfyBoxTemplate]) => (dict[el[0]] = el[1], dict), {})
store.set({
templates: data,
templatesByID
})
}
else {
store.set({
templates: [],
templatesByID: {}
store.update(s => {
s.userTemplates = data
s.templatesByID = { ...s.templatesByID, ...templatesByID }
return s;
})
}
}
@@ -119,10 +147,11 @@ function load() {
const templateStateStore: WritableTemplateStateStore =
{
...store,
add,
remove,
update,
getAllTemplates,
addTemplate,
removeTemplate,
updateTemplate,
save,
load,
load
}
export default templateStateStore;

View File

@@ -75,7 +75,9 @@ export function download(filename: string, text: string, type: string = "text/pl
}, 0);
}
export function getLocalStorageUsed(): number {
export const MAX_LOCAL_STORAGE_MB = 5;
export function getLocalStorageUsedMB(): number {
var total = 0;
for (const x in localStorage) {
// Value is multiplied by 2 due to data being stored in `utf-16` format, which requires twice the space.
@@ -598,3 +600,13 @@ export function calcNodesBoundingBox(nodes: SerializedLGraphNode[]): Vector4 {
return [min_x, min_y, max_x, max_y];
}
export async function readFileToText(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async () => {
resolve(reader.result as string);
};
reader.readAsText(file);
})
}

View File

@@ -146,6 +146,23 @@ body {
}
}
@mixin json-view {
--jsonPaddingLeft: 1rem;
--jsonBorderLeft: 1px dotted var(--neutral-600);
--jsonBracketColor: currentcolor;
--jsonBracketHoverBackground: var(--primary-200);
--jsonSeparatorColor: currentcolor;
--jsonKeyColor: var(--body-text-color);
--jsonValColor: var(--body-text-color-subdued);
--jsonValStringColor: var(--color-green-500);
--jsonValNumberColor: var(--color-blue-500);
--jsonValBooleanColor: var(--color-red-500);
--jsonCollapsedColor: var(--neutral-100);
--jsonCollapsedBackground: var(--primary-400);
font-family: monospace;
}
hr {
color: var(--panel-border-color);
}

View File

@@ -1,16 +1,11 @@
import { LGraph, LiteGraph, Subgraph, type SlotLayout } from "@litegraph-ts/core"
import { Watch } from "@litegraph-ts/nodes-basic"
import { expect } from 'vitest'
import UnitTest from "./UnitTest"
import ComfyGraph from "$lib/ComfyGraph";
import ComfyPromptSerializer from "$lib/components/ComfyPromptSerializer";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import { graphToGraphVis } from "$lib/utils";
import { ComfyNumberNode } from "$lib/nodes/widgets";
import { get } from "svelte/store";
import layoutStates from "$lib/stores/layoutStates";
import { ComfyBoxWorkflow } from "$lib/stores/workflowState";
import { LiteGraph, Subgraph } from "@litegraph-ts/core";
import { get } from "svelte/store";
import { expect } from 'vitest';
import UnitTest from "./UnitTest";
import { Watch } from "@litegraph-ts/nodes-basic";
export default class ComfyGraphTests extends UnitTest {
test__onNodeAdded__updatesLayoutState() {
@@ -92,4 +87,24 @@ export default class ComfyGraphTests extends UnitTest {
expect(Object.keys(state.allItems)).toHaveLength(3)
expect(Object.keys(state.allItemsByNode)).toHaveLength(0)
}
test__serialize__stripsLinkData() {
const [{ graph }, layoutState] = ComfyBoxWorkflow.create()
layoutState.initDefaultLayout()
const widget = LiteGraph.createNode(ComfyNumberNode);
const watch = LiteGraph.createNode(Watch);
graph.add(widget)
graph.add(watch)
widget.connect(0, watch, 0)
const link = widget.getOutputLinks(0)[0]
widget.setOutputData(0, 42);
const result = graph.serialize();
const serNode = result.nodes.find(n => n.id === widget.id);
expect(serNode.outputs[0]._data).toBeUndefined()
}
}

View File

@@ -7,6 +7,7 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'
import removeConsole from 'vite-plugin-svelte-console-remover';
import glsl from 'vite-plugin-glsl';
import { execSync } from "child_process"
import { visualizer } from "rollup-plugin-visualizer";
const isProduction = process.env.NODE_ENV === "production";
console.log("Production build: " + isProduction)
@@ -30,6 +31,7 @@ export default defineConfig({
isProduction && removeConsole(),
glsl(),
svelte(),
visualizer(),
viteStaticCopy({
targets: [
{