Fix mobile
This commit is contained in:
@@ -14,7 +14,9 @@ ComfyBox is a frontend to Stable Diffusion that lets you create custom image gen
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can import your existing workflows from ComfyUI into ComfyBox by simply clicking `Load` and choosing the `.json` or `.png` with embedded metadata, or dropping either file onto the graph viewer.
|
A preconfigured workflow is included for the most common txt2img and img2img use cases, so all it takes to start generating is clicking `Load Default` to load the default workflow and then `Queue Prompt`.
|
||||||
|
|
||||||
|
You can import your existing workflows from ComfyUI into ComfyBox by clicking `Load` and choosing the `.json` or `.png` with embedded metadata, or dropping either file onto the graph viewer.
|
||||||
|
|
||||||
## NOTE
|
## NOTE
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,14 @@
|
|||||||
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
|
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
|
||||||
let queueList: HTMLDivElement | null = null;
|
let queueList: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
type QueueUIEntryStatus = QueueEntryStatus | "pending" | "running";
|
||||||
|
|
||||||
type QueueUIEntry = {
|
type QueueUIEntry = {
|
||||||
entry: QueueEntry,
|
entry: QueueEntry,
|
||||||
message: string,
|
message: string,
|
||||||
submessage: string,
|
submessage: string,
|
||||||
date?: string,
|
date?: string,
|
||||||
status: QueueEntryStatus | "pending" | "running",
|
status: QueueUIEntryStatus,
|
||||||
images?: string[], // URLs
|
images?: string[], // URLs
|
||||||
details?: string // shown in a tooltip on hover
|
details?: string // shown in a tooltip on hover
|
||||||
}
|
}
|
||||||
@@ -39,21 +41,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mode: QueueItemType = "queue";
|
let mode: QueueItemType = "queue";
|
||||||
|
let changed = true;
|
||||||
|
|
||||||
function switchMode(newMode: QueueItemType) {
|
function switchMode(newMode: QueueItemType) {
|
||||||
const changed = mode !== newMode
|
changed = mode !== newMode
|
||||||
mode = newMode
|
mode = newMode
|
||||||
if (changed)
|
if (changed) {
|
||||||
|
_queuedEntries = []
|
||||||
|
_runningEntries = []
|
||||||
_entries = []
|
_entries = []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _queuedEntries: QueueUIEntry[] = []
|
||||||
|
let _runningEntries: QueueUIEntry[] = []
|
||||||
let _entries: QueueUIEntry[] = []
|
let _entries: QueueUIEntry[] = []
|
||||||
|
|
||||||
$: if (mode === "queue" && $queuePending && $queuePending.length != _entries.length) {
|
$: if (mode === "queue" && (changed || ($queuePending && $queuePending.length != _queuedEntries.length))) {
|
||||||
updateFromQueue();
|
updateFromQueue();
|
||||||
|
changed = false;
|
||||||
}
|
}
|
||||||
else if (mode === "history" && $queueCompleted && $queueCompleted.length != _entries.length) {
|
else if (mode === "history" && (changed || ($queueCompleted && $queueCompleted.length != _entries.length))) {
|
||||||
updateFromHistory();
|
updateFromHistory();
|
||||||
|
changed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(date: Date): string {
|
function formatDate(date: Date): string {
|
||||||
@@ -62,7 +72,7 @@
|
|||||||
return [time, day].join(", ")
|
return [time, day].join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertEntry(entry: QueueEntry): QueueUIEntry {
|
function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
|
||||||
let date = entry.finishedAt || entry.queuedAt;
|
let date = entry.finishedAt || entry.queuedAt;
|
||||||
let dateStr = null;
|
let dateStr = null;
|
||||||
if (date) {
|
if (date) {
|
||||||
@@ -93,13 +103,13 @@
|
|||||||
message,
|
message,
|
||||||
submessage,
|
submessage,
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
status: "pending",
|
status,
|
||||||
images: []
|
images: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertPendingEntry(entry: QueueEntry): QueueUIEntry {
|
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
|
||||||
const result = convertEntry(entry);
|
const result = convertEntry(entry, status);
|
||||||
|
|
||||||
const thumbnails = entry.extraData?.thumbnails
|
const thumbnails = entry.extraData?.thumbnails
|
||||||
if (thumbnails) {
|
if (thumbnails) {
|
||||||
@@ -110,8 +120,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
|
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
|
||||||
const result = convertEntry(entry.entry);
|
const result = convertEntry(entry.entry, entry.status);
|
||||||
result.status = entry.status;
|
|
||||||
|
|
||||||
const images = Object.values(entry.entry.outputs).flatMap(o => o.images)
|
const images = Object.values(entry.entry.outputs).flatMap(o => o.images)
|
||||||
.map(convertComfyOutputToComfyURL);
|
.map(convertComfyOutputToComfyURL);
|
||||||
@@ -128,12 +137,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateFromQueue() {
|
async function updateFromQueue() {
|
||||||
_entries = $queuePending.map(convertPendingEntry).reverse(); // newest entries appear at the top
|
// newest entries appear at the top
|
||||||
|
_queuedEntries = $queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
|
||||||
|
_runningEntries = $queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
|
||||||
|
_entries = [..._queuedEntries, ..._runningEntries]
|
||||||
if (queueList) {
|
if (queueList) {
|
||||||
await tick(); // Wait for list size to be recalculated
|
await tick(); // Wait for list size to be recalculated
|
||||||
queueList.scroll({ top: queueList.scrollHeight })
|
queueList.scroll({ top: queueList.scrollHeight })
|
||||||
}
|
}
|
||||||
console.warn("[ComfyQueue] BUILDQUEUE", _entries, $queuePending)
|
console.warn("[ComfyQueue] BUILDQUEUE", _entries, $queuePending, $queueRunning)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateFromHistory() {
|
async function updateFromHistory() {
|
||||||
@@ -368,6 +380,10 @@
|
|||||||
&:hover:not(:has(img:hover)) {
|
&:hover:not(:has(img:hover)) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--block-background-fill);
|
background: var(--block-background-fill);
|
||||||
|
|
||||||
|
&.running {
|
||||||
|
background: var(--comfy-accent-soft);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
@@ -382,10 +398,10 @@
|
|||||||
color: var(--comfy-disable-textbox-text-color);
|
color: var(--comfy-disable-textbox-text-color);
|
||||||
}
|
}
|
||||||
&.running {
|
&.running {
|
||||||
/* background: lightblue; */
|
background: var(--block-background-fill);
|
||||||
|
border: 3px dashed var(--neutral-500);
|
||||||
}
|
}
|
||||||
&.pending, &.unknown {
|
&.pending, &.unknown {
|
||||||
/* background: orange; */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
|
|||||||
link[3] = getNodeID(link[3])
|
link[3] = getNodeID(link[3])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Recurse!
|
// Recurse!
|
||||||
for (const node of vanillaWorkflow.nodes) {
|
for (const node of vanillaWorkflow.nodes) {
|
||||||
if (node.type === "graph/subgraph") {
|
if (node.type === "graph/subgraph") {
|
||||||
@@ -170,7 +169,8 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns [nodeType, inputType, addedWidgetCount] for a config type, like "FLOAT" -> ["ui/number", "number", 1]
|
* Returns [nodeType, inputType, addedWidgetCount] for a config type, like "FLOAT" -> ["ui/number", "number", 1]
|
||||||
* For "INT:seed" it's ["ui/number", "number", 2] since that type adds a randomizer combo widget
|
* For "INT:seed" it's ["ui/number", "number", 2] since that type adds a randomizer combo widget,
|
||||||
|
* so there will be 2 total widgets
|
||||||
*/
|
*/
|
||||||
function getWidgetTypesFromConfig(inputName: string, inputType: ComfyNodeDefInputType): [string, SlotType, number] | null {
|
function getWidgetTypesFromConfig(inputName: string, inputType: ComfyNodeDefInputType): [string, SlotType, number] | null {
|
||||||
let widgetNodeType = null;
|
let widgetNodeType = null;
|
||||||
@@ -266,6 +266,7 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
|
|||||||
widgetNodeType,
|
widgetNodeType,
|
||||||
value);
|
value);
|
||||||
|
|
||||||
|
// Set the UI node's min/max/step from the node def
|
||||||
configureWidgetNodeProperties(serWidgetNode, widgetOpts)
|
configureWidgetNodeProperties(serWidgetNode, widgetOpts)
|
||||||
|
|
||||||
let foundTitle = null;
|
let foundTitle = null;
|
||||||
@@ -276,7 +277,6 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
|
|||||||
const newLinkOutputSlot = serWidgetNode.outputs.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
|
const newLinkOutputSlot = serWidgetNode.outputs.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
|
||||||
if (newLinkOutputSlot !== -1) {
|
if (newLinkOutputSlot !== -1) {
|
||||||
const newLinkOutput = serWidgetNode.outputs[newLinkOutputSlot];
|
const newLinkOutput = serWidgetNode.outputs[newLinkOutputSlot];
|
||||||
// TODO other links need pruning?
|
|
||||||
for (const linkID of mainOutput.links) {
|
for (const linkID of mainOutput.links) {
|
||||||
const link = vanillaWorkflow.links.find(l => l[0] === linkID)
|
const link = vanillaWorkflow.links.find(l => l[0] === linkID)
|
||||||
if (link) {
|
if (link) {
|
||||||
@@ -293,6 +293,8 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
|
|||||||
// Make sure that the input type for the connected inputs is correct.
|
// Make sure that the input type for the connected inputs is correct.
|
||||||
// ComfyUI seems to set them to the input def type instead of the litegraph type.
|
// ComfyUI seems to set them to the input def type instead of the litegraph type.
|
||||||
// For example a "number" input gets changed to type "INT" or "FLOAT"
|
// For example a "number" input gets changed to type "INT" or "FLOAT"
|
||||||
|
// Also ensure the input is marked for serialization, else there
|
||||||
|
// will be random prompt validation errors on the backend
|
||||||
link[5] = widgetInputType // link data type
|
link[5] = widgetInputType // link data type
|
||||||
if (foundInput != null) {
|
if (foundInput != null) {
|
||||||
foundInput.type = widgetInputType;
|
foundInput.type = widgetInputType;
|
||||||
@@ -343,8 +345,6 @@ function removeSerializedNode(vanillaWorkflow: SerializedLGraph, node: Serialize
|
|||||||
/*
|
/*
|
||||||
* Converts a workflow saved with vanilla ComfyUI into a ComfyBox workflow,
|
* Converts a workflow saved with vanilla ComfyUI into a ComfyBox workflow,
|
||||||
* adding UI nodes for each widget.
|
* adding UI nodes for each widget.
|
||||||
*
|
|
||||||
* TODO: test this!
|
|
||||||
*/
|
*/
|
||||||
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
|
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
|
||||||
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
|
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
|
||||||
|
|||||||
@@ -181,6 +181,19 @@ function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Wri
|
|||||||
return [-1, null, null]
|
return [-1, null, null]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveToRunning(index: number, queue: Writable<QueueEntry[]>) {
|
||||||
|
const state = get(store)
|
||||||
|
|
||||||
|
const entry = get(queue)[index];
|
||||||
|
console.debug("[queueState] Move to running", entry.promptID, index)
|
||||||
|
// entry.startedAt = new Date() // Now
|
||||||
|
queue.update(qp => { qp.splice(index, 1); return qp });
|
||||||
|
state.queueRunning.update(qr => { qr.push(entry); return qr })
|
||||||
|
|
||||||
|
state.isInterrupting = false;
|
||||||
|
store.set(state)
|
||||||
|
}
|
||||||
|
|
||||||
function moveToCompleted(index: number, queue: Writable<QueueEntry[]>, status: QueueEntryStatus, message?: string, error?: string) {
|
function moveToCompleted(index: number, queue: Writable<QueueEntry[]>, status: QueueEntryStatus, message?: string, error?: string) {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
|
|
||||||
@@ -298,9 +311,12 @@ function executionStart(promptID: PromptID) {
|
|||||||
const [index, entry, queue] = findEntryInPending(promptID);
|
const [index, entry, queue] = findEntryInPending(promptID);
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
const entry = createNewQueueEntry(promptID);
|
const entry = createNewQueueEntry(promptID);
|
||||||
s.queuePending.update(qp => { qp.push(entry); return qp })
|
s.queueRunning.update(qr => { qr.push(entry); return qr })
|
||||||
console.debug("[queueState] ADD PROMPT", promptID)
|
console.debug("[queueState] ADD PROMPT", promptID)
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
moveToRunning(index, queue)
|
||||||
|
}
|
||||||
s.isInterrupting = false;
|
s.isInterrupting = false;
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
|
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
|
||||||
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
||||||
import type ComfyApp from "$lib/components/ComfyApp";
|
import type ComfyApp from "$lib/components/ComfyApp";
|
||||||
import type { ComfyWorkflow } from "$lib/components/ComfyApp";
|
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
|
import workflowState, { type ComfyWorkflow } from "$lib/stores/workflowState";
|
||||||
|
|
||||||
export let subworkflowID: number = -1;
|
export let subworkflowID: number = -1;
|
||||||
export let app: ComfyApp
|
export let app: ComfyApp
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
let workflow: ComfyWorkflow | null = null
|
let workflow: ComfyWorkflow | null = null
|
||||||
let layoutState: WritableLayoutStateStore | null = null;
|
let layoutState: WritableLayoutStateStore | null = null;
|
||||||
|
|
||||||
|
$: workflow = $workflowState.activeWorkflow;
|
||||||
$: layoutState = workflow ? workflow.layout : null;
|
$: layoutState = workflow ? workflow.layout : null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ body {
|
|||||||
--comfy-splitpanes-background-fill: var(--secondary-100);
|
--comfy-splitpanes-background-fill: var(--secondary-100);
|
||||||
--comfy-splitpanes-background-fill-hover: var(--secondary-300);
|
--comfy-splitpanes-background-fill-hover: var(--secondary-300);
|
||||||
--comfy-splitpanes-background-fill-active: var(--secondary-400);
|
--comfy-splitpanes-background-fill-active: var(--secondary-400);
|
||||||
|
--comfy-dropdown-list-background: white;
|
||||||
--comfy-dropdown-item-color-hover: white;
|
--comfy-dropdown-item-color-hover: white;
|
||||||
--comfy-dropdown-item-background-hover: var(--neutral-400);
|
--comfy-dropdown-item-background-hover: var(--neutral-400);
|
||||||
--comfy-dropdown-item-color-active: var(--neutral-100);
|
--comfy-dropdown-item-color-active: var(--neutral-100);
|
||||||
@@ -107,7 +108,7 @@ hr {
|
|||||||
color: var(--panel-border-color);
|
color: var(--panel-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea {
|
input:not(input[type=radio]), textarea {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user