Merge pull request #30 from space-nuko/output-pane2

Image upload widget
This commit is contained in:
space-nuko
2023-05-08 21:08:38 -05:00
committed by GitHub
25 changed files with 6535 additions and 3747 deletions

View File

@@ -37,9 +37,11 @@
"@gradio/atoms": "workspace:*",
"@gradio/button": "workspace:*",
"@gradio/client": "workspace:*",
"@gradio/file": "workspace:*",
"@gradio/form": "workspace:*",
"@gradio/gallery": "workspace:*",
"@gradio/icons": "workspace:*",
"@gradio/image": "workspace:*",
"@gradio/tabs": "workspace:*",
"@gradio/theme": "workspace:*",
"@gradio/upload": "workspace:*",

6
pnpm-lock.yaml generated
View File

@@ -16,6 +16,9 @@ importers:
'@gradio/client':
specifier: workspace:*
version: link:gradio/client/js
'@gradio/file':
specifier: workspace:*
version: link:gradio/js/file
'@gradio/form':
specifier: workspace:*
version: link:gradio/js/form
@@ -25,6 +28,9 @@ importers:
'@gradio/icons':
specifier: workspace:*
version: link:gradio/js/icons
'@gradio/image':
specifier: workspace:*
version: link:gradio/js/image
'@gradio/tabs':
specifier: workspace:*
version: link:gradio/js/tabs

View File

@@ -78,7 +78,7 @@
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
style={item?.attrs?.style || ""}
>
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
@@ -183,6 +183,7 @@
.animation-wrapper {
position: relative;
flex-grow: 100;
flex-basis: 0;
}
.handle-widget:hover {

View File

@@ -80,7 +80,7 @@
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
style={item?.attrs?.style || ""}
>
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
@@ -238,6 +238,7 @@
.animation-wrapper {
position: relative;
flex-grow: 100;
flex-basis: 0;
}
.handle-hidden {

View File

@@ -49,14 +49,15 @@
resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250);
}
function queuePrompt() {
console.log("Queuing!");
const workflow = $layoutState.attrs.defaultSubgraph;
app.queuePrompt(0, 1, workflow);
}
$: if (app?.lCanvas) {
app.lCanvas.allow_dragnodes = $uiState.uiUnlocked;
app.lCanvas.allow_interaction = $uiState.uiUnlocked;
$: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
$: if (app?.lCanvas) app.lCanvas.allow_interaction = !$uiState.graphLocked;
if (!$uiState.uiUnlocked) {
app.lCanvas.deselectAllNodes();
$layoutState.currentSelectionNodes = []
}
}
$: if ($uiState.uiEditMode)
$layoutState.currentSelection = []
@@ -240,9 +241,6 @@
</div>
<div id="bottombar">
<div class="left">
<Button variant="primary" on:click={queuePrompt}>
Queue Prompt
</Button>
<Button variant="secondary" on:click={toggleGraph}>
Toggle Graph
</Button>

View File

