Compare commits

...

1 Commits

2 changed files with 60 additions and 41 deletions

View File

@@ -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
@@ -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"
@@ -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,
@@ -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'
@@ -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,
@@ -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 }
)
@@ -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
@@ -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',

View File

@@ -5,6 +5,7 @@
<div
v-else
ref="nodeContainerRef"
tabindex="0"
:data-node-id="nodeData.id"
:class="
cn(
@@ -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,
@@ -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">
@@ -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'
@@ -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()
@@ -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) {