From e6d29656fa2c8a51b60acaba95a2c2ee3602eaba Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Thu, 15 Aug 2024 23:26:38 -0400 Subject: [PATCH] Queue media preview (#449) * output url * Basic image previews * Split out task item component * Move task actions to context menu * simplify * Move spinner * Lift context menu to tab scope * Better tag * Fix placeholder style * nit * Correctly handle cancelled * nit * Split out result item as separate component * nit * Fix center crop * nit * Simplify task item * Flat list * Show prompt id * Make image draggable * Disable preview for dragging * Fix key * Correctly handle task in expanded view * Add preview --- .../sidebar/tabs/QueueSidebarTab.vue | 192 ++++++++---------- .../sidebar/tabs/SidebarTabTemplate.vue | 1 + .../sidebar/tabs/queue/ResultItem.vue | 81 ++++++++ .../sidebar/tabs/queue/TaskItem.vue | 144 +++++++++++++ src/i18n.ts | 10 + src/stores/queueStore.ts | 73 ++++++- src/types/apiTypes.ts | 20 +- src/types/comfyWorkflow.ts | 1 + 8 files changed, 397 insertions(+), 125 deletions(-) create mode 100644 src/components/sidebar/tabs/queue/ResultItem.vue create mode 100644 src/components/sidebar/tabs/queue/TaskItem.vue diff --git a/src/components/sidebar/tabs/QueueSidebarTab.vue b/src/components/sidebar/tabs/QueueSidebarTab.vue index ef71f2c0e..ea8cc6f85 100644 --- a/src/components/sidebar/tabs/QueueSidebarTab.vue +++ b/src/components/sidebar/tabs/QueueSidebarTab.vue @@ -1,125 +1,82 @@ - - diff --git a/src/components/sidebar/tabs/SidebarTabTemplate.vue b/src/components/sidebar/tabs/SidebarTabTemplate.vue index ff0450515..3dd85e9a5 100644 --- a/src/components/sidebar/tabs/SidebarTabTemplate.vue +++ b/src/components/sidebar/tabs/SidebarTabTemplate.vue @@ -42,6 +42,7 @@ const props = defineProps({ border-top: none; border-radius: 0; padding: 0.25rem 1rem; + min-height: 2.5rem; } .comfy-vue-side-bar-header-span { diff --git a/src/components/sidebar/tabs/queue/ResultItem.vue b/src/components/sidebar/tabs/queue/ResultItem.vue new file mode 100644 index 000000000..54fca73be --- /dev/null +++ b/src/components/sidebar/tabs/queue/ResultItem.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/sidebar/tabs/queue/TaskItem.vue b/src/components/sidebar/tabs/queue/TaskItem.vue new file mode 100644 index 000000000..0916a3258 --- /dev/null +++ b/src/components/sidebar/tabs/queue/TaskItem.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/i18n.ts b/src/i18n.ts index 610e0de00..74200c5cf 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -2,6 +2,8 @@ import { createI18n } from 'vue-i18n' const messages = { en: { + delete: 'Delete', + loadWorkflow: 'Load Workflow', settings: 'Settings', searchSettings: 'Search Settings', noResultsFound: 'No Results Found', @@ -15,10 +17,15 @@ const messages = { nodeLibrary: 'Node Library', nodeLibraryTab: { sortOrder: 'Sort Order' + }, + queueTab: { + showFlatList: 'Show Flat List' } } }, zh: { + delete: '删除', + loadWorkflow: '加载工作流', settings: '设置', searchSettings: '搜索设置', noResultsFound: '未找到结果', @@ -32,6 +39,9 @@ const messages = { nodeLibrary: '节点库', nodeLibraryTab: { sortOrder: '排序顺序' + }, + queueTab: { + showFlatList: '平铺结果' } } } diff --git a/src/stores/queueStore.ts b/src/stores/queueStore.ts index c3c1efdd2..0f18f65b3 100644 --- a/src/stores/queueStore.ts +++ b/src/stores/queueStore.ts @@ -1,15 +1,17 @@ import { api } from '@/scripts/api' import { app } from '@/scripts/app' -import { - validateTaskItem, +import type { TaskItem, TaskType, TaskPrompt, TaskStatus, + StatusWsMessageStatus, TaskOutput, - StatusWsMessageStatus + ResultItem } from '@/types/apiTypes' -import { plainToClass } from 'class-transformer' +import { validateTaskItem } from '@/types/apiTypes' +import type { NodeId } from '@/types/comfyWorkflow' +import { instanceToPlain, plainToClass } from 'class-transformer' import _ from 'lodash' import { defineStore } from 'pinia' import { toRaw } from 'vue' @@ -25,11 +27,43 @@ export enum TaskItemDisplayStatus { Cancelled = 'Cancelled' } +export class ResultItemImpl { + filename: string + subfolder?: string + type: string + + nodeId: NodeId + // 'audio' | 'images' | ... + mediaType: string + + get url(): string { + return api.apiURL(`/view?filename=${encodeURIComponent(this.filename)}&type=${this.type}& + subfolder=${encodeURIComponent(this.subfolder || '')}&t=${+new Date()}`) + } +} + export class TaskItemImpl { taskType: TaskType prompt: TaskPrompt status?: TaskStatus - outputs?: TaskOutput + outputs: TaskOutput + + get flatOutputs(): ResultItemImpl[] { + if (!this.outputs) { + return [] + } + return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) => + Object.entries(nodeOutputs).flatMap(([mediaType, items]) => + (items as ResultItem[]).flatMap((item: ResultItem) => + plainToClass(ResultItemImpl, { + ...item, + nodeId, + mediaType + }) + ) + ) + ) + } get apiTaskType(): APITaskType { switch (this.taskType) { @@ -41,6 +75,10 @@ export class TaskItemImpl { } } + get key() { + return this.promptId + this.displayStatus + } + get queueIndex() { return this.prompt[0] } @@ -174,6 +212,31 @@ export const useQueueStore = defineStore('queue', { ...state.historyTasks ] }, + flatTasks(): TaskItemImpl[] { + return this.tasks.flatMap((task: TaskItemImpl) => { + if (task.displayStatus !== TaskItemDisplayStatus.Completed) { + return [task] + } + + return task.flatOutputs.map((output: ResultItemImpl, i: number) => + plainToClass(TaskItemImpl, { + ...instanceToPlain(task), + prompt: [ + task.queueIndex, + `${task.promptId}-${i}`, + task.promptInputs, + task.extraData, + task.outputsToExecute + ], + outputs: { + [output.nodeId]: { + [output.mediaType]: [instanceToPlain(output)] + } + } + }) + ) + }) + }, lastHistoryQueueIndex(state) { return state.historyTasks.length ? state.historyTasks[0].queueIndex : -1 } diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index f1318b5eb..b330ac5a8 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -6,11 +6,18 @@ import { colorPalettesSchema } from './colorPalette' const zNodeType = z.string() const zQueueIndex = z.number() const zPromptId = z.string() -const zImageResult = z.object({ +const zResultItem = z.object({ filename: z.string(), subfolder: z.string().optional(), type: z.string() }) +export type ResultItem = z.infer +const zOutputs = z + .object({ + audio: z.array(zResultItem).optional(), + images: z.array(zResultItem).optional() + }) + .passthrough() // WS messages const zStatusWsMessageStatus = z.object({ @@ -37,11 +44,7 @@ const zExecutingWsMessage = z.object({ }) const zExecutedWsMessage = zExecutingWsMessage.extend({ - outputs: z - .object({ - images: z.array(zImageResult) - }) - .passthrough() + outputs: zOutputs }) const zExecutionWsMessageBase = z.object({ @@ -144,9 +147,6 @@ const zStatus = z.object({ messages: z.array(zStatusMessage) }) -// TODO: this is a placeholder -const zOutput = z.any() - const zTaskPrompt = z.tuple([ zQueueIndex, zPromptId, @@ -170,7 +170,7 @@ const zPendingTaskItem = z.object({ prompt: zTaskPrompt }) -const zTaskOutput = z.record(zNodeId, zOutput) +const zTaskOutput = z.record(zNodeId, zOutputs) const zHistoryTaskItem = z.object({ taskType: z.literal('History'), diff --git a/src/types/comfyWorkflow.ts b/src/types/comfyWorkflow.ts index d8b36bc06..546e9df2a 100644 --- a/src/types/comfyWorkflow.ts +++ b/src/types/comfyWorkflow.ts @@ -5,6 +5,7 @@ import { fromZodError } from 'zod-validation-error' // innerNode.id = `${this.node.id}:${i}` // Remove it after GroupNode is redesigned. export const zNodeId = z.union([z.number().int(), z.string()]) +export type NodeId = z.infer export const zSlotIndex = z.union([ z.number().int(), z