mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-23 07:50:15 +00:00
Fix/vue nodes video (#5870)
## Summary Fix the video preview widget and associated dropdown to load and select videos. Fixes: - https://www.notion.so/comfy-org/Video-thumbnails-not-being-used-in-asset-explorer-dialog-27e6d73d365080ec8a3ee7c7ec413657?source=copy_link - https://www.notion.so/comfy-org/Image-Video-upload-dialog-doesnt-set-mime-type-27e6d73d365080c5bffdf08842855ba0?source=copy_link - https://www.notion.so/comfy-org/Video-Previews-are-not-displayed-2756d73d365080b2bfb9e0004e9d784d?source=copy_link - https://www.notion.so/comfy-org/Cannot-load-video-in-Load-Video-node-2756d73d365080009c21d3a67add96c4?source=copy_link ## Screenshots (if applicable) https://github.com/user-attachments/assets/b71dbecb-c9a7-4feb-83a3-c3e044a9c93c ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5870-Fix-vue-nodes-video-27e6d73d36508182b44bef8e90ef4018) by [Unito](https://www.unito.io) --------- Co-authored-by: JakeSchroeder <jake@axiom.co> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Jake Schroeder <jake.schroeder@isophex.com> Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
This commit is contained in:
@@ -83,6 +83,7 @@ const specDescriptor = computed<{
|
||||
const allowUpload =
|
||||
image_upload === true ||
|
||||
animated_image_upload === true ||
|
||||
video_upload === true ||
|
||||
audio_upload === true
|
||||
return {
|
||||
kind,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, provide, ref, watch } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import FormDropdown from './form/dropdown/FormDropdown.vue'
|
||||
import type {
|
||||
DropdownItem,
|
||||
FilterOption,
|
||||
SelectedKey
|
||||
import {
|
||||
AssetKindKey,
|
||||
type DropdownItem,
|
||||
type FilterOption,
|
||||
type SelectedKey
|
||||
} from './form/dropdown/types'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
@@ -31,6 +32,11 @@ const props = defineProps<{
|
||||
uploadFolder?: ResultItemType
|
||||
}>()
|
||||
|
||||
provide(
|
||||
AssetKindKey,
|
||||
computed(() => props.assetKind)
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | undefined]
|
||||
}>()
|
||||
@@ -69,7 +75,7 @@ const inputItems = computed<DropdownItem[]>(() => {
|
||||
|
||||
return values.map((value: string, index: number) => ({
|
||||
id: `input-${index}`,
|
||||
imageSrc: getMediaUrl(value, 'input'),
|
||||
mediaSrc: getMediaUrl(value, 'input'),
|
||||
name: value,
|
||||
metadata: ''
|
||||
}))
|
||||
@@ -99,7 +105,7 @@ const outputItems = computed<DropdownItem[]>(() => {
|
||||
|
||||
return Array.from(outputs).map((output, index) => ({
|
||||
id: `output-${index}`,
|
||||
imageSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
||||
mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
||||
name: output,
|
||||
metadata: ''
|
||||
}))
|
||||
@@ -145,6 +151,21 @@ const mediaPlaceholder = computed(() => {
|
||||
|
||||
const uploadable = computed(() => props.allowUpload === true)
|
||||
|
||||
const acceptTypes = computed(() => {
|
||||
// Be permissive with accept types because backend uses libraries
|
||||
// that can handle a wide range of formats
|
||||
switch (props.assetKind) {
|
||||
case 'image':
|
||||
return 'image/*'
|
||||
case 'video':
|
||||
return 'video/*'
|
||||
case 'audio':
|
||||
return 'audio/*'
|
||||
default:
|
||||
return undefined // model or unknown
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
localValue,
|
||||
(currentValue) => {
|
||||
@@ -269,6 +290,7 @@ function getMediaUrl(
|
||||
:placeholder="mediaPlaceholder"
|
||||
:multiple="false"
|
||||
:uploadable="uploadable"
|
||||
:accept="acceptTypes"
|
||||
:filter-options="filterOptions"
|
||||
v-bind="combinedProps"
|
||||
class="w-full"
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Props {
|
||||
|
||||
uploadable?: boolean
|
||||
disabled?: boolean
|
||||
accept?: string
|
||||
filterOptions?: FilterOption[]
|
||||
sortOptions?: SortOption[]
|
||||
isSelected?: (
|
||||
@@ -195,6 +196,7 @@ function handleSelection(item: DropdownItem, index: number) {
|
||||
:selected="selected"
|
||||
:uploadable="uploadable"
|
||||
:disabled="disabled"
|
||||
:accept="accept"
|
||||
@select-click="toggleDropdown"
|
||||
@file-change="handleFileChange"
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ interface Props {
|
||||
maxSelectable: number
|
||||
uploadable: boolean
|
||||
disabled: boolean
|
||||
accept?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -92,6 +93,7 @@ const theButtonStyle = computed(() => [
|
||||
class="opacity-0 absolute inset-0 -z-1"
|
||||
:multiple="maxSelectable > 1"
|
||||
:disabled="disabled"
|
||||
:accept="accept"
|
||||
@change="emit('file-change', $event)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -84,7 +84,7 @@ const searchQuery = defineModel<string>('searchQuery')
|
||||
:key="item.id"
|
||||
:index="index"
|
||||
:selected="isSelected(item, index)"
|
||||
:image-src="item.imageSrc"
|
||||
:media-src="item.mediaSrc"
|
||||
:name="item.name"
|
||||
:metadata="item.metadata"
|
||||
:layout="layoutMode"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import type { LayoutMode } from './types'
|
||||
import { AssetKindKey, type LayoutMode } from './types'
|
||||
|
||||
interface Props {
|
||||
index: number
|
||||
selected: boolean
|
||||
imageSrc: string
|
||||
mediaSrc: string
|
||||
name: string
|
||||
metadata?: string
|
||||
layout?: LayoutMode
|
||||
@@ -18,23 +18,36 @@ const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [index: number]
|
||||
imageLoad: [event: Event]
|
||||
mediaLoad: [event: Event]
|
||||
}>()
|
||||
|
||||
const actualDimensions = ref<string | null>(null)
|
||||
|
||||
const assetKind = inject(AssetKindKey)
|
||||
|
||||
const isVideo = computed(() => assetKind?.value === 'video')
|
||||
|
||||
function handleClick() {
|
||||
emit('click', props.index)
|
||||
}
|
||||
|
||||
function handleImageLoad(event: Event) {
|
||||
emit('imageLoad', event)
|
||||
emit('mediaLoad', event)
|
||||
if (!event.target || !(event.target instanceof HTMLImageElement)) return
|
||||
const img = event.target
|
||||
if (img.naturalWidth && img.naturalHeight) {
|
||||
actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`
|
||||
}
|
||||
}
|
||||
|
||||
function handleVideoLoad(event: Event) {
|
||||
emit('mediaLoad', event)
|
||||
if (!event.target || !(event.target instanceof HTMLVideoElement)) return
|
||||
const video = event.target
|
||||
if (video.videoWidth && video.videoHeight) {
|
||||
actualDimensions.value = `${video.videoWidth} x ${video.videoHeight}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,9 +94,17 @@ function handleImageLoad(event: Event) {
|
||||
>
|
||||
<i-lucide:check class="size-3 text-white -translate-y-[0.5px]" />
|
||||
</div>
|
||||
<video
|
||||
v-if="mediaSrc && isVideo"
|
||||
:src="mediaSrc"
|
||||
class="size-full object-cover"
|
||||
preload="metadata"
|
||||
muted
|
||||
@loadeddata="handleVideoLoad"
|
||||
/>
|
||||
<img
|
||||
v-if="imageSrc"
|
||||
:src="imageSrc"
|
||||
v-else-if="mediaSrc"
|
||||
:src="mediaSrc"
|
||||
class="size-full object-cover"
|
||||
@load="handleImageLoad"
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { ComputedRef, InjectionKey } from 'vue'
|
||||
|
||||
import type { AssetKind } from '@/types/widgetTypes'
|
||||
|
||||
export type OptionId = string | number | symbol
|
||||
export type SelectedKey = OptionId
|
||||
|
||||
export interface DropdownItem {
|
||||
id: SelectedKey
|
||||
imageSrc: string
|
||||
mediaSrc: string // URL for image, video, or other media
|
||||
name: string
|
||||
metadata: string
|
||||
}
|
||||
@@ -19,3 +23,6 @@ export interface FilterOption {
|
||||
}
|
||||
|
||||
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
||||
|
||||
export const AssetKindKey: InjectionKey<ComputedRef<AssetKind | undefined>> =
|
||||
Symbol('assetKind')
|
||||
|
||||
Reference in New Issue
Block a user