Merge pull request #60 from space-nuko/gallery-and-send-images

Grid view for gallery
This commit is contained in:
space-nuko
2023-05-22 14:24:58 -05:00
18 changed files with 752 additions and 255 deletions

View File

@@ -73,6 +73,7 @@
"klecks": "workspace:*", "klecks": "workspace:*",
"pollen-css": "^4.6.2", "pollen-css": "^4.6.2",
"radix-icons-svelte": "^1.2.1", "radix-icons-svelte": "^1.2.1",
"svelte-bootstrap-icons": "^2.3.1",
"svelte-feather-icons": "^4.0.0", "svelte-feather-icons": "^4.0.0",
"svelte-preprocess": "^5.0.3", "svelte-preprocess": "^5.0.3",
"svelte-select": "^5.5.3", "svelte-select": "^5.5.3",

7
pnpm-lock.yaml generated
View File

@@ -97,6 +97,9 @@ importers:
radix-icons-svelte: radix-icons-svelte:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
svelte-bootstrap-icons:
specifier: ^2.3.1
version: 2.3.1
svelte-feather-icons: svelte-feather-icons:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@@ -7908,6 +7911,10 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
/svelte-bootstrap-icons@2.3.1:
resolution: {integrity: sha512-Vqhgmcd55hEoB/MrPESvVYo4+m++2q9l00a5lzGkgbXTiO6go21PTU9DaeAJ446WBiEmg3Q8sv9QVOBmh5c1ww==}
dev: false
/svelte-check@2.2.6(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0): /svelte-check@2.2.6(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0):
resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==} resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==}
hasBin: true hasBin: true

View File

