Refactor UI queue state
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
|
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
|
||||||
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
|
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
|
||||||
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
|
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
|
||||||
|
import uiQueueState from "$lib/stores/uiQueueState";
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
|
|
||||||
@@ -52,46 +53,34 @@
|
|||||||
let displayMode: DisplayModeType = "list";
|
let displayMode: DisplayModeType = "list";
|
||||||
let imageSize: number = 40;
|
let imageSize: number = 40;
|
||||||
let gridColumns: number = 3;
|
let gridColumns: number = 3;
|
||||||
let changed = true;
|
|
||||||
|
|
||||||
function switchMode(newMode: QueueItemType) {
|
function switchMode(newMode: QueueItemType) {
|
||||||
changed = mode !== newMode
|
const changed = mode !== newMode
|
||||||
mode = newMode
|
mode = newMode
|
||||||
if (changed) {
|
if (changed) {
|
||||||
_queuedEntries = []
|
uiQueueState.updateEntries();
|
||||||
_runningEntries = []
|
|
||||||
_entries = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchDisplayMode(newDisplayMode: DisplayModeType) {
|
function switchDisplayMode(newDisplayMode: DisplayModeType) {
|
||||||
// changed = displayMode !== newDisplayMode
|
|
||||||
displayMode = newDisplayMode
|
displayMode = newDisplayMode
|
||||||
// if (changed) {
|
|
||||||
// _queuedEntries = []
|
|
||||||
// _runningEntries = []
|
|
||||||
// _entries = []
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _queuedEntries: QueueUIEntry[] = []
|
let _entries: ReadonlyArray<QueueUIEntry> = []
|
||||||
let _runningEntries: QueueUIEntry[] = []
|
$: if(mode === "queue") {
|
||||||
let _entries: QueueUIEntry[] = []
|
_entries = $uiQueueState.queueUIEntries
|
||||||
|
|
||||||
$: if (mode === "queue" && (changed || $queuePending.length != _queuedEntries.length || $queueRunning.length != _runningEntries.length)) {
|
|
||||||
updateFromQueue();
|
updateFromQueue();
|
||||||
changed = false;
|
|
||||||
}
|
}
|
||||||
else if (mode === "history" && (changed || $queueCompleted.length != _entries.length)) {
|
else {
|
||||||
|
_entries = $uiQueueState.historyUIEntries;
|
||||||
updateFromHistory();
|
updateFromHistory();
|
||||||
changed = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (mode === "queue" && !$queuePending && !$queueRunning) {
|
$: if (mode === "queue" && !$queuePending && !$queueRunning) {
|
||||||
_queuedEntries = []
|
uiQueueState.clearQueue();
|
||||||
_runningEntries = []
|
}
|
||||||
_entries = [];
|
else if (mode === "history" && !$queueCompleted) {
|
||||||
changed = true
|
uiQueueState.clearHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) {
|
async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) {
|
||||||
@@ -106,125 +95,26 @@
|
|||||||
await app.deleteQueueItem(mode, entry.entry.promptID);
|
await app.deleteQueueItem(mode, entry.entry.promptID);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "queue") {
|
uiQueueState.updateEntries(true)
|
||||||
_queuedEntries = []
|
|
||||||
_runningEntries = []
|
|
||||||
}
|
|
||||||
|
|
||||||
_entries = [];
|
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearQueue() {
|
async function clearQueue() {
|
||||||
await app.clearQueue(mode);
|
await app.clearQueue(mode);
|
||||||
|
uiQueueState.updateEntries(true)
|
||||||
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(',', '');
|
|
||||||
return [time, day].join(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
|
|
||||||
let date = entry.finishedAt || entry.queuedAt;
|
|
||||||
let dateStr = null;
|
|
||||||
if (date) {
|
|
||||||
dateStr = formatDate(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
|
|
||||||
|
|
||||||
let message = "Prompt";
|
|
||||||
if (entry.extraData?.workflowTitle != null) {
|
|
||||||
message = `${entry.extraData.workflowTitle}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subgraphs && subgraphs.length > 0) {
|
|
||||||
const subgraphsString = subgraphs.join(', ')
|
|
||||||
message += ` (${subgraphsString})`
|
|
||||||
}
|
|
||||||
|
|
||||||
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
|
||||||
|
|
||||||
if (Object.keys(entry.outputs).length > 0) {
|
|
||||||
const imageCount = Object.values(entry.outputs).filter(o => o.images).flatMap(o => o.images).length
|
|
||||||
submessage = `Images: ${imageCount}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
entry,
|
|
||||||
message,
|
|
||||||
submessage,
|
|
||||||
date: dateStr,
|
|
||||||
status,
|
|
||||||
images: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
|
|
||||||
const result = convertEntry(entry, status);
|
|
||||||
|
|
||||||
const thumbnails = entry.extraData?.thumbnails
|
|
||||||
if (thumbnails) {
|
|
||||||
result.images = thumbnails.map(convertComfyOutputToComfyURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputs = Object.values(entry.outputs)
|
|
||||||
.filter(o => o.images)
|
|
||||||
.flatMap(o => o.images)
|
|
||||||
.map(convertComfyOutputToComfyURL);
|
|
||||||
if (outputs) {
|
|
||||||
result.images = result.images.concat(outputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
|
|
||||||
const result = convertEntry(entry.entry, entry.status);
|
|
||||||
|
|
||||||
const images = Object.values(entry.entry.outputs)
|
|
||||||
.filter(o => o.images)
|
|
||||||
.flatMap(o => o.images)
|
|
||||||
.map(convertComfyOutputToComfyURL);
|
|
||||||
result.images = images
|
|
||||||
|
|
||||||
if (entry.message)
|
|
||||||
result.submessage = entry.message
|
|
||||||
else if (entry.status === "interrupted" || entry.status === "all_cached")
|
|
||||||
result.submessage = "Prompt was interrupted."
|
|
||||||
if (entry.error)
|
|
||||||
result.error = entry.error
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateFromQueue() {
|
async function updateFromQueue() {
|
||||||
// newest entries appear at the top
|
|
||||||
_queuedEntries = $queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
|
|
||||||
_runningEntries = $queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
|
|
||||||
_entries = [..._queuedEntries, ..._runningEntries]
|
|
||||||
if (queueList) {
|
if (queueList) {
|
||||||
await tick(); // Wait for list size to be recalculated
|
await tick(); // Wait for list size to be recalculated
|
||||||
queueList.scroll({ top: queueList.scrollHeight })
|
queueList.scroll({ top: queueList.scrollHeight })
|
||||||
}
|
}
|
||||||
console.warn("[ComfyQueue] BUILDQUEUE", _entries.length, $queuePending.length, $queueRunning.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateFromHistory() {
|
async function updateFromHistory() {
|
||||||
_entries = $queueCompleted.map(convertCompletedEntry).reverse();
|
|
||||||
if (queueList) {
|
if (queueList) {
|
||||||
|
await tick(); // Wait for list size to be recalculated
|
||||||
queueList.scrollTo(0, 0);
|
queueList.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
console.warn("[ComfyQueue] BUILDHISTORY", _entries.length, $queueCompleted.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function interrupt() {
|
async function interrupt() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { get, writable, type Writable } from "svelte/store";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState";
|
import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState";
|
||||||
import configState from "./configState";
|
import configState from "./configState";
|
||||||
|
import uiQueueState from "./uiQueueState";
|
||||||
|
|
||||||
export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown";
|
export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown";
|
||||||
|
|
||||||
|
|||||||
207
src/lib/stores/uiQueueState.ts
Normal file
207
src/lib/stores/uiQueueState.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import type { PromptID, QueueItemType } from '$lib/api';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
|
import queueState, { type CompletedQueueEntry, type QueueEntry } from './queueState';
|
||||||
|
import type { WorkflowError } from './workflowState';
|
||||||
|
import { convertComfyOutputToComfyURL } from '$lib/utils';
|
||||||
|
|
||||||
|
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
|
||||||
|
error?: WorkflowError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UIQueueState = {
|
||||||
|
mode: QueueItemType,
|
||||||
|
|
||||||
|
queuedEntries: QueueUIEntry[],
|
||||||
|
runningEntries: QueueUIEntry[],
|
||||||
|
|
||||||
|
queueUIEntries: QueueUIEntry[],
|
||||||
|
historyUIEntries: QueueUIEntry[],
|
||||||
|
}
|
||||||
|
|
||||||
|
type UIQueueStateOps = {
|
||||||
|
updateEntries: (force?: boolean) => void
|
||||||
|
clearAll: () => void
|
||||||
|
clearQueue: () => void
|
||||||
|
clearHistory: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WritableUIQueueStateStore = Writable<UIQueueState> & UIQueueStateOps;
|
||||||
|
const store: Writable<UIQueueState> = writable(
|
||||||
|
{
|
||||||
|
mode: "queue",
|
||||||
|
queuedEntries: [],
|
||||||
|
runningEntries: [],
|
||||||
|
completedEntries: [],
|
||||||
|
|
||||||
|
queueUIEntries: [],
|
||||||
|
historyUIEntries: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
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(',', '');
|
||||||
|
return [time, day].join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
|
||||||
|
let date = entry.finishedAt || entry.queuedAt;
|
||||||
|
let dateStr = null;
|
||||||
|
if (date) {
|
||||||
|
dateStr = formatDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
|
||||||
|
|
||||||
|
let message = "Prompt";
|
||||||
|
if (entry.extraData?.workflowTitle != null) {
|
||||||
|
message = `${entry.extraData.workflowTitle}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subgraphs && subgraphs.length > 0) {
|
||||||
|
const subgraphsString = subgraphs.join(', ')
|
||||||
|
message += ` (${subgraphsString})`
|
||||||
|
}
|
||||||
|
|
||||||
|
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
||||||
|
|
||||||
|
if (Object.keys(entry.outputs).length > 0) {
|
||||||
|
const imageCount = Object.values(entry.outputs).filter(o => o.images).flatMap(o => o.images).length
|
||||||
|
submessage = `Images: ${imageCount}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
message,
|
||||||
|
submessage,
|
||||||
|
date: dateStr,
|
||||||
|
status,
|
||||||
|
images: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
|
||||||
|
const result = convertEntry(entry, status);
|
||||||
|
|
||||||
|
const thumbnails = entry.extraData?.thumbnails
|
||||||
|
if (thumbnails) {
|
||||||
|
result.images = thumbnails.map(convertComfyOutputToComfyURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputs = Object.values(entry.outputs)
|
||||||
|
.filter(o => o.images)
|
||||||
|
.flatMap(o => o.images)
|
||||||
|
.map(convertComfyOutputToComfyURL);
|
||||||
|
if (outputs) {
|
||||||
|
result.images = result.images.concat(outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
|
||||||
|
const result = convertEntry(entry.entry, entry.status);
|
||||||
|
|
||||||
|
const images = Object.values(entry.entry.outputs)
|
||||||
|
.filter(o => o.images)
|
||||||
|
.flatMap(o => o.images)
|
||||||
|
.map(convertComfyOutputToComfyURL);
|
||||||
|
result.images = images
|
||||||
|
|
||||||
|
if (entry.message)
|
||||||
|
result.submessage = entry.message
|
||||||
|
else if (entry.status === "interrupted" || entry.status === "all_cached")
|
||||||
|
result.submessage = "Prompt was interrupted."
|
||||||
|
if (entry.error)
|
||||||
|
result.error = entry.error
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromQueue(queuePending: QueueEntry[], queueRunning: QueueEntry[]) {
|
||||||
|
store.update(s => {
|
||||||
|
// newest entries appear at the top
|
||||||
|
s.queuedEntries = queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
|
||||||
|
s.runningEntries = queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
|
||||||
|
s.queueUIEntries = s.queuedEntries.concat(s.runningEntries);
|
||||||
|
console.warn("[ComfyQueue] BUILDQUEUE", s.queuedEntries.length, s.runningEntries.length)
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromHistory(queueCompleted: CompletedQueueEntry[]) {
|
||||||
|
store.update(s => {
|
||||||
|
s.historyUIEntries = queueCompleted.map(convertCompletedEntry).reverse();
|
||||||
|
console.warn("[ComfyQueue] BUILDHISTORY", s.historyUIEntries.length)
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEntries(force: boolean = false) {
|
||||||
|
const state = get(store)
|
||||||
|
const qs = get(queueState)
|
||||||
|
const queuePending = qs.queuePending
|
||||||
|
const queueRunning = qs.queueRunning
|
||||||
|
const queueCompleted = qs.queueCompleted
|
||||||
|
|
||||||
|
const queueChanged = get(queuePending).length != state.queuedEntries.length
|
||||||
|
|| get(queueRunning).length != state.runningEntries.length;
|
||||||
|
const historyChanged = get(queueCompleted).length != state.historyUIEntries.length;
|
||||||
|
|
||||||
|
if (queueChanged || force) {
|
||||||
|
updateFromQueue(get(queuePending), get(queueRunning));
|
||||||
|
}
|
||||||
|
if (historyChanged || force) {
|
||||||
|
updateFromHistory(get(queueCompleted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
store.update(s => {
|
||||||
|
s.queuedEntries = []
|
||||||
|
s.runningEntries = []
|
||||||
|
s.historyUIEntries = []
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
updateEntries(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearQueue() {
|
||||||
|
store.update(s => {
|
||||||
|
s.queuedEntries = []
|
||||||
|
s.runningEntries = []
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
updateEntries(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
store.update(s => {
|
||||||
|
s.historyUIEntries = []
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
updateEntries(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
queueState.subscribe(s => {
|
||||||
|
updateEntries();
|
||||||
|
})
|
||||||
|
|
||||||
|
const uiStateStore: WritableUIQueueStateStore =
|
||||||
|
{
|
||||||
|
...store,
|
||||||
|
updateEntries,
|
||||||
|
clearAll,
|
||||||
|
clearQueue,
|
||||||
|
clearHistory,
|
||||||
|
}
|
||||||
|
export default uiStateStore;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Page, Navbar, Tabs, Tab, NavLeft, NavTitle, NavRight, Link } from "framework7-svelte"
|
import { Page, Navbar, Block, Tabs, Tab, NavLeft, NavTitle, NavRight, Link } from "framework7-svelte"
|
||||||
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
||||||
import type ComfyApp from "$lib/components/ComfyApp";
|
import type ComfyApp from "$lib/components/ComfyApp";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
@@ -12,6 +12,34 @@
|
|||||||
|
|
||||||
<Page name="gallery">
|
<Page name="gallery">
|
||||||
<Navbar title="Gallery" />
|
<Navbar title="Gallery" />
|
||||||
|
|
||||||
|
<Block>
|
||||||
|
<div class="grid grid-cols-3 grid-gap">
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 grid-gap">
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 grid-gap">
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 grid-gap">
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 grid-gap">
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
<div>3 cols</div>
|
||||||
|
</div>
|
||||||
|
</Block>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
Reference in New Issue
Block a user