Merge pull request #30 from space-nuko/output-pane2
Image upload widget
This commit is contained in:
Submodule litegraph updated: b7ebbf57d3...a5e7911ae7
@@ -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
6
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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>
|
||||
396
src/lib/components/gradio/gallery/Gallery.svelte
Normal file
396
src/lib/components/gradio/gallery/Gallery.svelte
Normal 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>
|
||||
1
src/lib/components/gradio/gallery/index.ts
Normal file
1
src/lib/components/gradio/gallery/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Gallery } from "./Gallery.svelte"
|
||||
71
src/lib/components/gradio/image/StaticImage.svelte
Normal file
71
src/lib/components/gradio/image/StaticImage.svelte
Normal 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>
|
||||
1
src/lib/components/gradio/image/index.ts
Normal file
1
src/lib/components/gradio/image/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as StaticImage } from "./StaticImage.svelte"
|
||||
26
src/lib/components/gradio/image/utils.ts
Normal file
26
src/lib/components/gradio/image/utils.ts
Normal 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
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
||||
import CheckboxWidget from "$lib/widgets/CheckboxWidget.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
|
||||
@@ -79,6 +80,8 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
override isBackendNode = false;
|
||||
override serialize_widgets = true;
|
||||
|
||||
|
||||
// TODO these are bad, create override methods instead
|
||||
// input slots
|
||||
inputIndex: number = 0;
|
||||
|
||||
@@ -86,6 +89,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
outputIndex: number | null = 0;
|
||||
changedIndex: number | null = 1;
|
||||
|
||||
|
||||
displayWidget: ITextWidget;
|
||||
|
||||
override size: Vector2 = [60, 40];
|
||||
@@ -203,7 +207,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
): boolean {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -279,7 +283,8 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
override onConfigure(o: SerializedLGraphNode) {
|
||||
this.value.set((o as any).comfyValue);
|
||||
const value = (o as any).comfyValue || LiteGraph.cloneObject(this.defaultValue);
|
||||
this.value.set(value);
|
||||
this.shownOutputProperties = (o as any).shownOutputProperties;
|
||||
}
|
||||
|
||||
@@ -530,7 +535,10 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
|
||||
{ name: "clear", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
{ name: "selected_index", type: "number" }
|
||||
outputs: [
|
||||
{ name: "selected_index", type: "number" },
|
||||
{ name: "width", type: "number" },
|
||||
{ name: "height", type: "number" },
|
||||
{ name: "any_selected", type: "boolean" },
|
||||
]
|
||||
}
|
||||
@@ -545,6 +553,8 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
override saveUserState = false;
|
||||
override outputIndex = null;
|
||||
override changedIndex = null;
|
||||
|
||||
anyImageSelected: boolean = false;
|
||||
|
||||
modeWidget: IComboWidget;
|
||||
|
||||
@@ -558,8 +568,13 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
this.modeWidget.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
imageSize: Vector2 = [1, 1]
|
||||
|
||||
override onExecute() {
|
||||
this.setOutputData(0, this.properties.index)
|
||||
this.setOutputData(1, this.imageSize[0])
|
||||
this.setOutputData(2, this.imageSize[1])
|
||||
this.setOutputData(3, this.anyImageSelected)
|
||||
}
|
||||
|
||||
@@ -582,6 +597,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
this.setValue(galleryItems)
|
||||
}
|
||||
}
|
||||
this.setProperty("index", 0)
|
||||
this.anyImageSelected = false;
|
||||
}
|
||||
}
|
||||
@@ -590,6 +606,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
return `Images: ${value?.length || 0}`
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
console.warn("SETVALUE", value)
|
||||
if (Array.isArray(value)) {
|
||||
super.setValue(value)
|
||||
@@ -597,6 +614,9 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
else {
|
||||
super.setValue([])
|
||||
}
|
||||
|
||||
if (!get(this.value))
|
||||
this.anyImageSelected = false
|
||||
|
||||
const len = get(this.value).length
|
||||
if (this.properties.index < 0 || this.properties.index >= len) {
|
||||
@@ -665,6 +685,10 @@ export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
||||
defaultValue: false,
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "boolean" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "boolean" },
|
||||
@@ -674,6 +698,10 @@ export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
||||
|
||||
override svelteComponentType = CheckboxWidget;
|
||||
override defaultValue = false;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, false)
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
value = Boolean(value)
|
||||
@@ -682,8 +710,9 @@ export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
||||
if (changed)
|
||||
this.triggerSlot(1, value)
|
||||
}
|
||||
constructor(name?: string) {
|
||||
super(name, false)
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
if (action === "store")
|
||||
this.setValue(Boolean(param))
|
||||
}
|
||||
}
|
||||
@@ -751,3 +780,63 @@ LiteGraph.registerNodeType({
|
||||
title: "UI.Radio",
|
||||
desc: "Radio that outputs a string and index",
|
||||
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: "width", type: "number" },
|
||||
{ name: "height", type: "number" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||
]
|
||||
}
|
||||
|
||||
override svelteComponentType = ImageUploadWidget;
|
||||
override defaultValue = [];
|
||||
override outputIndex = null;
|
||||
override changedIndex = 3;
|
||||
override saveUserState = false;
|
||||
|
||||
imageSize: Vector2 = [1, 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
|
||||
this.setOutputData(1, this.imageSize[0])
|
||||
this.setOutputData(2, this.imageSize[1])
|
||||
}
|
||||
else {
|
||||
this.setOutputData(0, "")
|
||||
this.setOutputData(1, 1)
|
||||
this.setOutputData(2, 1)
|
||||
}
|
||||
}
|
||||
|
||||
override formatValue(value: GradioFileData[]): string {
|
||||
return `Images: ${value?.length || 0}`
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
|
||||
:global(.svelte-select) {
|
||||
width: auto;
|
||||
max-width: 16rem;
|
||||
--font-size: 13px;
|
||||
--height: 32px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
252
src/lib/widgets/ImageUploadWidget.svelte
Normal file
252
src/lib/widgets/ImageUploadWidget.svelte
Normal 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>
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user