initial commit of a basic working audio player

This commit is contained in:
2022-01-07 10:07:15 -07:00
parent 71e22ae4b0
commit 6ad71a68f3
41 changed files with 37889 additions and 0 deletions

5
resources/css/app.css Normal file
View File

@ -0,0 +1,5 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'typography.css';

View File

@ -0,0 +1,47 @@
@font-face {
font-family: "Roboto";
src: url('/fonts/Roboto/Roboto-Light.woff2') format("woff2");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Roboto";
src: url('/fonts/Roboto/Roboto-LightItalic.woff2') format("woff2");
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Roboto";
src: url('/fonts/Roboto/Roboto-Regular.woff2') format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Roboto";
src: url('/fonts/Roboto/Roboto-Italic.woff2') format("woff2");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Roboto";
src: url('/fonts/Roboto/Roboto-Medium.woff2') format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Roboto";
src: url('/fonts/Roboto/Roboto-Bold.woff2') format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}

81
resources/js/App.vue Normal file
View File

@ -0,0 +1,81 @@
<template>
<div class="radio grid grid-cols-2 gap-x-8 p-8">
<player
class="col-span-1 py-4 bg-green-400"
:currentSong="currentSong"
@start-next-song="startNextSong"
@start-prev-song="startPrevSong"
></player>
<playlist
class="col-span-1 py-4 px-6 bg-blue-400"
:playlistSongs="playlistSongs"
@start-song="startSong"
></playlist>
</div>
</template>
<script>
import { defineComponent } from "vue"
import Player from "./Components/Player.vue"
import Playlist from "./Components/Playlist.vue"
export default defineComponent({
emits: [],
props: {},
components: {
Player,
Playlist,
},
setup(props, {emit}) {
let currentSong = {
title: 'Childhood Memories',
artist: 'mell-ø x Ambulo',
album: 'Afloat Again EP',
release_date: '2020',
cover_art_url: 'https://music.ditoforge.test/images/mell-ø - Afloat Again EP (2020).webp',
file_url: 'https://music.ditoforge.test/storage/music/mell-ø - Childhood Memories.mp3',
}
let playlistSongs = [currentSong, currentSong, currentSong]
return { currentSong, playlistSongs }
},
beforeMount() {},
mounted() {},
data() {
return {}
},
computed: {},
methods: {
startNextSong() {
console.log('App: starting the next song...')
},
startPrevSong() {
console.log('App: starting the previous song...')
},
startSong(song) {
console.log('App: starting an arbitrary song...')
console.log("App: here's the song:", song)
},
},
})
</script>
<style scoped>
.radio {
min-width: 1024px;
max-width: 1128px;
width: 100%;
}
</style>

View File

