mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 17:10:07 +00:00
feat: add download button to audio preview player (#8628)
## Summary - Adds a download icon button to the `AudioPreviewPlayer` widget for PreviewAudio and SaveAudio nodes - Reuses the existing `downloadFile` utility (same as video download) - Button appears inline next to volume/options controls, matching the player's existing UI style ## Test plan - [x] Add a PreviewAudio or SaveAudio node, run a workflow that produces audio output - [x] Verify the download icon appears in the audio player controls - [x] Click the download button and confirm the audio file downloads correctly - [x] Verify the button does not appear when no audio is loaded https://github.com/user-attachments/assets/7fb2df16-82a6-40aa-a938-aed57032e30b ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8628-feat-add-download-button-to-audio-preview-player-2fe6d73d365081e3997fc45d3bb8cffc) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
committed by
GitHub
parent
478cfc0b5e
commit
7d3d00858a
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
@@ -10,6 +10,7 @@
|
||||
"download": "Download",
|
||||
"downloadImage": "Download image",
|
||||
"downloadVideo": "Download video",
|
||||
"downloadAudio": "Download audio",
|
||||
"editOrMaskImage": "Edit or mask image",
|
||||
"editImage": "Edit image",
|
||||
"decrement": "Decrement",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import AudioPreviewPlayer from '@/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue'
|
||||
|
||||
const mockToastAdd = vi.fn()
|
||||
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: () => ({ add: mockToastAdd })
|
||||
}))
|
||||
|
||||
vi.mock('@/base/common/downloadUtil', () => ({
|
||||
downloadFile: vi.fn()
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: {} }
|
||||
})
|
||||
|
||||
function mountPlayer(modelValue?: string) {
|
||||
return mount(AudioPreviewPlayer, {
|
||||
props: {
|
||||
modelValue,
|
||||
hideWhenEmpty: false
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
components: { Button },
|
||||
stubs: {
|
||||
TieredMenu: true,
|
||||
Slider: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function findDownloadButton(wrapper: ReturnType<typeof mountPlayer>) {
|
||||
return wrapper.find('[aria-label="g.downloadAudio"]')
|
||||
}
|
||||
|
||||
describe('AudioPreviewPlayer', () => {
|
||||
describe('download button', () => {
|
||||
it('shows download button when audio is loaded', () => {
|
||||
const wrapper = mountPlayer('http://example.com/audio.mp3')
|
||||
|
||||
expect(findDownloadButton(wrapper).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('hides download button when no audio is loaded', () => {
|
||||
const wrapper = mountPlayer()
|
||||
|
||||
expect(findDownloadButton(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('calls downloadFile when download button is clicked', async () => {
|
||||
const { downloadFile } = await import('@/base/common/downloadUtil')
|
||||
|
||||
const wrapper = mountPlayer('http://example.com/audio.mp3')
|
||||
await findDownloadButton(wrapper).trigger('click')
|
||||
|
||||
expect(downloadFile).toHaveBeenCalledWith('http://example.com/audio.mp3')
|
||||
})
|
||||
|
||||
it('shows toast on download failure', async () => {
|
||||
const { downloadFile } = await import('@/base/common/downloadUtil')
|
||||
vi.mocked(downloadFile).mockImplementation(() => {
|
||||
throw new Error('download failed')
|
||||
})
|
||||
|
||||
const wrapper = mountPlayer('http://example.com/audio.mp3')
|
||||
await findDownloadButton(wrapper).trigger('click')
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: 'error'
|
||||
})
|
||||
)
|
||||
|
||||
vi.mocked(downloadFile).mockReset()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -16,11 +16,11 @@
|
||||
<!-- Left Actions -->
|
||||
<div class="relative flex shrink-0 items-center justify-start gap-2">
|
||||
<!-- Play/Pause Button -->
|
||||
<div
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="$t('g.playPause')"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="size-6 rounded"
|
||||
@click="togglePlayPause"
|
||||
>
|
||||
<i
|
||||
@@ -28,7 +28,7 @@
|
||||
class="text-secondary icon-[lucide--play] size-4"
|
||||
/>
|
||||
<i v-else class="text-secondary icon-[lucide--pause] size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<!-- Time Display -->
|
||||
<div class="text-sm font-normal text-nowrap text-base-foreground">
|
||||
@@ -57,11 +57,11 @@
|
||||
<!-- Right Actions -->
|
||||
<div class="relative flex shrink-0 items-center justify-start gap-2">
|
||||
<!-- Volume Button -->
|
||||
<div
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="$t('g.volume')"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="size-6 rounded"
|
||||
@click="toggleMute"
|
||||
>
|
||||
<i
|
||||
@@ -73,19 +73,32 @@
|
||||
class="text-secondary icon-[lucide--volume-1] size-4"
|
||||
/>
|
||||
<i v-else class="text-secondary icon-[lucide--volume-x] size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<!-- Download Button -->
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
size="icon-sm"
|
||||
variant="textonly"
|
||||
:aria-label="$t('g.downloadAudio')"
|
||||
:title="$t('g.downloadAudio')"
|
||||
class="size-6 hover:bg-interface-menu-component-surface-hovered"
|
||||
@click="handleDownload"
|
||||
>
|
||||
<i class="text-secondary icon-[lucide--download] size-4" />
|
||||
</Button>
|
||||
|
||||
<!-- Options Button -->
|
||||
<div
|
||||
<Button
|
||||
v-if="showOptionsButton"
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="$t('g.moreOptions')"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="size-6 rounded"
|
||||
@click="toggleOptionsMenu"
|
||||
>
|
||||
<i class="text-secondary icon-[lucide--more-vertical] size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Options Menu -->
|
||||
@@ -137,11 +150,16 @@ import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { whenever } from '@vueuse/core'
|
||||
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { formatTime } from '../../utils/audioUtils'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -187,6 +205,20 @@ const togglePlayPause = () => {
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!modelValue.value) return
|
||||
try {
|
||||
downloadFile(modelValue.value)
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadFile'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMute = () => {
|
||||
if (audioRef.value) {
|
||||
isMuted.value = !isMuted.value
|
||||
|
||||
Reference in New Issue
Block a user