Refactor widgets/nodes

This commit is contained in:
space-nuko
2023-05-16 20:15:42 -05:00
parent e8567708fc
commit 0d6395c072
26 changed files with 580 additions and 1153 deletions

View File

@@ -1,10 +1,9 @@
<script lang="ts">
import type { ComfyButtonNode } from "$lib/nodes/ComfyWidgetNodes";
import type { ComfySliderNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Button } from "@gradio/button";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { ComfyButtonNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyButtonNode | null = null;

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import { Checkbox } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { SelectData } from "@gradio/utils";
import type { SelectData } from "@gradio/utils";
import type { ComfyCheckboxNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;

View File

@@ -4,7 +4,7 @@
import Select from 'svelte-select';
// import VirtualList from '$lib/components/VirtualList.svelte';
import VirtualList from 'svelte-tiny-virtual-list';
import type { ComfyComboNode } from "$lib/nodes/index";
import type { ComfyComboNode } from "$lib/nodes/widgets";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, writable, type Writable } from "svelte/store";
import { isDisabled } from "./utils"

View File

@@ -1,40 +0,0 @@
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core";
export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T, V> {
name: string;
value: V;
node: ComfyGraphNode;
constructor(name: string, value: V, node: ComfyGraphNode) {
this.name = name;
this.value = value
this.node = node;
}
isVirtual?: boolean;
options?: T;
type?: WidgetTypes | string | any;
y?: number;
property?: string;
last_y?: number;
width?: number;
clicked?: boolean;
marker?: boolean;
disabled?: boolean;
callback?: WidgetCallback<this>;
setValue(value: V) {
this.value = value;
}
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number): void;
mouse?(event: MouseEvent, pos: Vector2, node: LGraphNode): boolean;
computeSize?(width: number): [number, number];
afterQueued?(): void;
serializeValue?(serialized: SerializedLGraphNode<LGraphNode>, slot: number): Promise<any>;
}

View File

