159 lines
4.4 KiB
Vue
159 lines
4.4 KiB
Vue
<script setup>
|
|
import { computed, getCurrentInstance, ref, useTemplateRef } from 'vue';
|
|
import { downloadFile } from '@chatwoot/utils';
|
|
import { useEmitter } from 'dashboard/composables/emitter';
|
|
import { emitter } from 'shared/helpers/mitt';
|
|
import Button from 'dashboard/components-next/button/Button.vue';
|
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
|
|
|
const props = defineProps({
|
|
src: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
fallbackDuration: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
});
|
|
|
|
const PLAYBACK_SPEEDS = [1, 1.5, 2];
|
|
|
|
const audioPlayer = useTemplateRef('audioPlayer');
|
|
const { uid } = getCurrentInstance();
|
|
|
|
const isPlaying = ref(false);
|
|
const currentTime = ref(0);
|
|
const duration = ref(props.fallbackDuration);
|
|
const playbackSpeed = ref(1);
|
|
|
|
const onLoadedMetadata = () => {
|
|
const loadedDuration = audioPlayer.value?.duration;
|
|
if (Number.isFinite(loadedDuration)) duration.value = loadedDuration;
|
|
};
|
|
|
|
const formatTime = time => {
|
|
if (!time || Number.isNaN(time)) return '00:00';
|
|
const minutes = Math.floor(time / 60);
|
|
const seconds = Math.floor(time % 60);
|
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const playbackSpeedLabel = computed(() => `${playbackSpeed.value}x`);
|
|
|
|
const displayedTime = computed(() =>
|
|
formatTime(
|
|
isPlaying.value || currentTime.value ? currentTime.value : duration.value
|
|
)
|
|
);
|
|
|
|
// Only one recording should play at a time across the list.
|
|
useEmitter('pause_playing_audio', currentPlayingId => {
|
|
if (currentPlayingId !== uid && isPlaying.value) {
|
|
audioPlayer.value?.pause();
|
|
isPlaying.value = false;
|
|
}
|
|
});
|
|
|
|
const playOrPause = () => {
|
|
if (isPlaying.value) {
|
|
audioPlayer.value.pause();
|
|
isPlaying.value = false;
|
|
} else {
|
|
emitter.emit('pause_playing_audio', uid);
|
|
audioPlayer.value.play();
|
|
isPlaying.value = true;
|
|
}
|
|
};
|
|
|
|
const onTimeUpdate = () => {
|
|
currentTime.value = audioPlayer.value?.currentTime;
|
|
};
|
|
|
|
const seek = event => {
|
|
const time = Number(event.target.value);
|
|
audioPlayer.value.currentTime = time;
|
|
currentTime.value = time;
|
|
};
|
|
|
|
const onEnd = () => {
|
|
isPlaying.value = false;
|
|
currentTime.value = 0;
|
|
};
|
|
|
|
const changePlaybackSpeed = () => {
|
|
const currentIndex = PLAYBACK_SPEEDS.indexOf(playbackSpeed.value);
|
|
playbackSpeed.value =
|
|
PLAYBACK_SPEEDS[(currentIndex + 1) % PLAYBACK_SPEEDS.length];
|
|
audioPlayer.value.playbackRate = playbackSpeed.value;
|
|
};
|
|
|
|
const downloadRecording = () => {
|
|
downloadFile({ url: props.src, type: 'audio' });
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="flex items-center justify-center h-8 gap-2 px-2 rounded-full bg-n-alpha-1 dark:bg-n-alpha-2 overflow-hidden"
|
|
@click.stop
|
|
>
|
|
<audio
|
|
ref="audioPlayer"
|
|
class="hidden"
|
|
playsinline
|
|
@loadedmetadata="onLoadedMetadata"
|
|
@timeupdate="onTimeUpdate"
|
|
@ended="onEnd"
|
|
>
|
|
<source :src="src" />
|
|
</audio>
|
|
<Button
|
|
variant="ghost"
|
|
color="slate"
|
|
size="xs"
|
|
class="!w-6 !p-0 text-n-slate-12"
|
|
@click="playOrPause"
|
|
>
|
|
<template #icon>
|
|
<Icon
|
|
:icon="isPlaying ? 'i-woot-audio-pause' : 'i-woot-audio-play'"
|
|
class="size-4 flex-shrink-0 text-n-slate-11"
|
|
/>
|
|
</template>
|
|
</Button>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
:max="duration || 0"
|
|
:value="currentTime"
|
|
class="flex-1 min-w-0 lg:grow-0 lg:basis-24 h-0.5 rounded-full appearance-none cursor-pointer bg-n-slate-12/30 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-2 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-n-slate-11 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:size-2 [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-n-slate-11"
|
|
@input="seek"
|
|
/>
|
|
<span class="text-label-small tabular-nums text-n-slate-11 shrink-0">
|
|
{{ displayedTime }}
|
|
</span>
|
|
<div class="w-px h-3.5 bg-n-slate-6 shrink-0" />
|
|
<Button
|
|
variant="ghost"
|
|
color="slate"
|
|
size="xs"
|
|
:label="playbackSpeedLabel"
|
|
class="!px-1 min-w-6 !text-n-slate-11"
|
|
@click="changePlaybackSpeed"
|
|
/>
|
|
<div class="w-px h-3.5 bg-n-slate-6 shrink-0" />
|
|
<Button
|
|
variant="ghost"
|
|
color="slate"
|
|
size="xs"
|
|
class="!w-6 !p-0 text-n-slate-11"
|
|
@click="downloadRecording"
|
|
>
|
|
<template #icon>
|
|
<Icon icon="i-lucide-download" class="size-4 flex-shrink-0" />
|
|
</template>
|
|
</Button>
|
|
</div>
|
|
</template>
|