Integrate klecks

This commit is contained in:
space-nuko
2023-05-13 12:43:34 -05:00
parent 4fdd373286
commit ea8502521b
12 changed files with 2086 additions and 353 deletions

4
.gitmodules vendored
View File

@@ -5,3 +5,7 @@
[submodule "litegraph"] [submodule "litegraph"]
path = litegraph path = litegraph
url = https://github.com/space-nuko/litegraph.ts url = https://github.com/space-nuko/litegraph.ts
[submodule "klecks"]
path = klecks
url = https://github.com/space-nuko/klecks
branch = comfybox

1
klecks Submodule

Submodule klecks added at 78c61a032e

View File

@@ -1,5 +1,5 @@
{ {
"name": "web2", "name": "ComfyBox",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -28,6 +28,7 @@
"svelte-dnd-action": "^0.9.22", "svelte-dnd-action": "^0.9.22",
"typescript": "^5.0.3", "typescript": "^5.0.3",
"vite": "^4.3.1", "vite": "^4.3.1",
"vite-plugin-glsl": "^1.1.2",
"vite-plugin-static-copy": "^0.14.0", "vite-plugin-static-copy": "^0.14.0",
"vite-plugin-svelte-console-remover": "^1.0.10", "vite-plugin-svelte-console-remover": "^1.0.10",
"vite-tsconfig-paths": "^4.0.8", "vite-tsconfig-paths": "^4.0.8",
@@ -62,6 +63,7 @@
"framework7": "^8.0.3", "framework7": "^8.0.3",
"framework7-svelte": "^8.0.3", "framework7-svelte": "^8.0.3",
"img-comparison-slider": "^8.0.0", "img-comparison-slider": "^8.0.0",
"klecks": "workspace:*",
"pollen-css": "^4.6.2", "pollen-css": "^4.6.2",
"radix-icons-svelte": "^1.2.1", "radix-icons-svelte": "^1.2.1",
"svelte-feather-icons": "^4.0.0", "svelte-feather-icons": "^4.0.0",

1667
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,3 +2,4 @@ packages:
- 'gradio/js/*' - 'gradio/js/*'
- 'gradio/client/js' - 'gradio/client/js'
- 'litegraph/packages/*' - 'litegraph/packages/*'
- 'klecks'

View File

@@ -0,0 +1,239 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { File as FileIcon } from "@gradio/icons";
import { ModifyUpload, Upload, blobToBase64, normalise_file } from "@gradio/upload";
import type { FileData as GradioFileData } from "@gradio/upload";
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
import { tick } from "svelte";
import notify from "$lib/notify";
import type { ComfyUploadImageAPIResponse } from "$lib/utils";
export let value: GradioFileData[] | null = null;
export let isMobile: boolean = false;
export let imgWidth: number = 0;
export let imgHeight: number = 0;
export let imgElem: HTMLImageElement | null = null
export let fileCount: "single" | "multiple" = "single"
export let elem_classes: string[] = []
export let style: string = ""
export let label: string = ""
// let propsChanged: Writable<number> | null = null;
let dragging = false;
let pending_upload = false;
let old_value: Array<GradioFileData> | null = null;
let _value: GradioFileData[] | null = null;
const root = "comf"
const root_url = "https//ComfyUI!"
const dispatch = createEventDispatcher<{
change: GradioFileData[];
upload: undefined;
clear: undefined;
}>();
$: _value = normalise_file(value, root, root_url);
$: if (!(_value && _value.length > 0 && imgElem)) {
imgWidth = 1
imgHeight = 1
}
function onChange() {
value = _value || []
dispatch("change", value)
}
function onUpload() {
dispatch("upload")
}
function onClear() {
dispatch("clear")
}
interface GradioUploadResponse {
error?: string;
files?: Array<string>;
}
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
console.debug("UPLOADILFES", root, files);
const url = `http://${location.hostname}:8188` // TODO make configurable
const requests = files.map(async (file) => {
const formData = new FormData();
formData.append("image", file, file.name);
return fetch(new Request(url + "/upload/image", {
body: formData,
method: 'POST'
}))
.then(r => r.json())
.catch(error => error);
});
return Promise.all(requests)
.then( (results) => {
const errors = []
const files = []
for (const r of results) {
if (r instanceof Error) {
errors.push(r.cause)
}
else {
// bare filename of image
files.push((r as ComfyUploadImageAPIResponse).name)
}
}
let error = null;
if (errors && errors.length > 0)
error = "Upload error(s):\n" + errors.join("\n");
return { error, files }
})
}
$: {
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
pending_upload = true;
old_value = _value;
if (_value == null)
_value = []
else if (!Array.isArray(_value))
_value = [_value]
const allBlobs = _value.map((file_data: GradioFileData) => file_data.blob)
if (allBlobs == null || allBlobs.length === 0) {
_value = null;
onChange();
pending_upload = false;
}
else if (!allBlobs.every(b => b != null)) {
_value = null;
pending_upload = false;
}
else {
let files = (Array.isArray(_value) ? _value : [_value]).map(
(file_data) => file_data.blob!
);
let upload_value = _value;
pending_upload = true;
upload_files(root, files).then((response) => {
if (JSON.stringify(upload_value) !== JSON.stringify(_value)) {
// value has changed since upload started
console.error("[ImageUpload] value has changed since upload started", upload_value, _value)
return;
}
pending_upload = false;
_value.forEach(
(file_data: GradioFileData, i: number) => {
if (response.files) {
file_data.orig_name = file_data.name;
file_data.name = response.files[i];
file_data.is_file = true;
}
}
);
if (response.error) {
notify(response.error, { type: "error" })
}
value = normalise_file(_value, root, root_url) as GradioFileData[];
onChange();
onUpload();
});
}
}
}
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
_value = Array.isArray(detail) ? detail : [detail];
await tick();
onChange();
onUpload();
}
function handle_clear(_e: CustomEvent<null>) {
_value = null;
onChange();
onClear();
}
function getImageUrl(image: GradioFileData) {
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
return `${baseUrl}/view?${params}`
}
</script>
<div class="image-upload" {style}>
{#if value}
<Block
visible={true}
variant={(value === null || value.length === 0) ? "dashed" : "solid"}
border_mode={dragging ? "focus" : "base"}
padding={true}
elem_id="comfy-image-upload-block"
{elem_classes}
>
<BlockLabel
label={label}
show_label={label != ""}
Icon={FileIcon}
float={label != ""}
/>
{#if _value && _value.length > 0 && !pending_upload}
{@const firstImage = _value[0]}
<ModifyUpload on:clear={handle_clear} absolute />
<img src={getImageUrl(firstImage)}
alt={firstImage.orig_name}
bind:this={imgElem}
bind:naturalWidth={imgWidth}
bind:naturalHeight={imgHeight}
/>
{:else}
<Upload
file_count={fileCount}
filetype="image/*"
on:change={({ detail }) => (value = detail)}
on:load={handle_upload}
bind:dragging
on:clear
on:select
parse_to_data_url={false}
>
<UploadText type="file" />
</Upload>
{/if}
</Block>
{/if}
</div>
<style lang="scss">
.image-upload {
height: var(--size-96);
:global(.block) {
height: inherit;
padding: 0;
border-radius: 0;
}
img {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
</style>

View File

@@ -1,22 +1,33 @@
<script> <script lang="ts">
import { createEventDispatcher } from "svelte";
export let showModal; // boolean export let showModal; // boolean
let dialog; // HTMLDialogElement let dialog; // HTMLDialogElement
const dispatch = createEventDispatcher<{
close: undefined;
}>();
$: if (dialog && showModal) dialog.showModal(); $: if (dialog && showModal) dialog.showModal();
function close() {
dialog.close();
dispatch("close")
}
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<dialog <dialog
bind:this={dialog} bind:this={dialog}
on:close={() => (showModal = false)} on:close={close}
on:click|self={() => dialog.close()} on:click|self={close}
> >
<div on:click|stopPropagation> <div on:click|stopPropagation>
<slot name="header" /> <slot name="header" />
<slot /> <slot />
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
<button autofocus on:click={() => dialog.close()}>Close</button> <button autofocus on:click={close}>Close</button>
</div> </div>
</dialog> </dialog>

View File

@@ -17,7 +17,7 @@ import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte"; import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
import RadioWidget from "$lib/widgets/RadioWidget.svelte"; import RadioWidget from "$lib/widgets/RadioWidget.svelte";
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte"; import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
import ImageCompareWidget from "$lib/widgets/ImageCompareWidget.svelte"; import ImageEditorWidget from "$lib/widgets/ImageEditorWidget.svelte";
export type AutoConfigOptions = { export type AutoConfigOptions = {
includeProperties?: Set<string> | null, includeProperties?: Set<string> | null,
@@ -33,7 +33,7 @@ export type AutoConfigOptions = {
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant" * - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
* attribute and set `validNodeTypes` to the type of the litegraph node * attribute and set `validNodeTypes` to the type of the litegraph node
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget * - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
* - Add an {#if widget.attrs.variant === <...>} statement in the corresponding Svelte component * - Add an {#if widget.attrs.variant === <...>} statement in the existing Svelte component
* *
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame! * Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
* You will have to either implement an internal delay on the event triggering * You will have to either implement an internal delay on the event triggering
@@ -965,80 +965,59 @@ LiteGraph.registerNodeType({
type: "ui/image_upload" type: "ui/image_upload"
}) })
export interface ComfyImageCompareNodeProperties extends ComfyWidgetProperties { export type FileNameOrGalleryData = string | GalleryOutputEntry;
export type MultiImageData = FileNameOrGalleryData[];
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
} }
export type FileNameOrGalleryData = string | GalleryOutputEntry; export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> {
export type ImageCompareData = [FileNameOrGalleryData, FileNameOrGalleryData] override properties: ComfyImageEditorNodeProperties = {
export class ComfyImageCompareNode extends ComfyWidgetNode<ImageCompareData> {
override properties: ComfyImageCompareNodeProperties = {
defaultValue: [], defaultValue: [],
tags: [], tags: [],
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "store", type: BuiltInSlotType.ACTION }, { name: "store", type: BuiltInSlotType.ACTION }
{ name: "left_image", type: "string" },
{ name: "right_image", type: "string" },
], ],
outputs: [ outputs: [
] ]
} }
override svelteComponentType = ImageCompareWidget; override svelteComponentType = ImageEditorWidget;
override defaultValue: ImageCompareData = ["", ""]; override defaultValue: MultiImageData = [];
override outputIndex = null; override outputIndex = null;
override changedIndex = 3; override changedIndex = null;
override storeActionName = "store"; override storeActionName = "store";
override saveUserState = false; override saveUserState = false;
constructor(name?: string) { constructor(name?: string) {
super(name, ["", ""]) super(name, [])
} }
override onExecute() { _value = null;
const valueA = this.getInputData(1)
const valueB = this.getInputData(2)
let current = get(this.value)
let changed = false;
if (valueA != null && current[0] != valueA) {
current[0] = valueA
changed = true;
}
if (valueB != null && current[1] != valueB) {
current[1] = valueB
changed = true;
}
if (changed)
this.value.set(current)
}
override parseValue(value: any): ImageCompareData { override parseValue(value: any): MultiImageData {
if (value == null) { if (value == null) {
return ["", ""] return []
} }
else if (typeof value === "string" && value !== "") { // Single filename else if (typeof value === "string" && value !== "") { // Single filename
const prevValue = get(this.value) const prevValue = get(this.value)
prevValue.push(value) prevValue.push(value)
if (prevValue.length > 2) if (prevValue.length > 2)
prevValue.splice(0, 1) prevValue.splice(0, 1)
return prevValue as ImageCompareData return prevValue as MultiImageData
} }
else if (typeof value === "object" && "images" in value && value.images.length > 0) { else if (typeof value === "object" && "images" in value && value.images.length > 0) {
const output = value as GalleryOutput const output = value as GalleryOutput
const prevValue = get(this.value) return [value]
prevValue.push(output.images[0].filename)
if (prevValue.length > 2)
prevValue.splice(0, 1)
return prevValue as ImageCompareData
} }
else if (Array.isArray(value) && typeof value[0] === "string" && typeof value[1] === "string") { else if (Array.isArray(value) && value.every(s => typeof s === "string")) {
return value as ImageCompareData return value as MultiImageData
} }
else { else {
return ["", ""] return []
} }
} }
@@ -1048,8 +1027,8 @@ export class ComfyImageCompareNode extends ComfyWidgetNode<ImageCompareData> {
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({
class: ComfyImageCompareNode, class: ComfyImageEditorNode,
title: "UI.ImageCompare", title: "UI.ImageEditor",
desc: "Widget that lets you compare two images", desc: "Widget that lets edit a multi-layered image",
type: "ui/image_compare" type: "ui/image_editor"
}) })

View File

@@ -1,80 +0,0 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import ImageComparison from "$lib/components/ImageComparison.svelte";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { SelectData } from "@gradio/utils";
import type { ComfyImageCompareNode, ImageCompareData } from "$lib/nodes/ComfyWidgetNodes";
import { convertFilenameToComfyURL } from "$lib/utils";
import { TabItem, Tabs } from "@gradio/tabs";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageCompareNode | null = null;
let nodeValue: Writable<ImageCompareData> | null = null;
let attrsChanged: Writable<number> | null = null;
let leftUrl: string = ""
let rightUrl: string = ""
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageCompareNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
const urlPattern = /^((http|https|ftp):\/\/)/;
$: updateUrls($nodeValue);
function updateUrls(value: ImageCompareData) {
leftUrl = ""
rightUrl = ""
console.warn("UPD", value)
if (typeof value[0] === "string") {
if (urlPattern.test(value[0]))
leftUrl = value[0]
else
leftUrl = convertFilenameToComfyURL(value[0])
}
if (typeof value[1] === "string") {
if (urlPattern.test(value[1]))
rightUrl = value[1]
else
rightUrl = convertFilenameToComfyURL(value[1])
}
}
</script>
<div class="wrapper comfy-compare-widget">
<Block>
<Tabs elem_classes={["gradio-tabs"]}>
<TabItem name="Slider">
<ImageComparison>
{#if leftUrl && leftUrl != ""}
{@const props = { slot: "first" }}
<img {...props} alt="Left" src={leftUrl} />
{/if}
{#if rightUrl && leftUrl != ""}
{@const props = { slot: "second" }}
<img {...props} alt="Right" src={rightUrl} />
{/if}
</ImageComparison>
</TabItem>
</Tabs>
</Block>
</div>
<style lang="scss">
.comfy-compare-widget {
max-width: 40rem;
img {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,148 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block, BlockTitle } from "@gradio/atoms";
import { get, type Writable, writable } from "svelte/store";
import Modal from "$lib/components/Modal.svelte";
import { Button } from "@gradio/button";
import type { ComfyImageEditorNode, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import { Embed as Klecks, KL, KlApp, klHistory, type KlAppOptionsEmbed } from "klecks";
import "klecks/style/style.scss";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageEditorNode | null = null;
let nodeValue: Writable<MultiImageData> | null = null;
let attrsChanged: Writable<number> | null = null;
let leftUrl: string = ""
let rightUrl: string = ""
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageEditorNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
const urlPattern = /^((http|https|ftp):\/\/)/;
$: updateUrls($nodeValue);
function updateUrls(value: MultiImageData) {
// leftUrl = ""
// rightUrl = ""
// console.warn("UPD", value)
//
// if (typeof value[0] === "string") {
// if (urlPattern.test(value[0]))
// leftUrl = value[0]
// else
// leftUrl = convertFilenameToComfyURL(value[0])
// }
// if (typeof value[1] === "string") {
// if (urlPattern.test(value[1]))
// rightUrl = value[1]
// else
// rightUrl = convertFilenameToComfyURL(value[1])
// }
}
let editorRoot: HTMLDivElement | null = null;
let showModal = false;
let kl: Klecks | null = null;
function disposeEditor() {
if (editorRoot) {
while (editorRoot.firstChild) {
editorRoot.removeChild(editorRoot.firstChild);
}
}
kl = null;
showModal = false;
}
function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
const data = kl.getPNG();
}
function generateBlankImage(fill: string = "#fff"): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = fill,
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
return canvas;
}
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.parentElement.parentElement
});
kl.openProject({
width: 512,
height: 512,
layers: [{
name: 'Image',
opacity: 1,
mixModeStr: 'source-over',
image: generateBlankImage(),
}]
});
setTimeout(function () {
kl.klApp?.out("yo");
}, 1000);
}
</script>
<div class="wrapper comfy-image-editor">
{#if isMobile}
<span>TODO mask editor</span>
{:else}
<Modal bind:showModal on:close={disposeEditor}>
<div id="klecks-loading-screen">
<span id="klecks-loading-screen-text"></span>
</div>
<div class="image-editor-root" bind:this={editorRoot} />
</Modal>
<div class="comfy-image-editor-panel">
<Block>
<BlockTitle>Image editor.</BlockTitle>
<Button variant="secondary" on:click={openImageEditor}>
Open
</Button>
</Block>
</div>
{/if}
</div>
<style lang="scss">
.image-editor-root {
width: 75vw;
height: 75vh;
overflow: hidden;
}
.comfy-image-editor {
:global(> dialog) {
overflow: hidden;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Block, BlockLabel, Empty } from "@gradio/atoms"; import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { File as FileIcon } from "@gradio/icons"; import { File as FileIcon } from "@gradio/icons";
import { ModifyUpload, Upload, blobToBase64, normalise_file } from "@gradio/upload"; import ImageUpload from "$lib/components/ImageUpload.svelte"
import type { WidgetLayout } from "$lib/stores/layoutState"; import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes"; import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
@@ -19,29 +19,12 @@
let dragging = false; let dragging = false;
let pending_upload = false; let pending_upload = false;
let old_value: Array<GradioFileData> | null = null; let old_value: Array<GradioFileData> | null = null;
let imgElem: HTMLImageElement | null = null
let imgWidth: number = 1; let imgWidth: number = 1;
let imgHeight: number = 1; let imgHeight: number = 1;
$: widget && setNodeValue(widget); $: widget && setNodeValue(widget);
let _value: GradioFileData[] | null = null; $: if (!(node && $nodeValue && $nodeValue.length > 0)) {
const root = "comf"
const root_url = "https//ComfyUI!"
$: _value = normalise_file($nodeValue, root, root_url);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageUploadNode
nodeValue = node.value;
propsChanged = node.propsChanged;
}
};
$: if (!(node && _value && _value.length > 0 && imgElem)) {
imgWidth = 1
imgHeight = 1
node.imageSize = [1, 1] node.imageSize = [1, 1]
} }
else if (imgWidth > 1 || imgHeight > 1) { else if (imgWidth > 1 || imgHeight > 1) {
@@ -51,178 +34,30 @@
node.imageSize = [1, 1] node.imageSize = [1, 1]
} }
function onChange() { function setNodeValue(widget: WidgetLayout) {
$nodeValue = _value || [] if (widget) {
} node = widget.node as ComfyImageUploadNode
nodeValue = node.value;
function onUpload() { propsChanged = node.propsChanged;
}
function onClear() {
}
interface GradioUploadResponse {
error?: string;
files?: Array<string>;
}
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
console.debug("UPLOADILFES", root, files);
const url = `http://${location.hostname}:8188` // TODO make configurable
const requests = files.map(async (file) => {
const formData = new FormData();
formData.append("image", file, file.name);
return fetch(new Request(url + "/upload/image", {
body: formData,
method: 'POST'
}))
.then(r => r.json())
.catch(error => error);
});
return Promise.all(requests)
.then( (results) => {
const errors = []
const files = []
for (const r of results) {
if (r instanceof Error) {
errors.push(r.cause)
}
else {
// bare filename of image
files.push((r as ComfyUploadImageAPIResponse).name)
}
}
let error = null;
if (errors && errors.length > 0)
error = "Upload error(s):\n" + errors.join("\n");
return { error, files }
})
}
$: {
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
pending_upload = true;
old_value = _value;
if (_value == null)
_value = []
else if (!Array.isArray(_value))
_value = [_value]
const allBlobs = _value.map((file_data: GradioFileData) => file_data.blob)
if (allBlobs == null || allBlobs.length === 0) {
_value = null;
onChange();
pending_upload = false;
}
else if (!allBlobs.every(b => b != null)) {
_value = null;
pending_upload = false;
}
else {
let files = (Array.isArray(_value) ? _value : [_value]).map(
(file_data) => file_data.blob!
);
let upload_value = _value;
pending_upload = true;
upload_files(root, files).then((response) => {
if (JSON.stringify(upload_value) !== JSON.stringify(_value)) {
// value has changed since upload started
console.error("[ImageUploadWidget] value has changed since upload started", upload_value, _value)
return;
}
pending_upload = false;
_value.forEach(
(file_data: GradioFileData, i: number) => {
if (response.files) {
file_data.orig_name = file_data.name;
file_data.name = response.files[i];
file_data.is_file = true;
}
}
);
if (response.error) {
notify(response.error, { type: "error" })
}
$nodeValue = normalise_file(_value, root, root_url) as GradioFileData[];
onChange();
onUpload();
});
}
} }
} };
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) { function onChange(e: CustomEvent<GradioFileData[]>) {
_value = Array.isArray(detail) ? detail : [detail]; $nodeValue = e.detail || []
await tick();
onChange();
onUpload();
}
function handle_clear(_e: CustomEvent<null>) {
_value = null;
onChange();
onClear();
}
function getImageUrl(image: GradioFileData) {
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
return `${baseUrl}/view?${params}`
} }
</script> </script>
<div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}> <div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}>
{#if widget && node && nodeValue} {#if widget && node && nodeValue}
<Block <ImageUpload value={$nodeValue}
visible={true} {isMobile}
variant={($nodeValue === null || $nodeValue.length === 0) ? "dashed" : "solid"} bind:imgWidth
border_mode={dragging ? "focus" : "base"} bind:imgHeight
padding={true} bind:fileCount={node.properties.fileCount}
elem_id="comfy-image-upload-block" elem_classes={widget.attrs.classes.split(",")}
elem_classes={widget.attrs.classes.split(",")} style={widget.attrs.style}
> label={widget.attrs.title}
<BlockLabel on:change={onChange}/>
label={widget.attrs.title}
show_label={widget.attrs.title != ""}
Icon={FileIcon}
float={widget.attrs.title != ""}
/>
{#if _value && _value.length > 0 && !pending_upload}
{@const firstImage = _value[0]}
<ModifyUpload on:clear={handle_clear} absolute />
<img src={getImageUrl(firstImage)}
alt={firstImage.orig_name}
bind:this={imgElem}
bind:naturalWidth={imgWidth}
bind:naturalHeight={imgHeight}
/>
{:else}
<Upload
file_count={node.properties.fileCount}
filetype="image/*"
on:change={({ detail }) => ($nodeValue = detail)}
on:load={handle_upload}
bind:dragging
on:clear
on:select
parse_to_data_url={false}
>
<UploadText type="file" />
</Upload>
{/if}
</Block>
{/if} {/if}
</div> </div>

View File

@@ -5,6 +5,7 @@ import tsconfigPaths from 'vite-tsconfig-paths';
import FullReload from 'vite-plugin-full-reload'; import FullReload from 'vite-plugin-full-reload';
import { viteStaticCopy } from 'vite-plugin-static-copy' import { viteStaticCopy } from 'vite-plugin-static-copy'
import removeConsole from 'vite-plugin-svelte-console-remover'; import removeConsole from 'vite-plugin-svelte-console-remover';
import glsl from 'vite-plugin-glsl';
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
console.log("Production build: " + isProduction) console.log("Production build: " + isProduction)
@@ -20,6 +21,7 @@ export default defineConfig({
// "src/**/ComfyApp.{ts,svelte}" // "src/**/ComfyApp.{ts,svelte}"
// ]), // ]),
isProduction && removeConsole(), isProduction && removeConsole(),
glsl(),
svelte(), svelte(),
viteStaticCopy({ viteStaticCopy({
targets: [ targets: [