Image upload widget
This commit is contained in:
@@ -37,9 +37,11 @@
|
|||||||
"@gradio/atoms": "workspace:*",
|
"@gradio/atoms": "workspace:*",
|
||||||
"@gradio/button": "workspace:*",
|
"@gradio/button": "workspace:*",
|
||||||
"@gradio/client": "workspace:*",
|
"@gradio/client": "workspace:*",
|
||||||
|
"@gradio/file": "workspace:*",
|
||||||
"@gradio/form": "workspace:*",
|
"@gradio/form": "workspace:*",
|
||||||
"@gradio/gallery": "workspace:*",
|
"@gradio/gallery": "workspace:*",
|
||||||
"@gradio/icons": "workspace:*",
|
"@gradio/icons": "workspace:*",
|
||||||
|
"@gradio/image": "workspace:*",
|
||||||
"@gradio/tabs": "workspace:*",
|
"@gradio/tabs": "workspace:*",
|
||||||
"@gradio/theme": "workspace:*",
|
"@gradio/theme": "workspace:*",
|
||||||
"@gradio/upload": "workspace:*",
|
"@gradio/upload": "workspace:*",
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -16,6 +16,9 @@ importers:
|
|||||||
'@gradio/client':
|
'@gradio/client':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:gradio/client/js
|
version: link:gradio/client/js
|
||||||
|
'@gradio/file':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:gradio/js/file
|
||||||
'@gradio/form':
|
'@gradio/form':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:gradio/js/form
|
version: link:gradio/js/form
|
||||||
@@ -25,6 +28,9 @@ importers:
|
|||||||
'@gradio/icons':
|
'@gradio/icons':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:gradio/js/icons
|
version: link:gradio/js/icons
|
||||||
|
'@gradio/image':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:gradio/js/image
|
||||||
'@gradio/tabs':
|
'@gradio/tabs':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:gradio/js/tabs
|
version: link:gradio/js/tabs
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<div class="animation-wrapper"
|
<div class="animation-wrapper"
|
||||||
class:hidden={hidden}
|
class:hidden={hidden}
|
||||||
animate:flip={{duration:flipDurationMs}}
|
animate:flip={{duration:flipDurationMs}}
|
||||||
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
|
style={item?.attrs?.style || ""}
|
||||||
>
|
>
|
||||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||||
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||||
@@ -183,6 +183,7 @@
|
|||||||
.animation-wrapper {
|
.animation-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 100;
|
flex-grow: 100;
|
||||||
|
flex-basis: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle-widget:hover {
|
.handle-widget:hover {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
<div class="animation-wrapper"
|
<div class="animation-wrapper"
|
||||||
class:hidden={hidden}
|
class:hidden={hidden}
|
||||||
animate:flip={{duration:flipDurationMs}}
|
animate:flip={{duration:flipDurationMs}}
|
||||||
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
|
style={item?.attrs?.style || ""}
|
||||||
>
|
>
|
||||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||||
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||||
@@ -238,6 +238,7 @@
|
|||||||
.animation-wrapper {
|
.animation-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 100;
|
flex-grow: 100;
|
||||||
|
flex-basis: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle-hidden {
|
.handle-hidden {
|
||||||
|
|||||||
@@ -715,7 +715,8 @@ export default class ComfyApp {
|
|||||||
comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
|
comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
|
||||||
const inputNode = node.getInputNode(index)
|
const inputNode = node.getInputNode(index)
|
||||||
|
|
||||||
if (inputNode && "doAutoConfig" in inputNode) {
|
if (inputNode && "doAutoConfig" in inputNode && comfyInput.widgetNodeType === inputNode.type) {
|
||||||
|
console.debug("[ComfyApp] Reconfiguring combo widget", inputNode.type, comfyInput.config.values)
|
||||||
const comfyComboNode = inputNode as nodes.ComfyComboNode;
|
const comfyComboNode = inputNode as nodes.ComfyComboNode;
|
||||||
comfyComboNode.doAutoConfig(comfyInput)
|
comfyComboNode.doAutoConfig(comfyInput)
|
||||||
if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {
|
if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
<div class="animation-wrapper"
|
<div class="animation-wrapper"
|
||||||
class:hidden={hidden}
|
class:hidden={hidden}
|
||||||
animate:flip={{duration:flipDurationMs}}
|
animate:flip={{duration:flipDurationMs}}
|
||||||
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}>
|
style={item?.attrs?.style || ""}>
|
||||||
<Block>
|
<Block>
|
||||||
<label for={String(item.id)}>
|
<label for={String(item.id)}>
|
||||||
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
|
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
|
||||||
@@ -198,6 +198,7 @@
|
|||||||
.animation-wrapper {
|
.animation-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 100;
|
flex-grow: 100;
|
||||||
|
flex-basis: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle-widget:hover {
|
.handle-widget:hover {
|
||||||
|
|||||||
38
src/lib/components/gradio/app/UploadText.svelte
Normal file
38
src/lib/components/gradio/app/UploadText.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let type: "video" | "image" | "audio" | "file" | "csv" = "file";
|
||||||
|
|
||||||
|
const defs = {
|
||||||
|
image: "interface.drop_image",
|
||||||
|
video: "interface.drop_video",
|
||||||
|
audio: "interface.drop_audio",
|
||||||
|
file: "interface.drop_file",
|
||||||
|
csv: "interface.drop_csv"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
{"Drop Image here"}
|
||||||
|
<span class="or">- {"or"} -</span>
|
||||||
|
{"Click to Upload"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: var(--size-60);
|
||||||
|
color: var(--block-label-text-color);
|
||||||
|
line-height: var(--line-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.or {
|
||||||
|
color: var(--body-text-color-subdued);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (--screen-md) {
|
||||||
|
.wrap {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -207,21 +207,29 @@ LiteGraph.registerNodeType({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties {
|
export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties {
|
||||||
tag: string | null,
|
targetTag: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
|
export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
|
||||||
override properties: ComfyExecuteSubgraphActionProperties = {
|
override properties: ComfyExecuteSubgraphActionProperties = {
|
||||||
tag: null
|
tags: [],
|
||||||
|
targetTag: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "execute", type: BuiltInSlotType.ACTION },
|
{ name: "execute", type: BuiltInSlotType.ACTION },
|
||||||
{ name: "tag", type: "string" }
|
{ name: "targetTag", type: "string" }
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayWidget: ITextWidget;
|
||||||
|
|
||||||
|
constructor(title?: string) {
|
||||||
|
super(title)
|
||||||
|
this.displayWidget = this.addWidget("text", "targetTag", this.properties.targetTag, "targetTag")
|
||||||
|
}
|
||||||
|
|
||||||
override onExecute() {
|
override onExecute() {
|
||||||
const tag = this.getInputData(1)
|
const tag = this.getInputData(1)
|
||||||
if (tag)
|
if (tag)
|
||||||
@@ -229,7 +237,7 @@ export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override onAction(action: any, param: any) {
|
override onAction(action: any, param: any) {
|
||||||
const tag = this.getInputData(1) || this.properties.tag;
|
const tag = this.getInputData(1) || this.properties.targetTag;
|
||||||
|
|
||||||
const app = (window as any)?.app;
|
const app = (window as any)?.app;
|
||||||
if (!app)
|
if (!app)
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties
|
|||||||
|
|
||||||
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
|
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
|
||||||
|
|
||||||
|
interface ComfyUploadImageAPIResponse {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A node that can act as both an input and output image node by uploading
|
* A node that can act as both an input and output image node by uploading
|
||||||
* the output file into ComfyUI's input folder.
|
* the output file into ComfyUI's input folder.
|
||||||
@@ -159,6 +163,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
else {
|
else {
|
||||||
this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
|
this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
|
||||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
this.onPropertyChanged("filenames", this.properties.filenames)
|
||||||
|
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
const params = new URLSearchParams(data)
|
const params = new URLSearchParams(data)
|
||||||
|
|
||||||
@@ -176,10 +181,10 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((json) => {
|
.then((json: ComfyUploadImageAPIResponse) => {
|
||||||
console.debug("Gottem", json)
|
console.debug("Gottem", json)
|
||||||
if (lastGenNumber === this.properties.genNumber) {
|
if (lastGenNumber === this.properties.genNumber) {
|
||||||
this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" }
|
this.properties.filenames[newIndex] = { filename: json.name, status: "cached" }
|
||||||
this.onPropertyChanged("filenames", this.properties.filenames)
|
this.onPropertyChanged("filenames", this.properties.filenames)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
|||||||
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
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";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* NOTE: If you want to add a new widget but it has the same input/output type
|
* NOTE: If you want to add a new widget but it has the same input/output type
|
||||||
@@ -79,6 +80,8 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
override isBackendNode = false;
|
override isBackendNode = false;
|
||||||
override serialize_widgets = true;
|
override serialize_widgets = true;
|
||||||
|
|
||||||
|
|
||||||
|
// TODO these are bad, create override methods instead
|
||||||
// input slots
|
// input slots
|
||||||
inputIndex: number = 0;
|
inputIndex: number = 0;
|
||||||
|
|
||||||
@@ -86,6 +89,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
outputIndex: number | null = 0;
|
outputIndex: number | null = 0;
|
||||||
changedIndex: number | null = 1;
|
changedIndex: number | null = 1;
|
||||||
|
|
||||||
|
|
||||||
displayWidget: ITextWidget;
|
displayWidget: ITextWidget;
|
||||||
|
|
||||||
override size: Vector2 = [60, 40];
|
override size: Vector2 = [60, 40];
|
||||||
@@ -203,7 +207,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
): boolean {
|
): boolean {
|
||||||
const anyConnected = range(this.outputs.length).some(i => this.getOutputLinks(i).length > 0);
|
const anyConnected = range(this.outputs.length).some(i => this.getOutputLinks(i).length > 0);
|
||||||
|
|
||||||
if (this.autoConfig && "config" in input && !anyConnected) {
|
if (this.autoConfig && "config" in input && !anyConnected && (input as IComfyInputSlot).widgetNodeType === this.type) {
|
||||||
this.doAutoConfig(input as IComfyInputSlot)
|
this.doAutoConfig(input as IComfyInputSlot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,3 +755,52 @@ LiteGraph.registerNodeType({
|
|||||||
desc: "Radio that outputs a string and index",
|
desc: "Radio that outputs a string and index",
|
||||||
type: "ui/radio"
|
type: "ui/radio"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export interface ComfyImageUploadProperties extends ComfyWidgetProperties {
|
||||||
|
fileCount: "single" | "multiple" // gradio File component format
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComfyImageUploadNode extends ComfyWidgetNode<Array<GradioFileData>> {
|
||||||
|
override properties: ComfyImageUploadProperties = {
|
||||||
|
defaultValue: [],
|
||||||
|
tags: [],
|
||||||
|
fileCount: "single",
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
outputs: [
|
||||||
|
{ name: "filename", type: "string" }, // TODO support batches
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = ImageUploadWidget;
|
||||||
|
override defaultValue = null;
|
||||||
|
override outputIndex = null;
|
||||||
|
override changedIndex = 1;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute(param: any, options: object) {
|
||||||
|
super.onExecute(param, options);
|
||||||
|
|
||||||
|
const value = get(this.value)
|
||||||
|
if (value.length > 0 && value[0].name)
|
||||||
|
this.setOutputData(0, value[0].name) // TODO when ComfyUI LoadImage supports loading an image batch
|
||||||
|
else
|
||||||
|
this.setOutputData(0, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override formatValue(value: GradioFileData[]): string {
|
||||||
|
return `Images: ${value.length}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyImageUploadNode,
|
||||||
|
title: "UI.ImageUpload",
|
||||||
|
desc: "Widget that lets you upload images into ComfyUI's input folder",
|
||||||
|
type: "ui/image_upload"
|
||||||
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { f7 } from "framework7-svelte"
|
|||||||
|
|
||||||
let notification;
|
let notification;
|
||||||
|
|
||||||
function notifyf7(text: string, title?: string) {
|
function notifyf7(text: string, title?: string, type?: string) {
|
||||||
if (!f7)
|
if (!f7)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ function notifyf7(text: string, title?: string) {
|
|||||||
notification.open();
|
notification.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifyToast(text: string, type?: string) {
|
function notifyToast(text: string, title?: string, type?: string) {
|
||||||
const options: SvelteToastOptions = {}
|
const options: SvelteToastOptions = {}
|
||||||
|
|
||||||
if (type === "error") {
|
if (type === "error") {
|
||||||
@@ -35,6 +35,6 @@ function notifyToast(text: string, type?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function notify(text: string, title?: string, type?: string) {
|
export default function notify(text: string, title?: string, type?: string) {
|
||||||
notifyf7(text, title);
|
notifyf7(text, title, type);
|
||||||
notifyToast(text, title);
|
notifyToast(text, title, type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,14 +133,9 @@ export type Attributes = {
|
|||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CSS height
|
* CSS styles
|
||||||
*/
|
*/
|
||||||
height?: string,
|
style?: string,
|
||||||
|
|
||||||
/*
|
|
||||||
* CSS Flex grow
|
|
||||||
*/
|
|
||||||
flexGrow?: number,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display variant for widgets/containers (e.g. number widget can act as slider/knob/dial)
|
* Display variant for widgets/containers (e.g. number widget can act as slider/knob/dial)
|
||||||
@@ -320,20 +315,6 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
defaultValue: "vertical",
|
defaultValue: "vertical",
|
||||||
canShow: (di: IDragItem) => di.type === "container"
|
canShow: (di: IDragItem) => di.type === "container"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "flexGrow",
|
|
||||||
type: "number",
|
|
||||||
location: "widget",
|
|
||||||
defaultValue: 100,
|
|
||||||
editable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "height",
|
|
||||||
type: "string",
|
|
||||||
location: "widget",
|
|
||||||
defaultValue: "auto",
|
|
||||||
editable: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "classes",
|
name: "classes",
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -341,6 +322,13 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
editable: true,
|
editable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "style",
|
||||||
|
type: "string",
|
||||||
|
location: "widget",
|
||||||
|
defaultValue: "",
|
||||||
|
editable: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "nodeDisabledState",
|
name: "nodeDisabledState",
|
||||||
type: "enum",
|
type: "enum",
|
||||||
|
|||||||
@@ -136,7 +136,6 @@
|
|||||||
|
|
||||||
:global(.svelte-select) {
|
:global(.svelte-select) {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 16rem;
|
|
||||||
--font-size: 13px;
|
--font-size: 13px;
|
||||||
--height: 32px;
|
--height: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Block, BlockLabel, Empty } from "@gradio/atoms";
|
import { Block, BlockLabel, Empty } from "@gradio/atoms";
|
||||||
import { Gallery } from "@gradio/gallery";
|
import { Gallery } from "@gradio/gallery";
|
||||||
import { Image } from "@gradio/icons";
|
import { Image } from "@gradio/icons";
|
||||||
|
import { StaticImage } from "@gradio/image";
|
||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
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";
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
let style: Styles = {
|
let style: Styles = {
|
||||||
grid_cols: [4],
|
grid_cols: [4],
|
||||||
grid_rows: [4],
|
grid_rows: [4],
|
||||||
// object_fit: "cover",
|
object_fit: "cover",
|
||||||
}
|
}
|
||||||
let element: HTMLDivElement;
|
let element: HTMLDivElement;
|
||||||
|
|
||||||
@@ -126,24 +127,21 @@
|
|||||||
|
|
||||||
{#if widget && node && nodeValue && $nodeValue != null}
|
{#if widget && node && nodeValue && $nodeValue != null}
|
||||||
{#if widget.attrs.variant === "image"}
|
{#if widget.attrs.variant === "image"}
|
||||||
<div class="wrapper comfy-image-widget" style="height: {widget.attrs.height || 'auto'}" bind:this={element}>
|
<div class="wrapper comfy-image-widget" style={widget.attrs.style || ""} bind:this={element}>
|
||||||
<Block variant="solid" padding={false}>
|
<Block variant="solid" padding={false}>
|
||||||
{#if widget.attrs.title}
|
|
||||||
<BlockLabel
|
|
||||||
show_label={true}
|
|
||||||
Icon={Image}
|
|
||||||
label={widget.attrs.title || "Image"}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if $nodeValue.length > 0}
|
{#if $nodeValue.length > 0}
|
||||||
<img src={$nodeValue[$nodeValue.length-1].data}/>
|
<StaticImage
|
||||||
|
value={$nodeValue[$nodeValue.length-1].data}
|
||||||
|
show_label={widget.attrs.title != ""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Empty size="large" unpadded_box={true}><Image /></Empty>
|
<Empty size="large" unpadded_box={true}><Image /></Empty>
|
||||||
{/if}
|
{/if}
|
||||||
</Block>
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="wrapper comfy-gallery-widget gradio-gallery" bind:this={element}>
|
<div class="wrapper comfy-gallery-widget gradio-gallery" style={widget.attrs.style || ""} bind:this={element}>
|
||||||
<Block variant="solid" padding={false}>
|
<Block variant="solid" padding={false}>
|
||||||
<div class="padding">
|
<div class="padding">
|
||||||
<Gallery
|
<Gallery
|
||||||
|
|||||||
231
src/lib/widgets/ImageUploadWidget.svelte
Normal file
231
src/lib/widgets/ImageUploadWidget.svelte
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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 { WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
let node: ComfyImageUploadNode | null = null;
|
||||||
|
let nodeValue: Writable<Array<GradioFileData>> | null = null;
|
||||||
|
let propsChanged: Writable<number> | null = null;
|
||||||
|
let dragging = false;
|
||||||
|
let pending_upload = false;
|
||||||
|
let old_value: Array<GradioFileData> | null = null;
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
let _value: GradioFileData[] | null = null;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function onChange() {
|
||||||
|
console.warn("CHANGED", _value)
|
||||||
|
$nodeValue = _value || []
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpload() {
|
||||||
|
console.warn("UPLOADED", _value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
console.warn("CLEARED", _value)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GradioUploadResponse {
|
||||||
|
error?: string;
|
||||||
|
files?: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
|
||||||
|
console.warn("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;
|
||||||
|
console.warn(_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, null, "error")
|
||||||
|
}
|
||||||
|
|
||||||
|
$nodeValue = 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
|
||||||
|
console.warn(image)
|
||||||
|
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
|
||||||
|
return `${baseUrl}/view?${params}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}>
|
||||||
|
{#if widget && node && nodeValue}
|
||||||
|
<Block
|
||||||
|
visible={true}
|
||||||
|
variant={($nodeValue === null || $nodeValue.length === 0) ? "dashed" : "solid"}
|
||||||
|
border_mode={dragging ? "focus" : "base"}
|
||||||
|
padding={true}
|
||||||
|
elem_id="comfy-image-upload-block"
|
||||||
|
elem_classes={widget.attrs.classes.split(",")}
|
||||||
|
>
|
||||||
|
<BlockLabel
|
||||||
|
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} />
|
||||||
|
{: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}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.comfy-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>
|
||||||
Reference in New Issue
Block a user