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:*",
|
||||
"pollen-css": "^4.6.2",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"svelte-bootstrap-icons": "^2.3.1",
|
||||
"svelte-feather-icons": "^4.0.0",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte-select": "^5.5.3",
|
||||
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -97,6 +97,9 @@ importers:
|
||||
radix-icons-svelte:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
svelte-bootstrap-icons:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
svelte-feather-icons:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -7908,6 +7911,10 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
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):
|
||||
resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==}
|
||||
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.
|
||||
* - "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 ComfyNodeDefInputOptions = {
|
||||
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,
|
||||
|
||||
// INT/FLOAT options
|
||||
|
||||
@@ -331,7 +331,7 @@ export default class ComfyAPI {
|
||||
* @param {string} type The type of item to delete, queue or history
|
||||
* @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] });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons";
|
||||
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
|
||||
import { Image, Gear } from "svelte-bootstrap-icons";
|
||||
import ComfyApp from "./ComfyApp";
|
||||
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 LightboxModal from "./LightboxModal.svelte";
|
||||
@@ -34,6 +36,16 @@
|
||||
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>
|
||||
|
||||
<svelte:head>
|
||||
@@ -42,13 +54,15 @@
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<svelte:window on:beforeunload={handleBeforeUnload} />
|
||||
|
||||
<div id="main" class:dark={uiTheme === "gradio-dark"}>
|
||||
<div id="container">
|
||||
<Sidebar selected="generate">
|
||||
<SidebarItem id="generate" name="Generate" icon={Image}>
|
||||
<ComfyWorkflowsView {app} {uiTheme} />
|
||||
</SidebarItem>
|
||||
<SidebarItem id="settings" name="Settings" icon={Settings}>
|
||||
<SidebarItem id="settings" name="Settings" icon={Gear}>
|
||||
</SidebarItem>
|
||||
</Sidebar>
|
||||
</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 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 EventEmitter from "events";
|
||||
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 { tick } from "svelte";
|
||||
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 configState from "$lib/stores/configState";
|
||||
import { blankGraph } from "$lib/defaultGraph";
|
||||
@@ -312,7 +312,7 @@ export default class ComfyApp {
|
||||
|
||||
const workflows = state.workflows as SerializedAppState[];
|
||||
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)
|
||||
notify(`Failed restoring previous workflow: ${error}`, { type: "error" })
|
||||
})
|
||||
@@ -509,6 +509,46 @@ export default class ComfyApp {
|
||||
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() {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
this.shiftDown = e.shiftKey;
|
||||
@@ -557,9 +597,12 @@ export default class ComfyApp {
|
||||
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) {
|
||||
const mes = `Invalid ComfyBox saved data format: ${data.version}`
|
||||
const mes = `Invalid ComfyBox saved data format: ${data.version} `
|
||||
notify(mes, { type: "error" })
|
||||
return Promise.reject(mes);
|
||||
}
|
||||
@@ -580,7 +623,7 @@ export default class ComfyApp {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (workflow.missingNodeTypes.size > 0) {
|
||||
if (workflow.missingNodeTypes.size > 0 && warnMissingNodeTypes) {
|
||||
modalState.pushModal({
|
||||
svelteComponent: MissingNodeTypesModal,
|
||||
svelteProps: {
|
||||
@@ -681,7 +724,7 @@ export default class ComfyApp {
|
||||
}
|
||||
catch (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)
|
||||
}
|
||||
await this.openWorkflow(state, defs)
|
||||
@@ -737,7 +780,7 @@ export default class ComfyApp {
|
||||
else {
|
||||
const date = new Date();
|
||||
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
|
||||
@@ -782,7 +825,7 @@ export default class ComfyApp {
|
||||
try {
|
||||
while (this.queueItems.length) {
|
||||
({ num, batchCount, workflow } = this.queueItems.pop());
|
||||
console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
|
||||
console.debug(`Queue get! ${num} ${batchCount} ${tag} `);
|
||||
|
||||
const thumbnails = []
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
@@ -850,7 +893,7 @@ export default class ComfyApp {
|
||||
|
||||
if (error != null) {
|
||||
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(promptToGraphVis(p))
|
||||
console.error("Error queuing prompt", error, num, p)
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<label class="number-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="number">
|
||||
<input type="number" bind:value {min} {max} {step} {disabled}>
|
||||
<input type="number" bind:value {min} {max} {step} {disabled} />
|
||||
</div>
|
||||
</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">
|
||||
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
|
||||
import ProgressBar from "./ProgressBar.svelte";
|
||||
import Spinner from "./Spinner.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 type { Writable } from "svelte/store";
|
||||
import type { QueueItemType } from "$lib/api";
|
||||
@@ -14,6 +28,8 @@
|
||||
import Modal from "./Modal.svelte";
|
||||
import DropZone from "./DropZone.svelte";
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
|
||||
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
@@ -22,25 +38,18 @@
|
||||
let queueCompleted: Writable<CompletedQueueEntry[]> | 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) {
|
||||
queuePending = $queueState.queuePending
|
||||
queueRunning = $queueState.queueRunning
|
||||
queueCompleted = $queueState.queueCompleted
|
||||
}
|
||||
|
||||
type DisplayModeType = "list" | "grid";
|
||||
|
||||
let mode: QueueItemType = "queue";
|
||||
let displayMode: DisplayModeType = "list";
|
||||
let imageSize: number = 40;
|
||||
let gridColumns: number = 3;
|
||||
let changed = true;
|
||||
|
||||
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 _runningEntries: QueueUIEntry[] = []
|
||||
let _entries: QueueUIEntry[] = []
|
||||
@@ -66,6 +85,39 @@
|
||||
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 {
|
||||
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(',', '');
|
||||
@@ -156,35 +208,15 @@
|
||||
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() {
|
||||
if ($queueState.isInterrupting)
|
||||
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 })
|
||||
});
|
||||
await app.interrupt();
|
||||
}
|
||||
|
||||
let showModal = false;
|
||||
let expandAll = false;
|
||||
let selectedPrompt = null;
|
||||
let selectedImages = [];
|
||||
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
||||
function showPrompt(entry: QueueUIEntry) {
|
||||
selectedPrompt = entry.entry.prompt;
|
||||
selectedImages = entry.images;
|
||||
showModal = true;
|
||||
@@ -219,48 +251,33 @@
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div class="queue">
|
||||
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
||||
<div class="queue {mode}-mode">
|
||||
{#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}
|
||||
{#each _entries as entry}
|
||||
<div class="queue-entry {entry.status}" on:click={(e) => showPrompt(entry, e)}>
|
||||
{#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>
|
||||
{#if mode === "history" && displayMode === "grid"}
|
||||
<ComfyQueueGridDisplay entries={_entries} {showPrompt} {clearQueue} {mode} bind:gridColumns />
|
||||
{:else}
|
||||
<!-- <div class="queue-entry-image-placeholder" /> -->
|
||||
<ComfyQueueListDisplay entries={_entries} {showPrompt} {clearQueue} {mode} {deleteEntry} bind:imageSize />
|
||||
{/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}
|
||||
<div class="queue-empty">
|
||||
<div class="queue-empty-container">
|
||||
<div class="queue-empty-icon">
|
||||
<List size="120rem" />
|
||||
<ListUl width="100%" height="10rem" />
|
||||
</div>
|
||||
<div class="queue-empty-message">
|
||||
(No entries)
|
||||
@@ -272,12 +289,12 @@
|
||||
<div class="mode-buttons">
|
||||
<div class="mode-button secondary"
|
||||
on:click={() => switchMode("queue")}
|
||||
class:mode-selected={mode === "queue"}>
|
||||
class:selected={mode === "queue"}>
|
||||
Queue
|
||||
</div>
|
||||
<div class="mode-button secondary"
|
||||
on:click={() => switchMode("history")}
|
||||
class:mode-selected={mode === "history"}>
|
||||
class:selected={mode === "history"}>
|
||||
History
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,10 +332,12 @@
|
||||
|
||||
<style lang="scss">
|
||||
$pending-height: 200px;
|
||||
$display-mode-buttons-height: 2rem;
|
||||
$bottom-bar-height: 70px;
|
||||
$workflow-tabs-height: 2.5rem;
|
||||
$mode-buttons-height: 30px;
|
||||
$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 {
|
||||
padding-left: 0.2rem;
|
||||
@@ -329,20 +348,22 @@
|
||||
}
|
||||
.queue {
|
||||
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 {
|
||||
height: $queue-height;
|
||||
max-height: $queue-height;
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
&.queue-mode > :first-child {
|
||||
// elements stick to bottom in queue mode only
|
||||
// next element in queue is on the bottom
|
||||
margin-top: auto !important;
|
||||
}
|
||||
height: $queue-height;
|
||||
|
||||
> .queue-empty {
|
||||
display: flex;
|
||||
@@ -368,106 +389,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.queue-entry {
|
||||
padding: 1.0rem;
|
||||
.display-mode-buttons {
|
||||
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;
|
||||
top: 0px;
|
||||
height: $display-mode-buttons-height;
|
||||
margin-bottom: auto;
|
||||
|
||||
&:hover:not(:has(img: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 {
|
||||
}
|
||||
}
|
||||
|
||||
.queue-entry-rest {
|
||||
> .mode-button {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
color: var(--neutral-500);
|
||||
|
||||
&.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;
|
||||
&.selected {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mode-buttons {
|
||||
display: flex;
|
||||
@@ -483,34 +420,8 @@
|
||||
.mode-button {
|
||||
height: calc($mode-buttons-height);
|
||||
padding: 0.2rem;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(85%);
|
||||
}
|
||||
&:active {
|
||||
filter: brightness(50%)
|
||||
}
|
||||
&.mode-selected {
|
||||
filter: brightness(80%)
|
||||
}
|
||||
@include square-button;
|
||||
}
|
||||
|
||||
:global(.dark) .mode-button {
|
||||
@@ -521,7 +432,7 @@
|
||||
&:active {
|
||||
filter: brightness(50%)
|
||||
}
|
||||
&.mode-selected {
|
||||
&.selected {
|
||||
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">
|
||||
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 { BlockTitle } from "@gradio/atoms";
|
||||
import ComfyWorkflowView from "./ComfyWorkflowView.svelte";
|
||||
@@ -253,7 +253,7 @@
|
||||
</div>
|
||||
<button class="workflow-add-new-button"
|
||||
on:click={createNewWorkflow}>
|
||||
<PlusSquare size="100%" strokeWidth={1.5} />
|
||||
<PlusSquareDotted width="100%" height="100%" />
|
||||
</button>
|
||||
</div>
|
||||
<div id="bottombar">
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{#if t.id === $selected_tab}
|
||||
<button class="selected">
|
||||
{#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}
|
||||
{t.name}
|
||||
{/if}
|
||||
@@ -75,7 +75,7 @@
|
||||
}}
|
||||
>
|
||||
{#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}
|
||||
{t.name}
|
||||
{/if}
|
||||
@@ -115,7 +115,7 @@
|
||||
> button {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
padding: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
color: var(--neutral-600);
|
||||
border-right: 3px solid transparent;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||
@@ -42,15 +42,17 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
|
||||
selectedFilename: string | null = null;
|
||||
|
||||
selectedIndexWidget: ITextWidget;
|
||||
selectedIndexWidget: INumberWidget;
|
||||
modeWidget: IComboWidget;
|
||||
|
||||
imageWidth: Writable<number> = writable(0);
|
||||
imageHeight: Writable<number> = writable(0);
|
||||
|
||||
selectedImage: Writable<number | null> = writable(null);
|
||||
|
||||
constructor(name?: string) {
|
||||
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.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() {
|
||||
const value = get(this.value)
|
||||
const index = get(this.selectedImage)
|
||||
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) {
|
||||
const image = value[this.properties.index];
|
||||
this.selectedIndexWidget.value = index;
|
||||
|
||||
if (index != null && value && value[index] != null) {
|
||||
const image = value[index];
|
||||
image.width = get(this.imageWidth)
|
||||
image.height = get(this.imageHeight)
|
||||
}
|
||||
@@ -91,6 +96,8 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
|
||||
if (this.properties.updateMode === "append") {
|
||||
const currentValue = get(this.value)
|
||||
if (meta.length > 0)
|
||||
this.selectedImage.set(currentValue.length);
|
||||
return currentValue.concat(meta)
|
||||
}
|
||||
else {
|
||||
@@ -101,7 +108,6 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
|
||||
override setValue(value: any, noChangedEvent: boolean = false) {
|
||||
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 */
|
||||
promptForWorkflowName: boolean,
|
||||
|
||||
/** When closing the tab, open the confirmation window if there's unsaved changes */
|
||||
confirmWhenUnloadingUnsavedChanges: boolean,
|
||||
}
|
||||
|
||||
type ConfigStateOps = {
|
||||
@@ -17,7 +20,8 @@ export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
|
||||
const store: Writable<ConfigState> = writable(
|
||||
{
|
||||
alwaysStripUserState: false,
|
||||
promptForWorkflowName: false
|
||||
promptForWorkflowName: false,
|
||||
confirmWhenUnloadingUnsavedChanges: true
|
||||
})
|
||||
|
||||
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 { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import notify from "$lib/notify";
|
||||
@@ -17,6 +17,8 @@ type QueueStateOps = {
|
||||
progressUpdated: (progress: Progress) => void
|
||||
getQueueEntry: (promptID: PromptID) => QueueEntry | null;
|
||||
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
|
||||
}
|
||||
|
||||
@@ -181,6 +183,39 @@ function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Wri
|
||||
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[]>) {
|
||||
const state = get(store)
|
||||
|
||||
@@ -294,7 +329,7 @@ function createNewQueueEntry(promptID: PromptID, number: number = -1, prompt: Se
|
||||
return {
|
||||
number,
|
||||
queuedAt: new Date(), // Now
|
||||
finishedAt: null,
|
||||
finishedAt: undefined,
|
||||
promptID,
|
||||
prompt,
|
||||
extraData,
|
||||
@@ -363,6 +398,37 @@ function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, outputs: ComfyExecu
|
||||
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 =
|
||||
{
|
||||
...store,
|
||||
@@ -375,6 +441,8 @@ const queueStateStore: WritableQueueStateStore =
|
||||
executionCached,
|
||||
executionError,
|
||||
afterQueued,
|
||||
queueItemDeleted,
|
||||
queueCleared,
|
||||
getQueueEntry,
|
||||
onExecuted
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { get } from "svelte/store";
|
||||
import type { ComfyNodeID } from "./api";
|
||||
import { type SerializedPrompt } from "./components/ComfyApp";
|
||||
import workflowState from "./stores/workflowState";
|
||||
import { ImageViewer } from "./ImageViewer";
|
||||
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
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
|
||||
}
|
||||
|
||||
export function capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export function basename(filepath: string): string {
|
||||
const filename = filepath.split('/').pop().split('\\').pop();
|
||||
return filename.split('.').slice(0, -1).join('.');
|
||||
@@ -501,3 +506,13 @@ export function comfyBoxImageToComfyFile(image: ComfyBoxImageMetadata): ComfyIma
|
||||
export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string {
|
||||
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 { WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||
import { f7 } from "framework7-svelte";
|
||||
@@ -18,10 +17,9 @@
|
||||
let node: ComfyGalleryNode | null = null;
|
||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
let imageWidth: 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);
|
||||
|
||||
@@ -32,6 +30,7 @@
|
||||
propsChanged = node.propsChanged;
|
||||
imageWidth = node.imageWidth
|
||||
imageHeight = node.imageHeight
|
||||
selected_image = node.selectedImage;
|
||||
|
||||
if ($nodeValue != null) {
|
||||
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
||||
@@ -81,7 +80,7 @@
|
||||
thumbs: images.map(i => i.url),
|
||||
type: 'popup',
|
||||
});
|
||||
mobileLightbox.open(selected_image)
|
||||
mobileLightbox.open($selected_image)
|
||||
}
|
||||
|
||||
function onClicked(e: CustomEvent<HTMLImageElement>) {
|
||||
@@ -97,20 +96,6 @@
|
||||
// Update index
|
||||
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>
|
||||
|
||||
{#if widget && node && nodeValue && $nodeValue}
|
||||
@@ -148,7 +133,7 @@
|
||||
on:clicked={onClicked}
|
||||
bind:imageWidth={$imageWidth}
|
||||
bind:imageHeight={$imageHeight}
|
||||
bind:selected_image
|
||||
bind:selected_image={$selected_image}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
@@ -81,6 +81,44 @@ body {
|
||||
--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 {
|
||||
-webkit-text-fill-color: var(--comfy-disabled-textbox-text-color);
|
||||
background-color: var(--comfy-disabled-textbox-background-fill);
|
||||
@@ -108,6 +146,12 @@ hr {
|
||||
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 {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user