All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m10s
- Validar que duration sea numero finito en MessageAudio - Obtener duracion del elemento audio si no esta en metadata - Agregar toast de error para envio de notas de voz
188 lines
5.4 KiB
Vue
188 lines
5.4 KiB
Vue
<template>
|
|
<div class="flex items-center gap-3 min-w-[200px] max-w-[280px]">
|
|
<!-- Play/Pause button -->
|
|
<button
|
|
class="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 transition-colors"
|
|
:class="fromMe ? 'bg-white/20 hover:bg-white/30' : 'bg-[var(--wa-green-light)] hover:bg-[var(--wa-green-dark)]'"
|
|
:disabled="loading || error"
|
|
@click="togglePlay"
|
|
>
|
|
<UIcon
|
|
v-if="loading"
|
|
name="i-lucide-loader-2"
|
|
class="w-5 h-5 animate-spin"
|
|
:class="fromMe ? 'text-white' : 'text-white'"
|
|
/>
|
|
<UIcon
|
|
v-else-if="error"
|
|
name="i-lucide-alert-circle"
|
|
class="w-5 h-5"
|
|
:class="fromMe ? 'text-white' : 'text-white'"
|
|
/>
|
|
<UIcon
|
|
v-else
|
|
:name="isPlaying ? 'i-lucide-pause' : 'i-lucide-play'"
|
|
class="w-5 h-5"
|
|
:class="[fromMe ? 'text-white' : 'text-white', !isPlaying && 'ml-0.5']"
|
|
/>
|
|
</button>
|
|
|
|
<div class="flex-1 min-w-0">
|
|
<!-- Waveform / Progress bar -->
|
|
<div class="relative h-6 flex items-center">
|
|
<!-- Waveform bars -->
|
|
<div
|
|
class="flex items-center gap-[2px] w-full h-full"
|
|
@click="seekTo"
|
|
>
|
|
<div
|
|
v-for="(bar, i) in waveformBars"
|
|
:key="i"
|
|
class="flex-1 rounded-full transition-colors cursor-pointer"
|
|
:class="i < playedBars ? (fromMe ? 'bg-white' : 'bg-[var(--wa-green-dark)]') : (fromMe ? 'bg-white/40' : 'bg-[var(--wa-text-muted)]/40')"
|
|
:style="{ height: `${bar}%` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Duration -->
|
|
<div class="flex justify-between mt-1">
|
|
<span class="text-xs" :class="fromMe ? 'text-white/70' : 'text-[var(--wa-text-muted)]'">
|
|
{{ currentTimeFormatted }}
|
|
</span>
|
|
<span class="text-xs" :class="fromMe ? 'text-white/70' : 'text-[var(--wa-text-muted)]'">
|
|
{{ durationFormatted }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PTT indicator (mic icon for voice messages) -->
|
|
<div
|
|
v-if="media.isPtt"
|
|
class="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0"
|
|
:class="fromMe ? 'bg-white/20' : 'bg-[var(--wa-bg-light)]'"
|
|
>
|
|
<UIcon
|
|
name="i-lucide-mic"
|
|
class="w-4 h-4"
|
|
:class="fromMe ? 'text-white' : 'text-[var(--wa-text-muted)]'"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden audio element -->
|
|
<audio
|
|
ref="audioRef"
|
|
:src="audioUrl"
|
|
preload="metadata"
|
|
@loadedmetadata="onLoadedMetadata"
|
|
@timeupdate="onTimeUpdate"
|
|
@play="isPlaying = true"
|
|
@pause="isPlaying = false"
|
|
@ended="onEnded"
|
|
@error="onError"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { MediaInfo } from '~/types/message'
|
|
import { formatDuration } from '~/types/message'
|
|
|
|
interface Props {
|
|
media: MediaInfo
|
|
instanceId: string
|
|
messageId: string
|
|
fromMe?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
fromMe: false
|
|
})
|
|
|
|
const audioRef = ref<HTMLAudioElement | null>(null)
|
|
const isPlaying = ref(false)
|
|
const loading = ref(true)
|
|
const error = ref(false)
|
|
const currentTime = ref(0)
|
|
// Ensure duration is a valid number
|
|
const duration = ref(
|
|
typeof props.media.duration === 'number' && isFinite(props.media.duration) && props.media.duration > 0
|
|
? props.media.duration
|
|
: 0
|
|
)
|
|
|
|
const audioUrl = computed(() => {
|
|
if (props.media.url) return props.media.url
|
|
return `/api/media/${props.instanceId}/${props.messageId}`
|
|
})
|
|
|
|
// Generar barras de waveform (fake si no hay datos reales)
|
|
const waveformBars = computed(() => {
|
|
if (props.media.waveform && props.media.waveform.length > 0) {
|
|
// Normalizar waveform real a porcentajes
|
|
const max = Math.max(...props.media.waveform)
|
|
return props.media.waveform.slice(0, 40).map(v => Math.max(15, (v / max) * 100))
|
|
}
|
|
|
|
// Generar waveform fake basado en messageId para que sea consistente
|
|
const seed = props.messageId.split('').reduce((a, b) => a + b.charCodeAt(0), 0)
|
|
return Array.from({ length: 40 }, (_, i) => {
|
|
const val = Math.sin(seed + i * 0.5) * 0.5 + 0.5
|
|
return 15 + val * 85
|
|
})
|
|
})
|
|
|
|
const playedBars = computed(() => {
|
|
if (duration.value === 0) return 0
|
|
return Math.floor((currentTime.value / duration.value) * waveformBars.value.length)
|
|
})
|
|
|
|
const currentTimeFormatted = computed(() => formatDuration(currentTime.value))
|
|
const durationFormatted = computed(() => formatDuration(duration.value))
|
|
|
|
const togglePlay = () => {
|
|
if (!audioRef.value || error.value) return
|
|
|
|
if (isPlaying.value) {
|
|
audioRef.value.pause()
|
|
} else {
|
|
audioRef.value.play()
|
|
}
|
|
}
|
|
|
|
const seekTo = (e: MouseEvent) => {
|
|
if (!audioRef.value || duration.value === 0) return
|
|
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
const percent = (e.clientX - rect.left) / rect.width
|
|
audioRef.value.currentTime = percent * duration.value
|
|
}
|
|
|
|
const onLoadedMetadata = () => {
|
|
loading.value = false
|
|
// Always try to get duration from audio element if we don't have a valid one
|
|
if (audioRef.value) {
|
|
const audioDuration = audioRef.value.duration
|
|
if (isFinite(audioDuration) && audioDuration > 0 && duration.value === 0) {
|
|
duration.value = audioDuration
|
|
}
|
|
}
|
|
}
|
|
|
|
const onTimeUpdate = () => {
|
|
if (audioRef.value) {
|
|
currentTime.value = audioRef.value.currentTime
|
|
}
|
|
}
|
|
|
|
const onEnded = () => {
|
|
isPlaying.value = false
|
|
currentTime.value = 0
|
|
}
|
|
|
|
const onError = () => {
|
|
loading.value = false
|
|
error.value = true
|
|
}
|
|
</script>
|