Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 37 additions & 39 deletions src/renderer/extensions/vueNodes/components/ImagePreview.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
<template>
<div
v-if="imageUrls.length > 0"
class="image-preview outline-none group relative flex size-full min-h-16 min-w-16 flex-col px-2 justify-center"
tabindex="0"
role="region"
:aria-label="$t('g.imagePreview')"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@keydown="handleKeyDown"
class="image-preview group relative flex size-full min-h-16 min-w-16 flex-col px-2 justify-center"
>
<!-- Image Wrapper -->
<div
class="h-full w-full overflow-hidden rounded-[5px] bg-node-component-surface relative"
tabindex="0"
role="img"
:aria-label="$t('g.imagePreview')"
:aria-busy="isLoading"
class="h-full w-full overflow-hidden rounded-[5px] bg-muted-background relative"
>
<!-- Error State -->
<div
v-if="imageError"
role="alert"
class="flex size-full flex-col items-center justify-center bg-muted-background text-center text-base-foreground py-8"
>
<i
Expand Down Expand Up @@ -48,11 +47,12 @@
)
"
@load="handleImageLoad"
@error="handleImageError"
/>

<!-- Floating Action Buttons (appear on hover) -->
<div v-if="isHovered" class="actions absolute top-2 right-2 flex gap-2.5">
<!-- Floating Action Buttons (appear on hover or focus) -->
<div
class="actions absolute top-2 right-2 flex gap-2.5 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity"
>
<!-- Mask/Edit Button -->
<button
v-if="!hasMultipleImages"
Expand Down Expand Up @@ -104,6 +104,7 @@
v-for="(_, index) in imageUrls"
:key="index"
:class="getNavigationDotClass(index)"
:aria-current="index === currentIndex ? 'true' : undefined"
:aria-label="
$t('g.viewImageOfTotal', {
index: index + 1,
Expand All @@ -117,9 +118,11 @@
</template>

<script setup lang="ts">
import { useImage } from '@vueuse/core'
import { useToast } from 'primevue'
import Skeleton from 'primevue/skeleton'
import { computed, ref, watch } from 'vue'
import type { ShallowRef } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

import { downloadFile } from '@/base/common/downloadUtil'
Expand All @@ -146,18 +149,34 @@ const actionButtonClass =

// Component state
const currentIndex = ref(0)
const isHovered = ref(false)
const actualDimensions = ref<string | null>(null)
const imageError = ref(false)
const isLoading = ref(false)

const currentImageEl = ref<HTMLImageElement>()
const loadedUrls = new Set<string>()

// Computed values
const currentImageUrl = computed(() => props.imageUrls[currentIndex.value])
const hasMultipleImages = computed(() => props.imageUrls.length > 1)
const imageAltText = computed(() => `Node output ${currentIndex.value + 1}`)

const { isLoading: imageIsLoading, error: imageError } = useImage(
computed(() => ({ src: currentImageUrl.value }))
)

// Only show loading if image is loading AND not already loaded in this batch
const isLoading = computed(
() => imageIsLoading.value && !loadedUrls.has(currentImageUrl.value)
)

// Listen for keydown events from parent node
const keyEvent = inject<ShallowRef<KeyboardEvent | null>>('keyEvent')

if (keyEvent) {
watch(keyEvent, (e) => {
if (!e) return
handleKeyDown(e)
})
}

// Watch for URL changes and reset state
watch(
() => props.imageUrls,
Expand All @@ -166,11 +185,8 @@ watch(
if (currentIndex.value >= newUrls.length) {
currentIndex.value = 0
}

// Reset loading and error states when URLs change
actualDimensions.value = null
imageError.value = false
isLoading.value = newUrls.length > 0
loadedUrls.clear()
},
{ deep: true }
)
Expand All @@ -179,19 +195,12 @@ watch(
const handleImageLoad = (event: Event) => {
if (!event.target || !(event.target instanceof HTMLImageElement)) return
const img = event.target
isLoading.value = false
imageError.value = false
loadedUrls.add(currentImageUrl.value)
if (img.naturalWidth && img.naturalHeight) {
actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`
}
}

const handleImageError = () => {
isLoading.value = false
imageError.value = true
actualDimensions.value = null
}

// In vueNodes mode, we need to set them manually before opening the mask editor.
const setupNodeForMaskEditor = () => {
if (!props.nodeId || !currentImageEl.value) return
Expand Down Expand Up @@ -230,20 +239,9 @@ const setCurrentIndex = (index: number) => {
if (currentIndex.value === index) return
if (index >= 0 && index < props.imageUrls.length) {
currentIndex.value = index
actualDimensions.value = null
isLoading.value = true
imageError.value = false
}
}

const handleMouseEnter = () => {
isHovered.value = true
}

const handleMouseLeave = () => {
isHovered.value = false
}

const getNavigationDotClass = (index: number) => {
return [
'w-2 h-2 rounded-full transition-all duration-200 border-0 cursor-pointer p-0',
Expand Down
25 changes: 23 additions & 2 deletions src/renderer/extensions/vueNodes/components/LGraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<div
v-else
ref="nodeContainerRef"
tabindex="0"
:data-node-id="nodeData.id"
:class="
cn(
Expand All @@ -16,7 +17,7 @@
// hover (only when node should handle events)
shouldHandleNodePointerEvents &&
'hover:ring-7 ring-node-component-ring',
'outline-transparent outline-2',
'outline-transparent outline-2 focus-visible:outline-node-component-outline',
borderClass,
outlineClass,
cursorClass,
Expand Down Expand Up @@ -48,6 +49,7 @@
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop.stop.prevent="handleDrop"
@keydown="handleNodeKeydown"
>
<div class="flex flex-col justify-center items-center relative">
<template v-if="isCollapsed">
Expand Down Expand Up @@ -130,7 +132,16 @@

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, nextTick, onErrorCaptured, onMounted, ref, watch } from 'vue'
import {
computed,
nextTick,
onErrorCaptured,
onMounted,
provide,
ref,
shallowRef,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'

import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
Expand Down Expand Up @@ -186,6 +197,14 @@ const { nodeData, error = null } = defineProps<LGraphNodeProps>()

const { t } = useI18n()

// Provide keydown events to child components (ImagePreview, VideoPreview, etc.)
const keyEvent = shallowRef<KeyboardEvent | null>(null)
provide('keyEvent', keyEvent)

const handleNodeKeydown = (event: KeyboardEvent) => {
keyEvent.value = event
}

const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
useNodeEventHandlers()
const { bringNodeToFront } = useNodeZIndex()
Expand Down Expand Up @@ -265,6 +284,8 @@ const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
const { startDrag } = useNodeDrag()

async function nodeOnPointerdown(event: PointerEvent) {
nodeContainerRef.value?.focus()

if (event.altKey && lgraphNode.value) {
const result = LGraphCanvas.cloneNodes([lgraphNode.value])
if (result?.created?.length) {
Expand Down
Loading