diff --git a/litegraph b/litegraph index 6ee800c..ed3ae7c 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 6ee800cc7207db0b90a48bcc0ed1ee4029c4f0f2 +Subproject commit ed3ae7c4772d27c3ed9acaecc08a8045b7ad3c54 diff --git a/src/AppMobile.svelte b/src/AppMobile.svelte index 013700d..8134b60 100644 --- a/src/AppMobile.svelte +++ b/src/AppMobile.svelte @@ -17,7 +17,7 @@ import GraphPage from './mobile/routes/graph.svelte'; import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte'; import SubWorkflowPage from './mobile/routes/subworkflow.svelte'; - import type { Framework7Parameters } from "framework7/types"; + import type { Framework7Parameters, Modal } from "framework7/types"; export let app: ComfyApp; @@ -26,6 +26,25 @@ // exitApp(); e.preventDefault(); } else { + const $ = f7.$ + const modalIn = $('.modal-in'); + if (modalIn.length && "f7Modal" in modalIn[0]) { + (modalIn[0].f7Modal as Modal.Modal).close(true); + e.preventDefault(); + return; + } + if ($('.panel-active').length) { + f7.panel.close(); + e.preventDefault(); + return; + } + const photoBrowserClose = $('.photo-browser-page a.link.popup-close') + if (photoBrowserClose.length > 0) { + (photoBrowserClose[0] as HTMLElement).click(); + e.preventDefault(); + return; + } + f7.dialog.close() f7.view.main.router.back() return false; diff --git a/src/lib/components/AccordionContainer.svelte b/src/lib/components/AccordionContainer.svelte index 8cd307a..0849883 100644 --- a/src/lib/components/AccordionContainer.svelte +++ b/src/lib/components/AccordionContainer.svelte @@ -117,12 +117,15 @@ height: fit-content; } - .edit > :global(.v-pane > .block) { + &.edit { border-color: var(--color-pink-500); border-width: 2px; border-style: dashed !important; - margin: 0.2em; - padding: 1.4em; + margin: 2em 0.2em; + + :global(> .v-pane) { + padding: 1.4em; + } } /* :global(.hide-block > .v-pane > .block) { diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index 3ed280d..4e31d75 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -65,6 +65,10 @@ let graphSize = 0; let graphTransitioning = false; + function queuePrompt() { + app.runDefaultQueueAction() + } + function toggleGraph() { if (graphSize == 0) { graphSize = 50; @@ -103,26 +107,7 @@ if (!app?.lGraph) return; - const promptFilename = true; // TODO - - let filename = "workflow.json"; - if (promptFilename) { - filename = prompt("Save workflow as:", filename); - if (!filename) return; - if (!filename.toLowerCase().endsWith(".json")) { - filename += ".json"; - } - } - else { - const date = new Date(); - const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", ""); - filename = `workflow-${formattedDate}.json` - } - - const indent = 2 - const json = JSON.stringify(app.serialize(), null, indent) - - download(filename, json, "application/json") + app.querySave() } function doLoad(): void { @@ -241,6 +226,11 @@
+ {#if $layoutState.attrs.queuePromptButtonName != ""} + + {/if} diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 671987c..ea0948a 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -28,7 +28,7 @@ import ComfyGraph from "$lib/ComfyGraph"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { get } from "svelte/store"; import uiState from "$lib/stores/uiState"; -import { promptToGraphVis, workflowToGraphVis } from "$lib/utils"; +import { download, promptToGraphVis, workflowToGraphVis } from "$lib/utils"; import notify from "$lib/notify"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -151,6 +151,7 @@ export default class ComfyApp { } } catch (err) { console.error("Error loading previous workflow", err); + notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null }) } // We failed to restore a workflow so load the default @@ -172,6 +173,8 @@ export default class ComfyApp { this.resizeCanvas(); window.addEventListener("resize", this.resizeCanvas.bind(this)); + this.requestPermissions(); + this.alreadySetup = true; return Promise.resolve(); @@ -348,6 +351,13 @@ export default class ComfyApp { }); } + private requestPermissions() { + if (Notification.permission === "default") { + Notification.requestPermission() + .then((result) => console.log("Notification status:", result)); + } + } + private setupColorScheme() { const setColor = (type: any, color: string) => { LGraphCanvas.DEFAULT_LINK_TYPE_COLORS[type] = color @@ -454,6 +464,37 @@ export default class ComfyApp { layoutState.initDefaultLayout(); } + runDefaultQueueAction() { + for (const node of this.lGraph.iterateNodesInOrder()) { + if ("onDefaultQueueAction" in node) { + (node as ComfyGraphNode).onDefaultQueueAction() + } + } + } + + querySave() { + const promptFilename = true; // TODO + + let filename = "workflow.json"; + if (promptFilename) { + filename = prompt("Save workflow as:", filename); + if (!filename) return; + if (!filename.toLowerCase().endsWith(".json")) { + filename += ".json"; + } + } + else { + const date = new Date(); + const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", ""); + filename = `workflow-${formattedDate}.json` + } + + const indent = 2 + const json = JSON.stringify(this.serialize(), null, indent) + + download(filename, json, "application/json") + } + /** * Converts the current graph workflow for sending to the API * @returns The workflow and node links @@ -644,7 +685,7 @@ export default class ComfyApp { } catch (error) { // this.ui.dialog.show(error.response || error.toString()); const mes = error.response || error.toString() - notify(`Error queuing prompt:\n${mes}`, null, "error") + notify(`Error queuing prompt:\n${mes}`, { type: "error" }) console.error(promptToGraphVis(p)) console.error("Error queuing prompt", mes, num, p) break; @@ -682,7 +723,7 @@ export default class ComfyApp { } else { console.error("No metadata found in image file.", pngInfo) - notify("No metadata found in image file.") + notify("No metadata found in image file.", { type: "error" }) } } } else if (file.type === "application/json" || file.name.endsWith(".json")) { @@ -727,7 +768,7 @@ export default class ComfyApp { if (inputNode && "doAutoConfig" in inputNode && comfyInput.widgetNodeType === inputNode.type) { console.debug("[ComfyApp] Reconfiguring combo widget", inputNode.type, comfyInput.config.values) const comfyComboNode = inputNode as nodes.ComfyComboNode; - comfyComboNode.doAutoConfig(comfyInput) + comfyComboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false }) if (!comfyInput.config.values.includes(get(comfyComboNode.value))) { comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0]) } diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts index a71bd1b..bc47b30 100644 --- a/src/lib/nodes/ComfyActionNodes.ts +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -6,24 +6,27 @@ import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWid import { get } from "svelte/store"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"; import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes"; +import type { NotifyOptions } from "$lib/notify"; +import { convertComfyOutputToGradio } from "$lib/utils"; export class ComfyQueueEvents extends ComfyGraphNode { static slotLayout: SlotLayout = { outputs: [ { name: "beforeQueued", type: BuiltInSlotType.EVENT }, - { name: "afterQueued", type: BuiltInSlotType.EVENT } + { name: "afterQueued", type: BuiltInSlotType.EVENT }, + { name: "onDefaultQueueAction", type: BuiltInSlotType.EVENT }, ], } private getActionParams(subgraph: string | null): any { let queue = get(queueState) - let remaining = 0; + let queueRemaining = 0; if (typeof queue.queueRemaining === "number") - remaining = queue.queueRemaining + queueRemaining = queue.queueRemaining return { - queueRemaining: remaining, + queueRemaining, subgraph } } @@ -36,6 +39,16 @@ export class ComfyQueueEvents extends ComfyGraphNode { this.triggerSlot(1, this.getActionParams(subgraph)) } + override onDefaultQueueAction() { + let queue = get(queueState) + let queueRemaining = 0; + + if (typeof queue.queueRemaining === "number") + queueRemaining = queue.queueRemaining + + this.triggerSlot(2, { queueRemaining }) + } + override onSerialize(o: SerializedLGraphNode) { super.onSerialize(o) } @@ -54,6 +67,7 @@ export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperti export class ComfyStoreImagesAction extends ComfyGraphNode { override properties: ComfyStoreImagesActionProperties = { + tags: [], images: null } @@ -175,13 +189,15 @@ LiteGraph.registerNodeType({ }) export interface ComfyNotifyActionProperties extends ComfyGraphNodeProperties { - message: string + message: string, + type: string } export class ComfyNotifyAction extends ComfyGraphNode { override properties: ComfyNotifyActionProperties = { + tags: [], message: "Nya.", - tags: [] + type: "info" } static slotLayout: SlotLayout = { @@ -192,10 +208,27 @@ export class ComfyNotifyAction extends ComfyGraphNode { } override onAction(action: any, param: any) { - const message = this.getInputData(0); - if (message) { - notify(message); + const message = this.getInputData(0) || this.properties.message; + if (!message) + return; + + const options: NotifyOptions = { + type: this.properties.type } + + // Check if this event was triggered from a backend node and has the + // onExecuted arguments. If so then use the first image as the icon for + // native notifications. + if (param != null && typeof param === "object") { + if ("images" in param) { + const output = param as GalleryOutput; + const converted = convertComfyOutputToGradio(output); + if (converted.length > 0) + options.imageUrl = converted[0].data; + } + } + + notify(message, options); }; } @@ -206,6 +239,40 @@ LiteGraph.registerNodeType({ type: "actions/notify" }) +export interface ComfyPlaySoundActionProperties extends ComfyGraphNodeProperties { + sound: string, +} + +export class ComfyPlaySoundAction extends ComfyGraphNode { + override properties: ComfyPlaySoundActionProperties = { + tags: [], + sound: "notification.mp3" + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "sound", type: "string" }, + { name: "trigger", type: BuiltInSlotType.ACTION } + ], + } + + override onAction(action: any, param: any) { + const sound = this.getInputData(0) || this.properties.sound; + if (sound) { + const url = `${location.origin}/sound/${sound}`; + const audio = new Audio(url); + audio.play(); + } + }; +} + +LiteGraph.registerNodeType({ + class: ComfyPlaySoundAction, + title: "Comfy.PlaySoundAction", + desc: "Plays a sound located under the sound/ directory.", + type: "actions/play_sound" +}) + export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties { targetTag: string } diff --git a/src/lib/nodes/ComfyConfigureQueuePromptButton.ts b/src/lib/nodes/ComfyConfigureQueuePromptButton.ts new file mode 100644 index 0000000..d465ecd --- /dev/null +++ b/src/lib/nodes/ComfyConfigureQueuePromptButton.ts @@ -0,0 +1,47 @@ +import layoutState from "$lib/stores/layoutState" +import { BuiltInSlotType, LGraphNode, LiteGraph, type ITextWidget, type OptionalSlots, type PropertyLayout, type SlotLayout, type Vector2 } from "@litegraph-ts/core" + +export interface ComfyConfigureQueuePromptButtonProperties extends Record { +} + +export default class ComfyConfigureQueuePromptButton extends LGraphNode { + override properties: ComfyConfigureQueuePromptButtonProperties = { + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "config", type: BuiltInSlotType.ACTION }, + ], + } + + static propertyLayout: PropertyLayout = [ + ] + + static optionalSlots: OptionalSlots = { + } + + override size: Vector2 = [60, 30]; + + constructor(title?: string) { + super(title) + } + + override onAction(action: any, param: any, options: { action_call?: string }) { + if (action === "config" && param != null) { + layoutState.update(state => { + if (typeof param === "string") + state.attrs.queuePromptButtonName = param || "" + else if (typeof param === "object" && "buttonName" in param) + state.attrs.queuePromptButtonName = param.buttonName || "" + return state + }) + } + } +} + +LiteGraph.registerNodeType({ + class: ComfyConfigureQueuePromptButton, + title: "Comfy.ConfigureQueuePromptButton", + desc: "Sets the properties of the global queue prompt button", + type: "workflow/configure_queue_prompt_button" +}) diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index 5ece8af..59cbda3 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -28,10 +28,36 @@ export default class ComfyGraphNode extends LGraphNode { isBackendNode?: boolean; + /* + * Triggered when the user presses the global "Queue Prompt" button in the fixed toolbar. + */ + onDefaultQueueAction?(): void; + + /* + * Triggered before a prompt containing this node is passed to the backend. + */ beforeQueued?(subgraph: string | null): void; + + /* + * Triggered after a prompt containing this node is passed to the backend. + */ afterQueued?(prompt: SerializedPrompt, subgraph: string | null): void; + + /* + * Triggered when the backend sends a finished output back with this node's ID. + * Valid for output nodes like SaveImage and PreviewImage. + */ onExecuted?(output: any): void; + /* + * Allows you to manually specify an auto-config for certain input slot + * indices, so that when a ComfyWidgetNode is connected to the input slot it + * receives the specified min/max/values/etc. + * Otherwise the config passed from the backend is used. + * + * Use this if you're creating a frontend-only node and want some input + * slots to have auto-configs, like for connected combo box widgets. + */ defaultWidgets?: DefaultWidgetLayout /* @@ -71,6 +97,12 @@ export default class ComfyGraphNode extends LGraphNode { this.addProperty("tags", [], "array") } + /* + * Adjusts output slot types to have the same type as the first connected + * input. Used for frontend-only nodes with inputs and outputs that act as + * wildcards, so that they can be connected to ComfyBackendNodes without + * rejection. + */ private inheritSlotTypes(type: LConnectionKind, isConnected: boolean) { // Prevent multiple connections to different types when we have no input if (isConnected && type === LConnectionKind.OUTPUT) { @@ -229,6 +261,7 @@ export default class ComfyGraphNode extends LGraphNode { } override onResize(size: Vector2) { + // Snap to grid if shift is held down. if ((window as any)?.app?.shiftDown) { const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.size[0] / LiteGraph.CANVAS_GRID_SIZE); const h = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.size[1] / LiteGraph.CANVAS_GRID_SIZE); @@ -241,6 +274,8 @@ export default class ComfyGraphNode extends LGraphNode { } override onSerialize(o: SerializedLGraphNode) { + // Resync the widget node types for each input. + // This is so combo widget nodes will be correctly detected by ComfyApp.refreshComboInNodes(). for (let index = 0; index < this.inputs.length; index++) { const input = this.inputs[index] const serInput = o.inputs[index] @@ -254,6 +289,7 @@ export default class ComfyGraphNode extends LGraphNode { (serInput as any).defaultWidgetNode = null } } + (o as any).saveUserState = this.saveUserState if (!this.saveUserState) { this.stripUserState(o) @@ -262,6 +298,7 @@ export default class ComfyGraphNode extends LGraphNode { } override onConfigure(o: SerializedLGraphNode) { + // Save the litegraph type of the default ComfyWidgetNode for each input. for (let index = 0; index < this.inputs.length; index++) { const input = this.inputs[index] const serInput = o.inputs[index] diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts index 24d93c2..b5545dc 100644 --- a/src/lib/nodes/ComfyWidgetNodes.ts +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -18,6 +18,12 @@ import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte"; import RadioWidget from "$lib/widgets/RadioWidget.svelte"; import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte"; +export type AutoConfigOptions = { + includeProperties?: Set | null, + setDefaultValue?: boolean + setWidgetTitle?: boolean +} + /* * NOTE: If you want to add a new widget but it has the same input/output type * as another one of the existing widgets, best to create a new "variant" of @@ -165,7 +171,9 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { setValue(value: any, noChangedEvent: boolean = false) { if (noChangedEvent) this._noChangedEvent = true; - this.value.set(this.parseValue(value)) + + const parsed = this.parseValue(value) + this.value.set(parsed) // In case value.set() does not trigger onValueUpdated, we need to reset // the counter here also. @@ -222,8 +230,6 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { if ("noChangedEvent" in param) noChangedEvent = Boolean(param.noChangedEvent) } - value = this.parseValue(value); - console.warn("[Widget] Store!", param, "=>", value, noChangedEvent) this.setValue(value, noChangedEvent) } } @@ -244,18 +250,24 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { return true; } - doAutoConfig(input: IComfyInputSlot) { + doAutoConfig(input: IComfyInputSlot, options: AutoConfigOptions = { setDefaultValue: true, setWidgetTitle: true }) { // Copy properties from default config in input slot const comfyInput = input as IComfyInputSlot; - for (const key in comfyInput.config) - this.setProperty(key, comfyInput.config[key]) + for (const key in comfyInput.config) { + if (options.includeProperties == null || options.includeProperties.has(key)) + this.setProperty(key, comfyInput.config[key]) + } - if ("defaultValue" in this.properties) - this.setValue(this.properties.defaultValue) + if (options.setDefaultValue) { + if ("defaultValue" in this.properties) + this.setValue(this.properties.defaultValue) + } - const widget = layoutState.findLayoutForNode(this.id) - if (widget && input.name !== "") { - widget.attrs.title = input.name; + if (options.setWidgetTitle) { + const widget = layoutState.findLayoutForNode(this.id) + if (widget && input.name !== "") { + widget.attrs.title = input.name; + } } console.debug("Property copy", input, this.properties) diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index edbebcc..1ccbfd0 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -1,8 +1,19 @@ export { default as ComfyReroute } from "./ComfyReroute" export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes" -export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyStoreImagesAction, ComfyExecuteSubgraphAction } from "./ComfyActionNodes" +export { + ComfyQueueEvents, + ComfyCopyAction, + ComfySwapAction, + ComfyNotifyAction, + ComfyPlaySoundAction, + ComfyStoreImagesAction, + ComfyExecuteSubgraphAction, + ComfySetNodeModeAction, + ComfySetNodeModeAdvancedAction +} from "./ComfyActionNodes" export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode" export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfySelector } from "./ComfySelector" export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode" export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode" +export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton" diff --git a/src/lib/notify.ts b/src/lib/notify.ts index 290434a..97fc382 100644 --- a/src/lib/notify.ts +++ b/src/lib/notify.ts @@ -1,40 +1,82 @@ import { toast } from "@zerodevx/svelte-toast"; import type { SvelteToastOptions } from "@zerodevx/svelte-toast/stores"; +import { type Notification } from "framework7/components/notification" import { f7 } from "framework7-svelte" -let notification; +export type NotifyOptions = { + title?: string, + type?: "neutral" | "info" | "warning" | "error" | "success", + imageUrl?: string, + timeout?: number | null +} -function notifyf7(text: string, title?: string, type?: string) { +function notifyf7(text: string, options: NotifyOptions) { if (!f7) return; - if (!notification) { - notification = f7.notification.create({ - title: title, - titleRightText: 'now', - // subtitle: 'Notification with close on click', - text: text, - closeOnClick: true, - closeTimeout: 3000, - }); - } - // Open it + let closeTimeout = options.timeout + if (closeTimeout === undefined) + closeTimeout = 3000; + + const notification = f7.notification.create({ + title: options.title, + titleRightText: 'now', + // subtitle: 'Notification with close on click', + text: text, + closeOnClick: true, + closeTimeout + }); notification.open(); } -function notifyToast(text: string, title?: string, type?: string) { - const options: SvelteToastOptions = {} +function notifyToast(text: string, options: NotifyOptions) { + const toastOptions: SvelteToastOptions = { + dismissable: options.timeout !== null, + } - if (type === "error") { - options.theme = { + if (options.type === "success") { + toastOptions.theme = { + '--toastBackground': 'var(--color-green-600)', + } + } + else if (options.type === "info") { + toastOptions.theme = { + '--toastBackground': 'var(--color-blue-500)', + } + } + else if (options.type === "error") { + toastOptions.theme = { '--toastBackground': 'var(--color-red-500)', } } - toast.push(text, options); + toast.push(text, toastOptions); } -export default function notify(text: string, title?: string, type?: string) { - notifyf7(text, title, type); - notifyToast(text, title, type); +function notifyNative(text: string, options: NotifyOptions) { + if (document.hasFocus()) + return; + + const title = options.title || "ComfyBox" + const nativeOptions: NotificationOptions = { + body: text, + } + + if (options.imageUrl) { + nativeOptions.icon = options.imageUrl + nativeOptions.image = options.imageUrl + } + if (options.timeout === null) { + nativeOptions.requireInteraction = true; + } + + const notification = new Notification(title, nativeOptions); + + notification.onclick = () => window.focus(); +} + +export default function notify(text: string, options: NotifyOptions = {}) { + notifyf7(text, options); + notifyToast(text, options); + notifyNative(text, options) } diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts index 4fd94c2..fae9cdd 100644 --- a/src/lib/stores/layoutState.ts +++ b/src/lib/stores/layoutState.ts @@ -28,13 +28,9 @@ type DragItemEntry = { */ export type LayoutAttributes = { /* - * Default subgraph to run when the "Queue Prompt" button in the bottom bar - * is pressed. - * - * If it's an empty string, all backend nodes will be included in the prompt - * instead. + * Name of the "Queue Prompt" button. Set to blank to hide the button. */ - defaultSubgraph: string + queuePromptButtonName: string, } /* @@ -522,11 +518,11 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ // Workflow { - name: "defaultSubgraph", + name: "queuePromptButtonName", type: "string", location: "workflow", editable: true, - defaultValue: "" + defaultValue: "Queue Prompt" } ] } @@ -544,10 +540,16 @@ for (const cat of Object.values(ALL_ATTRIBUTES)) { export { ALL_ATTRIBUTES }; const defaultWidgetAttributes: Attributes = {} as any +const defaultWorkflowAttributes: LayoutAttributes = {} as any for (const cat of Object.values(ALL_ATTRIBUTES)) { for (const spec of Object.values(cat.specs)) { - if (spec.location === "widget" && spec.defaultValue != null) { - defaultWidgetAttributes[spec.name] = spec.defaultValue; + if (spec.defaultValue != null) { + if (spec.location === "widget") { + defaultWidgetAttributes[spec.name] = spec.defaultValue; + } + else if (spec.location === "workflow") { + defaultWorkflowAttributes[spec.name] = spec.defaultValue; + } } } } @@ -634,7 +636,7 @@ const store: Writable = writable({ isMenuOpen: false, isConfiguring: true, attrs: { - defaultSubgraph: "" + ...defaultWorkflowAttributes } }) @@ -890,7 +892,7 @@ function initDefaultLayout() { isMenuOpen: false, isConfiguring: false, attrs: { - defaultSubgraph: "" + ...defaultWorkflowAttributes } }) @@ -1004,7 +1006,7 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) { currentSelectionNodes: [], isMenuOpen: false, isConfiguring: false, - attrs: data.attrs + attrs: { ...defaultWorkflowAttributes, ...data.attrs } } console.debug("[layoutState] deserialize", data, state) diff --git a/src/lib/widgets/CheckboxWidget.svelte b/src/lib/widgets/CheckboxWidget.svelte index 503ffae..1b84c5b 100644 --- a/src/lib/widgets/CheckboxWidget.svelte +++ b/src/lib/widgets/CheckboxWidget.svelte @@ -54,11 +54,11 @@ > .inner { padding: 2px; width: 100%; + height: 100%; display: flex; flex-direction: row; - height: min-content; - :global(> label) { + :global(> .block > label) { height: 100%; } } diff --git a/src/lib/widgets/GalleryWidget.svelte b/src/lib/widgets/GalleryWidget.svelte index ae167b5..32e646b 100644 --- a/src/lib/widgets/GalleryWidget.svelte +++ b/src/lib/widgets/GalleryWidget.svelte @@ -84,6 +84,7 @@ } }) + history.pushState({ type: "gallery" }, ""); mobileLightbox = f7.photoBrowser.create({ photos: images, @@ -152,7 +153,7 @@ $: node.anyImageSelected = selected_image != null; -{#if widget && node && nodeValue} +{#if widget && node && nodeValue && $nodeValue} {#if widget.attrs.variant === "image"}
diff --git a/src/lib/widgets/ImageUploadWidget.svelte b/src/lib/widgets/ImageUploadWidget.svelte index b06bff6..a7886b5 100644 --- a/src/lib/widgets/ImageUploadWidget.svelte +++ b/src/lib/widgets/ImageUploadWidget.svelte @@ -156,7 +156,7 @@ ); if (response.error) { - notify(response.error, null, "error") + notify(response.error, { type: "error" }) } $nodeValue = normalise_file(_value, root, root_url) as GradioFileData[]; diff --git a/src/mobile/GenToolbar.svelte b/src/mobile/GenToolbar.svelte index 2b1577f..36647af 100644 --- a/src/mobile/GenToolbar.svelte +++ b/src/mobile/GenToolbar.svelte @@ -2,6 +2,7 @@ import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import notify from "$lib/notify"; import uiState from "$lib/stores/uiState"; + import layoutState from "$lib/stores/layoutState"; import queueState from "$lib/stores/queueState"; import { getNodeInfo } from "$lib/utils" @@ -16,14 +17,28 @@ let fileInput: HTMLInputElement = undefined; function queuePrompt() { - app.queuePrompt(0, 1); - notify("Prompt was queued", "Queued"); + navigator.vibrate(20) + app.runDefaultQueueAction() + } + + function refreshCombos() { + navigator.vibrate(20) + app.refreshComboInNodes() + } + + function doSave(): void { + if (!app?.lGraph || !fileInput) + return; + + navigator.vibrate(20) + app.querySave() } function doLoad(): void { if (!app?.lGraph || !fileInput) return; + navigator.vibrate(20) fileInput.click(); } @@ -31,6 +46,15 @@ app.handleFile(fileInput.files[0]); fileInput.files = null; } + + function doSaveLocal(): void { + if (!app?.lGraph) + return; + + navigator.vibrate(20) + app.saveStateToLocalStorage(); + notify("Saved to local storage.") + }
@@ -51,7 +75,14 @@ {/if}
- app.refreshComboInNodes()}>🔄 + {#if $layoutState.attrs.queuePromptButtonName != ""} + + {$layoutState.attrs.queuePromptButtonName} + + {/if} + 🔄 + Save + Save Local Load diff --git a/src/mobile/routes/home.svelte b/src/mobile/routes/home.svelte index 3aa3b25..d413707 100644 --- a/src/mobile/routes/home.svelte +++ b/src/mobile/routes/home.svelte @@ -1,21 +1,17 @@ @@ -34,4 +30,7 @@ + + + diff --git a/src/mobile/routes/subworkflow.svelte b/src/mobile/routes/subworkflow.svelte index c910e3f..518430b 100644 --- a/src/mobile/routes/subworkflow.svelte +++ b/src/mobile/routes/subworkflow.svelte @@ -21,6 +21,9 @@