Refactor widgets/nodes
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyButtonNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Button } from "@gradio/button";
|
||||
import { get, type Writable, writable } from "svelte/store";
|
||||
import { isDisabled } from "./utils"
|
||||
import type { ComfyButtonNode } from "$lib/nodes/widgets";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyButtonNode | null = null;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Checkbox } from "@gradio/form";
|
||||
import { get, type Writable, writable } from "svelte/store";
|
||||
import { isDisabled } from "./utils"
|
||||
import type { SelectData } from "@gradio/utils";
|
||||
import type { SelectData } from "@gradio/utils";
|
||||
import type { ComfyCheckboxNode } from "$lib/nodes/widgets";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Select from 'svelte-select';
|
||||
// import VirtualList from '$lib/components/VirtualList.svelte';
|
||||
import VirtualList from 'svelte-tiny-virtual-list';
|
||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
||||
import type { ComfyComboNode } from "$lib/nodes/widgets";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { isDisabled } from "./utils"
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core";
|
||||
|
||||
export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T, V> {
|
||||
name: string;
|
||||
value: V;
|
||||
node: ComfyGraphNode;
|
||||
|
||||
constructor(name: string, value: V, node: ComfyGraphNode) {
|
||||
this.name = name;
|
||||
this.value = value
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
isVirtual?: boolean;
|
||||
options?: T;
|
||||
type?: WidgetTypes | string | any;
|
||||
y?: number;
|
||||
property?: string;
|
||||
last_y?: number;
|
||||
width?: number;
|
||||
clicked?: boolean;
|
||||
marker?: boolean;
|
||||
disabled?: boolean;
|
||||
callback?: WidgetCallback<this>;
|
||||
|
||||
setValue(value: V) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number): void;
|
||||
|
||||
mouse?(event: MouseEvent, pos: Vector2, node: LGraphNode): boolean;
|
||||
|
||||
computeSize?(width: number): [number, number];
|
||||
|
||||
afterQueued?(): void;
|
||||
|
||||
serializeValue?(serialized: SerializedLGraphNode<LGraphNode>, slot: number): Promise<any>;
|
||||
}
|
||||
@@ -7,11 +7,11 @@
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||
import { f7 } from "framework7-svelte";
|
||||
import type { ComfyGalleryNode } from "$lib/nodes/widgets";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { TextBox } from "@gradio/form";
|
||||
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import { Button } from "@gradio/button";
|
||||
import type { ComfyImageEditorNode, ComfyImageLocation } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import { Embed as Klecks } from "klecks";
|
||||
|
||||
import "klecks/style/style.scss";
|
||||
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
||||
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType } from "$lib/utils";
|
||||
import notify from "$lib/notify";
|
||||
import NumberInput from "$lib/components/NumberInput.svelte";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyImageEditorNode | null = null;
|
||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||
let attrsChanged: Writable<number> | null = null;
|
||||
|
||||
let imgWidth: number = 0;
|
||||
let imgHeight: number = 0;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
$: if ($nodeValue && $nodeValue.length > 0) {
|
||||
// TODO improve
|
||||
if (imgWidth > 0 && imgHeight > 0) {
|
||||
$nodeValue[0].width = imgWidth
|
||||
$nodeValue[0].height = imgHeight
|
||||
}
|
||||
else {
|
||||
$nodeValue[0].width = 0
|
||||
$nodeValue[0].height = 0
|
||||
}
|
||||
}
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfyImageEditorNode
|
||||
nodeValue = node.value;
|
||||
attrsChanged = widget.attrsChanged;
|
||||
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
|
||||
}
|
||||
};
|
||||
|
||||
let editorRoot: HTMLDivElement | null = null;
|
||||
let showModal = false;
|
||||
let kl: Klecks | null = null;
|
||||
|
||||
function disposeEditor() {
|
||||
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
||||
|
||||
if (editorRoot) {
|
||||
while (editorRoot.firstChild) {
|
||||
editorRoot.removeChild(editorRoot.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
kl = null;
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.save();
|
||||
ctx.fillStyle = fill,
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.restore();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve) => {
|
||||
const e = new Image();
|
||||
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
|
||||
e.addEventListener("load", () => { resolve(e); });
|
||||
e.src = imageURL;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
|
||||
const image = await loadImage(imageURL);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(image, 0, 0);
|
||||
ctx.restore();
|
||||
return [canvas, image.width, image.height];
|
||||
}
|
||||
|
||||
const FILENAME: string = "ComfyUITemp.png";
|
||||
const SUBFOLDER: string = "ComfyBox_Editor";
|
||||
const DIRECTORY: ComfyUploadImageType = "input";
|
||||
|
||||
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||
const blob = kl.getPNG();
|
||||
|
||||
status = "uploading"
|
||||
|
||||
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
|
||||
.then((entry: ComfyImageLocation) => {
|
||||
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
|
||||
$nodeValue = [meta] // TODO more than one image
|
||||
status = "uploaded"
|
||||
notify("Saved image to ComfyUI!", { type: "success" })
|
||||
onSuccess();
|
||||
})
|
||||
.catch(err => {
|
||||
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
|
||||
status = "error"
|
||||
uploadError = err;
|
||||
$nodeValue = []
|
||||
onError();
|
||||
})
|
||||
}
|
||||
|
||||
let closeDialog = null;
|
||||
|
||||
async function saveAndClose() {
|
||||
console.log(closeDialog, kl)
|
||||
if (!closeDialog || !kl)
|
||||
return;
|
||||
|
||||
submitKlecksToComfyUI(() => {}, () => {});
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
let blankImageWidth = 512;
|
||||
let blankImageHeight = 512;
|
||||
|
||||
async function openImageEditor() {
|
||||
if (!editorRoot)
|
||||
return;
|
||||
|
||||
showModal = true;
|
||||
|
||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||
|
||||
kl = new Klecks({
|
||||
embedUrl: url,
|
||||
onSubmit: submitKlecksToComfyUI,
|
||||
targetEl: editorRoot,
|
||||
warnOnPageClose: false
|
||||
});
|
||||
|
||||
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
|
||||
|
||||
let canvas = null;
|
||||
let width = blankImageWidth;
|
||||
let height = blankImageHeight;
|
||||
|
||||
if ($nodeValue && $nodeValue.length > 0) {
|
||||
const comfyImage = $nodeValue[0];
|
||||
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
|
||||
[canvas, width, height] = await generateImageCanvas(comfyURL);
|
||||
}
|
||||
else {
|
||||
canvas = generateBlankCanvas(width, height);
|
||||
}
|
||||
|
||||
kl.openProject({
|
||||
width: width,
|
||||
height: height,
|
||||
layers: [{
|
||||
name: 'Image',
|
||||
opacity: 1,
|
||||
mixModeStr: 'source-over',
|
||||
image: canvas
|
||||
}]
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
kl.klApp?.out("yo");
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
let status = "empty";
|
||||
let uploadError = null;
|
||||
|
||||
function onUploading() {
|
||||
console.warn("UPLOADING!!!")
|
||||
uploadError = null;
|
||||
status = "uploading"
|
||||
}
|
||||
|
||||
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
|
||||
console.warn("UPLOADED!!!")
|
||||
uploadError = null;
|
||||
status = "uploaded"
|
||||
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
console.warn("CLEAR!!!")
|
||||
uploadError = null;
|
||||
status = "empty"
|
||||
$nodeValue = []
|
||||
}
|
||||
|
||||
function onUploadError(e: CustomEvent<any>) {
|
||||
console.warn("ERROR!!!")
|
||||
status = "error"
|
||||
uploadError = e.detail
|
||||
$nodeValue = []
|
||||
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
|
||||
}
|
||||
|
||||
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
|
||||
}
|
||||
|
||||
let _value: ComfyImageLocation[] = []
|
||||
$: if ($nodeValue)
|
||||
_value = $nodeValue.map(comfyBoxImageToComfyFile)
|
||||
else
|
||||
_value = []
|
||||
|
||||
$: canEdit = status === "empty" || status === "uploaded";
|
||||
</script>
|
||||
|
||||
<div class="wrapper comfy-image-editor">
|
||||
{#if widget.attrs.variant === "fileUpload" || isMobile}
|
||||
<ImageUpload value={_value}
|
||||
bind:imgWidth
|
||||
bind:imgHeight
|
||||
fileCount={"single"}
|
||||
elem_classes={[]}
|
||||
style={""}
|
||||
label={widget.attrs.title}
|
||||
on:uploading={onUploading}
|
||||
on:uploaded={onUploaded}
|
||||
on:upload_error={onUploadError}
|
||||
on:clear={onClear}
|
||||
on:change={onChange}
|
||||
on:image_clicked={openImageEditor}
|
||||
/>
|
||||
{:else}
|
||||
<div class="comfy-image-editor-panel">
|
||||
<ImageUpload value={_value}
|
||||
bind:imgWidth
|
||||
bind:imgHeight
|
||||
fileCount={"single"}
|
||||
elem_classes={[]}
|
||||
style={""}
|
||||
label={widget.attrs.title}
|
||||
on:uploading={onUploading}
|
||||
on:uploaded={onUploaded}
|
||||
on:upload_error={onUploadError}
|
||||
on:clear={onClear}
|
||||
on:change={onChange}
|
||||
on:image_clicked={openImageEditor}
|
||||
/>
|
||||
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
|
||||
<div>
|
||||
<div id="klecks-loading-screen">
|
||||
<span id="klecks-loading-screen-text"></span>
|
||||
</div>
|
||||
<div class="image-editor-root" bind:this={editorRoot} />
|
||||
</div>
|
||||
<div slot="buttons">
|
||||
<Button variant="primary" on:click={saveAndClose}>
|
||||
Save and Close
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={closeDialog}>
|
||||
Discard Edits
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Block>
|
||||
{#if !$nodeValue || $nodeValue.length === 0}
|
||||
<Row>
|
||||
<Row>
|
||||
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||
Create Image
|
||||
</Button>
|
||||
<div>
|
||||
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||
</div>
|
||||
{#if uploadError}
|
||||
<div>
|
||||
Upload error: {uploadError}
|
||||
</div>
|
||||
{/if}
|
||||
</Row>
|
||||
<Row>
|
||||
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
|
||||
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
|
||||
</Row>
|
||||
</Row>
|
||||
{:else}
|
||||
<Row>
|
||||
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||
Edit Image
|
||||
</Button>
|
||||
<div>
|
||||
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||
</div>
|
||||
{#if uploadError}
|
||||
<div>
|
||||
Upload error: {uploadError}
|
||||
</div>
|
||||
{/if}
|
||||
</Row>
|
||||
{/if}
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.image-editor-root {
|
||||
width: 75vw;
|
||||
height: 75vh;
|
||||
overflow: hidden;
|
||||
|
||||
color: black;
|
||||
|
||||
:global(> .g-root) {
|
||||
height: calc(100% - 59px);
|
||||
}
|
||||
}
|
||||
|
||||
.comfy-image-editor {
|
||||
:global(> dialog) {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.kl-popup) {
|
||||
z-index: 999999999999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,343 @@
|
||||
<script lang="ts">
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { TextBox } from "@gradio/form";
|
||||
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import { Button } from "@gradio/button";
|
||||
import { Embed as Klecks } from "klecks";
|
||||
|
||||
import "klecks/style/style.scss";
|
||||
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
||||
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType, type ComfyImageLocation } from "$lib/utils";
|
||||
import notify from "$lib/notify";
|
||||
import NumberInput from "$lib/components/NumberInput.svelte";
|
||||
import type { ComfyImageEditorNode } from "$lib/nodes/widgets";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyImageEditorNode | null = null;
|
||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||
let attrsChanged: Writable<number> | null = null;
|
||||
|
||||
let imgWidth: number = 0;
|
||||
let imgHeight: number = 0;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
$: if ($nodeValue && $nodeValue.length > 0) {
|
||||
// TODO improve
|
||||
if (imgWidth > 0 && imgHeight > 0) {
|
||||
$nodeValue[0].width = imgWidth
|
||||
$nodeValue[0].height = imgHeight
|
||||
}
|
||||
else {
|
||||
$nodeValue[0].width = 0
|
||||
$nodeValue[0].height = 0
|
||||
}
|
||||
}
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfyImageEditorNode
|
||||
nodeValue = node.value;
|
||||
attrsChanged = widget.attrsChanged;
|
||||
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
|
||||
}
|
||||
};
|
||||
|
||||
let editorRoot: HTMLDivElement | null = null;
|
||||
let showModal = false;
|
||||
let kl: Klecks | null = null;
|
||||
|
||||
function disposeEditor() {
|
||||
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
||||
|
||||
if (editorRoot) {
|
||||
while (editorRoot.firstChild) {
|
||||
editorRoot.removeChild(editorRoot.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
kl = null;
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.save();
|
||||
ctx.fillStyle = fill,
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.restore();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve) => {
|
||||
const e = new Image();
|
||||
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
|
||||
e.addEventListener("load", () => { resolve(e); });
|
||||
e.src = imageURL;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
|
||||
const image = await loadImage(imageURL);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(image, 0, 0);
|
||||
ctx.restore();
|
||||
return [canvas, image.width, image.height];
|
||||
}
|
||||
|
||||
const FILENAME: string = "ComfyUITemp.png";
|
||||
const SUBFOLDER: string = "ComfyBox_Editor";
|
||||
const DIRECTORY: ComfyUploadImageType = "input";
|
||||
|
||||
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||
const blob = kl.getPNG();
|
||||
|
||||
status = "uploading"
|
||||
|
||||
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
|
||||
.then((entry: ComfyImageLocation) => {
|
||||
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
|
||||
$nodeValue = [meta] // TODO more than one image
|
||||
status = "uploaded"
|
||||
notify("Saved image to ComfyUI!", { type: "success" })
|
||||
onSuccess();
|
||||
})
|
||||
.catch(err => {
|
||||
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
|
||||
status = "error"
|
||||
uploadError = err;
|
||||
$nodeValue = []
|
||||
onError();
|
||||
})
|
||||
}
|
||||
|
||||
let closeDialog = null;
|
||||
|
||||
async function saveAndClose() {
|
||||
console.log(closeDialog, kl)
|
||||
if (!closeDialog || !kl)
|
||||
return;
|
||||
|
||||
submitKlecksToComfyUI(() => {}, () => {});
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
let blankImageWidth = 512;
|
||||
let blankImageHeight = 512;
|
||||
|
||||
async function openImageEditor() {
|
||||
if (!editorRoot)
|
||||
return;
|
||||
|
||||
showModal = true;
|
||||
|
||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||
|
||||
kl = new Klecks({
|
||||
embedUrl: url,
|
||||
onSubmit: submitKlecksToComfyUI,
|
||||
targetEl: editorRoot,
|
||||
warnOnPageClose: false
|
||||
});
|
||||
|
||||
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
|
||||
|
||||
let canvas = null;
|
||||
let width = blankImageWidth;
|
||||
let height = blankImageHeight;
|
||||
|
||||
if ($nodeValue && $nodeValue.length > 0) {
|
||||
const comfyImage = $nodeValue[0];
|
||||
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
|
||||
[canvas, width, height] = await generateImageCanvas(comfyURL);
|
||||
}
|
||||
else {
|
||||
canvas = generateBlankCanvas(width, height);
|
||||
}
|
||||
|
||||
kl.openProject({
|
||||
width: width,
|
||||
height: height,
|
||||
layers: [{
|
||||
name: 'Image',
|
||||
opacity: 1,
|
||||
mixModeStr: 'source-over',
|
||||
image: canvas
|
||||
}]
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
kl.klApp?.out("yo");
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
let status = "empty";
|
||||
let uploadError = null;
|
||||
|
||||
function onUploading() {
|
||||
console.warn("UPLOADING!!!")
|
||||
uploadError = null;
|
||||
status = "uploading"
|
||||
}
|
||||
|
||||
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
|
||||
console.warn("UPLOADED!!!")
|
||||
uploadError = null;
|
||||
status = "uploaded"
|
||||
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
console.warn("CLEAR!!!")
|
||||
uploadError = null;
|
||||
status = "empty"
|
||||
$nodeValue = []
|
||||
}
|
||||
|
||||
function onUploadError(e: CustomEvent<any>) {
|
||||
console.warn("ERROR!!!")
|
||||
status = "error"
|
||||
uploadError = e.detail
|
||||
$nodeValue = []
|
||||
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
|
||||
}
|
||||
|
||||
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
|
||||
}
|
||||
|
||||
let _value: ComfyImageLocation[] = []
|
||||
$: if ($nodeValue)
|
||||
_value = $nodeValue.map(comfyBoxImageToComfyFile)
|
||||
else
|
||||
_value = []
|
||||
|
||||
$: canEdit = status === "empty" || status === "uploaded";
|
||||
</script>
|
||||
|
||||
<div class="wrapper comfy-image-editor">
|
||||
{#if widget.attrs.variant === "fileUpload" || isMobile}
|
||||
<ImageUpload value={_value}
|
||||
bind:imgWidth
|
||||
bind:imgHeight
|
||||
fileCount={"single"}
|
||||
elem_classes={[]}
|
||||
style={""}
|
||||
label={widget.attrs.title}
|
||||
on:uploading={onUploading}
|
||||
on:uploaded={onUploaded}
|
||||
on:upload_error={onUploadError}
|
||||
on:clear={onClear}
|
||||
on:change={onChange}
|
||||
on:image_clicked={openImageEditor}
|
||||
/>
|
||||
{:else}
|
||||
<div class="comfy-image-editor-panel">
|
||||
<ImageUpload value={_value}
|
||||
bind:imgWidth
|
||||
bind:imgHeight
|
||||
fileCount={"single"}
|
||||
elem_classes={[]}
|
||||
style={""}
|
||||
label={widget.attrs.title}
|
||||
on:uploading={onUploading}
|
||||
on:uploaded={onUploaded}
|
||||
on:upload_error={onUploadError}
|
||||
on:clear={onClear}
|
||||
on:change={onChange}
|
||||
on:image_clicked={openImageEditor}
|
||||
/>
|
||||
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
|
||||
<div>
|
||||
<div id="klecks-loading-screen">
|
||||
<span id="klecks-loading-screen-text"></span>
|
||||
</div>
|
||||
<div class="image-editor-root" bind:this={editorRoot} />
|
||||
</div>
|
||||
<div slot="buttons">
|
||||
<Button variant="primary" on:click={saveAndClose}>
|
||||
Save and Close
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={closeDialog}>
|
||||
Discard Edits
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Block>
|
||||
{#if !$nodeValue || $nodeValue.length === 0}
|
||||
<Row>
|
||||
<Row>
|
||||
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||
Create Image
|
||||
</Button>
|
||||
<div>
|
||||
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||
</div>
|
||||
{#if uploadError}
|
||||
<div>
|
||||
Upload error: {uploadError}
|
||||
</div>
|
||||
{/if}
|
||||
</Row>
|
||||
<Row>
|
||||
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
|
||||
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
|
||||
</Row>
|
||||
</Row>
|
||||
{:else}
|
||||
<Row>
|
||||
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||
Edit Image
|
||||
</Button>
|
||||
<div>
|
||||
<TextBox label={""} show_label={false} disabled={true} lines={1} max_lines={1} value="Status: {status}"/>
|
||||
</div>
|
||||
{#if uploadError}
|
||||
<div>
|
||||
Upload error: {uploadError}
|
||||
</div>
|
||||
{/if}
|
||||
</Row>
|
||||
{/if}
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.image-editor-root {
|
||||
width: 75vw;
|
||||
height: 75vh;
|
||||
overflow: hidden;
|
||||
|
||||
color: black;
|
||||
|
||||
:global(> .g-root) {
|
||||
height: calc(100% - 59px);
|
||||
}
|
||||
}
|
||||
|
||||
.comfy-image-editor {
|
||||
:global(> dialog) {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.kl-popup) {
|
||||
z-index: 999999999999;
|
||||
}
|
||||
</style>
|
||||
|
||||
173
src/lib/widgets/NumberWidget.svelte
Normal file
173
src/lib/widgets/NumberWidget.svelte
Normal file
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Range } from "$lib/components/gradio/form";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { debounce } from "$lib/utils";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
import { isDisabled } from "./utils"
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyNumberNode | null = null;
|
||||
let nodeValue: Writable<number> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
let isDragging: boolean = false;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfyNumberNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
setOption($nodeValue); // don't react on option
|
||||
}
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
// I don't know why but this is necessary to watch for changes to node
|
||||
// properties from ComfyWidgetNode.
|
||||
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
|
||||
setOption($nodeValue)
|
||||
setNodeValue(widget)
|
||||
node.properties = node.properties
|
||||
}
|
||||
|
||||
function setOption(value: any) {
|
||||
option = value;
|
||||
}
|
||||
|
||||
function onRelease(e: Event) {
|
||||
if (nodeValue && option != null) {
|
||||
$nodeValue = option
|
||||
}
|
||||
}
|
||||
|
||||
function setBackgroundSize(input: HTMLInputElement) {
|
||||
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
|
||||
}
|
||||
|
||||
function getBackgroundSize(input: HTMLInputElement) {
|
||||
const min = +input.min || 0;
|
||||
const max = +input.max || 100;
|
||||
const value = +input.value;
|
||||
|
||||
return (value - min) / (max - min) * 100;
|
||||
}
|
||||
|
||||
function updateSliderForMobile() {
|
||||
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
|
||||
setBackgroundSize(target);
|
||||
}
|
||||
|
||||
let elem: HTMLDivElement = null;
|
||||
|
||||
$: if (elem) {
|
||||
updateSliderForMobile()
|
||||
}
|
||||
|
||||
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
|
||||
const slider = elem.querySelector("input[type='range']") as any
|
||||
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
|
||||
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
|
||||
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
|
||||
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
|
||||
const style = elem.style;
|
||||
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (!isMobile)
|
||||
return;
|
||||
|
||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||
}
|
||||
|
||||
let canVibrate = true;
|
||||
let lastDisplayValue = null;
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (!isMobile)
|
||||
return;
|
||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||
|
||||
if (canVibrate && lastDisplayValue != option) {
|
||||
lastDisplayValue = option;
|
||||
canVibrate = false;
|
||||
setTimeout(() => { canVibrate = true }, 30)
|
||||
navigator.vibrate(10)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
|
||||
{#if node !== null && option !== null}
|
||||
<Range
|
||||
bind:value={option}
|
||||
disabled={isDisabled(widget)}
|
||||
minimum={node.properties.min}
|
||||
maximum={node.properties.max}
|
||||
step={node.properties.step}
|
||||
label={widget.attrs.title}
|
||||
show_label={true}
|
||||
on:release={onRelease}
|
||||
on:change={updateSliderForMobile}
|
||||
on:pointerdown={onPointerDown}
|
||||
on:pointermove={onPointerMove}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
|
||||
:global(input[type=number]) {
|
||||
text-overflow: ellipsis;
|
||||
&:disabled {
|
||||
@include disable-input;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
// Prevent swiping on the slider track from accidentally changing the value
|
||||
:global(input[type="range"]) {
|
||||
pointer-events: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: default;
|
||||
height: 0.6rem;
|
||||
padding: initial;
|
||||
border: initial;
|
||||
margin: 0.8rem 0;
|
||||
width: 100%;
|
||||
|
||||
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
|
||||
background-size: var(--background-size, 0%) 100%;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--neutral-400);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
pointer-events: all;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-blue-600);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--neutral-100);
|
||||
box-shadow: 0px 0px 0px 1px var(--neutral-400);
|
||||
}
|
||||
|
||||
:global(input[type=number]) {
|
||||
font-size: 16px;
|
||||
height: var(--size-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyRadioNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Radio } from "@gradio/form";
|
||||
@@ -7,6 +6,7 @@
|
||||
import { isDisabled } from "./utils"
|
||||
import type { SelectData } from "@gradio/utils";
|
||||
import { clamp } from "$lib/utils";
|
||||
import type { ComfyRadioNode } from "$lib/nodes/widgets";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Range } from "$lib/components/gradio/form";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { debounce } from "$lib/utils";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
import { isDisabled } from "./utils"
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfySliderNode | null = null;
|
||||
let nodeValue: Writable<number> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
let isDragging: boolean = false;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfySliderNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
setOption($nodeValue); // don't react on option
|
||||
}
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
// I don't know why but this is necessary to watch for changes to node
|
||||
// properties from ComfyWidgetNode.
|
||||
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
|
||||
setOption($nodeValue)
|
||||
setNodeValue(widget)
|
||||
node.properties = node.properties
|
||||
}
|
||||
|
||||
function setOption(value: any) {
|
||||
option = value;
|
||||
}
|
||||
|
||||
function onRelease(e: Event) {
|
||||
if (nodeValue && option != null) {
|
||||
$nodeValue = option
|
||||
}
|
||||
}
|
||||
|
||||
function setBackgroundSize(input: HTMLInputElement) {
|
||||
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
|
||||
}
|
||||
|
||||
function getBackgroundSize(input: HTMLInputElement) {
|
||||
const min = +input.min || 0;
|
||||
const max = +input.max || 100;
|
||||
const value = +input.value;
|
||||
|
||||
return (value - min) / (max - min) * 100;
|
||||
}
|
||||
|
||||
function updateSliderForMobile() {
|
||||
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
|
||||
setBackgroundSize(target);
|
||||
}
|
||||
|
||||
let elem: HTMLDivElement = null;
|
||||
|
||||
$: if (elem) {
|
||||
updateSliderForMobile()
|
||||
}
|
||||
|
||||
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
|
||||
const slider = elem.querySelector("input[type='range']") as any
|
||||
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
|
||||
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
|
||||
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
|
||||
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
|
||||
const style = elem.style;
|
||||
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (!isMobile)
|
||||
return;
|
||||
|
||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||
}
|
||||
|
||||
let canVibrate = true;
|
||||
let lastDisplayValue = null;
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (!isMobile)
|
||||
return;
|
||||
interfaceState.showIndicator(e.clientX, e.clientY, option);
|
||||
|
||||
if (canVibrate && lastDisplayValue != option) {
|
||||
lastDisplayValue = option;
|
||||
canVibrate = false;
|
||||
setTimeout(() => { canVibrate = true }, 30)
|
||||
navigator.vibrate(10)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
|
||||
{#if node !== null && option !== null}
|
||||
<Range
|
||||
bind:value={option}
|
||||
disabled={isDisabled(widget)}
|
||||
minimum={node.properties.min}
|
||||
maximum={node.properties.max}
|
||||
step={node.properties.step}
|
||||
label={widget.attrs.title}
|
||||
show_label={true}
|
||||
on:release={onRelease}
|
||||
on:change={updateSliderForMobile}
|
||||
on:pointerdown={onPointerDown}
|
||||
on:pointermove={onPointerMove}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
|
||||
:global(input[type=number]) {
|
||||
text-overflow: ellipsis;
|
||||
&:disabled {
|
||||
@include disable-input;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
// Prevent swiping on the slider track from accidentally changing the value
|
||||
:global(input[type="range"]) {
|
||||
pointer-events: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: default;
|
||||
height: 0.6rem;
|
||||
padding: initial;
|
||||
border: initial;
|
||||
margin: 0.8rem 0;
|
||||
width: 100%;
|
||||
|
||||
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
|
||||
background-size: var(--background-size, 0%) 100%;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--neutral-400);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
pointer-events: all;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-blue-600);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--neutral-100);
|
||||
box-shadow: 0px 0px 0px 1px var(--neutral-400);
|
||||
}
|
||||
|
||||
:global(input[type=number]) {
|
||||
font-size: 16px;
|
||||
height: var(--size-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { TextBox } from "@gradio/form";
|
||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { type Writable } from "svelte/store";
|
||||
import { isDisabled } from "./utils"
|
||||
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyComboNode | null = null;
|
||||
|
||||
let node: ComfyTextNode | null = null;
|
||||
let nodeValue: Writable<string> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfySliderNode
|
||||
node = widget.node as ComfyTextNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as ComfyGalleryWidget } from "./ComfyGalleryWidget"
|
||||
export { default as ComfyGalleryWidget_Svelte } from "./ComfyGalleryWidget.svelte"
|
||||
Reference in New Issue
Block a user