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/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 {
|
||||||
|
|||||||
@@ -49,14 +49,15 @@
|
|||||||
resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250);
|
resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queuePrompt() {
|
$: if (app?.lCanvas) {
|
||||||
console.log("Queuing!");
|
app.lCanvas.allow_dragnodes = $uiState.uiUnlocked;
|
||||||
const workflow = $layoutState.attrs.defaultSubgraph;
|
app.lCanvas.allow_interaction = $uiState.uiUnlocked;
|
||||||
app.queuePrompt(0, 1, workflow);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
|
if (!$uiState.uiUnlocked) {
|
||||||
$: if (app?.lCanvas) app.lCanvas.allow_interaction = !$uiState.graphLocked;
|
app.lCanvas.deselectAllNodes();
|
||||||
|
$layoutState.currentSelectionNodes = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: if ($uiState.uiEditMode)
|
$: if ($uiState.uiEditMode)
|
||||||
$layoutState.currentSelection = []
|
$layoutState.currentSelection = []
|
||||||
@@ -240,9 +241,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="bottombar">
|
<div id="bottombar">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<Button variant="primary" on:click={queuePrompt}>
|
|
||||||
Queue Prompt
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" on:click={toggleGraph}>
|
<Button variant="secondary" on:click={toggleGraph}>
|
||||||
Toggle Graph
|
Toggle Graph
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ export default class ComfyApp {
|
|||||||
LiteGraph.release_link_on_empty_shows_menu = true;
|
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||||
LiteGraph.alt_drag_do_clone_nodes = 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;
|
(window as any).LiteGraph = LiteGraph;
|
||||||
|
|
||||||
// await this.#invokeExtensionsAsync("init");
|
// await this.#invokeExtensionsAsync("init");
|
||||||
@@ -542,6 +546,11 @@ export default class ComfyApp {
|
|||||||
// nodes have conditional logic that determines which link
|
// nodes have conditional logic that determines which link
|
||||||
// to follow backwards.
|
// to follow backwards.
|
||||||
while (isFrontendParent(parent)) {
|
while (isFrontendParent(parent)) {
|
||||||
|
if (!("getUpstreamLink" in parent)) {
|
||||||
|
console.warn("[graphToPrompt] Node does not support getUpstreamLink", parent.type)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const nextLink = parent.getUpstreamLink()
|
const nextLink = parent.getUpstreamLink()
|
||||||
if (nextLink == null) {
|
if (nextLink == null) {
|
||||||
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
|
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];
|
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>
|
||||||
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 {
|
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)
|
||||||
@@ -298,7 +306,6 @@ export class ComfySetNodeModeAction extends ComfyGraphNode {
|
|||||||
} else {
|
} else {
|
||||||
newMode = NodeMode.NEVER;
|
newMode = NodeMode.NEVER;
|
||||||
}
|
}
|
||||||
console.warn("CHANGEMODE", newMode == NodeMode.ALWAYS ? "ALWAYS" : "NEVER", tags, node)
|
|
||||||
node.changeMode(newMode);
|
node.changeMode(newMode);
|
||||||
if ("notifyPropsChanged" in node)
|
if ("notifyPropsChanged" in node)
|
||||||
(node as ComfyWidgetNode).notifyPropsChanged();
|
(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 ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -43,6 +47,8 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
override saveUserState = false;
|
||||||
|
|
||||||
private _uploadPromise: Promise<void> | null = null;
|
private _uploadPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
stateWidget: ITextWidget;
|
stateWidget: ITextWidget;
|
||||||
@@ -120,6 +126,14 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
|
|||||||
this.setOutputData(1, state)
|
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) {
|
private setIndex(newIndex: number, force: boolean = false) {
|
||||||
if (newIndex === this.properties.index && !force)
|
if (newIndex === this.properties.index && !force)
|
||||||
return;
|
return;
|
||||||
@@ -159,6 +173,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 +191,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 {
|
||||||
|
|||||||
@@ -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 ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
|
|
||||||
|
export type PickFirstMode = "anyActiveLink" | "truthy" | "dataNonNull"
|
||||||
|
|
||||||
export interface ComfyPickFirstNodeProperties extends ComfyGraphNodeProperties {
|
export interface ComfyPickFirstNodeProperties extends ComfyGraphNodeProperties {
|
||||||
acceptNullLinkData: boolean
|
mode: PickFirstMode
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextLetter(s: string): string {
|
function nextLetter(s: string): string {
|
||||||
@@ -20,7 +22,7 @@ function nextLetter(s: string): string {
|
|||||||
export default class ComfyPickFirstNode extends ComfyGraphNode {
|
export default class ComfyPickFirstNode extends ComfyGraphNode {
|
||||||
override properties: ComfyPickFirstNodeProperties = {
|
override properties: ComfyPickFirstNodeProperties = {
|
||||||
tags: [],
|
tags: [],
|
||||||
acceptNullLinkData: false
|
mode: "dataNonNull"
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -38,11 +40,13 @@ export default class ComfyPickFirstNode extends ComfyGraphNode {
|
|||||||
private selected: number = -1;
|
private selected: number = -1;
|
||||||
|
|
||||||
displayWidget: ITextWidget;
|
displayWidget: ITextWidget;
|
||||||
|
modeWidget: IComboWidget;
|
||||||
|
|
||||||
constructor(title?: string) {
|
constructor(title?: string) {
|
||||||
super(title);
|
super(title);
|
||||||
this.displayWidget = this.addWidget("text", "Value", "")
|
this.displayWidget = this.addWidget("text", "Value", "")
|
||||||
this.displayWidget.disabled = true;
|
this.displayWidget.disabled = true;
|
||||||
|
this.modeWidget = this.addWidget("combo", "Mode", this.properties.mode, null, { property: "mode", values: ["anyActiveLink", "truthy", "dataNonNull"] })
|
||||||
}
|
}
|
||||||
|
|
||||||
override onDrawBackground(ctx: CanvasRenderingContext2D) {
|
override onDrawBackground(ctx: CanvasRenderingContext2D) {
|
||||||
@@ -117,7 +121,12 @@ export default class ComfyPickFirstNode extends ComfyGraphNode {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
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
@@ -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",
|
||||||
@@ -436,6 +424,13 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
serialize: (s) => s === NodeMode.ALWAYS ? "ALWAYS" : "NEVER",
|
serialize: (s) => s === NodeMode.ALWAYS ? "ALWAYS" : "NEVER",
|
||||||
deserialize: (m) => m === "ALWAYS" ? NodeMode.ALWAYS : NodeMode.NEVER
|
deserialize: (m) => m === "ALWAYS" ? NodeMode.ALWAYS : NodeMode.NEVER
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "horizontal",
|
||||||
|
type: "boolean",
|
||||||
|
location: "nodeVars",
|
||||||
|
editable: true,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
|
||||||
// Node properties
|
// Node properties
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ImageViewer } from "$lib/ImageViewer";
|
import { ImageViewer } from "$lib/ImageViewer";
|
||||||
import { Block, BlockLabel, Empty } from "@gradio/atoms";
|
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 { Image } from "@gradio/icons";
|
||||||
|
import { StaticImage } from "$lib/components/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";
|
||||||
@@ -18,6 +19,9 @@
|
|||||||
let nodeValue: Writable<GradioFileData[]> | null = null;
|
let nodeValue: Writable<GradioFileData[]> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
let option: 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);
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
@@ -26,22 +30,31 @@
|
|||||||
node = widget.node as ComfyGalleryNode
|
node = widget.node as ComfyGalleryNode
|
||||||
nodeValue = node.value;
|
nodeValue = node.value;
|
||||||
propsChanged = node.propsChanged;
|
propsChanged = node.propsChanged;
|
||||||
|
node.anyImageSelected = false;
|
||||||
|
|
||||||
if ($nodeValue != null) {
|
if ($nodeValue != null) {
|
||||||
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
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 = {
|
let style: Styles = {
|
||||||
grid_cols: [4],
|
grid_cols: [3],
|
||||||
grid_rows: [4],
|
object_fit: "cover",
|
||||||
// object_fit: "cover",
|
|
||||||
}
|
}
|
||||||
let element: HTMLDivElement;
|
let element: HTMLDivElement;
|
||||||
|
|
||||||
|
$: if (node) {
|
||||||
|
if (imageWidth > 1 || imageHeight > 1) {
|
||||||
|
node.imageSize = [imageWidth, imageHeight]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node.imageSize = [1, 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mobileLightbox = null;
|
let mobileLightbox = null;
|
||||||
|
|
||||||
function showMobileLightbox(event: Event) {
|
function showMobileLightbox(event: Event) {
|
||||||
@@ -121,29 +134,43 @@
|
|||||||
|
|
||||||
// Update index
|
// Update index
|
||||||
node.setProperty("index", e.detail.index as number)
|
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>
|
</script>
|
||||||
|
|
||||||
{#if widget && node && nodeValue && $nodeValue != null}
|
{#if widget && node && nodeValue}
|
||||||
{#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}
|
{#if $nodeValue && $nodeValue.length > 0}
|
||||||
<BlockLabel
|
<StaticImage
|
||||||
show_label={true}
|
value={$nodeValue[$nodeValue.length-1].data}
|
||||||
Icon={Image}
|
show_label={widget.attrs.title != ""}
|
||||||
label={widget.attrs.title || "Image"}
|
label={widget.attrs.title}
|
||||||
|
bind:imageWidth
|
||||||
|
bind:imageHeight
|
||||||
/>
|
/>
|
||||||
{/if}
|
|
||||||
{#if $nodeValue.length > 0}
|
|
||||||
<img src={$nodeValue[$nodeValue.length-1].data}/>
|
|
||||||
{: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
|
||||||
@@ -154,6 +181,9 @@
|
|||||||
root={""}
|
root={""}
|
||||||
root_url={""}
|
root_url={""}
|
||||||
on:select={onSelect}
|
on:select={onSelect}
|
||||||
|
bind:imageWidth
|
||||||
|
bind:imageHeight
|
||||||
|
bind:selected_image
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Toolbar bottom>
|
<Toolbar bottom>
|
||||||
<Link on:click={queuePrompt}>Queue Prompt</Link>
|
|
||||||
<Link on:click={() => app.refreshComboInNodes()}>🔄</Link>
|
<Link on:click={() => app.refreshComboInNodes()}>🔄</Link>
|
||||||
<Link on:click={doLoad}>Load</Link>
|
<Link on:click={doLoad}>Load</Link>
|
||||||
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
|
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
|
||||||
|
|||||||
Reference in New Issue
Block a user