@@ -22,14 +22,14 @@ export type ComfyNodeDefInput = [ComfyNodeDefInputType, ComfyNodeDefInputOptions
/** /**
* - Array: Combo widget. Usually the values are strings but they can also be other stuff like booleans. * - Array: Combo widget. Usually the values are strings but they can also be other stuff like booleans.
* - "INT"/"FLOAT"/etc.: Non-combo type widgets. See ComfyWidgets type. * - "INT"/"FLOAT"/etc.: Non-combo type widgets. See ComfyWidgets type.
* - other string: Must be an input type, usually something lke "IMAGE" or "LATENT". * - other string: Must be a backend input type, usually something lke "IMAGE" or "LATENT".
*/ */
export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string
export type ComfyNodeDefInputOptions = { export type ComfyNodeDefInputOptions = {
forceInput?: boolean; forceInput?: boolean;
// NOTE: For COMBO type inputs, the default value is always the first entry the list. // NOTE: For COMBO type inputs, the default value is always the first entry in the list.
default?: any, default?: any,
// INT/FLOAT options // INT/FLOAT options

View File

@@ -331,7 +331,7 @@ export default class ComfyAPI {
* @param {string} type The type of item to delete, queue or history * @param {string} type The type of item to delete, queue or history
* @param {number} id The id of the item to delete * @param {number} id The id of the item to delete
*/ */
async deleteItem(type: QueueItemType, id: number): Promise<Response> { async deleteItem(type: QueueItemType, id: PromptID): Promise<Response> {
return this.postItem(type, { delete: [id] }); return this.postItem(type, { delete: [id] });
} }

View File

@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons"; import { Image, Gear } from "svelte-bootstrap-icons";
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp"; import ComfyApp from "./ComfyApp";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import configState from "$lib/stores/configState";
import workflowState from "$lib/stores/workflowState";
import { SvelteToast, toast } from '@zerodevx/svelte-toast' import { SvelteToast, toast } from '@zerodevx/svelte-toast'
import LightboxModal from "./LightboxModal.svelte"; import LightboxModal from "./LightboxModal.svelte";
@@ -34,6 +36,16 @@
document.getElementById("app-root").classList.remove("dark") document.getElementById("app-root").classList.remove("dark")
} }
function handleBeforeUnload(event: BeforeUnloadEvent) {
if (!$configState.confirmWhenUnloadingUnsavedChanges)
return;
const unsavedChanges = $workflowState.openedWorkflows.some(w => w.isModified);
if (unsavedChanges) {
event.preventDefault();
event.returnValue = '';
}
}
</script> </script>
<svelte:head> <svelte:head>
@@ -42,13 +54,15 @@
{/if} {/if}
</svelte:head> </svelte:head>
<svelte:window on:beforeunload={handleBeforeUnload} />
<div id="main" class:dark={uiTheme === "gradio-dark"}> <div id="main" class:dark={uiTheme === "gradio-dark"}>
<div id="container"> <div id="container">
<Sidebar selected="generate"> <Sidebar selected="generate">
<SidebarItem id="generate" name="Generate" icon={Image}> <SidebarItem id="generate" name="Generate" icon={Image}>
<ComfyWorkflowsView {app} {uiTheme} /> <ComfyWorkflowsView {app} {uiTheme} />
</SidebarItem> </SidebarItem>
<SidebarItem id="settings" name="Settings" icon={Settings}> <SidebarItem id="settings" name="Settings" icon={Gear}>
</SidebarItem> </SidebarItem>
</Sidebar> </Sidebar>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core"; import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api" import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID, type QueueItemType } from "$lib/api"
import { importA1111, parsePNGMetadata } from "$lib/pnginfo"; import { importA1111, parsePNGMetadata } from "$lib/pnginfo";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
@@ -33,7 +33,7 @@ import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { tick } from "svelte"; import { tick } from "svelte";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { basename, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils"; import { basename, capitalize, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
import notify from "$lib/notify"; import notify from "$lib/notify";
import configState from "$lib/stores/configState"; import configState from "$lib/stores/configState";
import { blankGraph } from "$lib/defaultGraph"; import { blankGraph } from "$lib/defaultGraph";
@@ -312,7 +312,7 @@ export default class ComfyApp {
const workflows = state.workflows as SerializedAppState[]; const workflows = state.workflows as SerializedAppState[];
await Promise.all(workflows.map(w => { await Promise.all(workflows.map(w => {
return this.openWorkflow(w, defs).catch(error => { return this.openWorkflow(w, defs, false).catch(error => {
console.error("Failed restoring previous workflow", error) console.error("Failed restoring previous workflow", error)
notify(`Failed restoring previous workflow: ${error}`, { type: "error" }) notify(`Failed restoring previous workflow: ${error}`, { type: "error" })
}) })
@@ -509,6 +509,46 @@ export default class ComfyApp {
this.api.init(); this.api.init();
} }
async interrupt() {
if (get(queueState).isInterrupting)
return
queueState.update(s => { s.isInterrupting = true; return s; })
await this.api.interrupt()
.finally(() => {
queueState.update(s => { s.isInterrupting = true; return s })
});
}
async deleteQueueItem(type: QueueItemType, promptID: PromptID) {
if (get(queueState).isInterrupting)
return
queueState.update(s => { s.isInterrupting = true; return s; })
await this.api.deleteItem(type, promptID)
.then(() => {
queueState.queueItemDeleted(type, promptID);
})
.finally(() => {
queueState.update(s => { s.isInterrupting = false; return s; })
});
}
async clearQueue(type: QueueItemType) {
if (get(queueState).isInterrupting)
return
queueState.update(s => { s.isInterrupting = true; return s; })
await this.api.clearItems(type)
.then(() => {
queueState.queueCleared(type);
notify(`${capitalize(type)} cleared.`);
})
.finally(() => {
queueState.update(s => { s.isInterrupting = false; return s; })
});
}
private addKeyboardHandler() { private addKeyboardHandler() {
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
this.shiftDown = e.shiftKey; this.shiftDown = e.shiftKey;
@@ -557,9 +597,12 @@ export default class ComfyApp {
setColor(BuiltInSlotType.ACTION, "lightseagreen") setColor(BuiltInSlotType.ACTION, "lightseagreen")
} }
async openWorkflow(data: SerializedAppState, refreshCombos: boolean | Record<string, ComfyNodeDef> = true): Promise<ComfyWorkflow> { async openWorkflow(data: SerializedAppState,
refreshCombos: boolean | Record<string, ComfyNodeDef> = true,
warnMissingNodeTypes: boolean = true
): Promise<ComfyWorkflow> {
if (data.version !== COMFYBOX_SERIAL_VERSION) { if (data.version !== COMFYBOX_SERIAL_VERSION) {
const mes = `Invalid ComfyBox saved data format: ${data.version}` const mes = `Invalid ComfyBox saved data format: ${data.version} `
notify(mes, { type: "error" }) notify(mes, { type: "error" })
return Promise.reject(mes); return Promise.reject(mes);
} }
@@ -580,7 +623,7 @@ export default class ComfyApp {
return Promise.reject(error) return Promise.reject(error)
} }
if (workflow.missingNodeTypes.size > 0) { if (workflow.missingNodeTypes.size > 0 && warnMissingNodeTypes) {
modalState.pushModal({ modalState.pushModal({
svelteComponent: MissingNodeTypesModal, svelteComponent: MissingNodeTypesModal,
svelteProps: { svelteProps: {
@@ -681,7 +724,7 @@ export default class ComfyApp {
} }
catch (error) { catch (error) {
console.error("Failed to load default graph", error) console.error("Failed to load default graph", error)
notify(`Failed to load default graph: ${error}`, { type: "error" }) notify(`Failed to load default graph: ${error} `, { type: "error" })
state = structuredClone(blankGraph) state = structuredClone(blankGraph)
} }
await this.openWorkflow(state, defs) await this.openWorkflow(state, defs)
@@ -737,7 +780,7 @@ export default class ComfyApp {
else { else {
const date = new Date(); const date = new Date();
const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", ""); const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", "");
filename = `workflow-${formattedDate}.json` filename = `workflow - ${formattedDate}.json`
} }
const indent = 2 const indent = 2
@@ -782,7 +825,7 @@ export default class ComfyApp {
try { try {
while (this.queueItems.length) { while (this.queueItems.length) {
({ num, batchCount, workflow } = this.queueItems.pop()); ({ num, batchCount, workflow } = this.queueItems.pop());
console.debug(`Queue get! ${num} ${batchCount} ${tag}`); console.debug(`Queue get! ${num} ${batchCount} ${tag} `);
const thumbnails = [] const thumbnails = []
for (const node of workflow.graph.iterateNodesInOrderRecursive()) { for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
@@ -850,7 +893,7 @@ export default class ComfyApp {
if (error != null) { if (error != null) {
const mes: string = error; const mes: string = error;
notify(`Error queuing prompt:\n${mes}`, { type: "error" }) notify(`Error queuing prompt: \n${mes} `, { type: "error" })
console.error(graphToGraphVis(workflow.graph)) console.error(graphToGraphVis(workflow.graph))
console.error(promptToGraphVis(p)) console.error(promptToGraphVis(p))
console.error("Error queuing prompt", error, num, p) console.error("Error queuing prompt", error, num, p)

View File

@@ -29,7 +29,7 @@
<label class="number-wrapper"> <label class="number-wrapper">
<BlockTitle>{name}</BlockTitle> <BlockTitle>{name}</BlockTitle>
<div class="number"> <div class="number">
<input type="number" bind:value {min} {max} {step} {disabled}> <input type="number" bind:value {min} {max} {step} {disabled} />
</div> </div>
</label> </label>

View File

@@ -1,9 +1,23 @@
<script lang="ts" context="module">
export type QueueUIEntryStatus = QueueEntryStatus | "pending" | "running";
export type QueueUIEntry = {
entry: QueueEntry,
message: string,
submessage: string,
date?: string,
status: QueueUIEntryStatus,
images?: string[], // URLs
details?: string // shown in a tooltip on hover
}
</script>
<script lang="ts"> <script lang="ts">
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState"; import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
import ProgressBar from "./ProgressBar.svelte"; import ProgressBar from "./ProgressBar.svelte";
import Spinner from "./Spinner.svelte"; import Spinner from "./Spinner.svelte";
import PromptDisplay from "./PromptDisplay.svelte"; import PromptDisplay from "./PromptDisplay.svelte";
import { ListIcon as List } from "svelte-feather-icons"; import { List, ListUl, Grid } from "svelte-bootstrap-icons";
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils" import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { QueueItemType } from "$lib/api"; import type { QueueItemType } from "$lib/api";
@@ -14,6 +28,8 @@
import Modal from "./Modal.svelte"; import Modal from "./Modal.svelte";
import DropZone from "./DropZone.svelte"; import DropZone from "./DropZone.svelte";
import workflowState from "$lib/stores/workflowState"; import workflowState from "$lib/stores/workflowState";
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
export let app: ComfyApp; export let app: ComfyApp;
@@ -22,25 +38,18 @@
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 = {
entry: QueueEntry,
message: string,
submessage: string,
date?: string,
status: QueueUIEntryStatus,
images?: string[], // URLs
details?: string // shown in a tooltip on hover
}
$: if ($queueState) { $: if ($queueState) {
queuePending = $queueState.queuePending queuePending = $queueState.queuePending
queueRunning = $queueState.queueRunning queueRunning = $queueState.queueRunning
queueCompleted = $queueState.queueCompleted queueCompleted = $queueState.queueCompleted
} }
type DisplayModeType = "list" | "grid";
let mode: QueueItemType = "queue"; let mode: QueueItemType = "queue";
let displayMode: DisplayModeType = "list";
let imageSize: number = 40;
let gridColumns: number = 3;
let changed = true; let changed = true;
function switchMode(newMode: QueueItemType) { function switchMode(newMode: QueueItemType) {
@@ -53,6 +62,16 @@
} }
} }
function switchDisplayMode(newDisplayMode: DisplayModeType) {
// changed = displayMode !== newDisplayMode
displayMode = newDisplayMode
// if (changed) {
// _queuedEntries = []
// _runningEntries = []
// _entries = []
// }
}
let _queuedEntries: QueueUIEntry[] = [] let _queuedEntries: QueueUIEntry[] = []
let _runningEntries: QueueUIEntry[] = [] let _runningEntries: QueueUIEntry[] = []
let _entries: QueueUIEntry[] = [] let _entries: QueueUIEntry[] = []
@@ -66,6 +85,39 @@
changed = false; changed = false;
} }
async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) {
event.preventDefault();
event.stopImmediatePropagation()
// TODO support interrupting from multiple running items!
if (entry.status === "running") {
await app.interrupt();
}
else {
await app.deleteQueueItem(mode, entry.entry.promptID);
}
if (mode === "queue") {
_queuedEntries = []
_runningEntries = []
}
_entries = [];
changed = true;
}
async function clearQueue() {
await app.clearQueue(mode);
if (mode === "queue") {
_queuedEntries = []
_runningEntries = []
}
_entries = [];
changed = true;
}
function formatDate(date: Date): string { function formatDate(date: Date): string {
const time = date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }); const time = date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
const day = date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }).replace(',', ''); const day = date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }).replace(',', '');
@@ -156,35 +208,15 @@
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted) console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
} }
function showLightbox(entry: QueueUIEntry, index: number, e: Event) {
e.preventDefault()
if (!entry.images)
return
ImageViewer.instance.showModal(entry.images, index);
e.stopPropagation()
}
async function interrupt() { async function interrupt() {
if ($queueState.isInterrupting) await app.interrupt();
return
const app = (window as any).app as ComfyApp;
if (!app || !app.api)
return;
await app.api.interrupt()
.then(() => {
queueState.update(s => { s.isInterrupting = true; return s })
});
} }
let showModal = false; let showModal = false;
let expandAll = false; let expandAll = false;
let selectedPrompt = null; let selectedPrompt = null;
let selectedImages = []; let selectedImages = [];
function showPrompt(entry: QueueUIEntry, e: MouseEvent) { function showPrompt(entry: QueueUIEntry) {
selectedPrompt = entry.entry.prompt; selectedPrompt = entry.entry.prompt;
selectedImages = entry.images; selectedImages = entry.images;
showModal = true; showModal = true;
@@ -219,48 +251,33 @@
</div> </div>
</Modal> </Modal>
<div class="queue"> <div class="queue {mode}-mode">
<div class="queue-entries {mode}-mode" bind:this={queueList}> {#if mode === "history"}
<div class="display-mode-buttons">
<div class="mode-button image-display-button ternary"
on:click={() => switchDisplayMode("list")}
class:selected={displayMode === "list"}>
<List width="100%" height="100%" />
</div>
<div class="mode-button image-display-button ternary"
on:click={() => switchDisplayMode("grid")}
class:selected={displayMode === "grid"}>
<Grid width="100%" height="100%" />
</div>
</div>
{/if}
<div class="queue-entries" bind:this={queueList}>
{#if _entries.length > 0} {#if _entries.length > 0}
{#each _entries as entry} {#if mode === "history" && displayMode === "grid"}
<div class="queue-entry {entry.status}" on:click={(e) => showPrompt(entry, e)}> <ComfyQueueGridDisplay entries={_entries} {showPrompt} {clearQueue} {mode} bind:gridColumns />
{#if entry.images.length > 0}
<div class="queue-entry-images"
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
{#each entry.images.slice(0, 4) as image, i}
<div>
<img class="queue-entry-image"
on:click={(e) => showLightbox(entry, i, e)}
src={image}
alt="thumbnail" />
</div>
{/each}
</div>
{:else} {:else}
<!-- <div class="queue-entry-image-placeholder" /> --> <ComfyQueueListDisplay entries={_entries} {showPrompt} {clearQueue} {mode} {deleteEntry} bind:imageSize />
{/if} {/if}
<div class="queue-entry-details">
<div class="queue-entry-message">
{truncateString(entry.message, 20)}
</div>
<div class="queue-entry-submessage">
{entry.submessage}
</div>
</div>
</div>
<div class="queue-entry-rest {entry.status}">
{#if entry.date != null}
<span class="queue-entry-queued-at">
{entry.date}
</span>
{/if}
</div>
{/each}
{:else} {:else}
<div class="queue-empty"> <div class="queue-empty">
<div class="queue-empty-container"> <div class="queue-empty-container">
<div class="queue-empty-icon"> <div class="queue-empty-icon">
<List size="120rem" /> <ListUl width="100%" height="10rem" />
</div> </div>
<div class="queue-empty-message"> <div class="queue-empty-message">
(No entries) (No entries)
@@ -272,12 +289,12 @@
<div class="mode-buttons"> <div class="mode-buttons">
<div class="mode-button secondary" <div class="mode-button secondary"
on:click={() => switchMode("queue")} on:click={() => switchMode("queue")}
class:mode-selected={mode === "queue"}> class:selected={mode === "queue"}>
Queue Queue
</div> </div>
<div class="mode-button secondary" <div class="mode-button secondary"
on:click={() => switchMode("history")} on:click={() => switchMode("history")}
class:mode-selected={mode === "history"}> class:selected={mode === "history"}>
History History
</div> </div>
</div> </div>
@@ -315,10 +332,12 @@
<style lang="scss"> <style lang="scss">
$pending-height: 200px; $pending-height: 200px;
$display-mode-buttons-height: 2rem;
$bottom-bar-height: 70px; $bottom-bar-height: 70px;
$workflow-tabs-height: 2.5rem; $workflow-tabs-height: 2.5rem;
$mode-buttons-height: 30px; $mode-buttons-height: 30px;
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem); $queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem);
$queue-height-history: calc(#{$queue-height} - #{$display-mode-buttons-height});
.prompt-modal-header { .prompt-modal-header {
padding-left: 0.2rem; padding-left: 0.2rem;
@@ -329,20 +348,22 @@
} }
.queue { .queue {
color: var(--body-text-color); color: var(--body-text-color);
&.queue-mode > .queue-entries {
height: $queue-height;
max-height: $queue-height;
}
&.history-mode > .queue-entries {
height: $queue-height-history;
max-height: $queue-height-history;
}
} }
.queue-entries { .queue-entries {
height: $queue-height;
max-height: $queue-height;
display: flex; display: flex;
overflow-y: auto;
flex-flow: column nowrap; flex-flow: column nowrap;
height: $queue-height;
&.queue-mode > :first-child {
// elements stick to bottom in queue mode only
// next element in queue is on the bottom
margin-top: auto !important;
}
> .queue-empty { > .queue-empty {
display: flex; display: flex;
@@ -368,106 +389,22 @@
} }
} }
.queue-entry { .display-mode-buttons {
padding: 1.0rem;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-bottom: 1px solid var(--block-border-color); top: 0px;
border-top: 1px solid var(--table-border-color); height: $display-mode-buttons-height;
background: var(--panel-background-fill); margin-bottom: auto;
max-height: 14rem;
&:hover:not(:has(img:hover)) { > .mode-button {
cursor: pointer;
background: var(--block-background-fill);
&.running {
background: var(--comfy-accent-soft);
}
}
&.success {
/* background: green; */
}
&.error {
background: red;
}
&.all_cached, &.interrupted {
filter: brightness(80%);
background: var(--comfy-disabled-textbox-background-fill);
color: var(--comfy-disable-textbox-text-color);
}
&.running {
background: var(--block-background-fill);
border: 3px dashed var(--neutral-500);
}
&.pending, &.unknown {
}
}
.queue-entry-rest {
width: 100%; width: 100%;
position: relative; color: var(--neutral-500);
&.all_cached, &.interrupted { &.selected {
filter: brightness(80%);
color: var(--comfy-accent-soft);
}
}
$thumbnails-size: 12rem;
.queue-entry-images {
--cols: 1;
margin: auto;
width: calc($thumbnails-size * 2);
display: grid;
display: inline-grid;
grid-template-columns: repeat(var(--cols), 1fr);
grid-template-rows: repeat(var(--cols), 1fr);
column-gap: 1px;
row-gap: 1px;
vertical-align: top;
flex: 1 1 40%;
img {
aspect-ratio: 1 / 1;
object-fit: cover;
&:hover {
cursor: pointer;
filter: brightness(120%) contrast(120%);
}
}
}
.queue-entry-details {
position: relative;
padding: 1rem;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.queue-entry-message {
font-size: 15px;
}
.queue-entry-submessage {
font-size: 12px;
}
.queue-entry-queued-at {
width: auto;
font-size: 12px;
position:absolute;
right: 0px;
bottom: 0px;
padding: 0.4rem 0.6rem;
color: var(--body-text-color); color: var(--body-text-color);
} }
}
}
.mode-buttons { .mode-buttons {
display: flex; display: flex;
@@ -483,34 +420,8 @@
.mode-button { .mode-button {
height: calc($mode-buttons-height); height: calc($mode-buttons-height);
padding: 0.2rem; padding: 0.2rem;
border: 1px solid var(--panel-border-color);
font-weight: bold;
text-align: center;
margin: auto;
&.primary { @include square-button;
background: var(--button-primary-background-fill);
&:hover {
background: var(--button-primary-background-fill-hover);
}
}
&.secondary {
background: var(--button-secondary-background-fill);
&:hover {
background: var(--button-secondary-background-fill-hover);
}
}
&:hover {
filter: brightness(85%);
}
&:active {
filter: brightness(50%)
}
&.mode-selected {
filter: brightness(80%)
}
} }
:global(.dark) .mode-button { :global(.dark) .mode-button {
@@ -521,7 +432,7 @@
&:active { &:active {
filter: brightness(50%) filter: brightness(50%)
} }
&.mode-selected { &.selected {
filter: brightness(150%) filter: brightness(150%)
} }
} }

View File

@@ -0,0 +1,139 @@
<script lang="ts">
import type { QueueItemType } from "$lib/api";
import { showLightbox } from "$lib/utils";
import type { QueueUIEntry } from "./ComfyQueue.svelte";
import queueState from "$lib/stores/queueState";
export let entries: QueueUIEntry[] = [];
export let showPrompt: (entry: QueueUIEntry) => void;
export let clearQueue: () => Promise<void>;
export let mode: QueueItemType = "queue";
export let gridColumns: number = 3;
let allEntries: [QueueUIEntry, string][] = []
let allImages: string[] = []
$: buildImageList(entries);
function buildImageList(entries: QueueUIEntry[]) {
allEntries = []
for (const entry of entries) {
for (const image of entry.images) {
allEntries.push([entry, image]);
}
}
allImages = allEntries.map(p => p[1]);
}
function handleClick(e: MouseEvent, entry: QueueUIEntry, index: number) {
if (e.ctrlKey) {
showPrompt(entry);
}
else {
showLightbox(allImages, index, e)
}
}
</script>
<div class="grid-wrapper {mode}-mode">
<div class="grid-controls">
<div>
<input type="range" bind:value={gridColumns} min={1} max={8} step={1} />
<div class="button-wrapper">
<button class="clear-queue-button secondary"
on:click={clearQueue}
disabled={$queueState.isInterrupting}>
🗑️
</button>
</div>
</div>
</div>
<div class="grid-entries">
<div class="grid" style:--cols={gridColumns}>
{#each allEntries as [entry, image], i}
<div class="grid-entry">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<img class="grid-entry-image"
on:click={(e) => handleClick(e, entry, i)}
src={image}
alt="thumbnail" />
</div>
{/each}
</div>
</div>
</div>
<style lang="scss">
$grid-controls-height: 3rem;
$grid-controls-margin: 0.25rem;
.grid-wrapper {
height: 100%;
}
.grid-controls {
height: $grid-controls-height;
position: relative;
display: flex;
flex-direction: column;
margin: $grid-controls-margin 1rem;
> div {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
input {
width: 100%;
margin: auto;
}
.button-wrapper {
padding: $grid-controls-margin;
.clear-queue-button {
@include square-button;
aspect-ratio: 1/1;
height: 100%;
padding: 0.25rem;
}
}
}
}
.grid-entries {
height: calc(100% - #{$grid-controls-height} - #{$grid-controls-margin} * 2);
padding: 0 var(--spacing-lg) var(--spacing-lg) var(--spacing-lg);
overflow-y: auto;
}
.grid {
--cols: 3;
width: 100%;
display: grid;
position: relative;
top: 0px;
grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
grid-auto-rows: 1fr;
gap: var(--spacing-lg);
}
.grid-entry {
display: flex;
justify-content: center;
align-items: center;
aspect-ratio: 1 / 1;
object-fit: cover;
}
.grid-entry-image {
aspect-ratio: 1 / 1;
object-fit: cover;
&:hover {
cursor: pointer;
filter: brightness(120%) contrast(120%);
}
}
</style>

View File

@@ -0,0 +1,260 @@
<script lang="ts">
import type { QueueItemType } from "$lib/api";
import { showLightbox, truncateString } from "$lib/utils";
import type { QueueUIEntry } from "./ComfyQueue.svelte";
import queueState from "$lib/stores/queueState";
export let entries: QueueUIEntry[] = [];
export let showPrompt: (entry: QueueUIEntry) => void;
export let clearQueue: () => void;
export let deleteEntry: (entry: QueueUIEntry, event: MouseEvent) => void;
export let mode: QueueItemType = "queue";
export let imageSize: number = 40;
</script>
<div class="list-wrapper {mode}-mode">
{#if mode === "history"}
<div class="list-controls">
<div>
<input type="range" bind:value={imageSize} min={10} max={100} step={0.1} />
<div class="button-wrapper">
<button class="clear-queue-button secondary"
on:click={clearQueue}
disabled={$queueState.isInterrupting}>
🗑️
</button>
</div>
</div>
</div>
{/if}
<div class="list-entries {mode}-mode" style:--imageSize={imageSize}>
{#each entries as entry}
<div class="list-entry {entry.status}" on:click={(e) => showPrompt(entry, e)}>
<button class="list-entry-delete-button secondary"
on:click={(e) => deleteEntry(entry, e)}
disabled={$queueState.isInterrupting}>
<span></span>
</button>
{#if entry.images.length > 0}
<div class="list-entry-images"
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
{#each entry.images.slice(0, 4) as image, i}
<div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<img class="list-entry-image"
on:click={(e) => showLightbox(entry.images, i, e)}
src={image}
alt="thumbnail" />
</div>
{/each}
</div>
{/if}
<div class="list-entry-details">
<div class="list-entry-message">
{truncateString(entry.message, 20)}
</div>
<div class="list-entry-submessage">
{entry.submessage}
</div>
</div>
</div>
<div class="list-entry-rest {entry.status}">
{#if entry.date != null}
<span class="list-entry-queued-at">
{entry.date}
</span>
{/if}
</div>
{/each}
</div>
</div>
<style lang="scss">
$list-controls-height: 3rem;
$list-controls-margin: 0.25rem;
.list-wrapper {
height: 100%;
&.queue-mode .list-entries > :global(:first-child) {
// elements stick to bottom in queue mode only
// next element in queue is on the bottom
margin-top: auto !important;
}
}
.list-entries {
--imageSize: 40;
height: 100%;
&.history-mode {
height: calc(100% - #{$list-controls-height} - #{$list-controls-margin} * 2);
}
overflow-y: auto;
display: flex;
flex-flow: column nowrap;
}
.list-controls {
height: $list-controls-height;
position: relative;
display: flex;
flex-direction: column;
margin: $list-controls-margin 1rem;
> div {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: center;
input {
width: 100%;
margin: auto;
}
.button-wrapper {
padding: $list-controls-margin;
.clear-queue-button {
@include square-button;
aspect-ratio: 1/1;
height: 100%;
padding: 0.25rem;
}
}
}
}
.list-entry {
padding: 1.0rem;
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--block-border-color);
border-top: 1px solid var(--table-border-color);
background: var(--panel-background-fill);
max-height: 14rem;
position: relative;
&:hover:not(:has(img:hover)):not(:has(button:hover)) {
cursor: pointer;
background: var(--block-background-fill);
&.running {
background: var(--comfy-accent-soft);
}
}
&.success {
/* background: green; */
}
&.error {
background: red;
}
&.all_cached, &.interrupted {
filter: brightness(80%);
background: var(--comfy-disabled-textbox-background-fill);
color: var(--comfy-disable-textbox-text-color);
}
&.running {
background: var(--block-background-fill);
border: 3px dashed var(--neutral-500);
}
&.pending, &.unknown {
}
}
.list-entry-delete-button {
@include square-button;
display: flex;
position: absolute;
width: 1.4rem;
height: 1.4rem;
text-align: center;
align-items: center;
font-size: 10pt;
justify-content: center;
text-align: center;
top:0;
right:0.5rem;
margin: 0.5rem;
z-index: 1000000000;
opacity: 70%;
background: var(--neutral-700);
color: var(--neutral-300);
&:hover {
opacity: 100%;
color: var(--neutral-100);
}
}
.list-entry-rest {
width: 100%;
position: relative;
&.all_cached, &.interrupted {
filter: brightness(80%);
color: var(--comfy-accent-soft);
}
}
$thumbnails-size: 12rem;
.list-entry-images {
--cols: 1;
margin: auto;
width: calc($thumbnails-size * 2);
display: grid;
display: inline-grid;
grid-template-columns: repeat(var(--cols), 1fr);
grid-template-rows: repeat(var(--cols), 1fr);
column-gap: 1px;
row-gap: 1px;
vertical-align: top;
flex-grow: 1;
flex-shrink: 1;
flex-basis: calc(var(--imageSize) * 1%);
img {
aspect-ratio: 1 / 1;
object-fit: cover;
&:hover {
cursor: pointer;
filter: brightness(120%) contrast(120%);
}
}
}
.list-entry-details {
position: relative;
padding: 1rem;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.list-entry-message {
font-size: 15px;
}
.list-entry-submessage {
font-size: 12px;
}
.list-entry-queued-at {
width: auto;
font-size: 12px;
position:absolute;
right: 0px;
bottom: 0px;
padding: 0.4rem 0.6rem;
color: var(--body-text-color);
}
</style>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'; import { Pane, Splitpanes } from 'svelte-splitpanes';
import { PlusSquareIcon as PlusSquare } from 'svelte-feather-icons'; import { PlusSquareDotted } from 'svelte-bootstrap-icons';
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import { BlockTitle } from "@gradio/atoms"; import { BlockTitle } from "@gradio/atoms";
import ComfyWorkflowView from "./ComfyWorkflowView.svelte"; import ComfyWorkflowView from "./ComfyWorkflowView.svelte";
@@ -253,7 +253,7 @@
</div> </div>
<button class="workflow-add-new-button" <button class="workflow-add-new-button"
on:click={createNewWorkflow}> on:click={createNewWorkflow}>
<PlusSquare size="100%" strokeWidth={1.5} /> <PlusSquareDotted width="100%" height="100%" />
</button> </button>
</div> </div>
<div id="bottombar"> <div id="bottombar">

View File

@@ -62,7 +62,7 @@
{#if t.id === $selected_tab} {#if t.id === $selected_tab}
<button class="selected"> <button class="selected">
{#if t.icon !== null} {#if t.icon !== null}
<svelte:component this={t.icon} size="100%" strokeWidth={1.5} /> <svelte:component this={t.icon} width="100%" height="100%" strokeWidth={1.5} />
{:else} {:else}
{t.name} {t.name}
{/if} {/if}
@@ -75,7 +75,7 @@
}} }}
> >
{#if t.icon !== null} {#if t.icon !== null}
<svelte:component this={t.icon} size="100%" strokeWidth={1.5} /> <svelte:component this={t.icon} width="100%" height="100%" strokeWidth={1.5} />
{:else} {:else}
{t.name} {t.name}
{/if} {/if}
@@ -115,7 +115,7 @@
> button { > button {
width: 4rem; width: 4rem;
height: 4rem; height: 4rem;
padding: 0.5rem; padding: 0.75rem;
color: var(--neutral-600); color: var(--neutral-600);
border-right: 3px solid transparent; border-right: 3px solid transparent;

View File

@@ -1,5 +1,5 @@
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils"; import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils";
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core"; import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout, type INumberWidget } from "@litegraph-ts/core";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte"; import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
@@ -42,15 +42,17 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
selectedFilename: string | null = null; selectedFilename: string | null = null;
selectedIndexWidget: ITextWidget; selectedIndexWidget: INumberWidget;
modeWidget: IComboWidget; modeWidget: IComboWidget;
imageWidth: Writable<number> = writable(0); imageWidth: Writable<number> = writable(0);
imageHeight: Writable<number> = writable(0); imageHeight: Writable<number> = writable(0);
selectedImage: Writable<number | null> = writable(null);
constructor(name?: string) { constructor(name?: string) {
super(name, []) super(name, [])
this.selectedIndexWidget = this.addWidget("text", "Selected", String(this.properties.index), "index") this.selectedIndexWidget = this.addWidget("number", "Selected", get(this.selectedImage))
this.selectedIndexWidget.disabled = true; this.selectedIndexWidget.disabled = true;
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] }) this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
} }
@@ -63,11 +65,14 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
override onExecute() { override onExecute() {
const value = get(this.value) const value = get(this.value)
const index = get(this.selectedImage)
this.setOutputData(0, value) this.setOutputData(0, value)
this.setOutputData(1, this.properties.index) this.setOutputData(1, index)
if (this.properties.index != null && value && value[this.properties.index] != null) { this.selectedIndexWidget.value = index;
const image = value[this.properties.index];
if (index != null && value && value[index] != null) {
const image = value[index];
image.width = get(this.imageWidth) image.width = get(this.imageWidth)
image.height = get(this.imageHeight) image.height = get(this.imageHeight)
} }
@@ -91,6 +96,8 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
if (this.properties.updateMode === "append") { if (this.properties.updateMode === "append") {
const currentValue = get(this.value) const currentValue = get(this.value)
if (meta.length > 0)
this.selectedImage.set(currentValue.length);
return currentValue.concat(meta) return currentValue.concat(meta)
} }
else { else {
@@ -101,7 +108,6 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
override setValue(value: any, noChangedEvent: boolean = false) { override setValue(value: any, noChangedEvent: boolean = false) {
super.setValue(value, noChangedEvent) super.setValue(value, noChangedEvent)
this.setProperty("index", null)
} }
} }

View File

@@ -8,6 +8,9 @@ export type ConfigState = {
/** When saving, always prompt for a name to save the workflow as */ /** When saving, always prompt for a name to save the workflow as */
promptForWorkflowName: boolean, promptForWorkflowName: boolean,
/** When closing the tab, open the confirmation window if there's unsaved changes */
confirmWhenUnloadingUnsavedChanges: boolean,
} }
type ConfigStateOps = { type ConfigStateOps = {
@@ -17,7 +20,8 @@ export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
const store: Writable<ConfigState> = writable( const store: Writable<ConfigState> = writable(
{ {
alwaysStripUserState: false, alwaysStripUserState: false,
promptForWorkflowName: false promptForWorkflowName: false,
confirmWhenUnloadingUnsavedChanges: true
}) })
const configStateStore: WritableConfigStateStore = const configStateStore: WritableConfigStateStore =

View File

@@ -1,4 +1,4 @@
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID } from "$lib/api"; import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID, QueueItemType } from "$lib/api";
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs, WorkflowInstID } from "$lib/components/ComfyApp"; import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs, WorkflowInstID } from "$lib/components/ComfyApp";
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes"; import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
import notify from "$lib/notify"; import notify from "$lib/notify";
@@ -17,6 +17,8 @@ type QueueStateOps = {
progressUpdated: (progress: Progress) => void progressUpdated: (progress: Progress) => void
getQueueEntry: (promptID: PromptID) => QueueEntry | null; getQueueEntry: (promptID: PromptID) => QueueEntry | null;
afterQueued: (workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void afterQueued: (workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
queueItemDeleted: (type: QueueItemType, id: PromptID) => void;
queueCleared: (type: QueueItemType) => void;
onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => QueueEntry | null onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => QueueEntry | null
} }
@@ -181,6 +183,39 @@ function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Wri
return [-1, null, null] return [-1, null, null]
} }
function deleteEntry(promptID: PromptID): boolean {
const state = get(store);
let index = get(state.queuePending).findIndex(e => e.promptID === promptID)
let found = false;
if (index !== -1) {
state.queuePending.update(qp => {
qp.splice(index, 1)
return qp;
})
found = true;
}
index = get(state.queueRunning).findIndex(e => e.promptID === promptID)
if (index !== -1) {
state.queueRunning.update(qr => {
qr.splice(index, 1)
return qr;
})
found = true;
}
index = get(state.queueCompleted).findIndex(e => e.entry.promptID === promptID)
if (index !== -1) {
state.queueCompleted.update(qc => {
qc.splice(index, 1)
return qc;
})
found = true;
}
return found;
}
function moveToRunning(index: number, queue: Writable<QueueEntry[]>) { function moveToRunning(index: number, queue: Writable<QueueEntry[]>) {
const state = get(store) const state = get(store)
@@ -294,7 +329,7 @@ function createNewQueueEntry(promptID: PromptID, number: number = -1, prompt: Se
return { return {
number, number,
queuedAt: new Date(), // Now queuedAt: new Date(), // Now
finishedAt: null, finishedAt: undefined,
promptID, promptID,
prompt, prompt,
extraData, extraData,
@@ -363,6 +398,37 @@ function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, outputs: ComfyExecu
return entry_; return entry_;
} }
function queueItemDeleted(type: QueueItemType, id: PromptID) {
console.debug("[queueState] queueItemDeleted", type, id)
store.update(s => {
if (!deleteEntry(id)) {
console.error("[queueState] Queue item to delete not found!", type, id);
}
s.isInterrupting = false;
return s;
})
}
function queueCleared(type: QueueItemType) {
console.debug("[queueState] queueCleared", type)
store.update(s => {
if (type === "queue") {
s.queuePending.set([]);
s.queueRunning.set([]);
s.queueRemaining = 0;
s.runningNodeID = null;
s.progress = null;
}
else {
s.queueCompleted.set([])
}
s.isInterrupting = false;
return s;
})
}
const queueStateStore: WritableQueueStateStore = const queueStateStore: WritableQueueStateStore =
{ {
...store, ...store,
@@ -375,6 +441,8 @@ const queueStateStore: WritableQueueStateStore =
executionCached, executionCached,
executionError, executionError,
afterQueued, afterQueued,
queueItemDeleted,
queueCleared,
getQueueEntry, getQueueEntry,
onExecuted onExecuted
} }

View File

@@ -6,6 +6,7 @@ import { get } from "svelte/store";
import type { ComfyNodeID } from "./api"; import type { ComfyNodeID } from "./api";
import { type SerializedPrompt } from "./components/ComfyApp"; import { type SerializedPrompt } from "./components/ComfyApp";
import workflowState from "./stores/workflowState"; import workflowState from "./stores/workflowState";
import { ImageViewer } from "./ImageViewer";
export function clamp(n: number, min: number, max: number): number { export function clamp(n: number, min: number, max: number): number {
return Math.min(Math.max(n, min), max) return Math.min(Math.max(n, min), max)
@@ -23,6 +24,10 @@ export function countNewLines(str: string): number {
return str.split(/\r\n|\r|\n/).length return str.split(/\r\n|\r|\n/).length
} }
export function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function basename(filepath: string): string { export function basename(filepath: string): string {
const filename = filepath.split('/').pop().split('\\').pop(); const filename = filepath.split('/').pop().split('\\').pop();
return filename.split('.').slice(0, -1).join('.'); return filename.split('.').slice(0, -1).join('.');
@@ -501,3 +506,13 @@ export function comfyBoxImageToComfyFile(image: ComfyBoxImageMetadata): ComfyIma
export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string { export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string {
return convertComfyOutputToComfyURL(image.comfyUIFile) return convertComfyOutputToComfyURL(image.comfyUIFile)
} }
export function showLightbox(images: string[], index: number, e: Event) {
e.preventDefault()
if (!images)
return
ImageViewer.instance.showModal(images, index);
e.stopPropagation()
}

View File

@@ -7,7 +7,6 @@
import type { Styles } from "@gradio/utils"; import type { Styles } from "@gradio/utils";
import type { WidgetLayout } from "$lib/stores/layoutStates"; import type { WidgetLayout } from "$lib/stores/layoutStates";
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
import type { FileData as GradioFileData } from "@gradio/upload";
import type { SelectData as GradioSelectData } from "@gradio/utils"; import type { SelectData as GradioSelectData } from "@gradio/utils";
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils"; import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
import { f7 } from "framework7-svelte"; import { f7 } from "framework7-svelte";
@@ -18,10 +17,9 @@
let node: ComfyGalleryNode | null = null; let node: ComfyGalleryNode | null = null;
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null; let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
let option: number | null = null;
let imageWidth: Writable<number> = writable(0); let imageWidth: Writable<number> = writable(0);
let imageHeight: Writable<number> = writable(0); let imageHeight: Writable<number> = writable(0);
let selected_image: number | null = null; let selected_image: Writable<number | null> = writable(null);
$: widget && setNodeValue(widget); $: widget && setNodeValue(widget);
@@ -32,6 +30,7 @@
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
imageWidth = node.imageWidth imageWidth = node.imageWidth
imageHeight = node.imageHeight imageHeight = node.imageHeight
selected_image = node.selectedImage;
if ($nodeValue != null) { if ($nodeValue != null) {
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) { if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
@@ -81,7 +80,7 @@
thumbs: images.map(i => i.url), thumbs: images.map(i => i.url),
type: 'popup', type: 'popup',
}); });
mobileLightbox.open(selected_image) mobileLightbox.open($selected_image)
} }
function onClicked(e: CustomEvent<HTMLImageElement>) { function onClicked(e: CustomEvent<HTMLImageElement>) {
@@ -97,20 +96,6 @@
// Update index // Update index
node.setProperty("index", e.detail.index as number) node.setProperty("index", e.detail.index as number)
} }
$: if ($propsChanged > -1 && widget && $nodeValue) {
if (widget.attrs.variant === "image") {
node.setProperty("index", selected_image)
}
else {
node.setProperty("index", $nodeValue.length > 0 ? 0 : null)
}
}
else {
node.setProperty("index", null)
}
$: node.setProperty("index", selected_image)
</script> </script>
{#if widget && node && nodeValue && $nodeValue} {#if widget && node && nodeValue && $nodeValue}
@@ -148,7 +133,7 @@
on:clicked={onClicked} on:clicked={onClicked}
bind:imageWidth={$imageWidth} bind:imageWidth={$imageWidth}
bind:imageHeight={$imageHeight} bind:imageHeight={$imageHeight}
bind:selected_image bind:selected_image={$selected_image}
/> />
</div> </div>
</Block> </Block>

View File

@@ -81,6 +81,44 @@ body {
--comfy-progress-bar-foreground: #B3D8A9 --comfy-progress-bar-foreground: #B3D8A9
} }
@mixin square-button {
border: 1px solid var(--panel-border-color);
font-weight: bold;
text-align: center;
margin: auto;
&.primary {
background: var(--button-primary-background-fill);
&:hover {
background: var(--button-primary-background-fill-hover);
}
}
&.secondary {
background: var(--button-secondary-background-fill);
&:hover {
background: var(--button-secondary-background-fill-hover);
}
}
&.ternary {
background: var(--panel-background-fill);
&:hover {
background: var(--block-background-fill);
}
}
&:hover {
filter: brightness(85%);
}
&:active {
filter: brightness(50%)
}
&.selected {
filter: brightness(80%)
}
}
@mixin disable-input { @mixin disable-input {
-webkit-text-fill-color: var(--comfy-disabled-textbox-text-color); -webkit-text-fill-color: var(--comfy-disabled-textbox-text-color);
background-color: var(--comfy-disabled-textbox-background-fill); background-color: var(--comfy-disabled-textbox-background-fill);
@@ -108,6 +146,12 @@ hr {
color: var(--panel-border-color); color: var(--panel-border-color);
} }
input {
color: var(--body-text-color);
background: var(--input-background-fill);
border: var(--input-border-width) solid var(--input-border-color)
}
input:not(input[type=radio]), textarea { input:not(input[type=radio]), textarea {
border-radius: 0 !important; border-radius: 0 !important;
} }