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:
bymyself
2025-09-14 22:31:21 -07:00
parent 96c73e2281
commit f2ba2f168a
6 changed files with 130 additions and 19 deletions

View File

@@ -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')

View File

@@ -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')

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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 }
}

View File

@@ -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
}
})