Files
BRETT/app/components/AudioPlayerDialog.vue
T
2026-10-03 23:45:04 +02:00

131 lines
4.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { useMediaControls } from '@vueuse/core'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { formatFileSize } from '~~/shared/utils/attachments'
import type { AttachmentAudioMeta } from '~~/shared/utils/attachments'
const props = defineProps<{
src: string
name: string
size?: number | null
cover?: string | null
meta?: AttachmentAudioMeta | null
}>()
const open = defineModel<boolean>('open', { required: true })
const audioRef = ref<HTMLAudioElement>()
const { playing, currentTime, duration, volume, muted } = useMediaControls(audioRef)
const metaLine = computed(() => {
const m = props.meta
if (!m) return null
const parts = [m.artist, m.title].filter(Boolean)
return parts.length ? parts.join(' – ') : (m.album || null)
})
// Die Komponente wird per v-if nur gemountet, wenn der Dialog offen ist
onMounted(async () => {
await nextTick()
playing.value = true
})
watch(open, (isOpen) => {
if (!isOpen) playing.value = false
})
function toggle() {
playing.value = !playing.value
}
function seek(event: Event) {
currentTime.value = Number((event.target as HTMLInputElement).value)
}
function formatTime(sec: number) {
if (!Number.isFinite(sec)) return '0:00'
const m = Math.floor(sec / 60)
const s = Math.floor(sec % 60).toString().padStart(2, '0')
return `${m}:${s}`
}
</script>
<template>
<Dialog v-model:open="open">
<DialogContent>
<DialogHeader>
<DialogTitle class="flex min-w-0 items-center gap-2 pr-10">
<Icon name="ph:file-audio-fill" class="size-4 shrink-0" />
<span class="min-w-0 truncate">{{ name }}</span>
</DialogTitle>
<DialogDescription v-if="metaLine" class="truncate">
{{ metaLine }}
</DialogDescription>
</DialogHeader>
<img
v-if="cover"
:src="cover"
:alt="name"
class="max-h-56 w-full rounded-md border bg-muted object-contain"
>
<audio ref="audioRef" :src="src" preload="metadata" class="hidden" />
<div class="flex min-w-0 items-center gap-2.5 rounded-md border bg-muted/50 px-3 py-2">
<button
type="button"
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
:aria-label="playing ? $t('audio.pause') : $t('audio.play')"
@click="toggle"
>
<Icon name="ph:pause-fill" v-if="playing" class="size-4" />
<Icon name="ph:play-fill" v-else class="size-4" />
</button>
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">{{ formatTime(currentTime) }}</span>
<input
type="range"
min="0"
:max="Number.isFinite(duration) ? duration : 0"
step="0.1"
:value="currentTime"
:aria-label="$t('audio.seek')"
class="min-w-0 flex-1 cursor-pointer accent-primary"
@input="seek"
>
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">{{ formatTime(duration) }}</span>
</div>
<div class="flex min-w-0 items-center gap-2">
<button
type="button"
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
:aria-label="muted ? $t('audio.unmute') : $t('audio.mute')"
@click="muted = !muted"
>
<Icon name="ph:speaker-x-fill" v-if="muted" class="size-4" />
<Icon name="ph:speaker-high-fill" v-else class="size-4" />
</button>
<input
v-model.number="volume"
type="range"
min="0"
max="1"
step="0.05"
:aria-label="$t('audio.volume')"
class="w-16 min-w-0 cursor-pointer accent-primary sm:w-20"
:class="muted && 'opacity-40'"
>
<span class="ml-auto shrink-0 text-xs text-muted-foreground">{{ formatFileSize(size) }}</span>
<Button variant="outline" size="sm" class="shrink-0" as-child>
<a :href="src" :download="name">
<Icon name="ph:download-fill" class="size-4" />
{{ $t('audio.download') }}
</a>
</Button>
</div>
</DialogContent>
</Dialog>
</template>