Enhanced notification & configure default queue action

This commit is contained in:
space-nuko
2023-05-09 16:01:41 -05:00
parent 30198c3808
commit e107b65db7
19 changed files with 412 additions and 103 deletions

View File

@@ -17,7 +17,7 @@
import GraphPage from './mobile/routes/graph.svelte'; import GraphPage from './mobile/routes/graph.svelte';
import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte'; import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte';
import SubWorkflowPage from './mobile/routes/subworkflow.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; export let app: ComfyApp;
@@ -26,6 +26,25 @@
// exitApp(); // exitApp();
e.preventDefault(); e.preventDefault();
} else { } 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.dialog.close()
f7.view.main.router.back() f7.view.main.router.back()
return false; return false;

View File

@@ -117,12 +117,15 @@
height: fit-content; height: fit-content;
} }
.edit > :global(.v-pane > .block) { &.edit {
border-color: var(--color-pink-500); border-color: var(--color-pink-500);
border-width: 2px; border-width: 2px;
border-style: dashed !important; border-style: dashed !important;
margin: 0.2em; margin: 2em 0.2em;
padding: 1.4em;
:global(> .v-pane) {
padding: 1.4em;
}
} }
/* :global(.hide-block > .v-pane > .block) { /* :global(.hide-block > .v-pane > .block) {

View File

@@ -65,6 +65,10 @@
let graphSize = 0; let graphSize = 0;
let graphTransitioning = false; let graphTransitioning = false;
function queuePrompt() {
app.runDefaultQueueAction()
}
function toggleGraph() { function toggleGraph() {
if (graphSize == 0) { if (graphSize == 0) {
graphSize = 50; graphSize = 50;
@@ -103,26 +107,7 @@
if (!app?.lGraph) if (!app?.lGraph)
return; return;
const promptFilename = true; // TODO app.querySave()
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")
} }
function doLoad(): void { function doLoad(): void {
@@ -241,6 +226,11 @@
</div> </div>
<div id="bottombar"> <div id="bottombar">
<div class="left"> <div class="left">
{#if $layoutState.attrs.queuePromptButtonName != ""}
<Button variant="primary" on:click={queuePrompt}>
{$layoutState.attrs.queuePromptButtonName}
</Button>
{/if}
<Button variant="secondary" on:click={toggleGraph}> <Button variant="secondary" on:click={toggleGraph}>
Toggle Graph Toggle Graph
</Button> </Button>

View File

@@ -28,7 +28,7 @@ import ComfyGraph from "$lib/ComfyGraph";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get } from "svelte/store"; import { get } from "svelte/store";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { promptToGraphVis, workflowToGraphVis } from "$lib/utils"; import { download, promptToGraphVis, workflowToGraphVis } from "$lib/utils";
import notify from "$lib/notify"; import notify from "$lib/notify";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -151,6 +151,7 @@ export default class ComfyApp {
} }
} catch (err) { } catch (err) {
console.error("Error loading previous workflow", 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 // We failed to restore a workflow so load the default
@@ -172,6 +173,8 @@ export default class ComfyApp {
this.resizeCanvas(); this.resizeCanvas();
window.addEventListener("resize", this.resizeCanvas.bind(this)); window.addEventListener("resize", this.resizeCanvas.bind(this));
this.requestPermissions();
this.alreadySetup = true; this.alreadySetup = true;
return Promise.resolve(); 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() { private setupColorScheme() {
const setColor = (type: any, color: string) => { const setColor = (type: any, color: string) => {
LGraphCanvas.DEFAULT_LINK_TYPE_COLORS[type] = color LGraphCanvas.DEFAULT_LINK_TYPE_COLORS[type] = color
@@ -454,6 +464,37 @@ export default class ComfyApp {
layoutState.initDefaultLayout(); 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 * Converts the current graph workflow for sending to the API
* @returns The workflow and node links * @returns The workflow and node links
@@ -644,7 +685,7 @@ export default class ComfyApp {
} catch (error) { } catch (error) {
// this.ui.dialog.show(error.response || error.toString()); // this.ui.dialog.show(error.response || error.toString());
const mes = 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(promptToGraphVis(p))
console.error("Error queuing prompt", mes, num, p) console.error("Error queuing prompt", mes, num, p)
break; break;
@@ -682,7 +723,7 @@ export default class ComfyApp {
} }
else { else {
console.error("No metadata found in image file.", pngInfo) 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")) { } 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) { if (inputNode && "doAutoConfig" in inputNode && comfyInput.widgetNodeType === inputNode.type) {
console.debug("[ComfyApp] Reconfiguring combo widget", inputNode.type, comfyInput.config.values) console.debug("[ComfyApp] Reconfiguring combo widget", inputNode.type, comfyInput.config.values)
const comfyComboNode = inputNode as nodes.ComfyComboNode; const comfyComboNode = inputNode as nodes.ComfyComboNode;
comfyComboNode.doAutoConfig(comfyInput) comfyComboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false })
if (!comfyInput.config.values.includes(get(comfyComboNode.value))) { if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {
comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0]) comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0])
} }

View File

@@ -6,24 +6,27 @@ import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWid
import { get } from "svelte/store"; import { get } from "svelte/store";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes";
import type { NotifyOptions } from "$lib/notify";
import { convertComfyOutputToGradio } from "$lib/utils";
export class ComfyQueueEvents extends ComfyGraphNode { export class ComfyQueueEvents extends ComfyGraphNode {
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
outputs: [ outputs: [
{ name: "beforeQueued", type: BuiltInSlotType.EVENT }, { 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 { private getActionParams(subgraph: string | null): any {
let queue = get(queueState) let queue = get(queueState)
let remaining = 0; let queueRemaining = 0;
if (typeof queue.queueRemaining === "number") if (typeof queue.queueRemaining === "number")
remaining = queue.queueRemaining queueRemaining = queue.queueRemaining
return { return {
queueRemaining: remaining, queueRemaining,
subgraph subgraph
} }
} }
@@ -36,6 +39,16 @@ export class ComfyQueueEvents extends ComfyGraphNode {
this.triggerSlot(1, this.getActionParams(subgraph)) 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) { override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o) super.onSerialize(o)
} }
@@ -54,6 +67,7 @@ export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperti
export class ComfyStoreImagesAction extends ComfyGraphNode { export class ComfyStoreImagesAction extends ComfyGraphNode {
override properties: ComfyStoreImagesActionProperties = { override properties: ComfyStoreImagesActionProperties = {
tags: [],
images: null images: null
} }
@@ -175,13 +189,15 @@ LiteGraph.registerNodeType({
}) })
export interface ComfyNotifyActionProperties extends ComfyGraphNodeProperties { export interface ComfyNotifyActionProperties extends ComfyGraphNodeProperties {
message: string message: string,
type: string
} }
export class ComfyNotifyAction extends ComfyGraphNode { export class ComfyNotifyAction extends ComfyGraphNode {
override properties: ComfyNotifyActionProperties = { override properties: ComfyNotifyActionProperties = {
tags: [],
message: "Nya.", message: "Nya.",
tags: [] type: "info"
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -192,10 +208,27 @@ export class ComfyNotifyAction extends ComfyGraphNode {
} }
override onAction(action: any, param: any) { override onAction(action: any, param: any) {
const message = this.getInputData(0); const message = this.getInputData(0) || this.properties.message;
if (message) { if (!message)
notify(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" 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 { export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties {
targetTag: string targetTag: string
} }

View File

@@ -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<string, any> {
}
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"
})

View File

@@ -28,10 +28,36 @@ export default class ComfyGraphNode extends LGraphNode {
isBackendNode?: boolean; 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; beforeQueued?(subgraph: string | null): void;
/*
* Triggered after a prompt containing this node is passed to the backend.
*/
afterQueued?(prompt: SerializedPrompt, subgraph: string | null): void; 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; 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 defaultWidgets?: DefaultWidgetLayout
/* /*
@@ -71,6 +97,12 @@ export default class ComfyGraphNode extends LGraphNode {
this.addProperty("tags", [], "array") 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) { private inheritSlotTypes(type: LConnectionKind, isConnected: boolean) {
// Prevent multiple connections to different types when we have no input // Prevent multiple connections to different types when we have no input
if (isConnected && type === LConnectionKind.OUTPUT) { if (isConnected && type === LConnectionKind.OUTPUT) {
@@ -229,6 +261,7 @@ export default class ComfyGraphNode extends LGraphNode {
} }
override onResize(size: Vector2) { override onResize(size: Vector2) {
// Snap to grid if shift is held down.
if ((window as any)?.app?.shiftDown) { if ((window as any)?.app?.shiftDown) {
const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.size[0] / LiteGraph.CANVAS_GRID_SIZE); 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); 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) { 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++) { for (let index = 0; index < this.inputs.length; index++) {
const input = this.inputs[index] const input = this.inputs[index]
const serInput = o.inputs[index] const serInput = o.inputs[index]
@@ -254,6 +289,7 @@ export default class ComfyGraphNode extends LGraphNode {
(serInput as any).defaultWidgetNode = null (serInput as any).defaultWidgetNode = null
} }
} }
(o as any).saveUserState = this.saveUserState (o as any).saveUserState = this.saveUserState
if (!this.saveUserState) { if (!this.saveUserState) {
this.stripUserState(o) this.stripUserState(o)
@@ -262,6 +298,7 @@ export default class ComfyGraphNode extends LGraphNode {
} }
override onConfigure(o: SerializedLGraphNode) { override onConfigure(o: SerializedLGraphNode) {
// Save the litegraph type of the default ComfyWidgetNode for each input.
for (let index = 0; index < this.inputs.length; index++) { for (let index = 0; index < this.inputs.length; index++) {
const input = this.inputs[index] const input = this.inputs[index]
const serInput = o.inputs[index] const serInput = o.inputs[index]

View File

@@ -18,6 +18,12 @@ 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";
export type AutoConfigOptions = {
includeProperties?: Set<string> | null,
setDefaultValue?: boolean
setWidgetTitle?: boolean
}
/* /*
* NOTE: If you want to add a new widget but it has the same input/output type * 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 * as another one of the existing widgets, best to create a new "variant" of
@@ -165,7 +171,9 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
setValue(value: any, noChangedEvent: boolean = false) { setValue(value: any, noChangedEvent: boolean = false) {
if (noChangedEvent) if (noChangedEvent)
this._noChangedEvent = true; 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 // In case value.set() does not trigger onValueUpdated, we need to reset
// the counter here also. // the counter here also.
@@ -222,8 +230,6 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if ("noChangedEvent" in param) if ("noChangedEvent" in param)
noChangedEvent = Boolean(param.noChangedEvent) noChangedEvent = Boolean(param.noChangedEvent)
} }
value = this.parseValue(value);
console.warn("[Widget] Store!", param, "=>", value, noChangedEvent)
this.setValue(value, noChangedEvent) this.setValue(value, noChangedEvent)
} }
} }
@@ -244,18 +250,24 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
return true; return true;
} }
doAutoConfig(input: IComfyInputSlot) { doAutoConfig(input: IComfyInputSlot, options: AutoConfigOptions = { setDefaultValue: true, setWidgetTitle: true }) {
// Copy properties from default config in input slot // Copy properties from default config in input slot
const comfyInput = input as IComfyInputSlot; const comfyInput = input as IComfyInputSlot;
for (const key in comfyInput.config) for (const key in comfyInput.config) {
this.setProperty(key, comfyInput.config[key]) if (options.includeProperties == null || options.includeProperties.has(key))
this.setProperty(key, comfyInput.config[key])
}
if ("defaultValue" in this.properties) if (options.setDefaultValue) {
this.setValue(this.properties.defaultValue) if ("defaultValue" in this.properties)
this.setValue(this.properties.defaultValue)
}
const widget = layoutState.findLayoutForNode(this.id) if (options.setWidgetTitle) {
if (widget && input.name !== "") { const widget = layoutState.findLayoutForNode(this.id)
widget.attrs.title = input.name; if (widget && input.name !== "") {
widget.attrs.title = input.name;
}
} }
console.debug("Property copy", input, this.properties) console.debug("Property copy", input, this.properties)

View File

@@ -1,8 +1,19 @@
export { default as ComfyReroute } from "./ComfyReroute" export { default as ComfyReroute } from "./ComfyReroute"
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes" 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 ComfyPickFirstNode } from "./ComfyPickFirstNode"
export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfyValueControl } from "./ComfyValueControl"
export { default as ComfySelector } from "./ComfySelector" export { default as ComfySelector } from "./ComfySelector"
export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode" export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode"
export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode" export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode"
export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton"

View File

@@ -1,40 +1,82 @@
import { toast } from "@zerodevx/svelte-toast"; import { toast } from "@zerodevx/svelte-toast";
import type { SvelteToastOptions } from "@zerodevx/svelte-toast/stores"; import type { SvelteToastOptions } from "@zerodevx/svelte-toast/stores";
import { type Notification } from "framework7/components/notification"
import { f7 } from "framework7-svelte" 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) if (!f7)
return; return;
if (!notification) { let closeTimeout = options.timeout
notification = f7.notification.create({ if (closeTimeout === undefined)
title: title, closeTimeout = 3000;
titleRightText: 'now',
// subtitle: 'Notification with close on click', const notification = f7.notification.create({
text: text, title: options.title,
closeOnClick: true, titleRightText: 'now',
closeTimeout: 3000, // subtitle: 'Notification with close on click',
}); text: text,
} closeOnClick: true,
// Open it closeTimeout
});
notification.open(); notification.open();
} }
function notifyToast(text: string, title?: string, type?: string) { function notifyToast(text: string, options: NotifyOptions) {
const options: SvelteToastOptions = {} const toastOptions: SvelteToastOptions = {
dismissable: options.timeout !== null,
}
if (type === "error") { if (options.type === "success") {
options.theme = { 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)', '--toastBackground': 'var(--color-red-500)',
} }
} }
toast.push(text, options); toast.push(text, toastOptions);
} }
export default function notify(text: string, title?: string, type?: string) { function notifyNative(text: string, options: NotifyOptions) {
notifyf7(text, title, type); if (document.hasFocus())
notifyToast(text, title, type); 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)
} }

View File

@@ -28,13 +28,9 @@ type DragItemEntry = {
*/ */
export type LayoutAttributes = { export type LayoutAttributes = {
/* /*
* Default subgraph to run when the "Queue Prompt" button in the bottom bar * Name of the "Queue Prompt" button. Set to blank to hide the button.
* is pressed.
*
* If it's an empty string, all backend nodes will be included in the prompt
* instead.
*/ */
defaultSubgraph: string queuePromptButtonName: string,
} }
/* /*
@@ -522,11 +518,11 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
// Workflow // Workflow
{ {
name: "defaultSubgraph", name: "queuePromptButtonName",
type: "string", type: "string",
location: "workflow", location: "workflow",
editable: true, editable: true,
defaultValue: "" defaultValue: "Queue Prompt"
} }
] ]
} }
@@ -544,10 +540,16 @@ for (const cat of Object.values(ALL_ATTRIBUTES)) {
export { ALL_ATTRIBUTES }; export { ALL_ATTRIBUTES };
const defaultWidgetAttributes: Attributes = {} as any const defaultWidgetAttributes: Attributes = {} as any
const defaultWorkflowAttributes: LayoutAttributes = {} as any
for (const cat of Object.values(ALL_ATTRIBUTES)) { for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const spec of Object.values(cat.specs)) { for (const spec of Object.values(cat.specs)) {
if (spec.location === "widget" && spec.defaultValue != null) { if (spec.defaultValue != null) {
defaultWidgetAttributes[spec.name] = spec.defaultValue; 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<LayoutState> = writable({
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true, isConfiguring: true,
attrs: { attrs: {
defaultSubgraph: "" ...defaultWorkflowAttributes
} }
}) })
@@ -890,7 +892,7 @@ function initDefaultLayout() {
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false, isConfiguring: false,
attrs: { attrs: {
defaultSubgraph: "" ...defaultWorkflowAttributes
} }
}) })
@@ -1004,7 +1006,7 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
currentSelectionNodes: [], currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false, isConfiguring: false,
attrs: data.attrs attrs: { ...defaultWorkflowAttributes, ...data.attrs }
} }
console.debug("[layoutState] deserialize", data, state) console.debug("[layoutState] deserialize", data, state)

View File

@@ -54,11 +54,11 @@
> .inner { > .inner {
padding: 2px; padding: 2px;
width: 100%; width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: min-content;
:global(> label) { :global(> .block > label) {
height: 100%; height: 100%;
} }
} }

View File

@@ -84,6 +84,7 @@
} }
}) })
history.pushState({ type: "gallery" }, "");
mobileLightbox = f7.photoBrowser.create({ mobileLightbox = f7.photoBrowser.create({
photos: images, photos: images,
@@ -152,7 +153,7 @@
$: node.anyImageSelected = selected_image != null; $: node.anyImageSelected = selected_image != null;
</script> </script>
{#if widget && node && nodeValue} {#if widget && node && nodeValue && $nodeValue}
{#if widget.attrs.variant === "image"} {#if widget.attrs.variant === "image"}
<div class="wrapper comfy-image-widget" style={widget.attrs.style || ""} bind:this={element}> <div class="wrapper comfy-image-widget" style={widget.attrs.style || ""} bind:this={element}>
<Block variant="solid" padding={false}> <Block variant="solid" padding={false}>

View File

@@ -156,7 +156,7 @@
); );
if (response.error) { if (response.error) {
notify(response.error, null, "error") notify(response.error, { type: "error" })
} }
$nodeValue = normalise_file(_value, root, root_url) as GradioFileData[]; $nodeValue = normalise_file(_value, root, root_url) as GradioFileData[];

View File

@@ -2,6 +2,7 @@
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import notify from "$lib/notify"; import notify from "$lib/notify";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import { getNodeInfo } from "$lib/utils" import { getNodeInfo } from "$lib/utils"
@@ -16,14 +17,28 @@
let fileInput: HTMLInputElement = undefined; let fileInput: HTMLInputElement = undefined;
function queuePrompt() { function queuePrompt() {
app.queuePrompt(0, 1); navigator.vibrate(20)
notify("Prompt was queued", "Queued"); 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 { function doLoad(): void {
if (!app?.lGraph || !fileInput) if (!app?.lGraph || !fileInput)
return; return;
navigator.vibrate(20)
fileInput.click(); fileInput.click();
} }
@@ -31,6 +46,15 @@
app.handleFile(fileInput.files[0]); app.handleFile(fileInput.files[0]);
fileInput.files = null; fileInput.files = null;
} }
function doSaveLocal(): void {
if (!app?.lGraph)
return;
navigator.vibrate(20)
app.saveStateToLocalStorage();
notify("Saved to local storage.")
}
</script> </script>
<div class="bottom"> <div class="bottom">
@@ -51,7 +75,14 @@
{/if} {/if}
</div> </div>
<Toolbar bottom> <Toolbar bottom>
<Link on:click={() => app.refreshComboInNodes()}>🔄</Link> {#if $layoutState.attrs.queuePromptButtonName != ""}
<Link on:click={queuePrompt}>
{$layoutState.attrs.queuePromptButtonName}
</Link>
{/if}
<Link on:click={refreshCombos}>🔄</Link>
<Link on:click={doSave}>Save</Link>
<Link on:click={doSaveLocal}>Save Local</Link>
<Link on:click={doLoad}>Load</Link> <Link on:click={doLoad}>Load</Link>
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} /> <input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
</Toolbar> </Toolbar>

View File

@@ -1,21 +1,17 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import { get } from "svelte/store";
import { Pane, Splitpanes } from 'svelte-splitpanes';
import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form"
import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
import { LGraph, LGraphNode } from "@litegraph-ts/core"; import { Page, Navbar, Button, BlockTitle, Block, List, ListItem } from "framework7-svelte"
import type { ComfyAPIStatus } from "$lib/api"; import defaultGraph from "$lib/defaultGraph";
import queueState from "$lib/stores/queueState";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte"
export let app: ComfyApp | null = null; export let app: ComfyApp | null = null;
async function doLoadDefault() {
var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?");
if (confirmed) {
await app.deserialize(defaultGraph)
}
}
</script> </script>
<Page name="home"> <Page name="home">
@@ -34,4 +30,7 @@
<i class="icon icon-f7" slot="media" /> <i class="icon icon-f7" slot="media" />
</ListItem> </ListItem>
</List> </List>
<Block strong outlineIos>
<Button fill={true} onClick={doLoadDefault}>Load Default Graph</Button>
</Block>
</Page> </Page>

View File

@@ -21,6 +21,9 @@
<style lang="scss"> <style lang="scss">
.container { .container {
overflow-x: hidden; overflow-x: hidden;
// Disable pull to refresh
overscroll-behavior-y: contain;
} }
// TODO generalize this to all properties! // TODO generalize this to all properties!

View File

@@ -6,3 +6,7 @@ body {
// Disable pull to refresh // Disable pull to refresh
overscroll-behavior-y: contain; overscroll-behavior-y: contain;
} }
:root {
--color-blue-500: #3985f5;
}