diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index c66d7cabe..c2596a1b4 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -114,6 +114,18 @@ :lod-level="lodLevel" :image-urls="nodeImageUrls" /> + +
+ preview +
@@ -138,6 +150,7 @@ import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState' import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout' import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD' +import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState' import { ExecutedWsMessage } from '@/schemas/apiSchema' import { app } from '@/scripts/app' import { useExecutionStore } from '@/stores/executionStore' @@ -312,6 +325,14 @@ const separatorClasses = 'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full' const progressClasses = 'h-2 bg-primary-500 transition-all duration-300' +const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState( + nodeData.id, + { + isMinimalLOD, + isCollapsed + } +) + // Common condition computations to avoid repetition const shouldShowWidgets = computed( () => shouldRenderWidgets.value && nodeData.widgets?.length diff --git a/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts new file mode 100644 index 000000000..427cf20c0 --- /dev/null +++ b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts @@ -0,0 +1,51 @@ +import { storeToRefs } from 'pinia' +import { type Ref, computed } from 'vue' + +import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { useNodeOutputStore } from '@/stores/imagePreviewStore' + +export const useNodePreviewState = ( + nodeId: string, + options?: { + isMinimalLOD?: Ref + isCollapsed?: Ref + } +) => { + const workflowStore = useWorkflowStore() + const { nodePreviewImages } = storeToRefs(useNodeOutputStore()) + + const locatorId = computed(() => workflowStore.nodeIdToNodeLocatorId(nodeId)) + + const previewUrls = computed(() => { + const key = locatorId.value + if (!key) return undefined + const urls = nodePreviewImages.value[key] + return urls?.length ? urls : undefined + }) + + const hasPreview = computed(() => !!previewUrls.value?.length) + + const latestPreviewUrl = computed(() => { + const urls = previewUrls.value + return urls?.length ? urls.at(-1) : '' + }) + + const shouldShowPreviewImg = computed(() => { + if (!options?.isMinimalLOD || !options?.isCollapsed) { + return hasPreview.value + } + return ( + !options.isMinimalLOD.value && + !options.isCollapsed.value && + hasPreview.value + ) + }) + + return { + locatorId, + previewUrls, + hasPreview, + latestPreviewUrl, + shouldShowPreviewImg + } +} diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts index 15920c1bd..4fc3adb80 100644 --- a/src/stores/imagePreviewStore.ts +++ b/src/stores/imagePreviewStore.ts @@ -1,3 +1,4 @@ +import { useTimeoutFn } from '@vueuse/core' import { defineStore } from 'pinia' import { ref } from 'vue' @@ -19,6 +20,8 @@ import type { NodeLocatorId } from '@/types/nodeIdentification' import { parseFilePath } from '@/utils/formatUtil' import { isVideoNode } from '@/utils/litegraphUtil' +const PREVIEW_REVOKE_DELAY_MS = 400 + const createOutputs = ( filenames: string[], type: ResultItemType, @@ -40,9 +43,26 @@ interface SetOutputOptions { export const useNodeOutputStore = defineStore('nodeOutput', () => { const { nodeIdToNodeLocatorId } = useWorkflowStore() const { executionIdToNodeLocatorId } = useExecutionStore() + const scheduledRevoke: Record void }> = {} + + function scheduleRevoke(locator: NodeLocatorId, cb: () => void) { + scheduledRevoke[locator]?.stop() + + const { stop } = useTimeoutFn(() => { + delete scheduledRevoke[locator] + cb() + }, PREVIEW_REVOKE_DELAY_MS) + + scheduledRevoke[locator] = { stop } + } const nodeOutputs = ref>({}) + // Reactive state for node preview images - mirrors app.nodePreviewImages + const nodePreviewImages = ref>( + app.nodePreviewImages || {} + ) + function getNodeOutputs( node: LGraphNode ): ExecutedWsMessage['output'] | undefined { @@ -196,8 +216,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { ) { const nodeLocatorId = executionIdToNodeLocatorId(executionId) if (!nodeLocatorId) return - + if (scheduledRevoke[nodeLocatorId]) { + scheduledRevoke[nodeLocatorId].stop() + delete scheduledRevoke[nodeLocatorId] + } app.nodePreviewImages[nodeLocatorId] = previewImages + nodePreviewImages.value[nodeLocatorId] = previewImages } /** @@ -212,7 +236,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { previewImages: string[] ) { const nodeLocatorId = nodeIdToNodeLocatorId(nodeId) + if (scheduledRevoke[nodeLocatorId]) { + scheduledRevoke[nodeLocatorId].stop() + delete scheduledRevoke[nodeLocatorId] + } app.nodePreviewImages[nodeLocatorId] = previewImages + nodePreviewImages.value[nodeLocatorId] = previewImages } /** @@ -224,8 +253,9 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { function revokePreviewsByExecutionId(executionId: string) { const nodeLocatorId = executionIdToNodeLocatorId(executionId) if (!nodeLocatorId) return - - revokePreviewsByLocatorId(nodeLocatorId) + scheduleRevoke(nodeLocatorId, () => + revokePreviewsByLocatorId(nodeLocatorId) + ) } /** @@ -243,6 +273,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { } delete app.nodePreviewImages[nodeLocatorId] + delete nodePreviewImages.value[nodeLocatorId] } /** @@ -259,6 +290,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { } } app.nodePreviewImages = {} + nodePreviewImages.value = {} } /** @@ -293,6 +325,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { // Clear preview images if (app.nodePreviewImages[nodeLocatorId]) { delete app.nodePreviewImages[nodeLocatorId] + delete nodePreviewImages.value[nodeLocatorId] } return hadOutputs @@ -318,6 +351,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { removeNodeOutputs, // State - nodeOutputs + nodeOutputs, + nodePreviewImages } }) diff --git a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts index 6e34e2450..07c0a3081 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts @@ -56,6 +56,13 @@ vi.mock( }) ) +vi.mock('@/renderer/extensions/vueNodes/preview/useNodePreviewState', () => ({ + useNodePreviewState: vi.fn(() => ({ + latestPreviewUrl: computed(() => ''), + shouldShowPreviewImg: computed(() => false) + })) +})) + const i18n = createI18n({ legacy: false, locale: 'en',