Merge pull request #60 from space-nuko/gallery-and-send-images
Grid view for gallery
This commit is contained in:
@@ -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
7
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
{:else}
|
||||||
<div class="queue-entry-images"
|
<ComfyQueueListDisplay entries={_entries} {showPrompt} {clearQueue} {mode} {deleteEntry} bind:imageSize />
|
||||||
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
|
{/if}
|
||||||
{#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}
|
|
||||||
<!-- <div class="queue-entry-image-placeholder" /> -->
|
|
||||||
{/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,105 +389,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
width: 100%;
|
||||||
background: var(--block-background-fill);
|
color: var(--neutral-500);
|
||||||
|
|
||||||
&.running {
|
&.selected {
|
||||||
background: var(--comfy-accent-soft);
|
color: var(--body-text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.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%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.all_cached, &.interrupted {
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-buttons {
|
.mode-buttons {
|
||||||
@@ -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%)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
src/lib/components/ComfyQueueGridDisplay.svelte
Normal file
139
src/lib/components/ComfyQueueGridDisplay.svelte
Normal 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>
|
||||||
260
src/lib/components/ComfyQueueListDisplay.svelte
Normal file
260
src/lib/components/ComfyQueueListDisplay.svelte
Normal 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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user