Templates view and store
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
@@ -46,14 +47,12 @@
|
||||
isOpen = container.isOpen
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// console.log(dragItems);
|
||||
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerConsider(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function handleFinalize(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerFinalize(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function handleClick(e: CustomEvent<boolean>) {
|
||||
@@ -85,6 +84,7 @@
|
||||
class:empty={children.length === 0}
|
||||
class:edit={edit}
|
||||
use:dndzone="{{
|
||||
type: "layout",
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
centreDraggedOnCursor: true,
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
|
||||
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 { flip } from 'svelte/animate';
|
||||
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
@@ -45,14 +45,12 @@
|
||||
attrsChanged = null
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// console.log(dragItems);
|
||||
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerConsider(layoutState, container, evt);
|
||||
};
|
||||
|
||||
function handleFinalize(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerFinalize(layoutState, container, evt);
|
||||
};
|
||||
|
||||
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||
@@ -83,6 +81,7 @@
|
||||
class:empty={children.length === 0}
|
||||
class:edit
|
||||
use:dndzone="{{
|
||||
type: "layout",
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
centreDraggedOnCursor: true,
|
||||
|
||||
@@ -36,7 +36,8 @@ import { type SvelteComponentDev } from "svelte/internal";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
||||
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;
|
||||
|
||||
@@ -234,6 +235,7 @@ export default class ComfyApp {
|
||||
this.addKeyboardHandler();
|
||||
|
||||
await this.updateHistoryAndQueue();
|
||||
templateState.load();
|
||||
|
||||
await this.initFrontendFeatures();
|
||||
|
||||
@@ -1004,7 +1006,7 @@ export default class ComfyApp {
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
|
||||
const templateAndSvg = await deserializeTemplate(file);
|
||||
const templateAndSvg = await deserializeTemplateFromSVG(file);
|
||||
modalState.pushModal({
|
||||
title: "ComfyBox Template Preview",
|
||||
svelteComponent: EditTemplateModal,
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
{#if workflow != 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} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -185,7 +185,7 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
#comfy-workflow-view {
|
||||
.comfy-workflow-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
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 type ComfyApp from './ComfyApp';
|
||||
import { onMount } from "svelte";
|
||||
@@ -22,8 +22,10 @@
|
||||
export let app: ComfyApp;
|
||||
export let uiTheme: string = "gradio-dark" // TODO config
|
||||
|
||||
type OpenedWorkflow = { id: WorkflowInstID };
|
||||
|
||||
let workflow: ComfyBoxWorkflow | null = null;
|
||||
let openedWorkflows = []
|
||||
let openedWorkflows: OpenedWorkflow[] = []
|
||||
|
||||
let containerElem: HTMLDivElement;
|
||||
let resizeTimeout: NodeJS.Timeout | null;
|
||||
@@ -165,7 +167,7 @@
|
||||
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)
|
||||
openedWorkflows = evt.detail.items;
|
||||
// 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;
|
||||
workflowState.update(s => {
|
||||
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 class="workflow-tab-items"
|
||||
use:dndzone="{{
|
||||
type: "workflowTab",
|
||||
items: openedWorkflows,
|
||||
flipDurationMs: 200,
|
||||
type: "workflow-tab",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script context="module" lang="ts">
|
||||
export type ComfyPaneMode = "none" | "activeWorkflow" | "graph" | "properties" | "queue"
|
||||
export type ComfyPaneMode = "none" | "activeWorkflow" | "graph" | "properties" | "templates" | "queue"
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -8,12 +8,13 @@
|
||||
*/
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
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 ComfyGraphView from "./ComfyGraphView.svelte";
|
||||
import ComfyProperties from "./ComfyProperties.svelte";
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
import ComfyTemplates from "./ComfyTemplates.svelte";
|
||||
import { SvelteComponent } from "svelte";
|
||||
|
||||
export let app: ComfyApp
|
||||
@@ -22,6 +23,7 @@
|
||||
|
||||
const MODES: [ComfyPaneMode, typeof SvelteComponent][] = [
|
||||
["properties", Sliders2],
|
||||
["templates", BoxSeam],
|
||||
["queue", LayoutTextSidebarReverse]
|
||||
]
|
||||
|
||||
@@ -39,6 +41,8 @@
|
||||
<ComfyGraphView {app} />
|
||||
{:else if mode === "properties"}
|
||||
<ComfyProperties workflow={$workflowState.activeWorkflow} />
|
||||
{:else if mode === "templates"}
|
||||
<ComfyTemplates {app} />
|
||||
{:else if mode === "queue"}
|
||||
<ComfyQueue {app} />
|
||||
{: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 type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
@@ -38,14 +39,12 @@
|
||||
// attrsChanged = writable(0)
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// console.log(dragItems);
|
||||
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerConsider(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function handleFinalize(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerFinalize(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function getTabName(container: ContainerLayout, i: number): string {
|
||||
@@ -89,6 +88,7 @@
|
||||
class:empty={children.length === 0}
|
||||
class:edit={edit}
|
||||
use:dndzone="{{
|
||||
type: "layout",
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
centreDraggedOnCursor: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 { Block, BlockTitle } from "@gradio/atoms";
|
||||
import SerializedLayoutPreviewNode from "./SerializedLayoutPreviewNode.svelte";
|
||||
@@ -10,7 +10,7 @@
|
||||
import Textbox from "@gradio/form/src/Textbox.svelte";
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
export let templateAndSvg: SerializedComfyBoxTemplateAndSVG;
|
||||
export let templateAndSvg: SerializedComfyBoxTemplate;
|
||||
let layout: SerializedLayoutState | null
|
||||
let root: SerializedDragEntry | null
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
: "";
|
||||
|
||||
$: if (templateAndSvg) {
|
||||
layout = templateAndSvg.template.layout;
|
||||
layout = templateAndSvg.layout;
|
||||
if (layout) {
|
||||
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)
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user