initial commit of a basic working audio player
This commit is contained in:
5
resources/css/app.css
Normal file
5
resources/css/app.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@import 'typography.css';
|
47
resources/css/typography.css
Normal file
47
resources/css/typography.css
Normal 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
81
resources/js/App.vue
Normal 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>
|
269
resources/js/Components/Player.vue
Normal file
269
resources/js/Components/Player.vue
Normal 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>
|
70
resources/js/Components/Playlist.vue
Normal file
70
resources/js/Components/Playlist.vue
Normal 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>
|
48
resources/js/Components/PlaylistCard.vue
Normal file
48
resources/js/Components/PlaylistCard.vue
Normal 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
10
resources/js/app.js
Normal 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");
|
Reference in New Issue
Block a user