fix: dropdown widget fetching output files

This commit is contained in:
Rizumu Ayaka
2025-11-18 19:14:13 +08:00
parent 7a0302ba7a
commit d2d5505c76
3 changed files with 112 additions and 37 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { capitalize } from 'es-toolkit'
import { computed, provide, ref, toRef, watch } from 'vue'
import { computed, onMounted, provide, ref, toRef, watch } from 'vue'
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
@@ -19,7 +19,7 @@ import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/compo
import type { ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAssetsStore } from '@/stores/assetsStore'
import { useQueueStore } from '@/stores/queueStore'
import { useOutputsStore } from '@/stores/outputsStore'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { AssetKind } from '@/types/widgetTypes'
import {
@@ -55,7 +55,8 @@ const { localValue, onChange } = useWidgetValue({
})
const toastStore = useToastStore()
const queueStore = useQueueStore()
const outputsStore = useOutputsStore()
const transformCompatProps = useTransformCompatOverlayProps()
@@ -121,33 +122,30 @@ const inputItems = computed<DropdownItem[]>(() => {
const outputItems = computed<DropdownItem[]>(() => {
if (!['image', 'video'].includes(props.assetKind ?? '')) return []
const outputs = new Set<string>()
const outputFiles = ((): string[] => {
switch (props.assetKind) {
case 'image':
return outputsStore.outputImages
case 'video':
return outputsStore.outputVideos
case 'audio':
return outputsStore.outputAudios
default:
return []
}
})()
// Extract output images/videos from queue history
queueStore.historyTasks.forEach((task) => {
task.flatOutputs.forEach((output) => {
const isTargetType =
(props.assetKind === 'image' && output.mediaType === 'images') ||
(props.assetKind === 'video' && output.mediaType === 'video')
if (output.type === 'output' && isTargetType) {
const path = output.subfolder
? `${output.subfolder}/${output.filename}`
: output.filename
// Add [output] annotation so the preview component knows the type
const annotatedPath = `${path} [output]`
outputs.add(annotatedPath)
}
})
return outputFiles.map((filename, index) => {
// Add [output] annotation so the preview component knows the type
const annotatedPath = `${filename} [output]`
return {
id: `output-${index}`,
mediaSrc: getMediaUrl(filename, 'output'),
name: annotatedPath,
label: getDisplayLabel(annotatedPath),
metadata: ''
}
})
return Array.from(outputs).map((output, index) => ({
id: `output-${index}`,
mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
name: output,
label: getDisplayLabel(output),
metadata: ''
}))
})
const allItems = computed<DropdownItem[]>(() => {
@@ -337,6 +335,11 @@ function getMediaUrl(
if (!['image', 'video'].includes(props.assetKind ?? '')) return ''
return `/api/view?filename=${encodeURIComponent(filename)}&type=${type}`
}
// Fetch output files on component mount
onMounted(() => {
outputsStore.fetchOutputFiles()
})
</script>
<template>

View File

@@ -67,15 +67,15 @@ const searchQuery = defineModel<string>('searchQuery')
"
>
<div class="pointer-events-none absolute inset-x-3 top-0 z-10 h-5" />
<div
v-if="items.length === 0"
class="absolute inset-0 flex items-center justify-center"
>
<i
title="No items"
class="icon-[lucide--circle-off] size-30 text-zinc-500/20"
/>
</div>
<template v-if="items.length === 0">
<div class="absolute inset-0 flex items-center justify-center">
<i
title="No items"
class="icon-[lucide--circle-off] size-30 text-zinc-500/20"
/>
</div>
<div class="min-h-50" />
</template>
<!-- Item -->
<FormDropdownMenuItem
v-for="(item, index) in items"

View File

@@ -0,0 +1,72 @@
import { api } from '@/scripts/api'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useOutputsStore = defineStore('outputs', () => {
const outputs = ref<string[]>([])
const isLoadingOutputFiles = ref(false)
const thePromise = ref<Promise<void> | null>(null)
const outputImages = computed(() => {
return outputs.value.filter((filename) => {
const ext = filename.toLowerCase().split('.').pop() || ''
return ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'].includes(ext)
})
})
const outputVideos = computed(() => {
return outputs.value.filter((filename) => {
const ext = filename.toLowerCase().split('.').pop() || ''
return ['mp4', 'webm', 'mov', 'avi', 'mkv'].includes(ext)
})
})
const outputAudios = computed(() => {
return outputs.value.filter((filename) => {
const ext = filename.toLowerCase().split('.').pop() || ''
return ['mp3', 'ogg', 'wav', 'flac'].includes(ext)
})
})
/**
* Fetch output files from the backend API
*/
async function _fetchOutputFiles() {
isLoadingOutputFiles.value = true
try {
const response = await fetch(api.internalURL('/files/output'), {
headers: {
'Comfy-User': api.user
}
})
if (!response.ok) {
console.error('Failed to fetch output files:', response.statusText)
return
}
outputs.value = await response.json()
} catch (error) {
console.error('Error fetching output files:', error)
} finally {
isLoadingOutputFiles.value = false
}
}
const fetchOutputFiles = async () => {
if (!thePromise.value) {
thePromise.value = _fetchOutputFiles()
thePromise.value.finally(() => {
thePromise.value = null
})
}
return thePromise.value
}
return {
isLoadingOutputFiles,
outputs,
fetchOutputFiles,
outputImages,
outputVideos,
outputAudios
}
})