234 lines
6.4 KiB
Vue
234 lines
6.4 KiB
Vue
<template>
|
|
<div class="w-full text-sm text-ink-gray-5">
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
class="text-ink-gray-5"
|
|
:icon="isPaused ? PlayIcon : PauseIcon"
|
|
@click="playPause"
|
|
/>
|
|
<div class="flex gap-2 items-center justify-between flex-1">
|
|
<input
|
|
class="w-full slider !h-[0.5] bg-surface-gray-3 [&::-webkit-slider-thumb]:shadow [&::-webkit-slider-thumb:hover]:outline [&::-webkit-slider-thumb:hover]:outline-[0.5px]"
|
|
:style="{
|
|
background: `linear-gradient(to right, var(--surface-gray-7, #171717) ${progress}%, var(--surface-gray-3, #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>
|
|
<Dropdown :options="options">
|
|
<Button
|
|
icon="more-horizontal"
|
|
variant="ghost"
|
|
@click="showPlaybackSpeed = false"
|
|
/>
|
|
</Dropdown>
|
|
</div>
|
|
</div>
|
|
|
|
<audio
|
|
ref="audio"
|
|
:src="src"
|
|
crossorigin="anonymous"
|
|
@loadedmetadata="setupDuration"
|
|
@timeupdate="updateCurrentTime"
|
|
@ended="isPaused = true"
|
|
></audio>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import PlayIcon from '@/components/Icons/PlayIcon.vue'
|
|
import PauseIcon from '@/components/Icons/PauseIcon.vue'
|
|
import VolumnLowIcon from '@/components/Icons/VolumnLowIcon.vue'
|
|
import VolumnHighIcon from '@/components/Icons/VolumnHighIcon.vue'
|
|
import MuteIcon from '@/components/Icons/MuteIcon.vue'
|
|
import PlaybackSpeedIcon from '@/components/Icons/PlaybackSpeedIcon.vue'
|
|
import PlaybackSpeedOption from '@/components/Activities/PlaybackSpeedOption.vue'
|
|
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
|
import { computed, h, 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
|
|
}
|
|
|
|
const showPlaybackSpeed = ref(false)
|
|
const currentPlaybackSpeed = ref(1)
|
|
|
|
const options = computed(() => {
|
|
let playbackSpeeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
|
|
|
let playbackSpeedOptions = playbackSpeeds.map((speed) => {
|
|
let label = `${speed}x`
|
|
if (speed === 1) {
|
|
label = __('Normal')
|
|
}
|
|
return {
|
|
component: () =>
|
|
h(PlaybackSpeedOption, {
|
|
label,
|
|
active: speed === currentPlaybackSpeed.value,
|
|
onClick: () => {
|
|
audio.value.playbackRate = speed
|
|
showPlaybackSpeed.value = false
|
|
currentPlaybackSpeed.value = speed
|
|
},
|
|
}),
|
|
}
|
|
})
|
|
let _options = [
|
|
{
|
|
icon: 'download',
|
|
label: __('Download'),
|
|
onClick: () => {
|
|
const a = document.createElement('a')
|
|
a.href = props.src
|
|
a.download = props.src.split('/').pop()
|
|
a.click()
|
|
},
|
|
},
|
|
{
|
|
icon: () => h(PlaybackSpeedIcon, { class: 'size-4' }),
|
|
label: __('Playback speed'),
|
|
onClick: (e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
showPlaybackSpeed.value = true
|
|
},
|
|
},
|
|
]
|
|
|
|
return showPlaybackSpeed.value ? playbackSpeedOptions : _options
|
|
})
|
|
</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>
|