@ -0,0 +1,269 @@
<template>
<div class="player__container flex flex-col">
<div class="cover-art__container flex justify-center rounded-lg">
<img :src="currentSong.cover_art_url" :alt="currentSong.album" height="64" width="64" class="shadow rounded-lg">
</div>
<div class="song-info flex flex-col items-center justify-center">
<div class="song-info_title font-bold text-2xl">{{ currentSong.title }}</div>
<div class="song-info_artist text-xl">{{ currentSong.artist }}</div>
</div>
<div class="controls">
<button type="button" @click="changeToPrevSong()">
<svg viewBox="0 0 24 24" width="28" height="28" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="">
<polygon points="19 20 9 12 19 4 19 20"></polygon>
<line x1="5" y1="19" x2="5" y2="5"></line>
</svg>
</button>
<button type="button" v-show="!isPlaying" @click="startPlaying">
<svg viewBox="0 0 24 24" width="48" height="48" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
</button>
<button type="button" v-show="isPlaying" @click="pausePlaying">
<svg viewBox="0 0 24 24" width="48" height="48" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="">
<rect x="6" y="4" width="4" height="16"></rect>
<rect x="14" y="4" width="4" height="16"></rect>
</svg>
</button>
<button type="button" @click="changeToNextSong()">
<svg viewBox="0 0 24 24" width="28" height="28" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="">
<polygon points="5 4 15 12 5 20 5 4"></polygon>
<line x1="19" y1="5" x2="19" y2="19"></line>
</svg>
</button>
</div>
<div class="progress__container flex flex-col mx-12">
<div class="flex flex-row items-center justify-between">
<div class="font-light">{{ timeElapsed }}</div>
<div class="font-light">{{ songLength }}</div>
</div>
<div class="progress-bars flex relative" ref="progressBars">
<progress class="progress-played absolute z-30 w-full" min="0" :max="duration" :value="audioPlayer.currentTime"></progress>
<progress class="progress-seeking absolute z-20 w-full" min="0" :max="duration" :value="seekingTime" ref="seekingBar"></progress>
<progress class="progress-buffered absolute z-10 w-full" min="0" :max="duration" :value="bufferedTime"></progress>
</div>
</div>
</div>
</template>
<script>
import { defineComponent, ref } from "vue"
export default defineComponent({
emits: [
'startNextSong',
'startPrevSong',
],
props: {
currentSong: Object,
},
components: {},
setup(props) {
let audioPlayer = new Audio(props.currentSong.file_url)
let duration = ref(0)
let bufferedTime = ref(0)
let currentTime = ref(0)
let seekingTime = ref(0)
let canPlaying = ref(false)
let isPlaying = ref(false)
return {
audioPlayer,
duration,
bufferedTime,
currentTime,
seekingTime,
canPlaying,
isPlaying,
}
},
beforeMount() {},
mounted() {
this.audioPlayer.addEventListener("loadedmetadata", event => {
this.duration = event.path[0].duration
})
this.audioPlayer.addEventListener("canplay", event => {
this.canPlay = true
})
this.audioPlayer.addEventListener("canplaythrough", event => {
this.canPlay = true
this.bufferedTime = this.duration
})
this.audioPlayer.addEventListener("loadeddata", event => {
this.bufferedTime = event.path[0].buffered.end(0)
})
this.audioPlayer.addEventListener("progress", event => {
if (this.duration > 0) {
for (let i = 0; i < this.audioPlayer.buffered.length; i++) {
if (this.audioPlayer.buffered.start(this.audioPlayer.buffered.length - 1 - i) < this.currentTime) {
console.log(this.audioPlayer.buffered.end(this.audioPlayer.buffered.length - 1 - i) / this.duration)
}
}
}
})
this.audioPlayer.addEventListener("timeupdate", event => {
this.currentTime = event.path[0].currentTime
})
/*this.audioPlayer.addEventListener("seeking", event => {
//
})
this.audioPlayer.addEventListener("seeked", event => {
//
})*/
this.$refs.progressBars.addEventListener("mousemove", event => {
this.$refs.seekingBar.value = this.duration * (event.layerX / event.path[0].clientWidth)
})
this.$refs.progressBars.addEventListener("mousedown", event => {
let newCurrentTime = this.duration * (event.layerX / event.path[0].clientWidth)
this.pausePlaying()
this.audioPlayer.currentTime = newCurrentTime
this.currentTime = newCurrentTime
this.startPlaying()
})
//this.audioPlayer.addEventListener("complete", this.changeToNextSong())
//this.audioPlayer.addEventListener("ended", this.changeToNextSong())
},
data() {
return {}
},
computed: {
timeElapsed() {
return this.secondsToHuman(this.currentTime)
},
songLength() {
return this.secondsToHuman(this.duration)
},
},
methods: {
startPlaying() {
this.isPlaying = true
this.audioPlayer.play()
},
pausePlaying() {
this.isPlaying = false
this.audioPlayer.pause()
},
changeToNextSong() {
console.log('Player: starting the next song...')
this.$emit('startNextSong')
},
changeToPrevSong() {
console.log('Player: starting the previous song...')
this.$emit('startPrevSong')
},
// helpers
secondsToHuman(lengthInSeconds) {
lengthInSeconds = Number.parseInt(lengthInSeconds)
let minutes = Number.parseInt(lengthInSeconds / 60)
let seconds = Number.parseInt(lengthInSeconds - Number.parseInt(minutes * 60))
if (seconds < 10) {
seconds = "0" + seconds
}
return minutes + ":" + seconds
},
},
})
</script>
<style scoped>
.player__container {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(4, min-content);
gap: 2em 0em;
grid-template-areas:
"."
"."
"."
".";
}
.cover-art__container img {
height: 256px;
width: 256px;
}
.controls {
align-items: center;
justify-content: center;
display: grid;
grid-template-columns: repeat(3, min-content);
grid-template-rows: 1fr;
gap: 0em 4em;
grid-template-areas: ". . .";
}
progress.progress-played,
progress.progress-seeking,
progress.progress-buffered {
height: 24px;
}
progress.progress-played::-webkit-progress-bar,
progress.progress-seeking::-webkit-progress-bar,
progress.progress-buffered::-webkit-progress-bar,
progress.progress-played::-webkit-progress-value,
progress.progress-seeking::-webkit-progress-value,
progress.progress-buffered::-webkit-progress-value {
border-radius: 0.75em;
}
progress.progress-played::-webkit-progress-bar,
progress.progress-seeking::-webkit-progress-bar,
progress.progress-buffered::-webkit-progress-bar {
padding: 0.25em;
}
progress.progress-played::-webkit-progress-bar {
background: transparent;
}
progress.progress-seeking::-webkit-progress-bar {
background: transparent;
}
progress.progress-buffered::-webkit-progress-bar {
background-color: #ffffff;
}
progress.progress-played::-webkit-progress-value {
background-color: #00ff00;
min-width: 15px;
}
progress.progress-seeking::-webkit-progress-value {
background-color: rgba(255, 0, 0, 0.5);
}
progress.progress-buffered::-webkit-progress-value {
background-color: rgba(0, 128, 64, 0.25);
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<aside>
<header>
<h3 class="text-gray-600 font-medium">Previous song</h3>
<playlist-card class="mt-4" :song="previousSong"></playlist-card>
</header>
<div class="playlist">
<h3 class="text-gray-600 font-medium">Up next</h3>
<div class="playlist-list_container mt-4">
<playlist-card v-for="song in nextSongs" :key="song.id" :song="song" @click="startSong(song)"></playlist-card>
</div>
</div>
</aside>
</template>
<script>
import { defineComponent } from "vue"
import PlaylistCard from "./PlaylistCard.vue"
export default defineComponent({
emits: [
'startSong'
],
props: {
playlistSongs: Array,
},
components: {
PlaylistCard,
},
setup(props, {emit}) {
let previousSong = {}
return { previousSong }
},
beforeMount() {},
mounted() {},
data() {
return {}
},
computed: {
nextSongs() {
return this.playlistSongs.slice(0, 4)
},
},
methods: {
startSong(song) {
console.log('Playlist: starting an arbitrary song...')
this.$emit('startSong', song)
},
},
})
</script>
<style scoped>
.playlist-list_container {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(4, min-content);
gap: 2em 0em;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="card flex flex-row items-center">
<div class="cover-art object-cover">
<img :src="song.cover_art_url" :alt="song.album" height="64" width="64">
</div>
<div class="flex flex-col ml-4">
<div class="font-bold">{{ song.title }}</div>
<div class="font-md">{{ song.artist }}</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue"
export default defineComponent({
emits: [],
props: {
song: Object,
},
components: {},
setup(props, {emit}) {
return {}
},
beforeMount() {},
mounted() {},
data() {
return {}
},
computed: {},
methods: {},
})
</script>
<style scoped>
.cover-art {
height: 64px;
width: 64px;
}
</style>

10
resources/js/app.js Normal file
View File

@ -0,0 +1,10 @@
window.Vue = require("vue");
import Notifications from "notiwind";
import App from "./App.vue";
const app = Vue.createApp(App);
app.use(Notifications);
//const el = document.getElementById("app");
const vm = app.mount("div#app");