feat: added AudioPlayer component
This commit is contained in:
parent
b12ed81aff
commit
9c98dc82b3
@ -165,9 +165,7 @@
|
|||||||
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-gray-200"
|
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-gray-200"
|
||||||
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
|
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
|
||||||
>
|
>
|
||||||
<div
|
<div class="z-10 flex h-7 w-7 items-center justify-center bg-white">
|
||||||
class="z-10 flex h-7 w-7 items-center justify-center bg-white"
|
|
||||||
>
|
|
||||||
<CommentIcon class="text-gray-800" />
|
<CommentIcon class="text-gray-800" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -373,6 +371,7 @@
|
|||||||
v-if="call.show_recording && call.recording_url"
|
v-if="call.show_recording && call.recording_url"
|
||||||
class="flex items-center justify-between rounded border"
|
class="flex items-center justify-between rounded border"
|
||||||
>
|
>
|
||||||
|
<AudioPlayer :src="call.recording_url" />
|
||||||
<audio class="audio-control" controls :src="call.recording_url" />
|
<audio class="audio-control" controls :src="call.recording_url" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@ -698,13 +697,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="activity.show_recording && activity.recording_url"
|
v-if="activity.show_recording && activity.recording_url"
|
||||||
class="flex items-center justify-between"
|
class="flex flex-col items-center justify-between"
|
||||||
>
|
>
|
||||||
<audio
|
<AudioPlayer :src="activity.recording_url" />
|
||||||
class="audio-control"
|
|
||||||
controls
|
|
||||||
:src="activity.recording_url"
|
|
||||||
></audio>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -930,6 +925,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import EmailContent from '@/components/Activities/EmailContent.vue'
|
import EmailContent from '@/components/Activities/EmailContent.vue'
|
||||||
|
import AudioPlayer from '@/components/Activities/AudioPlayer.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
|
|||||||
178
frontend/src/components/Activities/AudioPlayer.vue
Normal file
178
frontend/src/components/Activities/AudioPlayer.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<div class="player w-full text-sm text-gray-600">
|
||||||
|
<div class="player-controls flex items-center gap-2">
|
||||||
|
<Button variant="ghost" @click="playPause">
|
||||||
|
<template #icon>
|
||||||
|
<FeatherIcon class="size-4" :name="isPaused ? 'play' : 'pause'" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
class="player-timeline flex gap-2 items-center justify-between flex-1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full slider !h-[0.5] bg-gray-200 [&::-webkit-slider-thumb]:shadow [&::-webkit-slider-thumb:hover]:outline [&::-webkit-slider-thumb:hover]:outline-[0.5px]"
|
||||||
|
:style="{
|
||||||
|
background: `linear-gradient(to right, #171717 ${progress}%, #ededed ${progress}%)`,
|
||||||
|
}"
|
||||||
|
type="range"
|
||||||
|
id="track"
|
||||||
|
min="0"
|
||||||
|
:max="duration"
|
||||||
|
:value="currentTime"
|
||||||
|
step="0.01"
|
||||||
|
@input="(e) => (audio.currentTime = e.target.value)"
|
||||||
|
/>
|
||||||
|
<div class="shrink-0">
|
||||||
|
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="flex group gap-2 items-center">
|
||||||
|
<input
|
||||||
|
class="slider opacity-0 group-hover:opacity-100 w-0 group-hover:w-20 !h-[0.5] [&::-webkit-slider-thumb]:shadow [&::-webkit-slider-thumb:hover]:outline [&::-webkit-slider-thumb:hover]:outline-[0.5px]"
|
||||||
|
:style="{
|
||||||
|
background: `linear-gradient(to right, #171717 ${volumnProgress}%, #ededed ${volumnProgress}%)`,
|
||||||
|
}"
|
||||||
|
type="range"
|
||||||
|
id="volume"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
:value="currentVolumn"
|
||||||
|
step="0.01"
|
||||||
|
@input="(e) => updateVolumnProgress(e.target.value)"
|
||||||
|
/>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<template #icon>
|
||||||
|
<MuteIcon
|
||||||
|
v-if="volumnProgress == 0"
|
||||||
|
class="size-4"
|
||||||
|
@click="updateVolumnProgress('1')"
|
||||||
|
/>
|
||||||
|
<VolumnLowIcon
|
||||||
|
v-else-if="volumnProgress <= 40"
|
||||||
|
class="size-4"
|
||||||
|
@click="updateVolumnProgress('0')"
|
||||||
|
/>
|
||||||
|
<VolumnHighIcon
|
||||||
|
v-else-if="volumnProgress > 20"
|
||||||
|
class="size-4"
|
||||||
|
@click="updateVolumnProgress('0')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<template #icon>
|
||||||
|
<FeatherIcon class="size-4" name="more-horizontal" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio
|
||||||
|
ref="audio"
|
||||||
|
:src="src"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
@loadedmetadata="setupDuration"
|
||||||
|
@timeupdate="updateCurrentTime"
|
||||||
|
@ended="isPaused = true"
|
||||||
|
></audio>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import VolumnLowIcon from '@/components/Icons/VolumnLowIcon.vue'
|
||||||
|
import VolumnHighIcon from '@/components/Icons/VolumnHighIcon.vue'
|
||||||
|
import MuteIcon from '@/components/Icons/MuteIcon.vue'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const audio = ref(null)
|
||||||
|
const isPaused = ref(true)
|
||||||
|
|
||||||
|
const duration = ref(0)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const progress = computed(() => (currentTime.value / duration.value) * 100)
|
||||||
|
const currentVolumn = ref(1)
|
||||||
|
const volumnProgress = ref(100)
|
||||||
|
|
||||||
|
function setupDuration() {
|
||||||
|
duration.value = audio.value.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentTime() {
|
||||||
|
currentTime.value = audio.value.currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause() {
|
||||||
|
if (audio.value.paused) {
|
||||||
|
audio.value.play()
|
||||||
|
isPaused.value = false
|
||||||
|
} else {
|
||||||
|
audio.value.pause()
|
||||||
|
isPaused.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(time) {
|
||||||
|
if (isNaN(time)) return '00:00'
|
||||||
|
const minutes = Math.floor(time / 60)
|
||||||
|
const seconds = Math.floor(time % 60)
|
||||||
|
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVolumnProgress(value) {
|
||||||
|
audio.value.volume = value
|
||||||
|
currentVolumn.value = value
|
||||||
|
volumnProgress.value = value * 100
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.slider {
|
||||||
|
--trackHeight: 2px;
|
||||||
|
--thumbRadius: 14px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-runnable-track {
|
||||||
|
appearance: none;
|
||||||
|
height: var(--trackHeight);
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
width: var(--thumbRadius);
|
||||||
|
height: var(--thumbRadius);
|
||||||
|
margin-top: calc((var(--trackHeight) - var(--thumbRadius)) / 2);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 100px;
|
||||||
|
pointer-events: all;
|
||||||
|
appearance: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
width: var(--thumbRadius);
|
||||||
|
height: var(--thumbRadius);
|
||||||
|
margin-top: calc((var(--trackHeight) - var(--thumbRadius)) / 2);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 100px;
|
||||||
|
pointer-events: all;
|
||||||
|
appearance: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
frontend/src/components/Icons/MuteIcon.vue
Normal file
38
frontend/src/components/Icons/MuteIcon.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.05142 5.27273H2.6926C2.3798 5.27273 2.07981 5.38766 1.85863 5.59225C1.63744 5.79683 1.51318 6.07431 1.51318 6.36364V9.63636C1.51318 9.92569 1.63744 10.2032 1.85863 10.4078C2.07981 10.6123 2.3798 10.7273 2.6926 10.7273H5.05142L9.76908 14V2L5.05142 5.27273Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.5 5.5C11.8143 5.81281 12.066 6.1975 12.2384 6.62854C12.4108 7.05958 12.5 7.52708 12.5 8C12.5 8.47292 12.4108 8.94042 12.2384 9.37146C12.066 9.8025 11.8143 10.1872 11.5 10.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.5 3C13.1286 3.62561 13.632 4.39501 13.9768 5.25708C14.3217 6.11915 14.5 7.05416 14.5 8C14.5 8.94584 14.3217 9.88085 13.9768 10.7429C13.632 11.605 13.1286 12.3744 12.5 13"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.25 3.07596L13.7286 12.2609"
|
||||||
|
stroke="white"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2.97913 2.0192L14.0209 13.9808"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
28
frontend/src/components/Icons/VolumnHighIcon.vue
Normal file
28
frontend/src/components/Icons/VolumnHighIcon.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.05142 5.27273H2.6926C2.3798 5.27273 2.07981 5.38766 1.85863 5.59225C1.63744 5.79683 1.51318 6.07431 1.51318 6.36364V9.63636C1.51318 9.92569 1.63744 10.2032 1.85863 10.4078C2.07981 10.6123 2.3798 10.7273 2.6926 10.7273H5.05142L9.76908 14V2L5.05142 5.27273Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.5 5.5C11.8143 5.81281 12.066 6.1975 12.2384 6.62854C12.4108 7.05958 12.5 7.52708 12.5 8C12.5 8.47292 12.4108 8.94042 12.2384 9.37146C12.066 9.8025 11.8143 10.1872 11.5 10.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.5 3C13.1286 3.62561 13.632 4.39501 13.9768 5.25708C14.3217 6.11915 14.5 7.05416 14.5 8C14.5 8.94584 14.3217 9.88085 13.9768 10.7429C13.632 11.605 13.1286 12.3744 12.5 13"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
22
frontend/src/components/Icons/VolumnLowIcon.vue
Normal file
22
frontend/src/components/Icons/VolumnLowIcon.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.05142 5.27273H2.6926C2.3798 5.27273 2.07981 5.38766 1.85863 5.59225C1.63744 5.79683 1.51318 6.07431 1.51318 6.36364V9.63636C1.51318 9.92569 1.63744 10.2032 1.85863 10.4078C2.07981 10.6123 2.3798 10.7273 2.6926 10.7273H5.05142L9.76908 14V2L5.05142 5.27273Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.5 5.5C11.8143 5.81281 12.066 6.1975 12.2384 6.62854C12.4108 7.05958 12.5 7.52708 12.5 8C12.5 8.47292 12.4108 8.94042 12.2384 9.37146C12.066 9.8025 11.8143 10.1872 11.5 10.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
Loading…
x
Reference in New Issue
Block a user