@@ -7,11 +7,11 @@
import type { Styles } from "@gradio/utils";
import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store";
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload";
import type { SelectData as GradioSelectData } from "@gradio/utils";
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
import { f7 } from "framework7-svelte";
import type { ComfyGalleryNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;

View File

@@ -1,343 +0,0 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import { TextBox } from "@gradio/form";
import Row from "$lib/components/gradio/app/Row.svelte";
import { get, type Writable } from "svelte/store";
import Modal from "$lib/components/Modal.svelte";
import { Button } from "@gradio/button";
import type { ComfyImageEditorNode, ComfyImageLocation } from "$lib/nodes/ComfyWidgetNodes";
import { Embed as Klecks } from "klecks";
import "klecks/style/style.scss";
import ImageUpload from "$lib/components/ImageUpload.svelte";
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType } from "$lib/utils";
import notify from "$lib/notify";
import NumberInput from "$lib/components/NumberInput.svelte";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageEditorNode | null = null;
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
let attrsChanged: Writable<number> | null = null;
let imgWidth: number = 0;
let imgHeight: number = 0;
$: widget && setNodeValue(widget);
$: if ($nodeValue && $nodeValue.length > 0) {
// TODO improve
if (imgWidth > 0 && imgHeight > 0) {
$nodeValue[0].width = imgWidth
$nodeValue[0].height = imgHeight
}
else {
$nodeValue[0].width = 0
$nodeValue[0].height = 0
}
}
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageEditorNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
}
};
let editorRoot: HTMLDivElement | null = null;
let showModal = false;
let kl: Klecks | null = null;
function disposeEditor() {
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
if (editorRoot) {
while (editorRoot.firstChild) {
editorRoot.removeChild(editorRoot.firstChild);
}
}
kl = null;
showModal = false;
}
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = fill,
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
return canvas;
}
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
return new Promise((resolve) => {
const e = new Image();
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
e.addEventListener("load", () => { resolve(e); });
e.src = imageURL;
return e;
});
}
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
const image = await loadImage(imageURL);
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
ctx.restore();
return [canvas, image.width, image.height];
}
const FILENAME: string = "ComfyUITemp.png";
const SUBFOLDER: string = "ComfyBox_Editor";
const DIRECTORY: ComfyUploadImageType = "input";
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
const blob = kl.getPNG();
status = "uploading"
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
.then((entry: ComfyImageLocation) => {
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
$nodeValue = [meta] // TODO more than one image
status = "uploaded"
notify("Saved image to ComfyUI!", { type: "success" })
onSuccess();
})
.catch(err => {
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
status = "error"
uploadError = err;
$nodeValue = []
onError();
})
}
let closeDialog = null;
async function saveAndClose() {
console.log(closeDialog, kl)
if (!closeDialog || !kl)
return;
submitKlecksToComfyUI(() => {}, () => {});
closeDialog()
}
let blankImageWidth = 512;
let blankImageHeight = 512;
async 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,
warnOnPageClose: false
});
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
let canvas = null;
let width = blankImageWidth;
let height = blankImageHeight;
if ($nodeValue && $nodeValue.length > 0) {
const comfyImage = $nodeValue[0];
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
[canvas, width, height] = await generateImageCanvas(comfyURL);
}
else {
canvas = generateBlankCanvas(width, height);
}
kl.openProject({
width: width,
height: height,
layers: [{
name: 'Image',
opacity: 1,
mixModeStr: 'source-over',
image: canvas
}]
});
setTimeout(function () {
kl.klApp?.out("yo");
}, 1000);
}
let status = "empty";
let uploadError = null;
function onUploading() {
console.warn("UPLOADING!!!")
uploadError = null;
status = "uploading"
}
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
console.warn("UPLOADED!!!")
uploadError = null;
status = "uploaded"
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
}
function onClear() {
console.warn("CLEAR!!!")
uploadError = null;
status = "empty"
$nodeValue = []
}
function onUploadError(e: CustomEvent<any>) {
console.warn("ERROR!!!")
status = "error"
uploadError = e.detail
$nodeValue = []
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
}
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
}
let _value: ComfyImageLocation[] = []
$: if ($nodeValue)
_value = $nodeValue.map(comfyBoxImageToComfyFile)
else
_value = []
$: canEdit = status === "empty" || status === "uploaded";
</script>
<div class="wrapper comfy-image-editor">
{#if widget.attrs.variant === "fileUpload" || isMobile}
<ImageUpload value={_value}
bind:imgWidth
bind:imgHeight
fileCount={"single"}
elem_classes={[]}
style={""}
label={widget.attrs.title}
on:uploading={onUploading}
on:uploaded={onUploaded}
on:upload_error={onUploadError}
on:clear={onClear}
on:change={onChange}
on:image_clicked={openImageEditor}
/>
{:else}
<div class="comfy-image-editor-panel">
<ImageUpload value={_value}
bind:imgWidth
bind:imgHeight
fileCount={"single"}
elem_classes={[]}
style={""}
label={widget.attrs.title}
on:uploading={onUploading}
on:uploaded={onUploaded}
on:upload_error={onUploadError}
on:clear={onClear}
on:change={onChange}
on:image_clicked={openImageEditor}
/>
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
<div>
<div id="klecks-loading-screen">
<span id="klecks-loading-screen-text"></span>
</div>
<div class="image-editor-root" bind:this={editorRoot} />
</div>
<div slot="buttons">
<Button variant="primary" on:click={saveAndClose}>
Save and Close
</Button>
<Button variant="secondary" on:click={closeDialog}>
Discard Edits
</Button>
</div>
</Modal>
<Block>
{#if !$nodeValue || $nodeValue.length === 0}
<Row>
<Row>
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
Create Image
</Button>
<div>
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
</div>
{#if uploadError}
<div>
Upload error: {uploadError}
</div>
{/if}
</Row>
<Row>
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
</Row>
</Row>
{:else}
<Row>
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
Edit Image
</Button>
<div>
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
</div>
{#if uploadError}
<div>
Upload error: {uploadError}
</div>
{/if}
</Row>
{/if}
</Block>
</div>
{/if}
</div>
<style lang="scss">
.image-editor-root {
width: 75vw;
height: 75vh;
overflow: hidden;
color: black;
:global(> .g-root) {
height: calc(100% - 59px);
}
}
.comfy-image-editor {
:global(> dialog) {
overflow: hidden;
}
}
:global(.kl-popup) {
z-index: 999999999999;
}
</style>

View File

@@ -0,0 +1,343 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import { TextBox } from "@gradio/form";
import Row from "$lib/components/gradio/app/Row.svelte";
import { get, type Writable } from "svelte/store";
import Modal from "$lib/components/Modal.svelte";
import { Button } from "@gradio/button";
import { Embed as Klecks } from "klecks";
import "klecks/style/style.scss";
import ImageUpload from "$lib/components/ImageUpload.svelte";
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType, type ComfyImageLocation } from "$lib/utils";
import notify from "$lib/notify";
import NumberInput from "$lib/components/NumberInput.svelte";
import type { ComfyImageEditorNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageEditorNode | null = null;
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
let attrsChanged: Writable<number> | null = null;
let imgWidth: number = 0;
let imgHeight: number = 0;
$: widget && setNodeValue(widget);
$: if ($nodeValue && $nodeValue.length > 0) {
// TODO improve
if (imgWidth > 0 && imgHeight > 0) {
$nodeValue[0].width = imgWidth
$nodeValue[0].height = imgHeight
}
else {
$nodeValue[0].width = 0
$nodeValue[0].height = 0
}
}
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyImageEditorNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
}
};
let editorRoot: HTMLDivElement | null = null;
let showModal = false;
let kl: Klecks | null = null;
function disposeEditor() {
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
if (editorRoot) {
while (editorRoot.firstChild) {
editorRoot.removeChild(editorRoot.firstChild);
}
}
kl = null;
showModal = false;
}
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = fill,
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
return canvas;
}
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
return new Promise((resolve) => {
const e = new Image();
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
e.addEventListener("load", () => { resolve(e); });
e.src = imageURL;
return e;
});
}
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
const image = await loadImage(imageURL);
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
ctx.restore();
return [canvas, image.width, image.height];
}
const FILENAME: string = "ComfyUITemp.png";
const SUBFOLDER: string = "ComfyBox_Editor";
const DIRECTORY: ComfyUploadImageType = "input";
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
const blob = kl.getPNG();
status = "uploading"
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
.then((entry: ComfyImageLocation) => {
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
$nodeValue = [meta] // TODO more than one image
status = "uploaded"
notify("Saved image to ComfyUI!", { type: "success" })
onSuccess();
})
.catch(err => {
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
status = "error"
uploadError = err;
$nodeValue = []
onError();
})
}
let closeDialog = null;
async function saveAndClose() {
console.log(closeDialog, kl)
if (!closeDialog || !kl)
return;
submitKlecksToComfyUI(() => {}, () => {});
closeDialog()
}
let blankImageWidth = 512;
let blankImageHeight = 512;
async 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,
warnOnPageClose: false
});
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
let canvas = null;
let width = blankImageWidth;
let height = blankImageHeight;
if ($nodeValue && $nodeValue.length > 0) {
const comfyImage = $nodeValue[0];
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
[canvas, width, height] = await generateImageCanvas(comfyURL);
}
else {
canvas = generateBlankCanvas(width, height);
}
kl.openProject({
width: width,
height: height,
layers: [{
name: 'Image',
opacity: 1,
mixModeStr: 'source-over',
image: canvas
}]
});
setTimeout(function () {
kl.klApp?.out("yo");
}, 1000);
}
let status = "empty";
let uploadError = null;
function onUploading() {
console.warn("UPLOADING!!!")
uploadError = null;
status = "uploading"
}
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
console.warn("UPLOADED!!!")
uploadError = null;
status = "uploaded"
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
}
function onClear() {
console.warn("CLEAR!!!")
uploadError = null;
status = "empty"
$nodeValue = []
}
function onUploadError(e: CustomEvent<any>) {
console.warn("ERROR!!!")
status = "error"
uploadError = e.detail
$nodeValue = []
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
}
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
}
let _value: ComfyImageLocation[] = []
$: if ($nodeValue)
_value = $nodeValue.map(comfyBoxImageToComfyFile)
else
_value = []
$: canEdit = status === "empty" || status === "uploaded";
</script>
<div class="wrapper comfy-image-editor">
{#if widget.attrs.variant === "fileUpload" || isMobile}
<ImageUpload value={_value}
bind:imgWidth
bind:imgHeight
fileCount={"single"}
elem_classes={[]}
style={""}
label={widget.attrs.title}
on:uploading={onUploading}
on:uploaded={onUploaded}
on:upload_error={onUploadError}
on:clear={onClear}
on:change={onChange}
on:image_clicked={openImageEditor}
/>
{:else}
<div class="comfy-image-editor-panel">
<ImageUpload value={_value}
bind:imgWidth
bind:imgHeight
fileCount={"single"}
elem_classes={[]}
style={""}
label={widget.attrs.title}
on:uploading={onUploading}
on:uploaded={onUploaded}
on:upload_error={onUploadError}
on:clear={onClear}
on:change={onChange}
on:image_clicked={openImageEditor}
/>
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
<div>
<div id="klecks-loading-screen">
<span id="klecks-loading-screen-text"></span>
</div>
<div class="image-editor-root" bind:this={editorRoot} />
</div>
<div slot="buttons">
<Button variant="primary" on:click={saveAndClose}>
Save and Close
</Button>
<Button variant="secondary" on:click={closeDialog}>
Discard Edits
</Button>
</div>
</Modal>
<Block>
{#if !$nodeValue || $nodeValue.length === 0}
<Row>
<Row>
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
Create Image
</Button>
<div>
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
</div>
{#if uploadError}
<div>
Upload error: {uploadError}
</div>
{/if}
</Row>
<Row>
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
</Row>
</Row>
{:else}
<Row>
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
Edit Image
</Button>
<div>
<TextBox label={""} show_label={false} disabled={true} lines={1} max_lines={1} value="Status: {status}"/>
</div>
{#if uploadError}
<div>
Upload error: {uploadError}
</div>
{/if}
</Row>
{/if}
</Block>
</div>
{/if}
</div>
<style lang="scss">
.image-editor-root {
width: 75vw;
height: 75vh;
overflow: hidden;
color: black;
:global(> .g-root) {
height: calc(100% - 59px);
}
}
.comfy-image-editor {
:global(> dialog) {
overflow: hidden;
}
}
:global(.kl-popup) {
z-index: 999999999999;
}
</style>

View File

@@ -0,0 +1,173 @@
<script lang="ts">
import type { ComfyNumberNode } from "$lib/nodes/widgets";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Range } from "$lib/components/gradio/form";
import { get, type Writable } from "svelte/store";
import { debounce } from "$lib/utils";
import interfaceState from "$lib/stores/interfaceState";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyNumberNode | null = null;
let nodeValue: Writable<number> | null = null;
let propsChanged: Writable<number> | null = null;
let option: number | null = null;
let isDragging: boolean = false;
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyNumberNode
nodeValue = node.value;
propsChanged = node.propsChanged;
setOption($nodeValue); // don't react on option
}
isDragging = false;
};
// I don't know why but this is necessary to watch for changes to node
// properties from ComfyWidgetNode.
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
setOption($nodeValue)
setNodeValue(widget)
node.properties = node.properties
}
function setOption(value: any) {
option = value;
}
function onRelease(e: Event) {
if (nodeValue && option != null) {
$nodeValue = option
}
}
function setBackgroundSize(input: HTMLInputElement) {
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
}
function getBackgroundSize(input: HTMLInputElement) {
const min = +input.min || 0;
const max = +input.max || 100;
const value = +input.value;
return (value - min) / (max - min) * 100;
}
function updateSliderForMobile() {
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
setBackgroundSize(target);
}
let elem: HTMLDivElement = null;
$: if (elem) {
updateSliderForMobile()
}
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
const slider = elem.querySelector("input[type='range']") as any
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
const style = elem.style;
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
}
function onPointerDown(e: PointerEvent) {
if (!isMobile)
return;
interfaceState.showIndicator(e.clientX, e.clientY, option);
}
let canVibrate = true;
let lastDisplayValue = null;
function onPointerMove(e: PointerEvent) {
if (!isMobile)
return;
interfaceState.showIndicator(e.clientX, e.clientY, option);
if (canVibrate && lastDisplayValue != option) {
lastDisplayValue = option;
canVibrate = false;
setTimeout(() => { canVibrate = true }, 30)
navigator.vibrate(10)
}
}
</script>
<div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
{#if node !== null && option !== null}
<Range
bind:value={option}
disabled={isDisabled(widget)}
minimum={node.properties.min}
maximum={node.properties.max}
step={node.properties.step}
label={widget.attrs.title}
show_label={true}
on:release={onRelease}
on:change={updateSliderForMobile}
on:pointerdown={onPointerDown}
on:pointermove={onPointerMove}
/>
{/if}
</div>
<style lang="scss">
.wrapper {
padding: 2px;
width: 100%;
:global(input[type=number]) {
text-overflow: ellipsis;
&:disabled {
@include disable-input;
}
}
&.mobile {
// Prevent swiping on the slider track from accidentally changing the value
:global(input[type="range"]) {
pointer-events: none;
-webkit-appearance: none;
appearance: none;
cursor: default;
height: 0.6rem;
padding: initial;
border: initial;
margin: 0.8rem 0;
width: 100%;
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
background-size: var(--background-size, 0%) 100%;
background-repeat: no-repeat;
border-radius: 1rem;
border: 1px solid var(--neutral-400);
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
pointer-events: all;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
background: var(--color-blue-600);
cursor: pointer;
border: 2px solid var(--neutral-100);
box-shadow: 0px 0px 0px 1px var(--neutral-400);
}
:global(input[type=number]) {
font-size: 16px;
height: var(--size-6);
}
}
}
}
</style>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import type { ComfyRadioNode } from "$lib/nodes/ComfyWidgetNodes";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import { Radio } from "@gradio/form";
@@ -7,6 +6,7 @@
import { isDisabled } from "./utils"
import type { SelectData } from "@gradio/utils";
import { clamp } from "$lib/utils";
import type { ComfyRadioNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;

View File

@@ -1,173 +0,0 @@
<script lang="ts">
import type { ComfySliderNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Range } from "$lib/components/gradio/form";
import { get, type Writable } from "svelte/store";
import { debounce } from "$lib/utils";
import interfaceState from "$lib/stores/interfaceState";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfySliderNode | null = null;
let nodeValue: Writable<number> | null = null;
let propsChanged: Writable<number> | null = null;
let option: number | null = null;
let isDragging: boolean = false;
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfySliderNode
nodeValue = node.value;
propsChanged = node.propsChanged;
setOption($nodeValue); // don't react on option
}
isDragging = false;
};
// I don't know why but this is necessary to watch for changes to node
// properties from ComfyWidgetNode.
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
setOption($nodeValue)
setNodeValue(widget)
node.properties = node.properties
}
function setOption(value: any) {
option = value;
}
function onRelease(e: Event) {
if (nodeValue && option != null) {
$nodeValue = option
}
}
function setBackgroundSize(input: HTMLInputElement) {
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
}
function getBackgroundSize(input: HTMLInputElement) {
const min = +input.min || 0;
const max = +input.max || 100;
const value = +input.value;
return (value - min) / (max - min) * 100;
}
function updateSliderForMobile() {
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
setBackgroundSize(target);
}
let elem: HTMLDivElement = null;
$: if (elem) {
updateSliderForMobile()
}
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
const slider = elem.querySelector("input[type='range']") as any
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
const style = elem.style;
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
}
function onPointerDown(e: PointerEvent) {
if (!isMobile)
return;
interfaceState.showIndicator(e.clientX, e.clientY, option);
}
let canVibrate = true;
let lastDisplayValue = null;
function onPointerMove(e: PointerEvent) {
if (!isMobile)
return;
interfaceState.showIndicator(e.clientX, e.clientY, option);
if (canVibrate && lastDisplayValue != option) {
lastDisplayValue = option;
canVibrate = false;
setTimeout(() => { canVibrate = true }, 30)
navigator.vibrate(10)
}
}
</script>
<div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
{#if node !== null && option !== null}
<Range
bind:value={option}
disabled={isDisabled(widget)}
minimum={node.properties.min}
maximum={node.properties.max}
step={node.properties.step}
label={widget.attrs.title}
show_label={true}
on:release={onRelease}
on:change={updateSliderForMobile}
on:pointerdown={onPointerDown}
on:pointermove={onPointerMove}
/>
{/if}
</div>
<style lang="scss">
.wrapper {
padding: 2px;
width: 100%;
:global(input[type=number]) {
text-overflow: ellipsis;
&:disabled {
@include disable-input;
}
}
&.mobile {
// Prevent swiping on the slider track from accidentally changing the value
:global(input[type="range"]) {
pointer-events: none;
-webkit-appearance: none;
appearance: none;
cursor: default;
height: 0.6rem;
padding: initial;
border: initial;
margin: 0.8rem 0;
width: 100%;
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
background-size: var(--background-size, 0%) 100%;
background-repeat: no-repeat;
border-radius: 1rem;
border: 1px solid var(--neutral-400);
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
pointer-events: all;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
background: var(--color-blue-600);
cursor: pointer;
border: 2px solid var(--neutral-100);
box-shadow: 0px 0px 0px 1px var(--neutral-400);
}
:global(input[type=number]) {
font-size: 16px;
height: var(--size-6);
}
}
}
}
</style>

View File

@@ -1,21 +1,21 @@
<script lang="ts">
import { TextBox } from "@gradio/form";
import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store";
import { type Writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { ComfyTextNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyComboNode | null = null;
let node: ComfyTextNode | null = null;
let nodeValue: Writable<string> | null = null;
let propsChanged: Writable<number> | null = null;
let itemValue: WidgetUIStateStore | null = null;
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfySliderNode
node = widget.node as ComfyTextNode
nodeValue = node.value;
propsChanged = node.propsChanged;
}

View File

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