mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
Fix doubled player on VHS LoadAudio in vue (#8206)
In vue mode, the VHS Load Audio (Upload) node had 2 audio previews. This occurred because the native AudioPreview widget was being applied to any combo widget with the name `audio`. This native preview does not support the advanced preview functions VHS provides like seeking to specific start time, trimming to a target duration, or converting from formats the browser may not support. This is fixed through a fairly involved cleanup to instead display the litegraph AudioUI widget as an AudioPreview widget when in vue mode. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8206-Fix-doubled-player-on-VHS-LoadAudio-in-vue-2ef6d73d365081ce8907dca2706214a1) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
@@ -43,7 +43,7 @@ const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const isEditing = ref(false)
|
||||
|
||||
const widgetComponent = computed(() => {
|
||||
const component = getComponent(widget.type, widget.name)
|
||||
const component = getComponent(widget.type)
|
||||
return component || WidgetLegacy
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MediaRecorder as ExtendableMediaRecorder } from 'extendable-media-recorder'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
@@ -25,6 +24,17 @@ import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
import { api } from '../../scripts/api'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
function updateUIWidget(
|
||||
audioUIWidget: DOMWidget<HTMLAudioElement, string>,
|
||||
url: string = ''
|
||||
) {
|
||||
audioUIWidget.element.src = url
|
||||
audioUIWidget.value = url
|
||||
audioUIWidget.callback?.(url)
|
||||
if (url) audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
else audioUIWidget.element.classList.add('empty-audio-widget')
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
audioWidget: IStringWidget,
|
||||
audioUIWidget: DOMWidget<HTMLAudioElement, string>,
|
||||
@@ -55,10 +65,10 @@ async function uploadFile(
|
||||
}
|
||||
|
||||
if (updateNode) {
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(...splitFilePath(path))
|
||||
updateUIWidget(
|
||||
audioUIWidget,
|
||||
api.apiURL(getResourceURL(...splitFilePath(path)))
|
||||
)
|
||||
audioWidget.value = path
|
||||
|
||||
// Manually trigger the callback to update VueNodes
|
||||
audioWidget.callback?.(path)
|
||||
@@ -118,26 +128,18 @@ app.registerExtension({
|
||||
const audios = output.audio
|
||||
if (!audios?.length) return
|
||||
const audio = audios[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(
|
||||
audio.subfolder ?? '',
|
||||
audio.filename ?? '',
|
||||
audio.type
|
||||
)
|
||||
const resourceUrl = getResourceURL(
|
||||
audio.subfolder ?? '',
|
||||
audio.filename ?? '',
|
||||
audio.type
|
||||
)
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
updateUIWidget(audioUIWidget, api.apiURL(resourceUrl))
|
||||
}
|
||||
}
|
||||
|
||||
audioUIWidget.onRemove = useChainCallback(
|
||||
audioUIWidget.onRemove,
|
||||
() => {
|
||||
if (!audioUIWidget.element) return
|
||||
audioUIWidget.element.pause()
|
||||
audioUIWidget.element.src = ''
|
||||
audioUIWidget.element.remove()
|
||||
}
|
||||
)
|
||||
let value = ''
|
||||
audioUIWidget.options.getValue = () => value
|
||||
audioUIWidget.options.setValue = (v) => (value = v)
|
||||
|
||||
return { widget: audioUIWidget }
|
||||
}
|
||||
@@ -156,10 +158,12 @@ app.registerExtension({
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
const audio = output.audio[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder ?? '', audio.filename ?? '', audio.type)
|
||||
const resourceUrl = getResourceURL(
|
||||
audio.subfolder ?? '',
|
||||
audio.filename ?? '',
|
||||
audio.type
|
||||
)
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
updateUIWidget(audioUIWidget, api.apiURL(resourceUrl))
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -183,18 +187,18 @@ app.registerExtension({
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
audioUIWidget.options.canvasOnly = true
|
||||
|
||||
const onAudioWidgetUpdate = () => {
|
||||
if (typeof audioWidget.value !== 'string') return
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(...splitFilePath(audioWidget.value))
|
||||
updateUIWidget(
|
||||
audioUIWidget,
|
||||
api.apiURL(
|
||||
getResourceURL(...splitFilePath(audioWidget.value ?? ''))
|
||||
)
|
||||
)
|
||||
}
|
||||
// Initially load default audio file to audioUIWidget.
|
||||
if (audioWidget.value) {
|
||||
onAudioWidgetUpdate()
|
||||
}
|
||||
onAudioWidgetUpdate()
|
||||
|
||||
audioWidget.callback = onAudioWidgetUpdate
|
||||
|
||||
// Load saved audio file widget values if restoring from workflow
|
||||
@@ -202,9 +206,7 @@ app.registerExtension({
|
||||
node.onGraphConfigured = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onGraphConfigured?.apply(this, arguments)
|
||||
if (audioWidget.value) {
|
||||
onAudioWidgetUpdate()
|
||||
}
|
||||
onAudioWidgetUpdate()
|
||||
}
|
||||
|
||||
const handleUpload = async (files: File[]) => {
|
||||
@@ -328,7 +330,7 @@ app.registerExtension({
|
||||
URL.revokeObjectURL(audioUIWidget.element.src)
|
||||
}
|
||||
|
||||
audioUIWidget.element.src = URL.createObjectURL(audioBlob)
|
||||
updateUIWidget(audioUIWidget, URL.createObjectURL(audioBlob))
|
||||
|
||||
isRecording = false
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
if (!shouldRenderAsVue(widget)) continue
|
||||
|
||||
const vueComponent =
|
||||
getComponent(widget.type, widget.name) ||
|
||||
getComponent(widget.type) ||
|
||||
(widget.isDOMWidget ? WidgetDOM : WidgetLegacy)
|
||||
|
||||
const { slotMetadata, options } = widget
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full col-span-2 widget-expands grid grid-cols-[minmax(80px,max-content)_minmax(125px,auto)] gap-y-3 p-3"
|
||||
>
|
||||
<WidgetSelect v-model="modelValue" :widget class="col-span-2" />
|
||||
<AudioPreviewPlayer
|
||||
class="col-span-2"
|
||||
:audio-url="audioUrlFromWidget"
|
||||
:readonly="readonly"
|
||||
:hide-when-empty="isOutputNodeRef"
|
||||
:show-options-button="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { isOutputNode } from '@/utils/nodeFilterUtil'
|
||||
|
||||
import { getAudioUrlFromPath } from '../utils/audioUtils'
|
||||
import WidgetSelect from './WidgetSelect.vue'
|
||||
import AudioPreviewPlayer from './audio/AudioPreviewPlayer.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string | undefined>
|
||||
readonly?: boolean
|
||||
nodeId: string
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>('modelValue')
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// Get litegraph node
|
||||
const litegraphNode = computed(() => {
|
||||
if (!props.nodeId || !app.canvas.graph) return null
|
||||
return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null
|
||||
})
|
||||
|
||||
// Check if this is an output node (PreviewAudio, SaveAudio, etc)
|
||||
const isOutputNodeRef = computed(() => {
|
||||
const node = litegraphNode.value
|
||||
if (!node) return false
|
||||
return isOutputNode(node)
|
||||
})
|
||||
|
||||
const audioFilePath = computed(() => props.widget.value as string)
|
||||
|
||||
// Computed audio URL from widget value (for input files)
|
||||
const audioUrlFromWidget = computed(() => {
|
||||
const path = audioFilePath.value
|
||||
if (!path) return ''
|
||||
return getAudioUrlFromPath(path, 'input')
|
||||
})
|
||||
</script>
|
||||
@@ -1,17 +1,13 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div
|
||||
v-if="!hidden"
|
||||
:class="
|
||||
cn(
|
||||
'bg-component-node-widget-background box-border flex gap-4 items-center justify-start relative rounded-lg w-full h-16 px-4 py-0',
|
||||
{ hidden: hideWhenEmpty && !hasAudio }
|
||||
)
|
||||
"
|
||||
v-if="!hideWhenEmpty || modelValue"
|
||||
class="bg-component-node-widget-background box-border flex gap-4 items-center justify-start relative rounded-lg w-full h-16 px-4 py-0"
|
||||
>
|
||||
<!-- Hidden audio element -->
|
||||
<audio
|
||||
ref="audioRef"
|
||||
:src="modelValue"
|
||||
@loadedmetadata="handleLoadedMetadata"
|
||||
@timeupdate="handleTimeUpdate"
|
||||
@ended="handleEnded"
|
||||
@@ -137,18 +133,13 @@
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { whenever } from '@vueuse/core'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { getLocatorIdFromNodeData } from '@/utils/graphTraversalUtil'
|
||||
import { isOutputNode } from '@/utils/nodeFilterUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { formatTime, getResourceURL } from '../../utils/audioUtils'
|
||||
import { formatTime } from '../../utils/audioUtils'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -156,8 +147,6 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
hideWhenEmpty?: boolean
|
||||
showOptionsButton?: boolean
|
||||
nodeId?: string
|
||||
audioUrl?: string
|
||||
}>(),
|
||||
{
|
||||
hideWhenEmpty: true
|
||||
@@ -165,14 +154,13 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
// Refs
|
||||
const audioRef = ref<HTMLAudioElement>()
|
||||
const audioRef = useTemplateRef('audioRef')
|
||||
const optionsMenu = ref()
|
||||
const isPlaying = ref(false)
|
||||
const isMuted = ref(false)
|
||||
const volume = ref(1)
|
||||
const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
const hasAudio = ref(false)
|
||||
const playbackRate = ref(1)
|
||||
|
||||
// Computed
|
||||
@@ -180,61 +168,11 @@ const progressPercentage = computed(() => {
|
||||
if (!duration.value || duration.value === 0) return 0
|
||||
return (currentTime.value / duration.value) * 100
|
||||
})
|
||||
const modelValue = defineModel<string>()
|
||||
|
||||
const showVolumeTwo = computed(() => !isMuted.value && volume.value > 0.5)
|
||||
const showVolumeOne = computed(() => isMuted.value && volume.value > 0)
|
||||
|
||||
const litegraphNode = computed(() => {
|
||||
if (!props.nodeId || !app.canvas.graph) return null
|
||||
return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null
|
||||
})
|
||||
|
||||
const hidden = computed(() => {
|
||||
if (!litegraphNode.value) return false
|
||||
// dont show if its a LoadAudio and we have nodeId
|
||||
const isLoadAudio =
|
||||
litegraphNode.value.constructor?.comfyClass === 'LoadAudio'
|
||||
return isLoadAudio && !!props.nodeId
|
||||
})
|
||||
|
||||
// Check if this is an output node
|
||||
const isOutputNodeRef = computed(() => {
|
||||
const node = litegraphNode.value
|
||||
return !!node && isOutputNode(node)
|
||||
})
|
||||
|
||||
const nodeLocatorId = computed(() => {
|
||||
const node = litegraphNode.value
|
||||
if (!node) return null
|
||||
return getLocatorIdFromNodeData(node)
|
||||
})
|
||||
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
// Computed audio URL from node output (for output nodes)
|
||||
const audioUrlFromOutput = computed(() => {
|
||||
if (!isOutputNodeRef.value || !nodeLocatorId.value) return ''
|
||||
|
||||
const nodeOutput = nodeOutputStore.nodeOutputs[nodeLocatorId.value]
|
||||
if (!nodeOutput?.audio || nodeOutput.audio.length === 0) return ''
|
||||
|
||||
const audio = nodeOutput.audio[0]
|
||||
if (!audio.filename) return ''
|
||||
|
||||
return api.apiURL(
|
||||
getResourceURL(
|
||||
audio.subfolder || '',
|
||||
audio.filename,
|
||||
audio.type || 'output'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// Combined audio URL (output takes precedence for output nodes)
|
||||
const finalAudioUrl = computed(() => {
|
||||
return audioUrlFromOutput.value || props.audioUrl || ''
|
||||
})
|
||||
|
||||
// Playback controls
|
||||
const togglePlayPause = () => {
|
||||
if (!audioRef.value || !audioRef.value.src) {
|
||||
@@ -335,36 +273,15 @@ const menuItems = computed(() => [
|
||||
}
|
||||
])
|
||||
|
||||
// Load audio from URL
|
||||
const loadAudioFromUrl = (url: string) => {
|
||||
if (!audioRef.value) return
|
||||
isPlaying.value = false
|
||||
audioRef.value.pause()
|
||||
audioRef.value.src = url
|
||||
void audioRef.value.load()
|
||||
hasAudio.value = !!url
|
||||
}
|
||||
|
||||
// Watch for finalAudioUrl changes
|
||||
watch(
|
||||
finalAudioUrl,
|
||||
(newUrl) => {
|
||||
if (newUrl) {
|
||||
void nextTick(() => {
|
||||
loadAudioFromUrl(newUrl)
|
||||
})
|
||||
}
|
||||
whenever(
|
||||
modelValue,
|
||||
() => {
|
||||
isPlaying.value = false
|
||||
audioRef.value?.pause()
|
||||
void audioRef.value?.load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Cleanup
|
||||
onUnmounted(() => {
|
||||
if (audioRef.value) {
|
||||
audioRef.value.pause()
|
||||
audioRef.value.src = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
|
||||
|
||||
const {
|
||||
WidgetAudioUI,
|
||||
WidgetButton,
|
||||
WidgetColorPicker,
|
||||
WidgetInputNumber,
|
||||
@@ -44,75 +43,75 @@ describe('widgetRegistry', () => {
|
||||
// Test number type mappings
|
||||
describe('number types', () => {
|
||||
it('should map int types to slider widget', () => {
|
||||
expect(getComponent('int', 'bar')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('INT', 'bar')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('int')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('INT')).toBe(WidgetInputNumber)
|
||||
})
|
||||
|
||||
it('should map float types to slider widget', () => {
|
||||
expect(getComponent('float', 'cfg')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('FLOAT', 'cfg')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('number', 'cfg')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('slider', 'cfg')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('float')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('FLOAT')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('number')).toBe(WidgetInputNumber)
|
||||
expect(getComponent('slider')).toBe(WidgetInputNumber)
|
||||
})
|
||||
})
|
||||
|
||||
// Test text type mappings
|
||||
describe('text types', () => {
|
||||
it('should map text variations to input text widget', () => {
|
||||
expect(getComponent('text', 'text')).toBe(WidgetInputText)
|
||||
expect(getComponent('string', 'text')).toBe(WidgetInputText)
|
||||
expect(getComponent('STRING', 'text')).toBe(WidgetInputText)
|
||||
expect(getComponent('text')).toBe(WidgetInputText)
|
||||
expect(getComponent('string')).toBe(WidgetInputText)
|
||||
expect(getComponent('STRING')).toBe(WidgetInputText)
|
||||
})
|
||||
|
||||
it('should map multiline text types to textarea widget', () => {
|
||||
expect(getComponent('multiline', 'text')).toBe(WidgetTextarea)
|
||||
expect(getComponent('textarea', 'text')).toBe(WidgetTextarea)
|
||||
expect(getComponent('TEXTAREA', 'text')).toBe(WidgetTextarea)
|
||||
expect(getComponent('customtext', 'text')).toBe(WidgetTextarea)
|
||||
expect(getComponent('multiline')).toBe(WidgetTextarea)
|
||||
expect(getComponent('textarea')).toBe(WidgetTextarea)
|
||||
expect(getComponent('TEXTAREA')).toBe(WidgetTextarea)
|
||||
expect(getComponent('customtext')).toBe(WidgetTextarea)
|
||||
})
|
||||
|
||||
it('should map markdown to markdown widget', () => {
|
||||
expect(getComponent('MARKDOWN', 'text')).toBe(WidgetMarkdown)
|
||||
expect(getComponent('markdown', 'text')).toBe(WidgetMarkdown)
|
||||
expect(getComponent('MARKDOWN')).toBe(WidgetMarkdown)
|
||||
expect(getComponent('markdown')).toBe(WidgetMarkdown)
|
||||
})
|
||||
})
|
||||
|
||||
// Test selection type mappings
|
||||
describe('selection types', () => {
|
||||
it('should map combo types to select widget', () => {
|
||||
expect(getComponent('combo', 'image')).toBe(WidgetSelect)
|
||||
expect(getComponent('COMBO', 'video')).toBe(WidgetSelect)
|
||||
expect(getComponent('combo')).toBe(WidgetSelect)
|
||||
expect(getComponent('COMBO')).toBe(WidgetSelect)
|
||||
})
|
||||
})
|
||||
|
||||
// Test boolean type mappings
|
||||
describe('boolean types', () => {
|
||||
it('should map boolean types to toggle switch widget', () => {
|
||||
expect(getComponent('toggle', 'image')).toBe(WidgetToggleSwitch)
|
||||
expect(getComponent('boolean', 'image')).toBe(WidgetToggleSwitch)
|
||||
expect(getComponent('BOOLEAN', 'image')).toBe(WidgetToggleSwitch)
|
||||
expect(getComponent('toggle')).toBe(WidgetToggleSwitch)
|
||||
expect(getComponent('boolean')).toBe(WidgetToggleSwitch)
|
||||
expect(getComponent('BOOLEAN')).toBe(WidgetToggleSwitch)
|
||||
})
|
||||
})
|
||||
|
||||
// Test advanced widget mappings
|
||||
describe('advanced widgets', () => {
|
||||
it('should map color types to color picker widget', () => {
|
||||
expect(getComponent('color', 'color')).toBe(WidgetColorPicker)
|
||||
expect(getComponent('COLOR', 'color')).toBe(WidgetColorPicker)
|
||||
expect(getComponent('color')).toBe(WidgetColorPicker)
|
||||
expect(getComponent('COLOR')).toBe(WidgetColorPicker)
|
||||
})
|
||||
|
||||
it('should map button types to button widget', () => {
|
||||
expect(getComponent('button', '')).toBe(WidgetButton)
|
||||
expect(getComponent('BUTTON', '')).toBe(WidgetButton)
|
||||
expect(getComponent('button')).toBe(WidgetButton)
|
||||
expect(getComponent('BUTTON')).toBe(WidgetButton)
|
||||
})
|
||||
})
|
||||
|
||||
// Test fallback behavior
|
||||
describe('fallback behavior', () => {
|
||||
it('should return null for unknown types', () => {
|
||||
expect(getComponent('unknown', 'unknown')).toBe(null)
|
||||
expect(getComponent('custom_widget', 'custom_widget')).toBe(null)
|
||||
expect(getComponent('', '')).toBe(null)
|
||||
expect(getComponent('unknown')).toBe(null)
|
||||
expect(getComponent('custom_widget')).toBe(null)
|
||||
expect(getComponent('')).toBe(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -176,16 +175,10 @@ describe('widgetRegistry', () => {
|
||||
|
||||
it('should handle case sensitivity correctly through aliases', () => {
|
||||
// Test that both lowercase and uppercase work
|
||||
expect(getComponent('string', '')).toBe(WidgetInputText)
|
||||
expect(getComponent('STRING', '')).toBe(WidgetInputText)
|
||||
expect(getComponent('combo', '')).toBe(WidgetSelect)
|
||||
expect(getComponent('COMBO', '')).toBe(WidgetSelect)
|
||||
})
|
||||
|
||||
it('should handle combo additional widgets', () => {
|
||||
// Test that both lowercase and uppercase work
|
||||
expect(getComponent('combo', 'audio')).toBe(WidgetAudioUI)
|
||||
expect(getComponent('combo', 'image')).toBe(WidgetSelect)
|
||||
expect(getComponent('string')).toBe(WidgetInputText)
|
||||
expect(getComponent('STRING')).toBe(WidgetInputText)
|
||||
expect(getComponent('combo')).toBe(WidgetSelect)
|
||||
expect(getComponent('COMBO')).toBe(WidgetSelect)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,9 +48,6 @@ const WidgetRecordAudio = defineAsyncComponent(
|
||||
const AudioPreviewPlayer = defineAsyncComponent(
|
||||
() => import('../components/audio/AudioPreviewPlayer.vue')
|
||||
)
|
||||
const WidgetAudioUI = defineAsyncComponent(
|
||||
() => import('../components/WidgetAudioUI.vue')
|
||||
)
|
||||
const Load3D = defineAsyncComponent(
|
||||
() => import('@/components/load3d/Load3D.vue')
|
||||
)
|
||||
@@ -62,7 +59,6 @@ const WidgetBoundingBox = defineAsyncComponent(
|
||||
)
|
||||
|
||||
export const FOR_TESTING = {
|
||||
WidgetAudioUI,
|
||||
WidgetButton,
|
||||
WidgetColorPicker,
|
||||
WidgetInputNumber,
|
||||
@@ -182,10 +178,6 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
|
||||
]
|
||||
]
|
||||
|
||||
const getComboWidgetAdditions = (): Map<string, Component> => {
|
||||
return new Map([['audio', WidgetAudioUI]])
|
||||
}
|
||||
|
||||
// Build lookup maps
|
||||
const widgets = new Map<string, WidgetDefinition>()
|
||||
const aliasMap = new Map<string, string>()
|
||||
@@ -200,13 +192,7 @@ for (const [type, def] of coreWidgetDefinitions) {
|
||||
// Utility functions
|
||||
const getCanonicalType = (type: string): string => aliasMap.get(type) || type
|
||||
|
||||
export const getComponent = (type: string, name: string): Component | null => {
|
||||
if (type == 'combo') {
|
||||
const comboAdditions = getComboWidgetAdditions()
|
||||
if (comboAdditions.has(name)) {
|
||||
return comboAdditions.get(name) || null
|
||||
}
|
||||
}
|
||||
export const getComponent = (type: string): Component | null => {
|
||||
const canonicalType = getCanonicalType(type)
|
||||
return widgets.get(canonicalType)?.component || null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
/**
|
||||
@@ -13,17 +12,6 @@ export function formatTime(seconds: number): string {
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full audio URL from path
|
||||
*/
|
||||
export function getAudioUrlFromPath(
|
||||
path: string,
|
||||
type: ResultItemType = 'input'
|
||||
): string {
|
||||
const [subfolder, filename] = splitFilePath(path)
|
||||
return api.apiURL(getResourceURL(subfolder, filename, type))
|
||||
}
|
||||
|
||||
export function getResourceURL(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
|
||||
Reference in New Issue
Block a user