Skip to content

Instantly share code, notes, and snippets.

@tobiaswatzek
Created February 26, 2023 17:45
Show Gist options
  • Save tobiaswatzek/9c7b53c685d317a0763e16bfb866b369 to your computer and use it in GitHub Desktop.
Save tobiaswatzek/9c7b53c685d317a0763e16bfb866b369 to your computer and use it in GitHub Desktop.
Sounds like Vue
<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