Templates view and store
This commit is contained in:
8
src/global.d.ts
vendored
Normal file
8
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
declare type Item = import('svelte-dnd-action').Item;
|
||||||
|
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>;
|
||||||
|
declare namespace svelte.JSX {
|
||||||
|
interface HTMLAttributes<T> {
|
||||||
|
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void;
|
||||||
|
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Subgraph, type LGraphNode, type LLink, type SerializedLGraphNode, type SerializedLLink, LGraph, type NodeID } from "@litegraph-ts/core"
|
import { Subgraph, type LGraphNode, type LLink, type SerializedLGraphNode, type SerializedLLink, LGraph, type NodeID, type UUID } from "@litegraph-ts/core"
|
||||||
import layoutStates, { isComfyWidgetNode, type ContainerLayout, type SerializedDragEntry, type WidgetLayout, type DragItemID, type WritableLayoutStateStore, type DragItemEntry, type SerializedLayoutState } from "./stores/layoutStates"
|
import layoutStates, { isComfyWidgetNode, type ContainerLayout, type SerializedDragEntry, type WidgetLayout, type DragItemID, type WritableLayoutStateStore, type DragItemEntry, type SerializedLayoutState } from "./stores/layoutStates"
|
||||||
import type { ComfyWidgetNode } from "./nodes/widgets"
|
import type { ComfyWidgetNode } from "./nodes/widgets"
|
||||||
import type ComfyGraphCanvas from "./ComfyGraphCanvas"
|
import type ComfyGraphCanvas from "./ComfyGraphCanvas"
|
||||||
import C2S from "canvas-to-svg";
|
import C2S from "canvas-to-svg";
|
||||||
import { download } from "./utils";
|
import { download } from "./utils";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* In ComfyBox a template contains a subset of nodes in the graph and the set of
|
* In ComfyBox a template contains a subset of nodes in the graph and the set of
|
||||||
@@ -11,6 +12,7 @@ import { download } from "./utils";
|
|||||||
*/
|
*/
|
||||||
export type ComfyBoxTemplate = {
|
export type ComfyBoxTemplate = {
|
||||||
version: 1,
|
version: 1,
|
||||||
|
id: UUID,
|
||||||
metadata: ComfyBoxTemplateMetadata,
|
metadata: ComfyBoxTemplateMetadata,
|
||||||
nodes: LGraphNode[],
|
nodes: LGraphNode[],
|
||||||
links: LLink[],
|
links: LLink[],
|
||||||
@@ -19,6 +21,17 @@ export type ComfyBoxTemplate = {
|
|||||||
|
|
||||||
export type SerializedTemplateLink = [NodeID, number, NodeID, number];
|
export type SerializedTemplateLink = [NodeID, number, NodeID, number];
|
||||||
|
|
||||||
|
export type ComfyBoxTemplateMetadata = {
|
||||||
|
title: string,
|
||||||
|
author: string,
|
||||||
|
description: string,
|
||||||
|
tags: string[],
|
||||||
|
category: string,
|
||||||
|
createdAt: number
|
||||||
|
|
||||||
|
// TODO required/optional python extensions
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* In ComfyBox a template contains a subset of nodes in the graph and the set of
|
* In ComfyBox a template contains a subset of nodes in the graph and the set of
|
||||||
* components they represent in the UI.
|
* components they represent in the UI.
|
||||||
@@ -26,6 +39,7 @@ export type SerializedTemplateLink = [NodeID, number, NodeID, number];
|
|||||||
export type SerializedComfyBoxTemplate = {
|
export type SerializedComfyBoxTemplate = {
|
||||||
isComfyBoxTemplate: true,
|
isComfyBoxTemplate: true,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
id: UUID,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Serialized metadata
|
* Serialized metadata
|
||||||
@@ -46,29 +60,23 @@ export type SerializedComfyBoxTemplate = {
|
|||||||
* Serialized container type drag item
|
* Serialized container type drag item
|
||||||
*/
|
*/
|
||||||
layout?: SerializedLayoutState
|
layout?: SerializedLayoutState
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SVG of the graph ndoes
|
||||||
|
*/
|
||||||
|
svg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSerializedComfyBoxTemplate(param: any): param is SerializedComfyBoxTemplate {
|
function isSerializedComfyBoxTemplate(param: any): param is SerializedComfyBoxTemplate {
|
||||||
return param && param.isComfyBoxTemplate;
|
return param && param.isComfyBoxTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedComfyBoxTemplateAndSVG = {
|
|
||||||
template: SerializedComfyBoxTemplate,
|
|
||||||
svg: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ComfyBoxTemplateMetadata = {
|
|
||||||
title: string,
|
|
||||||
author: string,
|
|
||||||
tags: string[],
|
|
||||||
|
|
||||||
// TODO required/optional python extensions
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_TEMPLATE_METADATA = {
|
const DEFAULT_TEMPLATE_METADATA = {
|
||||||
title: "New Template",
|
title: "New Template",
|
||||||
author: "Anonymous",
|
author: "Anonymous",
|
||||||
tags: []
|
description: "A brand-new ComfyBox template",
|
||||||
|
tags: [],
|
||||||
|
category: "general"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComfyBoxTemplateError = {
|
export type ComfyBoxTemplateError = {
|
||||||
@@ -145,7 +153,7 @@ function unescapeXml(safe) {
|
|||||||
|
|
||||||
const TEMPLATE_SVG_PADDING: number = 50;
|
const TEMPLATE_SVG_PADDING: number = 50;
|
||||||
|
|
||||||
function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: SerializedComfyBoxTemplate, padding: number): string {
|
function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, extraData?: SerializedComfyBoxTemplate | null): string {
|
||||||
// Calculate the min max bounds for the nodes on the graph
|
// Calculate the min max bounds for the nodes on the graph
|
||||||
const bounds = graph._nodes.reduce(
|
const bounds = graph._nodes.reduce(
|
||||||
(p, n) => {
|
(p, n) => {
|
||||||
@@ -247,11 +255,16 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: Serialize
|
|||||||
|
|
||||||
canvas.draw(true, true);
|
canvas.draw(true, true);
|
||||||
|
|
||||||
// Convert to SVG, embed graph and save
|
let svg = svgCtx.getSerializedSvg(true)
|
||||||
// const json = JSON.stringify(app.graph.serialize());
|
return svg
|
||||||
const json = JSON.stringify(extraData);
|
}
|
||||||
const svg = svgCtx.getSerializedSvg(true).replace("</svg>", `<desc>${escapeXml(json)}</desc></svg>`);
|
|
||||||
|
|
||||||
|
export function embedTemplateInSvg(template: SerializedComfyBoxTemplate): string {
|
||||||
|
let oldSvg = template.svg;
|
||||||
|
template.svg = undefined;
|
||||||
|
const json = JSON.stringify(template);
|
||||||
|
const svg = oldSvg.replace("</svg>", `<desc>${escapeXml(json)}</desc></svg>`);
|
||||||
|
template.svg = oldSvg;
|
||||||
return svg
|
return svg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +298,7 @@ function convLinkForTemplate(link: LLink): SerializedTemplateLink {
|
|||||||
return [link.origin_id, link.origin_slot, link.target_id, link.target_slot];
|
return [link.origin_id, link.origin_slot, link.target_id, link.target_slot];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTemplate): SerializedComfyBoxTemplateAndSVG {
|
export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTemplate): SerializedComfyBoxTemplate {
|
||||||
let graph: LGraph
|
let graph: LGraph
|
||||||
if (template.nodes.length === 1 && template.nodes[0].is(Subgraph)) {
|
if (template.nodes.length === 1 && template.nodes[0].is(Subgraph)) {
|
||||||
graph = template.nodes[0].subgraph
|
graph = template.nodes[0].subgraph
|
||||||
@@ -305,21 +318,23 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
|
|||||||
|
|
||||||
[nodes, links] = pruneDetachedLinks(nodes, links);
|
[nodes, links] = pruneDetachedLinks(nodes, links);
|
||||||
|
|
||||||
let serTemplate: SerializedComfyBoxTemplate = {
|
const svg = renderSvg(canvas, graph, TEMPLATE_SVG_PADDING);
|
||||||
|
|
||||||
|
const serTemplate: SerializedComfyBoxTemplate = {
|
||||||
isComfyBoxTemplate: true,
|
isComfyBoxTemplate: true,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
id: template.id,
|
||||||
metadata,
|
metadata,
|
||||||
nodes,
|
nodes,
|
||||||
links,
|
links,
|
||||||
layout,
|
layout,
|
||||||
|
svg
|
||||||
}
|
}
|
||||||
|
|
||||||
const svg = renderSvg(canvas, graph, serTemplate, TEMPLATE_SVG_PADDING)
|
return serTemplate;
|
||||||
|
|
||||||
return { svg, template: serTemplate }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeTemplate(file: File): Promise<SerializedComfyBoxTemplateAndSVG> {
|
export function deserializeTemplateFromSVG(file: File): Promise<SerializedComfyBoxTemplate> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
@@ -340,10 +355,8 @@ export function deserializeTemplate(file: File): Promise<SerializedComfyBoxTempl
|
|||||||
reject("Invalid template format!")
|
reject("Invalid template format!")
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const result: SerializedComfyBoxTemplateAndSVG = {
|
template.svg = svg;
|
||||||
svg, template
|
resolve(template)
|
||||||
}
|
|
||||||
resolve(result)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@@ -364,10 +377,15 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
|||||||
const layout = layoutStates.getLayoutByNode(nodes[0])
|
const layout = layoutStates.getLayoutByNode(nodes[0])
|
||||||
if (layout == null) {
|
if (layout == null) {
|
||||||
return {
|
return {
|
||||||
error: "Subgraph not contained in a layout!"
|
error: "Node(s) not contained in a layout!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let title = "New Template";
|
||||||
|
if (nodes.length === 1) {
|
||||||
|
title = nodes[0].title || title;
|
||||||
|
}
|
||||||
|
|
||||||
if (widgetNodes.length > 0) {
|
if (widgetNodes.length > 0) {
|
||||||
// Find the highest-level container that contains all these nodes and
|
// Find the highest-level container that contains all these nodes and
|
||||||
// contains no other widgets
|
// contains no other widgets
|
||||||
@@ -386,7 +404,8 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
metadata: { ...DEFAULT_TEMPLATE_METADATA },
|
id: uuidv4(),
|
||||||
|
metadata: { ...DEFAULT_TEMPLATE_METADATA, title, createdAt: Date.now() },
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
links: links,
|
links: links,
|
||||||
container: container
|
container: container
|
||||||
@@ -396,7 +415,8 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
|||||||
// No UI to serialize.
|
// No UI to serialize.
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
metadata: { ...DEFAULT_TEMPLATE_METADATA },
|
id: uuidv4(),
|
||||||
|
metadata: { ...DEFAULT_TEMPLATE_METADATA, title, createdAt: Date.now() },
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
links: links,
|
links: links,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ComfyReroute } from "./nodes";
|
|||||||
import layoutStates from "./stores/layoutStates";
|
import layoutStates from "./stores/layoutStates";
|
||||||
import queueState from "./stores/queueState";
|
import queueState from "./stores/queueState";
|
||||||
import selectionState from "./stores/selectionState";
|
import selectionState from "./stores/selectionState";
|
||||||
|
import templateState from "./stores/templateState";
|
||||||
import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
||||||
import notify from "./notify";
|
import notify from "./notify";
|
||||||
import { v4 as uuidv4 } from "uuid"
|
import { v4 as uuidv4 } from "uuid"
|
||||||
@@ -523,11 +524,20 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
|
|
||||||
const template = result as ComfyBoxTemplate;
|
const template = result as ComfyBoxTemplate;
|
||||||
|
|
||||||
console.warn("TEMPLATEFOUND", template)
|
|
||||||
|
|
||||||
const serialized = serializeTemplate(this, template);
|
const serialized = serializeTemplate(this, template);
|
||||||
|
|
||||||
download("template.svg", serialized.svg, "image/svg+xml");
|
try {
|
||||||
|
if (templateState.add(serialized)) {
|
||||||
|
notify("Template saved!", { type: "success" })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notify("Failed to save template: already exists in LocalStorage", { type: "error" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Quota exceeded?
|
||||||
|
notify(`Failed to save template: ${error}`, { type: "error", timeout: 10000 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override getCanvasMenuOptions(): ContextMenuItem[] {
|
override getCanvasMenuOptions(): ContextMenuItem[] {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||||
|
|
||||||
export let layoutState: WritableLayoutStateStore;
|
export let layoutState: WritableLayoutStateStore;
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
@@ -46,14 +47,12 @@
|
|||||||
isOpen = container.isOpen
|
isOpen = container.isOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConsider(evt: any) {
|
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = handleContainerConsider(layoutState, container, evt)
|
||||||
// console.log(dragItems);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleFinalize(evt: any) {
|
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = handleContainerFinalize(layoutState, container, evt)
|
||||||
// Ensure dragging is stopped on drag finish
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleClick(e: CustomEvent<boolean>) {
|
function handleClick(e: CustomEvent<boolean>) {
|
||||||
@@ -85,6 +84,7 @@
|
|||||||
class:empty={children.length === 0}
|
class:empty={children.length === 0}
|
||||||
class:edit={edit}
|
class:edit={edit}
|
||||||
use:dndzone="{{
|
use:dndzone="{{
|
||||||
|
type: "layout",
|
||||||
items: children,
|
items: children,
|
||||||
flipDurationMs,
|
flipDurationMs,
|
||||||
centreDraggedOnCursor: true,
|
centreDraggedOnCursor: true,
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
|
|
||||||
import {fade} from 'svelte/transition';
|
import {fade} from 'svelte/transition';
|
||||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
|
||||||
import {cubicIn} from 'svelte/easing';
|
import {cubicIn} from 'svelte/easing';
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||||
|
|
||||||
export let layoutState: WritableLayoutStateStore;
|
export let layoutState: WritableLayoutStateStore;
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
@@ -45,14 +45,12 @@
|
|||||||
attrsChanged = null
|
attrsChanged = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConsider(evt: any) {
|
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = handleContainerConsider(layoutState, container, evt);
|
||||||
// console.log(dragItems);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleFinalize(evt: any) {
|
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = handleContainerFinalize(layoutState, container, evt);
|
||||||
// Ensure dragging is stopped on drag finish
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function _startDrag(e: MouseEvent | TouchEvent) {
|
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||||
@@ -83,6 +81,7 @@
|
|||||||
class:empty={children.length === 0}
|
class:empty={children.length === 0}
|
||||||
class:edit
|
class:edit
|
||||||
use:dndzone="{{
|
use:dndzone="{{
|
||||||
|
type: "layout",
|
||||||
items: children,
|
items: children,
|
||||||
flipDurationMs,
|
flipDurationMs,
|
||||||
centreDraggedOnCursor: true,
|
centreDraggedOnCursor: true,
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ import { type SvelteComponentDev } from "svelte/internal";
|
|||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
||||||
import DanbooruTags from "$lib/DanbooruTags";
|
import DanbooruTags from "$lib/DanbooruTags";
|
||||||
import { deserializeTemplate } from "$lib/ComfyBoxTemplate";
|
import { deserializeTemplateFromSVG } from "$lib/ComfyBoxTemplate";
|
||||||
|
import templateState from "$lib/stores/templateState";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
|
|
||||||
@@ -234,6 +235,7 @@ export default class ComfyApp {
|
|||||||
this.addKeyboardHandler();
|
this.addKeyboardHandler();
|
||||||
|
|
||||||
await this.updateHistoryAndQueue();
|
await this.updateHistoryAndQueue();
|
||||||
|
templateState.load();
|
||||||
|
|
||||||
await this.initFrontendFeatures();
|
await this.initFrontendFeatures();
|
||||||
|
|
||||||
@@ -1004,7 +1006,7 @@ export default class ComfyApp {
|
|||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
} else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
|
} else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
|
||||||
const templateAndSvg = await deserializeTemplate(file);
|
const templateAndSvg = await deserializeTemplateFromSVG(file);
|
||||||
modalState.pushModal({
|
modalState.pushModal({
|
||||||
title: "ComfyBox Template Preview",
|
title: "ComfyBox Template Preview",
|
||||||
svelteComponent: EditTemplateModal,
|
svelteComponent: EditTemplateModal,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
|
|
||||||
{#if workflow != null}
|
{#if workflow != null}
|
||||||
{#if layoutState != null}
|
{#if layoutState != null}
|
||||||
<div id="comfy-workflow-view" on:contextmenu={onRightClick}>
|
<div class="comfy-workflow-view" on:contextmenu={onRightClick}>
|
||||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
|
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#comfy-workflow-view {
|
.comfy-workflow-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import interfaceState from "$lib/stores/interfaceState";
|
import interfaceState from "$lib/stores/interfaceState";
|
||||||
import workflowState, { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
import workflowState, { ComfyBoxWorkflow, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||||
import selectionState from "$lib/stores/selectionState";
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import type ComfyApp from './ComfyApp';
|
import type ComfyApp from './ComfyApp';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
@@ -22,8 +22,10 @@
|
|||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
export let uiTheme: string = "gradio-dark" // TODO config
|
export let uiTheme: string = "gradio-dark" // TODO config
|
||||||
|
|
||||||
|
type OpenedWorkflow = { id: WorkflowInstID };
|
||||||
|
|
||||||
let workflow: ComfyBoxWorkflow | null = null;
|
let workflow: ComfyBoxWorkflow | null = null;
|
||||||
let openedWorkflows = []
|
let openedWorkflows: OpenedWorkflow[] = []
|
||||||
|
|
||||||
let containerElem: HTMLDivElement;
|
let containerElem: HTMLDivElement;
|
||||||
let resizeTimeout: NodeJS.Timeout | null;
|
let resizeTimeout: NodeJS.Timeout | null;
|
||||||
@@ -165,7 +167,7 @@
|
|||||||
app.closeWorkflow(workflow.id);
|
app.closeWorkflow(workflow.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConsider(evt: any) {
|
function handleConsider(evt: CustomEvent<DndEvent<OpenedWorkflow>>) {
|
||||||
console.warn(openedWorkflows.length, openedWorkflows, evt.detail.items.length, evt.detail.items)
|
console.warn(openedWorkflows.length, openedWorkflows, evt.detail.items.length, evt.detail.items)
|
||||||
openedWorkflows = evt.detail.items;
|
openedWorkflows = evt.detail.items;
|
||||||
// openedWorkflows = evt.detail.items.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID);
|
// openedWorkflows = evt.detail.items.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID);
|
||||||
@@ -175,7 +177,7 @@
|
|||||||
// })
|
// })
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleFinalize(evt: any) {
|
function handleFinalize(evt: CustomEvent<DndEvent<OpenedWorkflow>>) {
|
||||||
openedWorkflows = evt.detail.items;
|
openedWorkflows = evt.detail.items;
|
||||||
workflowState.update(s => {
|
workflowState.update(s => {
|
||||||
s.openedWorkflows = openedWorkflows.filter(w => w.id !== SHADOW_PLACEHOLDER_ITEM_ID).map(w => workflowState.getWorkflow(w.id));
|
s.openedWorkflows = openedWorkflows.filter(w => w.id !== SHADOW_PLACEHOLDER_ITEM_ID).map(w => workflowState.getWorkflow(w.id));
|
||||||
@@ -206,6 +208,7 @@
|
|||||||
<div id="workflow-tabs">
|
<div id="workflow-tabs">
|
||||||
<div class="workflow-tab-items"
|
<div class="workflow-tab-items"
|
||||||
use:dndzone="{{
|
use:dndzone="{{
|
||||||
|
type: "workflowTab",
|
||||||
items: openedWorkflows,
|
items: openedWorkflows,
|
||||||
flipDurationMs: 200,
|
flipDurationMs: 200,
|
||||||
type: "workflow-tab",
|
type: "workflow-tab",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
export type ComfyPaneMode = "none" | "activeWorkflow" | "graph" | "properties" | "queue"
|
export type ComfyPaneMode = "none" | "activeWorkflow" | "graph" | "properties" | "templates" | "queue"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -8,12 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
import workflowState from "$lib/stores/workflowState";
|
import workflowState from "$lib/stores/workflowState";
|
||||||
import type ComfyApp from "./ComfyApp";
|
import type ComfyApp from "./ComfyApp";
|
||||||
import { Sliders2, LayoutTextSidebarReverse } from "svelte-bootstrap-icons";
|
import { Sliders2, BoxSeam, LayoutTextSidebarReverse } from "svelte-bootstrap-icons";
|
||||||
|
|
||||||
import ComfyBoxWorkflowView from "./ComfyBoxWorkflowView.svelte";
|
import ComfyBoxWorkflowView from "./ComfyBoxWorkflowView.svelte";
|
||||||
import ComfyGraphView from "./ComfyGraphView.svelte";
|
import ComfyGraphView from "./ComfyGraphView.svelte";
|
||||||
import ComfyProperties from "./ComfyProperties.svelte";
|
import ComfyProperties from "./ComfyProperties.svelte";
|
||||||
import ComfyQueue from "./ComfyQueue.svelte";
|
import ComfyQueue from "./ComfyQueue.svelte";
|
||||||
|
import ComfyTemplates from "./ComfyTemplates.svelte";
|
||||||
import { SvelteComponent } from "svelte";
|
import { SvelteComponent } from "svelte";
|
||||||
|
|
||||||
export let app: ComfyApp
|
export let app: ComfyApp
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
|
|
||||||
const MODES: [ComfyPaneMode, typeof SvelteComponent][] = [
|
const MODES: [ComfyPaneMode, typeof SvelteComponent][] = [
|
||||||
["properties", Sliders2],
|
["properties", Sliders2],
|
||||||
|
["templates", BoxSeam],
|
||||||
["queue", LayoutTextSidebarReverse]
|
["queue", LayoutTextSidebarReverse]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -39,6 +41,8 @@
|
|||||||
<ComfyGraphView {app} />
|
<ComfyGraphView {app} />
|
||||||
{:else if mode === "properties"}
|
{:else if mode === "properties"}
|
||||||
<ComfyProperties workflow={$workflowState.activeWorkflow} />
|
<ComfyProperties workflow={$workflowState.activeWorkflow} />
|
||||||
|
{:else if mode === "templates"}
|
||||||
|
<ComfyTemplates {app} />
|
||||||
{:else if mode === "queue"}
|
{:else if mode === "queue"}
|
||||||
<ComfyQueue {app} />
|
<ComfyQueue {app} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
186
src/lib/components/ComfyTemplates.svelte
Normal file
186
src/lib/components/ComfyTemplates.svelte
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||||
|
import templateState from "$lib/stores/templateState";
|
||||||
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import { truncateString } from "$lib/utils";
|
||||||
|
import type ComfyApp from "./ComfyApp";
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
import {fade} from 'svelte/transition';
|
||||||
|
import {cubicIn} from 'svelte/easing';
|
||||||
|
import { dndzone, TRIGGERS, SHADOW_PLACEHOLDER_ITEM_ID, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action';
|
||||||
|
import { defaultWidgetAttributes, type TemplateLayout } from "$lib/stores/layoutStates";
|
||||||
|
import { v4 as uuidv4 } from "uuid"
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let app: ComfyApp
|
||||||
|
|
||||||
|
type DNDConsiderOrFinalizeEvent<T> = {
|
||||||
|
items: T[],
|
||||||
|
info: any,
|
||||||
|
el: Node,
|
||||||
|
id: string,
|
||||||
|
trigger?: string,
|
||||||
|
source?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let _sorted: TemplateLayout[] = []
|
||||||
|
|
||||||
|
$: rebuildTemplates($templateState.templates);
|
||||||
|
|
||||||
|
function rebuildTemplates(templates: SerializedComfyBoxTemplate[]) {
|
||||||
|
console.error("recreate");
|
||||||
|
_sorted = Array.from(templates).map(t => {
|
||||||
|
return {
|
||||||
|
type: "template", id: uuidv4(), template: t, attrs: {...defaultWidgetAttributes}, attrsChanged: writable(0)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_sorted.sort(t => t.template.metadata.createdAt || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flipDurationMs = 200;
|
||||||
|
let shouldIgnoreDndEvents = false;
|
||||||
|
|
||||||
|
function handleDndConsider(e: CustomEvent<DNDConsiderOrFinalizeEvent<TemplateLayout>>) {
|
||||||
|
// console.warn(`got consider ${JSON.stringify(e.detail, null, 2)}`);
|
||||||
|
const {trigger, id} = e.detail.info;
|
||||||
|
if (trigger === TRIGGERS.DRAG_STARTED) {
|
||||||
|
// console.warn(`copying ${id}`);
|
||||||
|
const idx = _sorted.findIndex(item => item.id === id);
|
||||||
|
const newId = `${id}_copy_${Math.round(Math.random()*1000000)}`;
|
||||||
|
|
||||||
|
e.detail.items = e.detail.items.filter(item => !item[SHADOW_ITEM_MARKER_PROPERTY_NAME]);
|
||||||
|
e.detail.items.splice(idx, 0, {..._sorted[idx], id: newId});
|
||||||
|
|
||||||
|
_sorted = e.detail.items;
|
||||||
|
shouldIgnoreDndEvents = true;
|
||||||
|
}
|
||||||
|
else if (!shouldIgnoreDndEvents) {
|
||||||
|
_sorted = e.detail.items;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_sorted = _sorted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDndFinalize(e: CustomEvent<DNDConsiderOrFinalizeEvent<TemplateLayout>>) {
|
||||||
|
if (!shouldIgnoreDndEvents) {
|
||||||
|
_sorted = e.detail.items;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_sorted = _sorted;
|
||||||
|
shouldIgnoreDndEvents = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="template-list">
|
||||||
|
<div class="template-entries">
|
||||||
|
{#if _sorted.length > 0}
|
||||||
|
{@const draggable = $uiState.uiUnlocked}
|
||||||
|
<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)}
|
||||||
|
<div class="template-entry" class:draggable>
|
||||||
|
<div class="template-name">{truncateString(item.template.metadata.title, 16)}</div>
|
||||||
|
<div class="template-desc">{truncateString(item.template.metadata.description, 24)}</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>
|
||||||
|
{:else}
|
||||||
|
<div class="no-templates">
|
||||||
|
<span>(No templates)</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.template-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-entries {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-category-header {
|
||||||
|
color: var(--body-text-color);
|
||||||
|
background: var(--block-background-fill);
|
||||||
|
border-color: var(--panel-border-color);
|
||||||
|
padding: 0.8rem 1.0rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-entry {
|
||||||
|
padding: 1.0rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: 1px solid var(--block-border-color);
|
||||||
|
border-top: 1px solid var(--table-border-color);
|
||||||
|
color: var(--body-text-color);
|
||||||
|
background: var(--panel-background-fill);
|
||||||
|
max-height: 14rem;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
font-size: 13pt;
|
||||||
|
.template-desc {
|
||||||
|
opacity: 65%;
|
||||||
|
font-size: 11pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:has(img:hover)):not(:has(button:hover)) {
|
||||||
|
&.draggable {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
background: var(--block-background-fill);
|
||||||
|
|
||||||
|
&.running {
|
||||||
|
background: var(--comfy-accent-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-templates {
|
||||||
|
display: flex;
|
||||||
|
color: var(--comfy-accent-soft);
|
||||||
|
flex-direction: row;
|
||||||
|
margin: auto;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--comfy-accent-soft);
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-drag-item-shadow {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left:0; right: 0; bottom: 0;
|
||||||
|
visibility: visible;
|
||||||
|
border: 1px dashed grey;
|
||||||
|
background: lightblue;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||||
|
|
||||||
export let layoutState: WritableLayoutStateStore;
|
export let layoutState: WritableLayoutStateStore;
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
@@ -38,14 +39,12 @@
|
|||||||
// attrsChanged = writable(0)
|
// attrsChanged = writable(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConsider(evt: any) {
|
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = handleContainerConsider(layoutState, container, evt)
|
||||||
// console.log(dragItems);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleFinalize(evt: any) {
|
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = handleContainerFinalize(layoutState, container, evt)
|
||||||
// Ensure dragging is stopped on drag finish
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getTabName(container: ContainerLayout, i: number): string {
|
function getTabName(container: ContainerLayout, i: number): string {
|
||||||
@@ -89,6 +88,7 @@
|
|||||||
class:empty={children.length === 0}
|
class:empty={children.length === 0}
|
||||||
class:edit={edit}
|
class:edit={edit}
|
||||||
use:dndzone="{{
|
use:dndzone="{{
|
||||||
|
type: "layout",
|
||||||
items: children,
|
items: children,
|
||||||
flipDurationMs,
|
flipDurationMs,
|
||||||
centreDraggedOnCursor: true,
|
centreDraggedOnCursor: true,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyBoxTemplate, SerializedComfyBoxTemplateAndSVG } from "$lib/ComfyBoxTemplate";
|
import type { ComfyBoxTemplate, SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||||
import type { SerializedDragEntry, SerializedLayoutState } from "$lib/stores/layoutStates";
|
import type { SerializedDragEntry, SerializedLayoutState } from "$lib/stores/layoutStates";
|
||||||
import { Block, BlockTitle } from "@gradio/atoms";
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
import SerializedLayoutPreviewNode from "./SerializedLayoutPreviewNode.svelte";
|
import SerializedLayoutPreviewNode from "./SerializedLayoutPreviewNode.svelte";
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
import Textbox from "@gradio/form/src/Textbox.svelte";
|
import Textbox from "@gradio/form/src/Textbox.svelte";
|
||||||
const DOMPurify = createDOMPurify(window);
|
const DOMPurify = createDOMPurify(window);
|
||||||
|
|
||||||
export let templateAndSvg: SerializedComfyBoxTemplateAndSVG;
|
export let templateAndSvg: SerializedComfyBoxTemplate;
|
||||||
let layout: SerializedLayoutState | null
|
let layout: SerializedLayoutState | null
|
||||||
let root: SerializedDragEntry | null
|
let root: SerializedDragEntry | null
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
$: if (templateAndSvg) {
|
$: if (templateAndSvg) {
|
||||||
layout = templateAndSvg.template.layout;
|
layout = templateAndSvg.layout;
|
||||||
if (layout) {
|
if (layout) {
|
||||||
root = layout.allItems[layout.root];
|
root = layout.allItems[layout.root];
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/lib/components/utils.ts
Normal file
19
src/lib/components/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { type ContainerLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
|
||||||
|
|
||||||
|
export function handleContainerConsider(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] {
|
||||||
|
return layoutState.updateChildren(container, evt.detail.items)
|
||||||
|
};
|
||||||
|
|
||||||
|
export function handleContainerFinalize(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] {
|
||||||
|
const dnd = evt.detail
|
||||||
|
const info = dnd.info;
|
||||||
|
const droppedItem = dnd.items.find(i => i.id === info.id);
|
||||||
|
const isDroppingTemplate = droppedItem?.type === "template"
|
||||||
|
|
||||||
|
if (isDroppingTemplate) {
|
||||||
|
return layoutState.updateChildren(container, dnd.items.filter(i => i.id !== info.id));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return layoutState.updateChildren(container, dnd.items)
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import type { ComfyWidgetNode } from '$lib/nodes/widgets';
|
|||||||
import type ComfyGraph from '$lib/ComfyGraph';
|
import type ComfyGraph from '$lib/ComfyGraph';
|
||||||
import type { ComfyBoxWorkflow, WorkflowAttributes, WorkflowInstID } from './workflowState';
|
import type { ComfyBoxWorkflow, WorkflowAttributes, WorkflowInstID } from './workflowState';
|
||||||
import workflowState from './workflowState';
|
import workflowState from './workflowState';
|
||||||
|
import type { SerializedComfyBoxTemplate } from '$lib/ComfyBoxTemplate';
|
||||||
|
|
||||||
export function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode {
|
export function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode {
|
||||||
return "svelteComponentType" in node
|
return "svelteComponentType" in node
|
||||||
@@ -635,7 +636,7 @@ for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
|||||||
export { ALL_ATTRIBUTES };
|
export { ALL_ATTRIBUTES };
|
||||||
|
|
||||||
// TODO Should be nested by category for name uniqueness?
|
// TODO Should be nested by category for name uniqueness?
|
||||||
const defaultWidgetAttributes: Attributes = {} as any
|
export const defaultWidgetAttributes: Attributes = {} as any
|
||||||
export const defaultWorkflowAttributes: WorkflowAttributes = {} as any
|
export const defaultWorkflowAttributes: WorkflowAttributes = {} as any
|
||||||
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||||
for (const spec of Object.values(cat.specs)) {
|
for (const spec of Object.values(cat.specs)) {
|
||||||
@@ -657,7 +658,7 @@ export interface IDragItem {
|
|||||||
/*
|
/*
|
||||||
* Type of the item.
|
* Type of the item.
|
||||||
*/
|
*/
|
||||||
type: "container" | "widget",
|
type: "container" | "widget" | "template",
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Unique ID of the item.
|
* Unique ID of the item.
|
||||||
@@ -707,6 +708,21 @@ export interface WidgetLayout extends IDragItem {
|
|||||||
node: ComfyWidgetNode
|
node: ComfyWidgetNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A template being dragged into the workflow UI.
|
||||||
|
*
|
||||||
|
* These will never be saved, instead they will be expanded inside
|
||||||
|
* updateChildren() and then removed.
|
||||||
|
*/
|
||||||
|
export interface TemplateLayout extends IDragItem {
|
||||||
|
type: "template",
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Template to expand
|
||||||
|
*/
|
||||||
|
template: SerializedComfyBoxTemplate
|
||||||
|
}
|
||||||
|
|
||||||
export type DefaultLayout = {
|
export type DefaultLayout = {
|
||||||
root: ContainerLayout,
|
root: ContainerLayout,
|
||||||
left: ContainerLayout,
|
left: ContainerLayout,
|
||||||
@@ -874,7 +890,7 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
if (newChildren)
|
if (newChildren)
|
||||||
state.allItems[parent.id].children = newChildren;
|
state.allItems[parent.id].children = newChildren;
|
||||||
for (const child of state.allItems[parent.id].children) {
|
for (const child of state.allItems[parent.id].children) {
|
||||||
if (child.id === SHADOW_PLACEHOLDER_ITEM_ID)
|
if (child.id === SHADOW_PLACEHOLDER_ITEM_ID || child.type === "template")
|
||||||
continue;
|
continue;
|
||||||
state.allItems[child.id].parent = parent;
|
state.allItems[child.id].parent = parent;
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/lib/stores/templateState.ts
Normal file
101
src/lib/stores/templateState.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type { SerializedComfyBoxTemplate } from '$lib/ComfyBoxTemplate';
|
||||||
|
import type { UUID } from '@litegraph-ts/core';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type TemplateState = {
|
||||||
|
templates: SerializedComfyBoxTemplate[]
|
||||||
|
templatesByID: Record<UUID, SerializedComfyBoxTemplate>
|
||||||
|
}
|
||||||
|
|
||||||
|
type TemplateStateOps = {
|
||||||
|
save: () => void,
|
||||||
|
load: () => void,
|
||||||
|
add: (template: SerializedComfyBoxTemplate) => boolean,
|
||||||
|
remove: (templateID: UUID) => boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WritableTemplateStateStore = Writable<TemplateState> & TemplateStateOps;
|
||||||
|
const store: Writable<TemplateState> = writable(
|
||||||
|
{
|
||||||
|
templates: [],
|
||||||
|
templatesByID: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
function add(template: SerializedComfyBoxTemplate): boolean {
|
||||||
|
const state = get(store);
|
||||||
|
if (state.templatesByID[template.id]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(s => {
|
||||||
|
s.templates.push(template);
|
||||||
|
s.templatesByID[template.id] = template;
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
|
||||||
|
save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(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);
|
||||||
|
delete s.templatesByID[templateID];
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
|
||||||
|
save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
const json = JSON.stringify(get(store).templates)
|
||||||
|
localStorage.setItem("templates", json)
|
||||||
|
store.set(get(store))
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
const json = localStorage.getItem("templates")
|
||||||
|
if (!json) {
|
||||||
|
console.info("No templates in local storage, creating store")
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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), {})
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
templates: data,
|
||||||
|
templatesByID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
store.set({
|
||||||
|
templates: [],
|
||||||
|
templatesByID: {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateStateStore: WritableTemplateStateStore =
|
||||||
|
{
|
||||||
|
...store,
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
save,
|
||||||
|
load,
|
||||||
|
}
|
||||||
|
export default templateStateStore;
|
||||||
@@ -219,3 +219,8 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fixes drop item shadow not appearing when dragging template items onto workflow
|
||||||
|
:global([data-is-dnd-shadow-item]) {
|
||||||
|
min-height: 5rem;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user