Sounds like Vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script setup> | |
import { ref, reactive, computed, onMounted } from "vue"; | |
const props = defineProps(["src"]); | |
const state = reactive({ | |
playing: false, | |
seekingPosition: undefined, | |
position: 0, | |
duration: 0, | |
}); | |
const audioElement = ref(null); | |
const currentPosition = computed(() => | |
Math.floor( | |
state.seekingPosition == undefined ? state.position : state.seekingPosition | |
) | |
); | |
const flooredDuration = computed(() => Math.floor(state.duration)); | |
function secondsToHMS(s) { | |
const hours = Math.floor(s / 3600); | |
const minutes = Math.floor((s % 3600) / 60); | |
const seconds = Math.floor(s % 60); | |
return { hours, minutes, seconds }; | |
} | |
function formatSecondsToDisplay(s) { | |
const { hours, minutes, seconds } = secondsToHMS(s); | |
return [hours, minutes, seconds] | |
.map((n) => String(n).padStart(2, "0")) | |
.join(":"); | |
} | |
function pluralize(s, n) { | |
return `${n} ${s}${n === 0 || n > 1 ? "s" : ""}`; | |
} | |
function formatSecondToValuetext(s) { | |
const { hours, minutes, seconds } = secondsToHMS(s); | |
const hoursText = hours === 0 ? "" : pluralize("Hour", hours); | |
const minutesText = minutes === 0 ? "" : pluralize("Minute", minutes); | |
const secondsText = pluralize("Second", seconds); | |
return `${hoursText} ${minutesText} ${secondsText}`.trim(); | |
} | |
function formatSecondsToDuration(s) { | |
const { hours, minutes, seconds } = secondsToHMS(s); | |
return `PT${hours}H${minutes}M${seconds}S`; | |
} | |
function togglePlayPause() { | |
if (state.playing) { | |
audioElement.value.pause(); | |
} else { | |
audioElement.value.play(); | |
} | |
} | |
function goBack() { | |
setPosition(Math.max(0, state.position - 10)); | |
} | |
function goForward() { | |
setPosition(Math.min(state.duration, state.position + 30)); | |
} | |
function setPosition(value) { | |
state.position = value; | |
audioElement.value.currentTime = state.position; | |
state.seekingPosition = undefined; | |
} | |
function onTimeupdate(e) { | |
if (state.seekingPosition != undefined) { | |
return; | |
} | |
state.position = e.target.currentTime; | |
} | |
onMounted(() => { | |
audioElement.value.readyState > 0 | |
? (state.duration = audioElement.value.duration) | |
: undefined; | |
}); | |
</script> | |
<template> | |
<div | |
class="flex flex-col gap-2 p-2 border-2 border-pink-400" | |
role="region" | |
aria-label="Audio Player" | |
> | |
<div class="flex flex-col gap-2 md:flex-row"> | |
<audio | |
ref="audioElement" | |
:src="src" | |
preload="metadata" | |
@loadedmetadata="state.duration = $event.target.duration" | |
@pause="state.playing = false" | |
@play="state.playing = true" | |
@timeupdate="onTimeupdate" | |
></audio> | |
<div class="flex flex-col md:block md:flex-grow md:self-center"> | |
<div class="flex items-center justify-around gap-6 my-4"> | |
<button | |
class="p-2 font-bold transition-all rounded hover:text-white hover:bg-green-600" | |
@click="goBack()" | |
> | |
-10 Seconds | |
</button> | |
<button | |
class="p-2 font-bold transition-all rounded hover:text-white hover:bg-green-600" | |
@click="togglePlayPause()" | |
> | |
{{ state.playing ? "Pause" : "Play" }} | |
</button> | |
<button | |
class="p-2 font-bold transition-all rounded hover:text-white hover:bg-green-600" | |
@click="goForward()" | |
> | |
+30 Seconds | |
</button> | |
</div> | |
<div class="w-100"> | |
<input | |
class="col-start-1 col-span-full" | |
type="range" | |
aria-label="Position" | |
:style="{ | |
'--min': 0, | |
'--max': flooredDuration, | |
'--val': currentPosition, | |
}" | |
:aria-valuetext="formatSecondToValuetext(currentPosition)" | |
:max="flooredDuration" | |
:value="currentPosition" | |
@input="state.seekingPosition = $event.target.value" | |
@change="setPosition($event.target.value)" | |
/> | |
<div class="flex flex-row justify-between"> | |
<div class="text-left tabular-nums"> | |
<time :datetime="formatSecondsToDuration(currentPosition)">{{ | |
formatSecondsToDisplay(currentPosition) | |
}}</time> | |
</div> | |
<div class="text-right tabular-nums"> | |
<time :datetime="formatSecondsToDuration(state.duration)">{{ | |
formatSecondsToDisplay(state.duration) | |
}}</time> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
<style scoped> | |
input[type="range"] { | |
--range: calc(var(--max) - var(--min)); | |
--ratio: calc((var(--val) - var(--min)) / var(--range)); | |
--sx: calc(0.5 * 1.5em + var(--ratio) * (100% - 1.5em)); | |
--progress-color: theme(colors.pink[500]); | |
--range-color: theme(colors.neutral[300]); | |
@apply appearance-none | |
h-4 | |
w-full | |
bg-transparent; | |
} | |
input[type="range"]::-webkit-slider-thumb { | |
@apply appearance-none | |
bg-pink-400 | |
border-transparent | |
h-4 | |
w-4 | |
rounded-full | |
shadow-md | |
cursor-grab; | |
margin-top: -0.25rem; | |
} | |
input[type="range"]::-webkit-slider-thumb:active { | |
@apply cursor-grabbing; | |
} | |
input[type="range"]::-moz-range-thumb { | |
@apply appearance-none | |
bg-pink-400 | |
border-transparent | |
h-4 | |
w-4 | |
rounded-full | |
shadow-md | |
cursor-grab; | |
} | |
input[type="range"]::-moz-range-thumb:active { | |
@apply cursor-grabbing; | |
} | |
input[type="range"]::-moz-range-track { | |
@apply appearance-none | |
h-2 | |
rounded | |
cursor-pointer; | |
background: var(--range-color); | |
} | |
input[type="range"]::-moz-range-progress { | |
@apply appearance-none | |
h-2 | |
rounded-l | |
cursor-pointer; | |
background: var(--progress-color); | |
} | |
input[type="range"]::-webkit-slider-runnable-track { | |
@apply appearance-none | |
h-2 | |
rounded | |
cursor-pointer | |
bg-black; | |
background: linear-gradient(var(--progress-color), var(--progress-color)) 0 / | |
var(--sx) 100% no-repeat var(--range-color); | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment