mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
feat: optimize Vue node preview image display with reactive store
- Move preview display logic from inline ternaries to computed properties - Add useNodePreviewState composable for preview state management - Implement reactive store approach using Pinia storeToRefs - Use VueUse useTimeoutFn for modern timeout management instead of window.setTimeout - Add v-memo optimization for preview image template rendering - Maintain proper sync between app.nodePreviewImages and reactive store state 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
@@ -113,7 +114,10 @@ import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import {
|
||||
NodePreviewImagesKey,
|
||||
SelectedNodeIdsKey
|
||||
} from '@/renderer/core/canvas/injectionKeys'
|
||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
|
||||
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||
@@ -128,6 +132,7 @@ import { newUserService } from '@/services/newUserService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
@@ -207,6 +212,9 @@ provide(SelectedNodeIdsKey, selectedNodeIds)
|
||||
|
||||
// Provide execution state to all Vue nodes
|
||||
useExecutionStateProvider()
|
||||
// Provide preview images state to all Vue nodes
|
||||
const { nodePreviewImages } = storeToRefs(useNodeOutputStore())
|
||||
provide(NodePreviewImagesKey, nodePreviewImages)
|
||||
|
||||
watchEffect(() => {
|
||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||
|
||||
@@ -23,3 +23,10 @@ export const ExecutingNodeIdsKey: InjectionKey<Ref<Set<string>>> =
|
||||
export const NodeProgressStatesKey: InjectionKey<
|
||||
Ref<Record<string, NodeProgressState>>
|
||||
> = Symbol('nodeProgressStates')
|
||||
|
||||
/**
|
||||
* Injection key for providing node preview image URLs to Vue node components.
|
||||
* Maps NodeLocatorId (string) to an array of preview blob URLs.
|
||||
*/
|
||||
export const NodePreviewImagesKey: InjectionKey<Ref<Record<string, string[]>>> =
|
||||
Symbol('nodePreviewImages')
|
||||
|
||||
@@ -120,6 +120,18 @@
|
||||
:lod-level="lodLevel"
|
||||
:image-urls="nodeImageUrls"
|
||||
/>
|
||||
<!-- Live preview image -->
|
||||
<div
|
||||
v-if="shouldShowPreviewImg"
|
||||
v-memo="[latestPreviewUrl]"
|
||||
class="px-4 min-h-[220px]"
|
||||
>
|
||||
<img
|
||||
:src="latestPreviewUrl"
|
||||
alt="preview"
|
||||
class="w-full h-[220px] object-contain rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -144,6 +156,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'
|
||||
@@ -316,6 +329,14 @@ const separatorClasses =
|
||||
'bg-sand-100 dark-theme:bg-charcoal-300 h-[1px] mx-0 w-full'
|
||||
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
|
||||
|
||||
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
props.nodeData.id,
|
||||
{
|
||||
isMinimalLOD,
|
||||
isCollapsed
|
||||
}
|
||||
)
|
||||
|
||||
// Common condition computations to avoid repetition
|
||||
const shouldShowWidgets = computed(
|
||||
() => shouldRenderWidgets.value && nodeData.widgets?.length
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { type Ref, computed, inject, ref } from 'vue'
|
||||
|
||||
import { NodePreviewImagesKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
export const useNodePreviewState = (
|
||||
nodeId: string,
|
||||
options?: {
|
||||
isMinimalLOD?: Ref<boolean>
|
||||
isCollapsed?: Ref<boolean>
|
||||
}
|
||||
) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodePreviewImages = inject(
|
||||
NodePreviewImagesKey,
|
||||
ref<Record<string, string[]>>({})
|
||||
)
|
||||
|
||||
const locatorId = computed(() => workflowStore.nodeIdToNodeLocatorId(nodeId))
|
||||
|
||||
const previewUrls = computed(() => {
|
||||
const key = locatorId.value
|
||||
if (!key) return undefined
|
||||
const urls = nodePreviewImages.value[key]
|
||||
return urls && urls.length ? urls : undefined
|
||||
})
|
||||
|
||||
const hasPreview = computed(() => !!previewUrls.value?.length)
|
||||
|
||||
const latestPreviewUrl = computed(() => {
|
||||
const urls = previewUrls.value
|
||||
return urls && urls.length ? urls[urls.length - 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
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { computed, provide } from 'vue'
|
||||
|
||||
import { NodePreviewImagesKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
export const usePreviewStateProvider = () => {
|
||||
// Provide reactive access to app.nodePreviewImages
|
||||
// No need to duplicate data - app.nodePreviewImages is already reactive to changes
|
||||
const nodePreviewImages = computed(() => app.nodePreviewImages)
|
||||
|
||||
provide(NodePreviewImagesKey, nodePreviewImages)
|
||||
|
||||
return { nodePreviewImages }
|
||||
}
|
||||
@@ -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,28 @@ interface SetOutputOptions {
|
||||
export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
const { nodeIdToNodeLocatorId } = useWorkflowStore()
|
||||
const { executionIdToNodeLocatorId } = useExecutionStore()
|
||||
const scheduledRevoke: Record<NodeLocatorId, { stop: () => void }> = {}
|
||||
|
||||
function scheduleRevoke(locator: NodeLocatorId, cb: () => void) {
|
||||
if (scheduledRevoke[locator]) {
|
||||
scheduledRevoke[locator].stop()
|
||||
}
|
||||
|
||||
const { stop } = useTimeoutFn(() => {
|
||||
delete scheduledRevoke[locator]
|
||||
cb()
|
||||
}, PREVIEW_REVOKE_DELAY_MS)
|
||||
|
||||
scheduledRevoke[locator] = { stop }
|
||||
}
|
||||
|
||||
const nodeOutputs = ref<Record<string, ExecutedWsMessage['output']>>({})
|
||||
|
||||
// Reactive state for node preview images - mirrors app.nodePreviewImages
|
||||
const nodePreviewImages = ref<Record<string, string[]>>(
|
||||
app.nodePreviewImages || {}
|
||||
)
|
||||
|
||||
function getNodeOutputs(
|
||||
node: LGraphNode
|
||||
): ExecutedWsMessage['output'] | undefined {
|
||||
@@ -196,8 +218,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 +238,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 +255,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 +275,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
}
|
||||
|
||||
delete app.nodePreviewImages[nodeLocatorId]
|
||||
delete nodePreviewImages.value[nodeLocatorId]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,6 +292,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
}
|
||||
}
|
||||
app.nodePreviewImages = {}
|
||||
nodePreviewImages.value = {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,6 +327,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 +353,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
removeNodeOutputs,
|
||||
|
||||
// State
|
||||
nodeOutputs
|
||||
nodeOutputs,
|
||||
nodePreviewImages
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user