@@ -131,6 +131,10 @@ export default class ComfyApp {
LiteGraph.release_link_on_empty_shows_menu = true;
LiteGraph.alt_drag_do_clone_nodes = true;
const uiUnlocked = get(uiState).uiUnlocked;
this.lCanvas.allow_dragnodes = uiUnlocked;
this.lCanvas.allow_interaction = uiUnlocked;
(window as any).LiteGraph = LiteGraph;
// await this.#invokeExtensionsAsync("init");
@@ -542,6 +546,11 @@ export default class ComfyApp {
// nodes have conditional logic that determines which link
// to follow backwards.
while (isFrontendParent(parent)) {
if (!("getUpstreamLink" in parent)) {
console.warn("[graphToPrompt] Node does not support getUpstreamLink", parent.type)
break;
}
const nextLink = parent.getUpstreamLink()
if (nextLink == null) {
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
@@ -715,7 +724,8 @@ export default class ComfyApp {
comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
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;
comfyComboNode.doAutoConfig(comfyInput)
if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {

View File

@@ -92,7 +92,7 @@
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}>
style={item?.attrs?.style || ""}>
<Block>
<label for={String(item.id)}>
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
@@ -198,6 +198,7 @@
.animation-wrapper {
position: relative;
flex-grow: 100;
flex-basis: 0;
}
.handle-widget:hover {

View 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>

View File

@@ -0,0 +1,396 @@
<script lang="ts">
import { BlockLabel, Empty } from "@gradio/atoms";
import { ModifyUpload } from "@gradio/upload";
import type { SelectData } from "@gradio/utils";
import { createEventDispatcher } from "svelte";
import { tick } from "svelte";
import type { Styles } from "@gradio/utils";
import { get_styles } from "@gradio/utils";
import { Image } from "@gradio/icons";
import type { FileData } from "@gradio/upload";
import { normalise_file } from "@gradio/upload";
export let show_label: boolean = true;
export let label: string;
export let root: string = "";
export let root_url: null | string = null;
export let value: Array<string> | Array<FileData> | null = null;
export let style: Styles = {
grid_cols: [2],
object_fit: "cover",
height: "auto"
};
export let imageWidth: number = 1;
export let imageHeight: number = 1;
const dispatch = createEventDispatcher<{
select: SelectData;
}>();
// tracks whether the value of the gallery was reset
let was_reset: boolean = true;
$: if (selected_image == null || was_reset) {
imageWidth = 1;
imageHeight = 1;
}
$: was_reset = value == null || value.length == 0 ? true : was_reset;
$: _value =
value === null
? null
: value.map((img) =>
Array.isArray(img)
? [normalise_file(img[0], root, root_url), img[1]]
: [normalise_file(img, root, root_url), null]
);
let prevValue: string[] | FileData[] | null = value;
export let selected_image: number | null = null;
let old_selected_image: number | null = null;
$: if (prevValue !== value) {
// When value is falsy (clear button or first load),
// style.preview determines the selected image
if (was_reset) {
selected_image = style.preview && value?.length ? 0 : null;
was_reset = false;
// Otherwise we keep the selected_image the same if the
// gallery has at least as many elements as it did before
} else {
selected_image =
selected_image !== null &&
value !== null &&
selected_image < value.length
? selected_image
: null;
}
prevValue = value;
}
$: previous =
((selected_image ?? 0) + (_value?.length ?? 0) - 1) % (_value?.length ?? 0);
$: next = ((selected_image ?? 0) + 1) % (_value?.length ?? 0);
function on_keydown(e: KeyboardEvent) {
switch (e.code) {
case "Escape":
e.preventDefault();
selected_image = null;
break;
case "ArrowLeft":
e.preventDefault();
selected_image = previous;
break;
case "ArrowRight":
e.preventDefault();
selected_image = next;
break;
default:
break;
}
}
$: {
if (selected_image !== old_selected_image) {
old_selected_image = selected_image;
if (selected_image !== null) {
dispatch("select", {
index: selected_image,
value: _value?.[selected_image][1]
});
}
}
}
$: scroll_to_img(selected_image);
let el: Array<HTMLButtonElement> = [];
let container: HTMLDivElement;
async function scroll_to_img(index: number | null) {
if (typeof index !== "number") return;
await tick();
el[index].focus();
const { left: container_left, width: container_width } =
container.getBoundingClientRect();
const { left, width } = el[index].getBoundingClientRect();
const relative_left = left - container_left;
const pos =
relative_left + width / 2 - container_width / 2 + container.scrollLeft;
container.scrollTo({
left: pos < 0 ? 0 : pos,
behavior: "smooth"
});
}
$: can_zoom = window_height >= height;
function add_height_to_styles(style: Styles): string {
styles = get_styles(style, ["grid_cols", "grid_rows", "object_fit"]).styles;
return styles + ` height: ${style.height}`;
}
$: styles = add_height_to_styles(style);
let height = 0;
let window_height = 0;
</script>
<svelte:window bind:innerHeight={window_height} />
{#if show_label}
<BlockLabel
{show_label}
Icon={Image}
label={label || "Gallery"}
disable={typeof style.container === "boolean" && !style.container}
/>
{/if}
{#if value === null || _value === null || _value.length === 0}
<Empty size="large" unpadded_box={true}><Image /></Empty>
{:else}
{#if selected_image !== null}
<div
on:keydown={on_keydown}
class="preview"
class:fixed-height={style.height !== "auto"}
>
<ModifyUpload on:clear={() => (selected_image = null)} />
<img
on:click={() => (selected_image = next)}
src={_value[selected_image][0].data}
alt={_value[selected_image][1] || ""}
title={_value[selected_image][1] || null}
class:with-caption={!!_value[selected_image][1]}
style="height: calc(100% - {_value[selected_image][1]
? '80px'
: '60px'})"
bind:naturalWidth={imageWidth}
bind:naturalHeight={imageHeight}
/>
{#if _value[selected_image][1]}
<div class="caption">
{_value[selected_image][1]}
</div>
{/if}
<div bind:this={container} class="thumbnails scroll-hide">
{#each _value as image, i}
<button
bind:this={el[i]}
on:click={() => (selected_image = i)}
class="thumbnail-item thumbnail-small"
class:selected={selected_image === i}
>
<img
src={image[0].data}
title={image[1] || null}
alt={image[1] || null}
/>
</button>
{/each}
</div>
</div>
{/if}
<div
bind:clientHeight={height}
class="grid-wrap"
class:fixed-height={!style.height || style.height == "auto"}
>
<div class="grid-container" style={styles} class:pt-6={show_label}>
{#each _value as [image, caption], i}
<button
class="thumbnail-item thumbnail-lg"
class:selected={selected_image === i}
on:click={() => (selected_image = can_zoom ? i : selected_image)}
>
<img
alt={caption || ""}
src={typeof image === "string" ? image : image.data}
/>
{#if caption}
<div class="caption-label">
{caption}
</div>
{/if}
</button>
{/each}
</div>
</div>
{/if}
<style lang="postcss">
.preview {
display: flex;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
flex-direction: column;
z-index: var(--layer-2);
backdrop-filter: blur(8px);
background: var(--background-fill-primary);
height: var(--size-full);
}
.fixed-height {
min-height: var(--size-80);
max-height: 55vh;
}
@media (--screen-xl) {
.fixed-height {
min-height: 450px;
}
}
.preview img {
width: var(--size-full);
height: calc(var(--size-full) - 60px);
object-fit: contain;
}
.preview img.with-caption {
height: calc(var(--size-full) - 80px);
}
.caption {
padding: var(--size-2) var(--size-3);
overflow: hidden;
color: var(--block-label-text-color);
font-weight: var(--weight-semibold);
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.thumbnails {
display: flex;
position: absolute;
bottom: 0;
justify-content: center;
align-items: center;
gap: var(--spacing-lg);
width: var(--size-full);
height: var(--size-14);
overflow-x: scroll;
}
.thumbnail-item {
--ring-color: transparent;
position: relative;
outline: none;
box-shadow: 0 0 0 2px var(--ring-color), var(--shadow-drop);
border: 1px solid var(--border-color-primary);
border-radius: var(--button-small-radius);
background: var(--background-fill-secondary);
aspect-ratio: var(--ratio-square);
width: var(--size-full);
height: var(--size-full);
overflow: clip;
}
.thumbnail-item:hover {
--ring-color: var(--border-color-accent);
filter: brightness(1.1);
border-color: var(--border-color-accent);
}
.thumbnail-small {
flex: none;
transform: scale(0.9);
transition: 0.075s;
width: var(--size-9);
height: var(--size-9);
}
.thumbnail-small.selected {
--ring-color: var(--color-accent);
transform: scale(1);
border-color: var(--color-accent);
}
.thumbnail-small > img {
width: var(--size-full);
height: var(--size-full);
overflow: hidden;
object-fit: var(--object-fit);
}
.grid-wrap {
padding: var(--size-2);
height: var(--size-full);
overflow-y: auto;
}
.grid-container {
display: grid;
grid-template-rows: var(--grid-rows);
grid-template-columns: var(--grid-cols);
gap: var(--spacing-lg);
}
@media (--screen-sm) {
.grid-container {
grid-template-columns: var(--sm-grid-cols);
}
}
@media (--screen-md) {
.grid-container {
grid-template-columns: var(--md-grid-cols);
}
}
@media (--screen-lg) {
.grid-container {
grid-template-columns: var(--lg-grid-cols);
}
}
@media (--screen-xl) {
.grid-container {
grid-template-columns: var(--xl-grid-cols);
}
}
@media (--screen-xxl) {
.grid-container {
grid-template-columns: var(--2xl-grid-cols);
}
}
.thumbnail-lg > img {
width: var(--size-full);
height: var(--size-full);
overflow: hidden;
object-fit: var(--object-fit);
}
.thumbnail-lg:hover .caption-label {
opacity: 0.5;
}
.caption-label {
position: absolute;
right: var(--block-label-margin);
bottom: var(--block-label-margin);
z-index: var(--layer-1);
border-top: 1px solid var(--border-color-primary);
border-left: 1px solid var(--border-color-primary);
border-radius: var(--block-label-radius);
background: var(--background-fill-secondary);
padding: var(--block-label-padding);
max-width: 80%;
overflow: hidden;
font-size: var(--block-label-text-size);
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1 @@
export { default as Gallery } from "./Gallery.svelte"

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { SelectData } from "@gradio/utils";
import { BlockLabel, Empty, IconButton } from "@gradio/atoms";
import { Download } from "@gradio/icons";
import { get_coordinates_of_clicked_image } from "./utils";
import { Image } from "@gradio/icons";
export let value: null | string;
export let label: string | undefined = undefined;
export let show_label: boolean;
export let selectable: boolean = false;
export let imageWidth: number = 1;
export let imageHeight: number = 1;
let imageElem: HTMLImageElement | null = null;
const dispatch = createEventDispatcher<{
change: string;
select: SelectData;
}>();
$: value && dispatch("change", value);
$: if (value == null || !imageElem) {
imageWidth = 1;
imageHeight = 1;
}
const handle_click = (evt: MouseEvent) => {
let coordinates = get_coordinates_of_clicked_image(evt);
if (coordinates) {
dispatch("select", { index: coordinates, value: null });
}
};
</script>
<BlockLabel {show_label} Icon={Image} label={label || "Image"} />
{#if value === null}
<Empty size="large" unpadded_box={true}><Image /></Empty>
{:else}
<div class="download">
<a
href={value}
target={window.__is_colab__ ? "_blank" : null}
download={"image"}
>
<IconButton Icon={Download} label="Download" />
</a>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<img src={value} alt="" class:selectable on:click={handle_click} bind:naturalWidth={imageWidth} bind:naturalHeight={imageHeight} />
{/if}
<style>
img {
width: var(--size-full);
height: var(--size-full);
object-fit: contain;
}
.selectable {
cursor: crosshair;
}
.download {
position: absolute;
top: 6px;
right: 6px;
}
</style>

View File

@@ -0,0 +1 @@
export { default as StaticImage } from "./StaticImage.svelte"

View File

@@ -0,0 +1,26 @@
export const get_coordinates_of_clicked_image = (
evt: MouseEvent
): [number, number] | null => {
let image = evt.currentTarget as HTMLImageElement;
const imageRect = image.getBoundingClientRect();
const xScale = image.naturalWidth / imageRect.width;
const yScale = image.naturalHeight / imageRect.height;
if (xScale > yScale) {
const displayed_width = imageRect.width;
const displayed_height = image.naturalHeight / xScale;
const y_offset = (imageRect.height - displayed_height) / 2;
var x = Math.round((evt.clientX - imageRect.left) * xScale);
var y = Math.round((evt.clientY - imageRect.top - y_offset) * xScale);
} else {
const displayed_width = image.naturalWidth / yScale;
const displayed_height = imageRect.height;
const x_offset = (imageRect.width - displayed_width) / 2;
var x = Math.round((evt.clientX - imageRect.left - x_offset) * yScale);
var y = Math.round((evt.clientY - imageRect.top) * yScale);
}
if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
return null;
}
return [x, y];
};

File diff suppressed because it is too large Load Diff

View File

@@ -207,21 +207,29 @@ LiteGraph.registerNodeType({
})
export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties {
tag: string | null,
targetTag: string
}
export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
override properties: ComfyExecuteSubgraphActionProperties = {
tag: null
tags: [],
targetTag: ""
}
static slotLayout: SlotLayout = {
inputs: [
{ 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() {
const tag = this.getInputData(1)
if (tag)
@@ -229,7 +237,7 @@ export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
}
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;
if (!app)
@@ -298,7 +306,6 @@ export class ComfySetNodeModeAction extends ComfyGraphNode {
} else {
newMode = NodeMode.NEVER;
}
console.warn("CHANGEMODE", newMode == NodeMode.ALWAYS ? "ALWAYS" : "NEVER", tags, node)
node.changeMode(newMode);
if ("notifyPropsChanged" in node)
(node as ComfyWidgetNode).notifyPropsChanged();

View File

@@ -1,4 +1,4 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core";
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes";
@@ -12,6 +12,10 @@ export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties
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
* the output file into ComfyUI's input folder.
@@ -43,6 +47,8 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
]
override saveUserState = false;
private _uploadPromise: Promise<void> | null = null;
stateWidget: ITextWidget;
@@ -120,6 +126,14 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
this.setOutputData(1, state)
}
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
o.properties.images = null
o.properties.index = 0
o.properties.filenames = {}
o.properties.genNumber = 0
}
private setIndex(newIndex: number, force: boolean = false) {
if (newIndex === this.properties.index && !force)
return;
@@ -159,6 +173,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
else {
this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
this.onPropertyChanged("filenames", this.properties.filenames)
const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(data)
@@ -176,10 +191,10 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
)
})
.then((r) => r.json())
.then((json) => {
.then((json: ComfyUploadImageAPIResponse) => {
console.debug("Gottem", json)
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)
}
else {

View File

@@ -1,9 +1,11 @@
import { BuiltInSlotType, LiteGraph, NodeMode, type INodeInputSlot, type SlotLayout, type INodeOutputSlot, LLink, LConnectionKind, type ITextWidget } from "@litegraph-ts/core";
import { BuiltInSlotType, LiteGraph, NodeMode, type INodeInputSlot, type SlotLayout, type INodeOutputSlot, LLink, LConnectionKind, type ITextWidget, type SerializedLGraphNode, type IComboWidget } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic";
export type PickFirstMode = "anyActiveLink" | "truthy" | "dataNonNull"
export interface ComfyPickFirstNodeProperties extends ComfyGraphNodeProperties {
acceptNullLinkData: boolean
mode: PickFirstMode
}
function nextLetter(s: string): string {
@@ -20,7 +22,7 @@ function nextLetter(s: string): string {
export default class ComfyPickFirstNode extends ComfyGraphNode {
override properties: ComfyPickFirstNodeProperties = {
tags: [],
acceptNullLinkData: false
mode: "dataNonNull"
}
static slotLayout: SlotLayout = {
@@ -38,11 +40,13 @@ export default class ComfyPickFirstNode extends ComfyGraphNode {
private selected: number = -1;
displayWidget: ITextWidget;
modeWidget: IComboWidget;
constructor(title?: string) {
super(title);
this.displayWidget = this.addWidget("text", "Value", "")
this.displayWidget.disabled = true;
this.modeWidget = this.addWidget("combo", "Mode", this.properties.mode, null, { property: "mode", values: ["anyActiveLink", "truthy", "dataNonNull"] })
}
override onDrawBackground(ctx: CanvasRenderingContext2D) {
@@ -117,7 +121,12 @@ export default class ComfyPickFirstNode extends ComfyGraphNode {
return true;
}
else {
return link.data != null || this.properties.acceptNullLinkData;
if (this.properties.mode === "dataNonNull")
return link.data != null;
else if (this.properties.mode === "truthy")
return Boolean(link.data)
else // anyActiveLink
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ import { f7 } from "framework7-svelte"
let notification;
function notifyf7(text: string, title?: string) {
function notifyf7(text: string, title?: string, type?: string) {
if (!f7)
return;
@@ -22,7 +22,7 @@ function notifyf7(text: string, title?: string) {
notification.open();
}
function notifyToast(text: string, type?: string) {
function notifyToast(text: string, title?: string, type?: string) {
const options: SvelteToastOptions = {}
if (type === "error") {
@@ -35,6 +35,6 @@ function notifyToast(text: string, type?: string) {
}
export default function notify(text: string, title?: string, type?: string) {
notifyf7(text, title);
notifyToast(text, title);
notifyf7(text, title, type);
notifyToast(text, title, type);
}

View File

@@ -133,14 +133,9 @@ export type Attributes = {
disabled?: boolean,
/*
* CSS height
* CSS styles
*/
height?: string,
/*
* CSS Flex grow
*/
flexGrow?: number,
style?: string,
/**
* 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",
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",
type: "string",
@@ -341,6 +322,13 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "",
editable: true,
},
{
name: "style",
type: "string",
location: "widget",
defaultValue: "",
editable: true
},
{
name: "nodeDisabledState",
type: "enum",
@@ -436,6 +424,13 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
serialize: (s) => s === NodeMode.ALWAYS ? "ALWAYS" : "NEVER",
deserialize: (m) => m === "ALWAYS" ? NodeMode.ALWAYS : NodeMode.NEVER
},
{
name: "horizontal",
type: "boolean",
location: "nodeVars",
editable: true,
defaultValue: false
},
// Node properties
{

View File

@@ -136,7 +136,6 @@
:global(.svelte-select) {
width: auto;
max-width: 16rem;
--font-size: 13px;
--height: 32px;
}

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import { ImageViewer } from "$lib/ImageViewer";
import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { Gallery } from "@gradio/gallery";
import { Gallery } from "$lib/components/gradio/gallery";
import { Image } from "@gradio/icons";
import { StaticImage } from "$lib/components/gradio/image";
import type { Styles } from "@gradio/utils";
import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store";
@@ -18,6 +19,9 @@
let nodeValue: Writable<GradioFileData[]> | null = null;
let propsChanged: Writable<number> | null = null;
let option: number | null = null;
let imageWidth: number = 1;
let imageHeight: number = 1;
let selected_image: number | null = null;
$: widget && setNodeValue(widget);
@@ -26,22 +30,31 @@
node = widget.node as ComfyGalleryNode
nodeValue = node.value;
propsChanged = node.propsChanged;
node.anyImageSelected = false;
if ($nodeValue != null) {
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
node.setProperty("index", clamp(node.properties.index, 0, $nodeValue))
node.setProperty("index", clamp(node.properties.index, 0, $nodeValue.length))
}
}
}
};
let style: Styles = {
grid_cols: [4],
grid_rows: [4],
// object_fit: "cover",
grid_cols: [3],
object_fit: "cover",
}
let element: HTMLDivElement;
$: if (node) {
if (imageWidth > 1 || imageHeight > 1) {
node.imageSize = [imageWidth, imageHeight]
}
else {
node.imageSize = [1, 1]
}
}
let mobileLightbox = null;
function showMobileLightbox(event: Event) {
@@ -121,29 +134,43 @@
// Update index
node.setProperty("index", e.detail.index as number)
node.anyImageSelected = true;
}
$: if ($propsChanged > -1 && widget && $nodeValue) {
if (widget.attrs.variant === "image") {
selected_image = $nodeValue.length - 1
node.setProperty("index", selected_image)
node.anyImageSelected = true;
}
}
else {
node.setProperty("index", null)
node.anyImageSelected = false;
}
$: node.anyImageSelected = selected_image != null;
</script>
{#if widget && node && nodeValue && $nodeValue != null}
{#if widget && node && nodeValue}
{#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}>
{#if widget.attrs.title}
<BlockLabel
show_label={true}
Icon={Image}
label={widget.attrs.title || "Image"}
{#if $nodeValue && $nodeValue.length > 0}
<StaticImage
value={$nodeValue[$nodeValue.length-1].data}
show_label={widget.attrs.title != ""}
label={widget.attrs.title}
bind:imageWidth
bind:imageHeight
/>
{/if}
{#if $nodeValue.length > 0}
<img src={$nodeValue[$nodeValue.length-1].data}/>
{:else}
<Empty size="large" unpadded_box={true}><Image /></Empty>
{/if}
</Block>
</div>
{: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}>
<div class="padding">
<Gallery
@@ -154,6 +181,9 @@
root={""}
root_url={""}
on:select={onSelect}
bind:imageWidth
bind:imageHeight
bind:selected_image
/>
</div>
</Block>

View File

@@ -0,0 +1,252 @@
<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";
import type { Vector2 } from "@litegraph-ts/core";
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;
let imgElem: HTMLImageElement | null = null
let imgWidth: number = 1;
let imgHeight: number = 1;
$: 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;
}
};
$: if (!(node && _value && _value.length > 0 && imgElem)) {
imgWidth = 1
imgHeight = 1
node.imageSize = [1, 1]
}
else if (imgWidth > 1 || imgHeight > 1) {
node.imageSize = [imgWidth, imgHeight]
}
else {
node.imageSize = [1, 1]
}
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}
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}
</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>

View File

@@ -51,7 +51,6 @@
{/if}
</div>
<Toolbar bottom>
<Link on:click={queuePrompt}>Queue Prompt</Link>
<Link on:click={() => app.refreshComboInNodes()}>🔄</Link>
<Link on:click={doLoad}>Load</Link>
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />