Integrate klecks
This commit is contained in:
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -5,3 +5,7 @@
|
|||||||
[submodule "litegraph"]
|
[submodule "litegraph"]
|
||||||
path = litegraph
|
path = litegraph
|
||||||
url = https://github.com/space-nuko/litegraph.ts
|
url = https://github.com/space-nuko/litegraph.ts
|
||||||
|
[submodule "klecks"]
|
||||||
|
path = klecks
|
||||||
|
url = https://github.com/space-nuko/klecks
|
||||||
|
branch = comfybox
|
||||||
|
|||||||
1
klecks
Submodule
1
klecks
Submodule
Submodule klecks added at 78c61a032e
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "web2",
|
"name": "ComfyBox",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"svelte-dnd-action": "^0.9.22",
|
"svelte-dnd-action": "^0.9.22",
|
||||||
"typescript": "^5.0.3",
|
"typescript": "^5.0.3",
|
||||||
"vite": "^4.3.1",
|
"vite": "^4.3.1",
|
||||||
|
"vite-plugin-glsl": "^1.1.2",
|
||||||
"vite-plugin-static-copy": "^0.14.0",
|
"vite-plugin-static-copy": "^0.14.0",
|
||||||
"vite-plugin-svelte-console-remover": "^1.0.10",
|
"vite-plugin-svelte-console-remover": "^1.0.10",
|
||||||
"vite-tsconfig-paths": "^4.0.8",
|
"vite-tsconfig-paths": "^4.0.8",
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
"framework7": "^8.0.3",
|
"framework7": "^8.0.3",
|
||||||
"framework7-svelte": "^8.0.3",
|
"framework7-svelte": "^8.0.3",
|
||||||
"img-comparison-slider": "^8.0.0",
|
"img-comparison-slider": "^8.0.0",
|
||||||
|
"klecks": "workspace:*",
|
||||||
"pollen-css": "^4.6.2",
|
"pollen-css": "^4.6.2",
|
||||||
"radix-icons-svelte": "^1.2.1",
|
"radix-icons-svelte": "^1.2.1",
|
||||||
"svelte-feather-icons": "^4.0.0",
|
"svelte-feather-icons": "^4.0.0",
|
||||||
|
|||||||
1667
pnpm-lock.yaml
generated
1667
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,4 @@ packages:
|
|||||||
- 'gradio/js/*'
|
- 'gradio/js/*'
|
||||||
- 'gradio/client/js'
|
- 'gradio/client/js'
|
||||||
- 'litegraph/packages/*'
|
- 'litegraph/packages/*'
|
||||||
|
- 'klecks'
|
||||||
|
|||||||
239
src/lib/components/ImageUpload.svelte
Normal file
239
src/lib/components/ImageUpload.svelte
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { Block, BlockLabel, Empty } from "@gradio/atoms";
|
||||||
|
import { File as FileIcon } from "@gradio/icons";
|
||||||
|
import { ModifyUpload, Upload, blobToBase64, normalise_file } from "@gradio/upload";
|
||||||
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
|
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
|
||||||
|
import { tick } from "svelte";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
import type { ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||||
|
|
||||||
|
export let value: GradioFileData[] | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
export let imgWidth: number = 0;
|
||||||
|
export let imgHeight: number = 0;
|
||||||
|
export let imgElem: HTMLImageElement | null = null
|
||||||
|
export let fileCount: "single" | "multiple" = "single"
|
||||||
|
export let elem_classes: string[] = []
|
||||||
|
export let style: string = ""
|
||||||
|
export let label: string = ""
|
||||||
|
// let propsChanged: Writable<number> | null = null;
|
||||||
|
let dragging = false;
|
||||||
|
let pending_upload = false;
|
||||||
|
let old_value: Array<GradioFileData> | null = null;
|
||||||
|
|
||||||
|
let _value: GradioFileData[] | null = null;
|
||||||
|
const root = "comf"
|
||||||
|
const root_url = "https//ComfyUI!"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
change: GradioFileData[];
|
||||||
|
upload: undefined;
|
||||||
|
clear: undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
$: _value = normalise_file(value, root, root_url);
|
||||||
|
|
||||||
|
$: if (!(_value && _value.length > 0 && imgElem)) {
|
||||||
|
imgWidth = 1
|
||||||
|
imgHeight = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange() {
|
||||||
|
value = _value || []
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpload() {
|
||||||
|
dispatch("upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
dispatch("clear")
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GradioUploadResponse {
|
||||||
|
error?: string;
|
||||||
|
files?: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
|
||||||
|
console.debug("UPLOADILFES", root, files);
|
||||||
|
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
|
||||||
|
const requests = files.map(async (file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", file, file.name);
|
||||||
|
return fetch(new Request(url + "/upload/image", {
|
||||||
|
body: formData,
|
||||||
|
method: 'POST'
|
||||||
|
}))
|
||||||
|
.then(r => r.json())
|
||||||
|
.catch(error => error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(requests)
|
||||||
|
.then( (results) => {
|
||||||
|
const errors = []
|
||||||
|
const files = []
|
||||||
|
|
||||||
|
for (const r of results) {
|
||||||
|
if (r instanceof Error) {
|
||||||
|
errors.push(r.cause)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// bare filename of image
|
||||||
|
files.push((r as ComfyUploadImageAPIResponse).name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
if (errors && errors.length > 0)
|
||||||
|
error = "Upload error(s):\n" + errors.join("\n");
|
||||||
|
|
||||||
|
return { error, files }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
|
||||||
|
pending_upload = true;
|
||||||
|
|
||||||
|
old_value = _value;
|
||||||
|
|
||||||
|
if (_value == null)
|
||||||
|
_value = []
|
||||||
|
else if (!Array.isArray(_value))
|
||||||
|
_value = [_value]
|
||||||
|
|
||||||
|
const allBlobs = _value.map((file_data: GradioFileData) => file_data.blob)
|
||||||
|
|
||||||
|
if (allBlobs == null || allBlobs.length === 0) {
|
||||||
|
_value = null;
|
||||||
|
onChange();
|
||||||
|
pending_upload = false;
|
||||||
|
}
|
||||||
|
else if (!allBlobs.every(b => b != null)) {
|
||||||
|
_value = null;
|
||||||
|
pending_upload = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let files = (Array.isArray(_value) ? _value : [_value]).map(
|
||||||
|
(file_data) => file_data.blob!
|
||||||
|
);
|
||||||
|
let upload_value = _value;
|
||||||
|
pending_upload = true;
|
||||||
|
upload_files(root, files).then((response) => {
|
||||||
|
if (JSON.stringify(upload_value) !== JSON.stringify(_value)) {
|
||||||
|
// value has changed since upload started
|
||||||
|
console.error("[ImageUpload] value has changed since upload started", upload_value, _value)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_upload = false;
|
||||||
|
_value.forEach(
|
||||||
|
(file_data: GradioFileData, i: number) => {
|
||||||
|
if (response.files) {
|
||||||
|
file_data.orig_name = file_data.name;
|
||||||
|
file_data.name = response.files[i];
|
||||||
|
file_data.is_file = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
notify(response.error, { type: "error" })
|
||||||
|
}
|
||||||
|
|
||||||
|
value = normalise_file(_value, root, root_url) as GradioFileData[];
|
||||||
|
onChange();
|
||||||
|
onUpload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
|
||||||
|
_value = Array.isArray(detail) ? detail : [detail];
|
||||||
|
await tick();
|
||||||
|
onChange();
|
||||||
|
onUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_clear(_e: CustomEvent<null>) {
|
||||||
|
_value = null;
|
||||||
|
onChange();
|
||||||
|
onClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageUrl(image: GradioFileData) {
|
||||||
|
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
|
||||||
|
return `${baseUrl}/view?${params}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="image-upload" {style}>
|
||||||
|
{#if value}
|
||||||
|
<Block
|
||||||
|
visible={true}
|
||||||
|
variant={(value === null || value.length === 0) ? "dashed" : "solid"}
|
||||||
|
border_mode={dragging ? "focus" : "base"}
|
||||||
|
padding={true}
|
||||||
|
elem_id="comfy-image-upload-block"
|
||||||
|
{elem_classes}
|
||||||
|
>
|
||||||
|
<BlockLabel
|
||||||
|
label={label}
|
||||||
|
show_label={label != ""}
|
||||||
|
Icon={FileIcon}
|
||||||
|
float={label != ""}
|
||||||
|
/>
|
||||||
|
{#if _value && _value.length > 0 && !pending_upload}
|
||||||
|
{@const firstImage = _value[0]}
|
||||||
|
<ModifyUpload on:clear={handle_clear} absolute />
|
||||||
|
<img src={getImageUrl(firstImage)}
|
||||||
|
alt={firstImage.orig_name}
|
||||||
|
bind:this={imgElem}
|
||||||
|
bind:naturalWidth={imgWidth}
|
||||||
|
bind:naturalHeight={imgHeight}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Upload
|
||||||
|
file_count={fileCount}
|
||||||
|
filetype="image/*"
|
||||||
|
on:change={({ detail }) => (value = detail)}
|
||||||
|
on:load={handle_upload}
|
||||||
|
bind:dragging
|
||||||
|
on:clear
|
||||||
|
on:select
|
||||||
|
parse_to_data_url={false}
|
||||||
|
>
|
||||||
|
<UploadText type="file" />
|
||||||
|
</Upload>
|
||||||
|
{/if}
|
||||||
|
</Block>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-upload {
|
||||||
|
height: var(--size-96);
|
||||||
|
|
||||||
|
:global(.block) {
|
||||||
|
height: inherit;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,33 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let showModal; // boolean
|
export let showModal; // boolean
|
||||||
|
|
||||||
let dialog; // HTMLDialogElement
|
let dialog; // HTMLDialogElement
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
close: undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
$: if (dialog && showModal) dialog.showModal();
|
$: if (dialog && showModal) dialog.showModal();
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
dialog.close();
|
||||||
|
dispatch("close")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<dialog
|
<dialog
|
||||||
bind:this={dialog}
|
bind:this={dialog}
|
||||||
on:close={() => (showModal = false)}
|
on:close={close}
|
||||||
on:click|self={() => dialog.close()}
|
on:click|self={close}
|
||||||
>
|
>
|
||||||
<div on:click|stopPropagation>
|
<div on:click|stopPropagation>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
<slot />
|
<slot />
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<button autofocus on:click={() => dialog.close()}>Close</button>
|
<button autofocus on:click={close}>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
|||||||
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
||||||
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
||||||
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
||||||
import ImageCompareWidget from "$lib/widgets/ImageCompareWidget.svelte";
|
import ImageEditorWidget from "$lib/widgets/ImageEditorWidget.svelte";
|
||||||
|
|
||||||
export type AutoConfigOptions = {
|
export type AutoConfigOptions = {
|
||||||
includeProperties?: Set<string> | null,
|
includeProperties?: Set<string> | null,
|
||||||
@@ -33,7 +33,7 @@ export type AutoConfigOptions = {
|
|||||||
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
||||||
* attribute and set `validNodeTypes` to the type of the litegraph node
|
* attribute and set `validNodeTypes` to the type of the litegraph node
|
||||||
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
|
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
|
||||||
* - Add an {#if widget.attrs.variant === <...>} statement in the corresponding Svelte component
|
* - Add an {#if widget.attrs.variant === <...>} statement in the existing Svelte component
|
||||||
*
|
*
|
||||||
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
||||||
* You will have to either implement an internal delay on the event triggering
|
* You will have to either implement an internal delay on the event triggering
|
||||||
@@ -965,80 +965,59 @@ LiteGraph.registerNodeType({
|
|||||||
type: "ui/image_upload"
|
type: "ui/image_upload"
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyImageCompareNodeProperties extends ComfyWidgetProperties {
|
export type FileNameOrGalleryData = string | GalleryOutputEntry;
|
||||||
|
export type MultiImageData = FileNameOrGalleryData[];
|
||||||
|
|
||||||
|
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileNameOrGalleryData = string | GalleryOutputEntry;
|
export class ComfyImageEditorNode extends ComfyWidgetNode<MultiImageData> {
|
||||||
export type ImageCompareData = [FileNameOrGalleryData, FileNameOrGalleryData]
|
override properties: ComfyImageEditorNodeProperties = {
|
||||||
|
|
||||||
export class ComfyImageCompareNode extends ComfyWidgetNode<ImageCompareData> {
|
|
||||||
override properties: ComfyImageCompareNodeProperties = {
|
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION },
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
{ name: "left_image", type: "string" },
|
|
||||||
{ name: "right_image", type: "string" },
|
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
override svelteComponentType = ImageCompareWidget;
|
override svelteComponentType = ImageEditorWidget;
|
||||||
override defaultValue: ImageCompareData = ["", ""];
|
override defaultValue: MultiImageData = [];
|
||||||
override outputIndex = null;
|
override outputIndex = null;
|
||||||
override changedIndex = 3;
|
override changedIndex = null;
|
||||||
override storeActionName = "store";
|
override storeActionName = "store";
|
||||||
override saveUserState = false;
|
override saveUserState = false;
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
super(name, ["", ""])
|
super(name, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExecute() {
|
_value = null;
|
||||||
const valueA = this.getInputData(1)
|
|
||||||
const valueB = this.getInputData(2)
|
|
||||||
let current = get(this.value)
|
|
||||||
let changed = false;
|
|
||||||
if (valueA != null && current[0] != valueA) {
|
|
||||||
current[0] = valueA
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (valueB != null && current[1] != valueB) {
|
|
||||||
current[1] = valueB
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed)
|
|
||||||
this.value.set(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): ImageCompareData {
|
override parseValue(value: any): MultiImageData {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return ["", ""]
|
return []
|
||||||
}
|
}
|
||||||
else if (typeof value === "string" && value !== "") { // Single filename
|
else if (typeof value === "string" && value !== "") { // Single filename
|
||||||
const prevValue = get(this.value)
|
const prevValue = get(this.value)
|
||||||
prevValue.push(value)
|
prevValue.push(value)
|
||||||
if (prevValue.length > 2)
|
if (prevValue.length > 2)
|
||||||
prevValue.splice(0, 1)
|
prevValue.splice(0, 1)
|
||||||
return prevValue as ImageCompareData
|
return prevValue as MultiImageData
|
||||||
}
|
}
|
||||||
else if (typeof value === "object" && "images" in value && value.images.length > 0) {
|
else if (typeof value === "object" && "images" in value && value.images.length > 0) {
|
||||||
const output = value as GalleryOutput
|
const output = value as GalleryOutput
|
||||||
const prevValue = get(this.value)
|
return [value]
|
||||||
prevValue.push(output.images[0].filename)
|
|
||||||
if (prevValue.length > 2)
|
|
||||||
prevValue.splice(0, 1)
|
|
||||||
return prevValue as ImageCompareData
|
|
||||||
}
|
}
|
||||||
else if (Array.isArray(value) && typeof value[0] === "string" && typeof value[1] === "string") {
|
else if (Array.isArray(value) && value.every(s => typeof s === "string")) {
|
||||||
return value as ImageCompareData
|
return value as MultiImageData
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return ["", ""]
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,8 +1027,8 @@ export class ComfyImageCompareNode extends ComfyWidgetNode<ImageCompareData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
LiteGraph.registerNodeType({
|
||||||
class: ComfyImageCompareNode,
|
class: ComfyImageEditorNode,
|
||||||
title: "UI.ImageCompare",
|
title: "UI.ImageEditor",
|
||||||
desc: "Widget that lets you compare two images",
|
desc: "Widget that lets edit a multi-layered image",
|
||||||
type: "ui/image_compare"
|
type: "ui/image_editor"
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
|
||||||
import { Block } from "@gradio/atoms";
|
|
||||||
import ImageComparison from "$lib/components/ImageComparison.svelte";
|
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
|
||||||
import { isDisabled } from "./utils"
|
|
||||||
import type { SelectData } from "@gradio/utils";
|
|
||||||
import type { ComfyImageCompareNode, ImageCompareData } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import { convertFilenameToComfyURL } from "$lib/utils";
|
|
||||||
import { TabItem, Tabs } from "@gradio/tabs";
|
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
|
||||||
export let isMobile: boolean = false;
|
|
||||||
let node: ComfyImageCompareNode | null = null;
|
|
||||||
let nodeValue: Writable<ImageCompareData> | null = null;
|
|
||||||
let attrsChanged: Writable<number> | null = null;
|
|
||||||
let leftUrl: string = ""
|
|
||||||
let rightUrl: string = ""
|
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
|
||||||
if (widget) {
|
|
||||||
node = widget.node as ComfyImageCompareNode
|
|
||||||
nodeValue = node.value;
|
|
||||||
attrsChanged = widget.attrsChanged;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const urlPattern = /^((http|https|ftp):\/\/)/;
|
|
||||||
|
|
||||||
$: updateUrls($nodeValue);
|
|
||||||
|
|
||||||
function updateUrls(value: ImageCompareData) {
|
|
||||||
leftUrl = ""
|
|
||||||
rightUrl = ""
|
|
||||||
console.warn("UPD", value)
|
|
||||||
|
|
||||||
if (typeof value[0] === "string") {
|
|
||||||
if (urlPattern.test(value[0]))
|
|
||||||
leftUrl = value[0]
|
|
||||||
else
|
|
||||||
leftUrl = convertFilenameToComfyURL(value[0])
|
|
||||||
}
|
|
||||||
if (typeof value[1] === "string") {
|
|
||||||
if (urlPattern.test(value[1]))
|
|
||||||
rightUrl = value[1]
|
|
||||||
else
|
|
||||||
rightUrl = convertFilenameToComfyURL(value[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper comfy-compare-widget">
|
|
||||||
<Block>
|
|
||||||
<Tabs elem_classes={["gradio-tabs"]}>
|
|
||||||
<TabItem name="Slider">
|
|
||||||
<ImageComparison>
|
|
||||||
{#if leftUrl && leftUrl != ""}
|
|
||||||
{@const props = { slot: "first" }}
|
|
||||||
<img {...props} alt="Left" src={leftUrl} />
|
|
||||||
{/if}
|
|
||||||
{#if rightUrl && leftUrl != ""}
|
|
||||||
{@const props = { slot: "second" }}
|
|
||||||
<img {...props} alt="Right" src={rightUrl} />
|
|
||||||
{/if}
|
|
||||||
</ImageComparison>
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
</Block>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.comfy-compare-widget {
|
|
||||||
max-width: 40rem;
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
148
src/lib/widgets/ImageEditorWidget.svelte
Normal file
148
src/lib/widgets/ImageEditorWidget.svelte
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import type { ComfyImageEditorNode, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
|
import { Embed as Klecks, KL, KlApp, klHistory, type KlAppOptionsEmbed } from "klecks";
|
||||||
|
|
||||||
|
import "klecks/style/style.scss";
|
||||||
|
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
let node: ComfyImageEditorNode | null = null;
|
||||||
|
let nodeValue: Writable<MultiImageData> | null = null;
|
||||||
|
let attrsChanged: Writable<number> | null = null;
|
||||||
|
let leftUrl: string = ""
|
||||||
|
let rightUrl: string = ""
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
|
if (widget) {
|
||||||
|
node = widget.node as ComfyImageEditorNode
|
||||||
|
nodeValue = node.value;
|
||||||
|
attrsChanged = widget.attrsChanged;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlPattern = /^((http|https|ftp):\/\/)/;
|
||||||
|
|
||||||
|
$: updateUrls($nodeValue);
|
||||||
|
|
||||||
|
function updateUrls(value: MultiImageData) {
|
||||||
|
// leftUrl = ""
|
||||||
|
// rightUrl = ""
|
||||||
|
// console.warn("UPD", value)
|
||||||
|
//
|
||||||
|
// if (typeof value[0] === "string") {
|
||||||
|
// if (urlPattern.test(value[0]))
|
||||||
|
// leftUrl = value[0]
|
||||||
|
// else
|
||||||
|
// leftUrl = convertFilenameToComfyURL(value[0])
|
||||||
|
// }
|
||||||
|
// if (typeof value[1] === "string") {
|
||||||
|
// if (urlPattern.test(value[1]))
|
||||||
|
// rightUrl = value[1]
|
||||||
|
// else
|
||||||
|
// rightUrl = convertFilenameToComfyURL(value[1])
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
let editorRoot: HTMLDivElement | null = null;
|
||||||
|
let showModal = false;
|
||||||
|
let kl: Klecks | null = null;
|
||||||
|
|
||||||
|
function disposeEditor() {
|
||||||
|
if (editorRoot) {
|
||||||
|
while (editorRoot.firstChild) {
|
||||||
|
editorRoot.removeChild(editorRoot.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kl = null;
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||||
|
const data = kl.getPNG();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBlankImage(fill: string = "#fff"): HTMLCanvasElement {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 512;
|
||||||
|
canvas.height = 512;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = fill,
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.restore();
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openImageEditor() {
|
||||||
|
if (!editorRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
showModal = true;
|
||||||
|
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
|
||||||
|
kl = new Klecks({
|
||||||
|
embedUrl: url,
|
||||||
|
onSubmit: submitKlecksToComfyUI,
|
||||||
|
targetEl: editorRoot.parentElement.parentElement
|
||||||
|
});
|
||||||
|
|
||||||
|
kl.openProject({
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
layers: [{
|
||||||
|
name: 'Image',
|
||||||
|
opacity: 1,
|
||||||
|
mixModeStr: 'source-over',
|
||||||
|
image: generateBlankImage(),
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
kl.klApp?.out("yo");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper comfy-image-editor">
|
||||||
|
{#if isMobile}
|
||||||
|
<span>TODO mask editor</span>
|
||||||
|
{:else}
|
||||||
|
<Modal bind:showModal on:close={disposeEditor}>
|
||||||
|
<div id="klecks-loading-screen">
|
||||||
|
<span id="klecks-loading-screen-text"></span>
|
||||||
|
</div>
|
||||||
|
<div class="image-editor-root" bind:this={editorRoot} />
|
||||||
|
</Modal>
|
||||||
|
<div class="comfy-image-editor-panel">
|
||||||
|
<Block>
|
||||||
|
<BlockTitle>Image editor.</BlockTitle>
|
||||||
|
<Button variant="secondary" on:click={openImageEditor}>
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-editor-root {
|
||||||
|
width: 75vw;
|
||||||
|
height: 75vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-image-editor {
|
||||||
|
:global(> dialog) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Block, BlockLabel, Empty } from "@gradio/atoms";
|
import { Block, BlockLabel, Empty } from "@gradio/atoms";
|
||||||
import { File as FileIcon } from "@gradio/icons";
|
import { File as FileIcon } from "@gradio/icons";
|
||||||
import { ModifyUpload, Upload, blobToBase64, normalise_file } from "@gradio/upload";
|
import ImageUpload from "$lib/components/ImageUpload.svelte"
|
||||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
import type { WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
@@ -19,29 +19,12 @@
|
|||||||
let dragging = false;
|
let dragging = false;
|
||||||
let pending_upload = false;
|
let pending_upload = false;
|
||||||
let old_value: Array<GradioFileData> | null = null;
|
let old_value: Array<GradioFileData> | null = null;
|
||||||
let imgElem: HTMLImageElement | null = null
|
|
||||||
let imgWidth: number = 1;
|
let imgWidth: number = 1;
|
||||||
let imgHeight: number = 1;
|
let imgHeight: number = 1;
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
let _value: GradioFileData[] | null = null;
|
$: if (!(node && $nodeValue && $nodeValue.length > 0)) {
|
||||||
const root = "comf"
|
|
||||||
const root_url = "https//ComfyUI!"
|
|
||||||
|
|
||||||
$: _value = normalise_file($nodeValue, root, root_url);
|
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
|
||||||
if (widget) {
|
|
||||||
node = widget.node as ComfyImageUploadNode
|
|
||||||
nodeValue = node.value;
|
|
||||||
propsChanged = node.propsChanged;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$: if (!(node && _value && _value.length > 0 && imgElem)) {
|
|
||||||
imgWidth = 1
|
|
||||||
imgHeight = 1
|
|
||||||
node.imageSize = [1, 1]
|
node.imageSize = [1, 1]
|
||||||
}
|
}
|
||||||
else if (imgWidth > 1 || imgHeight > 1) {
|
else if (imgWidth > 1 || imgHeight > 1) {
|
||||||
@@ -51,178 +34,30 @@
|
|||||||
node.imageSize = [1, 1]
|
node.imageSize = [1, 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChange() {
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
$nodeValue = _value || []
|
if (widget) {
|
||||||
}
|
node = widget.node as ComfyImageUploadNode
|
||||||
|
nodeValue = node.value;
|
||||||
function onUpload() {
|
propsChanged = node.propsChanged;
|
||||||
}
|
|
||||||
|
|
||||||
function onClear() {
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GradioUploadResponse {
|
|
||||||
error?: string;
|
|
||||||
files?: Array<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {
|
|
||||||
console.debug("UPLOADILFES", root, files);
|
|
||||||
|
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
|
||||||
|
|
||||||
const requests = files.map(async (file) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("image", file, file.name);
|
|
||||||
return fetch(new Request(url + "/upload/image", {
|
|
||||||
body: formData,
|
|
||||||
method: 'POST'
|
|
||||||
}))
|
|
||||||
.then(r => r.json())
|
|
||||||
.catch(error => error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(requests)
|
|
||||||
.then( (results) => {
|
|
||||||
const errors = []
|
|
||||||
const files = []
|
|
||||||
|
|
||||||
for (const r of results) {
|
|
||||||
if (r instanceof Error) {
|
|
||||||
errors.push(r.cause)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// bare filename of image
|
|
||||||
files.push((r as ComfyUploadImageAPIResponse).name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let error = null;
|
|
||||||
if (errors && errors.length > 0)
|
|
||||||
error = "Upload error(s):\n" + errors.join("\n");
|
|
||||||
|
|
||||||
return { error, files }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (JSON.stringify(_value) !== JSON.stringify(old_value)) {
|
|
||||||
pending_upload = true;
|
|
||||||
|
|
||||||
old_value = _value;
|
|
||||||
|
|
||||||
if (_value == null)
|
|
||||||
_value = []
|
|
||||||
else if (!Array.isArray(_value))
|
|
||||||
_value = [_value]
|
|
||||||
|
|
||||||
const allBlobs = _value.map((file_data: GradioFileData) => file_data.blob)
|
|
||||||
|
|
||||||
if (allBlobs == null || allBlobs.length === 0) {
|
|
||||||
_value = null;
|
|
||||||
onChange();
|
|
||||||
pending_upload = false;
|
|
||||||
}
|
|
||||||
else if (!allBlobs.every(b => b != null)) {
|
|
||||||
_value = null;
|
|
||||||
pending_upload = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let files = (Array.isArray(_value) ? _value : [_value]).map(
|
|
||||||
(file_data) => file_data.blob!
|
|
||||||
);
|
|
||||||
let upload_value = _value;
|
|
||||||
pending_upload = true;
|
|
||||||
upload_files(root, files).then((response) => {
|
|
||||||
if (JSON.stringify(upload_value) !== JSON.stringify(_value)) {
|
|
||||||
// value has changed since upload started
|
|
||||||
console.error("[ImageUploadWidget] value has changed since upload started", upload_value, _value)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending_upload = false;
|
|
||||||
_value.forEach(
|
|
||||||
(file_data: GradioFileData, i: number) => {
|
|
||||||
if (response.files) {
|
|
||||||
file_data.orig_name = file_data.name;
|
|
||||||
file_data.name = response.files[i];
|
|
||||||
file_data.is_file = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
notify(response.error, { type: "error" })
|
|
||||||
}
|
|
||||||
|
|
||||||
$nodeValue = normalise_file(_value, root, root_url) as GradioFileData[];
|
|
||||||
onChange();
|
|
||||||
onUpload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function handle_upload({ detail }: CustomEvent<GradioFileData | Array<GradioFileData>>) {
|
function onChange(e: CustomEvent<GradioFileData[]>) {
|
||||||
_value = Array.isArray(detail) ? detail : [detail];
|
$nodeValue = e.detail || []
|
||||||
await tick();
|
|
||||||
onChange();
|
|
||||||
onUpload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_clear(_e: CustomEvent<null>) {
|
|
||||||
_value = null;
|
|
||||||
onChange();
|
|
||||||
onClear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageUrl(image: GradioFileData) {
|
|
||||||
const baseUrl = `http://${location.hostname}:8188` // TODO make configurable
|
|
||||||
const params = new URLSearchParams({ filename: image.name, subfolder: "", type: "input" })
|
|
||||||
return `${baseUrl}/view?${params}`
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}>
|
<div class="wrapper gradio-file comfy-image-upload" style={widget.attrs.style}>
|
||||||
{#if widget && node && nodeValue}
|
{#if widget && node && nodeValue}
|
||||||
<Block
|
<ImageUpload value={$nodeValue}
|
||||||
visible={true}
|
{isMobile}
|
||||||
variant={($nodeValue === null || $nodeValue.length === 0) ? "dashed" : "solid"}
|
bind:imgWidth
|
||||||
border_mode={dragging ? "focus" : "base"}
|
bind:imgHeight
|
||||||
padding={true}
|
bind:fileCount={node.properties.fileCount}
|
||||||
elem_id="comfy-image-upload-block"
|
elem_classes={widget.attrs.classes.split(",")}
|
||||||
elem_classes={widget.attrs.classes.split(",")}
|
style={widget.attrs.style}
|
||||||
>
|
label={widget.attrs.title}
|
||||||
<BlockLabel
|
on:change={onChange}/>
|
||||||
label={widget.attrs.title}
|
|
||||||
show_label={widget.attrs.title != ""}
|
|
||||||
Icon={FileIcon}
|
|
||||||
float={widget.attrs.title != ""}
|
|
||||||
/>
|
|
||||||
{#if _value && _value.length > 0 && !pending_upload}
|
|
||||||
{@const firstImage = _value[0]}
|
|
||||||
<ModifyUpload on:clear={handle_clear} absolute />
|
|
||||||
<img src={getImageUrl(firstImage)}
|
|
||||||
alt={firstImage.orig_name}
|
|
||||||
bind:this={imgElem}
|
|
||||||
bind:naturalWidth={imgWidth}
|
|
||||||
bind:naturalHeight={imgHeight}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Upload
|
|
||||||
file_count={node.properties.fileCount}
|
|
||||||
filetype="image/*"
|
|
||||||
on:change={({ detail }) => ($nodeValue = detail)}
|
|
||||||
on:load={handle_upload}
|
|
||||||
bind:dragging
|
|
||||||
on:clear
|
|
||||||
on:select
|
|
||||||
parse_to_data_url={false}
|
|
||||||
>
|
|
||||||
<UploadText type="file" />
|
|
||||||
</Upload>
|
|
||||||
{/if}
|
|
||||||
</Block>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import tsconfigPaths from 'vite-tsconfig-paths';
|
|||||||
import FullReload from 'vite-plugin-full-reload';
|
import FullReload from 'vite-plugin-full-reload';
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
import removeConsole from 'vite-plugin-svelte-console-remover';
|
import removeConsole from 'vite-plugin-svelte-console-remover';
|
||||||
|
import glsl from 'vite-plugin-glsl';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
console.log("Production build: " + isProduction)
|
console.log("Production build: " + isProduction)
|
||||||
@@ -20,6 +21,7 @@ export default defineConfig({
|
|||||||
// "src/**/ComfyApp.{ts,svelte}"
|
// "src/**/ComfyApp.{ts,svelte}"
|
||||||
// ]),
|
// ]),
|
||||||
isProduction && removeConsole(),
|
isProduction && removeConsole(),
|
||||||
|
glsl(),
|
||||||
svelte(),
|
svelte(),
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
|
|||||||
Reference in New Issue
Block a user