We’ll guide you through the process of building a music player application using Vue.js, a popular JavaScript framework for building user interfaces. The application will allow users to play, pause, and skip through a collection of songs, as well as view and select songs from a library.
Preview
PrerequisitesApproach - Setting up the project structure and organizing the components.
- Implementing the core functionality, such as playing and pausing songs, updating the song progress, and managing the song library.
- Styling the application using CSS to create an intuitive and visually appealing user interface.
Steps to setup the project- Create a new Vue.js project
npm install -g @vue/cli - Once the project is created, navigate into the project directory and install the required dependencies:
vue create music-player Project Structure:
- Install additional dependencies:
npm install uuid npm install --save @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome npm install @fortawesome/vue-fontawesome @fortawesome/fontawesome-svg-core Updated dependencies will look like:
"dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/vue-fontawesome": "^3.0.6", "core-js": "^3.8.3", "uuid": "^9.0.1", "vue": "^3.2.13" }, Manage and replace files
- replace src/App.vue,main.js
- create src/data.js
- create src/components/LibrarySong.vue, MusicPlayer.vue, MusicSong.vue
JavaScript
// LibrarySong.vue
<template>
<div
class="library-song"
:class="{ 'library-song--active': song.active }"
@click="selectSong"
>
<img :src="song.cover" alt="Cover art" class="library-song__cover" />
<div class="library-song__info">
<h3 class="library-song__title">{{ song.name }}</h3>
<h4 class="library-song__artist">{{ song.artist }}</h4>
</div>
<h4 v-if="isPlaying && song.id === id" class="library-song__playing">
Playing
</h4>
</div>
</template>
<script>
export default {
name: 'LibrarySong',
props: {
song: {
type: Object,
required: true,
},
libraryStatus: {
type: Boolean,
required: true,
},
setLibraryStatus: {
type: Function,
required: true,
},
setSongs: {
type: Function,
required: true,
},
isPlaying: {
type: Boolean,
required: true,
},
setCurrentSong: {
type: Function,
required: true,
},
id: {
type: String,
required: true,
},
},
methods: {
async selectSong() {
await this.setCurrentSong(this.song);
if (this.isPlaying) {
this.$emit('pauseAudio');
this.$emit('setAudioSource', this.song.audio);
this.$emit('playAudio');
}
this.setLibraryStatus(false);
},
},
};
</script>
<style scoped>
.library-song {
display: flex;
align-items: center;
padding: 1rem 2rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.library-song:hover {
background-color: #f1f1f1;
}
.library-song__cover {
width: 4rem;
height: 4rem;
border-radius: 0.5rem;
margin-right: 1rem;
}
.library-song__info {
flex-grow: 1;
}
.library-song__title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.library-song__artist {
font-size: 1rem;
color: #666;
}
.library-song__playing {
font-size: 1rem;
color: #2ab3bf;
font-weight: 600;
}
</style>
JavaScript
// MusicPlayer.vue
<template>
<div class="music-player">
<div class="time-control">
<p class="time-control__current">{{ getTime(songInfo.currentTime) }}</p>
<div class="track">
<div
class="track__bar"
:style="{
background:
`linear-gradient(to right, ${currentSong.color[0]}, ${currentSong.color[1]})`
}"
>
<input
type="range"
min="0"
:max="songInfo.duration || 0"
:value="songInfo.currentTime"
@input="dragHandler"
class="track__input"
/>
<div class="track__animate" :style="trackAnim"></div>
</div>
</div>
<p class="time-control__total">
{{ songInfo.duration ? getTime(songInfo.duration) : '00:00' }}
</p>
</div>
<div class="play-control">
<FontAwesomeIcon
@click="skipTrackHandler('skip-back')"
size="2x"
class="play-control__button play-control__button--skip-back"
:icon="faAngleLeft"
/>
<FontAwesomeIcon
v-if="!isPlaying"
@click="playSongHandler"
size="2x"
class="play-control__button play-control__button--play"
:icon="faPlay"
/>
<FontAwesomeIcon
v-else
@click="playSongHandler"
size="2x"
class="play-control__button play-control__button--pause"
:icon="faPause"
/>
<FontAwesomeIcon
@click="skipTrackHandler('skip-forward')"
size="2x"
class="play-control__button play-control__button--skip-forward"
:icon="faAngleRight"
/>
</div>
</div>
</template>
<script>
import { faPlay, faAngleLeft, faAngleRight, faPause } from '@fortawesome/free-solid-svg-icons'
export default {
name: 'MusicPlayer',
props: {
currentSong: {
type: Object,
required: true
},
isPlaying: {
type: Boolean,
required: true
},
setIsPlaying: {
type: Function,
required: true
},
audioRef: {
type: Object,
required: true
},
songInfo: {
type: Object,
required: true
},
songs: {
type: Array,
required: true
},
setCurrentSong: {
type: Function,
required: true
},
setSongs: {
type: Function,
required: true
},
setSongInfo: {
type: Function,
required: true
}
},
data() {
return {
faPlay,
faAngleLeft,
faAngleRight,
faPause
}
},
methods: {
dragHandler(e) {
const currentTime = e.target.value
this.$emit('updateAudioCurrentTime', currentTime)
},
playSongHandler() {
if (this.isPlaying) {
this.$emit('pauseAudio')
this.setIsPlaying(false)
} else {
this.$emit('playAudio')
this.setIsPlaying(true)
}
},
getTime(time) {
return `${Math.floor(time / 60)}:${('0' + Math.floor(time % 60)).slice(-2)}`
},
skipTrackHandler(direction) {
let currentIndex = this.songs
.findIndex((song) => song.id === this.currentSong.id)
if (direction === 'skip-forward') {
this.setCurrentSong(this.songs[(currentIndex + 1) % this.songs.length])
this.activeLibraryHandler(this
.songs[(currentIndex + 1) % this.songs.length])
}
if (direction === 'skip-back') {
if ((currentIndex - 1) % this.songs.length === -1) {
this.setCurrentSong(this.songs[this.songs.length - 1])
this.activeLibraryHandler(this.songs[this.songs.length - 1])
return
}
this.setCurrentSong(this
.songs[(currentIndex - 1) % this.songs.length])
this.activeLibraryHandler(this
.songs[(currentIndex - 1) % this.songs.length])
}
if (this.isPlaying) this.$emit('playAudio')
},
activeLibraryHandler(nextPrev) {
const newSongs = this.songs.map((song) => {
if (song.id === nextPrev.id) {
return { ...song, active: true }
} else {
return { ...song, active: false }
}
})
this.setSongs(newSongs)
}
},
computed: {
trackAnim() {
const percentage = (this.songInfo.currentTime /
this.songInfo.duration) * 100
return {
width: `${percentage}%`
}
}
}
}
</script>
<style scoped>
.music-player {
width: 100%;
max-width: 800px;
background-color: #fff;
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.time-control {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.time-control__current,
.time-control__total {
font-size: 1.2rem;
color: #666;
}
.track {
width: 100%;
height: 1rem;
background-color: #eee;
border-radius: 0.5rem;
margin: 0 1rem;
position: relative;
cursor: pointer;
}
.track__bar {
height: 100%;
background: linear-gradient(to right, #2ab3bf, #205950);
border-radius: 0.5rem;
display: flex;
align-items: center;
}
.track__input {
width: 100%;
-webkit-appearance: none;
background-color: transparent;
cursor: pointer;
}
.track__input:focus {
outline: none;
}
.track__input::-webkit-slider-thumb {
-webkit-appearance: none;
height: 1.5rem;
width: 1.5rem;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.track__input::-moz-range-thumb {
height: 1.5rem;
width: 1.5rem;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.track__animate {
background: linear-gradient(to right, #2ab3bf, #205950);
width: 100%;
height: 100%;
border-radius: 0.5rem;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.play-control {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1.5rem;
}
.play-control__button {
color: #2ab3bf;
cursor: pointer;
margin: 0 0.5rem;
transition: color 0.3s ease;
}
.play-control__button:hover {
color: #205950;
}
.play-control__button--skip-back,
.play-control__button--skip-forward {
font-size: 1.5rem;
}
.play-control__button--play,
.play-control__button--pause {
font-size: 2rem;
}
</style>
JavaScript
// MusicSong.vue
<template>
<div class="music-song">
<img :src="currentSong.cover"
alt="Cover art" class="music-song__cover" />
<h2 class="music-song__title">{{ currentSong.name }}</h2>
<h3 class="music-song__artist">{{ currentSong.artist }}</h3>
</div>
</template>
<script>
export default {
name: 'MusicSong',
props: {
currentSong: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
.music-song {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 2rem;
}
.music-song__cover {
width: 300px;
height: 300px;
object-fit: cover;
border-radius: 50%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.music-song__title {
margin-top: 1.5rem;
font-size: 2rem;
font-weight: 600;
}
.music-song__artist {
font-size: 1.5rem;
color: #666;
}
</style>
JavaScript
// data.js
export default function chillHop() {
return [
{
name: 'Beaver Creek',
cover:
'https://chillhop.com/wp-content/uploads/2020/09/0255e8b8c74c90d4a27c594b3452b2daafae608d-1024x1024.jpg',
artist: 'Aso, Middle School, Aviino',
audio: 'https://mp3.chillhop.com/serve.php/?mp3=10075',
color: ['#205950', '#2ab3bf'],
id: '0',
active: true,
},
{
name: 'Daylight',
cover:
'https://chillhop.com/wp-content/uploads/2020/07/ef95e219a44869318b7806e9f0f794a1f9c451e4-1024x1024.jpg',
artist: 'Aiguille',
audio: 'https://mp3.chillhop.com/serve.php/?mp3=9272',
color: ['#a6c4ff', '#a2ffec'],
id: '1',
active: false,
},
{
name: 'Keep Going',
cover:
'https://chillhop.com/wp-content/uploads/2020/07/ff35dede32321a8aa0953809812941bcf8a6bd35-1024x1024.jpg',
artist: 'Swørn',
audio: 'https://mp3.chillhop.com/serve.php/?mp3=9222',
color: ['#cd607d', '#c94043'],
id: '2',
active: false,
},
{
name: 'Nightfall',
cover:
'https://chillhop.com/wp-content/uploads/2020/07/ef95e219a44869318b7806e9f0f794a1f9c451e4-1024x1024.jpg',
artist: 'Aiguille',
audio: 'https://mp3.chillhop.com/serve.php/?mp3=9148',
color: ['#a6c4ff', '#a2ffec'],
id: '3',
active: false,
},
{
name: 'Reflection',
cover:
'https://chillhop.com/wp-content/uploads/2020/07/ff35dede32321a8aa0953809812941bcf8a6bd35-1024x1024.jpg',
artist: 'Swørn',
audio: 'https://mp3.chillhop.com/serve.php/?mp3=9228',
color: ['#cd607d', '#c94043'],
id: '4',
active: false,
},
{
name: 'Under the City Stars',
cover:
'https://chillhop.com/wp-content/uploads/2020/09/0255e8b8c74c90d4a27c594b3452b2daafae608d-1024x1024.jpg',
artist: 'Aso, Middle School, Aviino',
audio: 'https://mp3.chillhop.com/serve.php/?mp3=10074',
color: ['#205950', '#2ab3bf'],
id: '5',
active: false,
},
];
}
JavaScript
// App.vue
<template>
<div class="app">
<nav class="navbar">
<h1 class="navbar__title">GeeksforGeeks Music Player</h1>
<button class="btn btn--library" @click="toggleLibraryStatus">
<h4>{{ libraryStatus ? '' : '' }}</h4>
</button>
</nav>
<div class="content">
<div class="main-content">
<MusicSong :current-song="currentSong" />
<MusicPlayer
:id="currentSong.id"
:songs="songs"
:song-info="songInfo"
@update-song-info="updateSongInfo"
:audio-ref="audioRef"
:is-playing="isPlaying"
@set-is-playing="toggleIsPlaying"
:current-song="currentSong"
@set-current-song="setCurrentSong"
@set-songs="setSongs"
:setIsPlaying="setIsPlaying"
:audioRef="audioRef"
@pauseAudio="pauseAudio"
@playAudio="playAudio"
@setAudioSource="setAudioSource"
@updateAudioCurrentTime="updateAudioCurrentTime"
:set-song-info="setSongInfo"
/>
</div>
<div class="library" :class="{ 'library--active': libraryStatus }">
<h2 class="library__heading">Library</h2>
<div class="library__songs">
<LibrarySong
v-for="song in songs"
:key="song.id"
:song="song"
:library-status="libraryStatus"
:set-library-status="setLibraryStatus"
:is-playing="isPlaying"
:set-songs="setSongs"
:audio-ref="audioRef"
:songs="songs"
:set-current-song="setCurrentSong"
:id="currentSong.id"
@pauseAudio="pauseAudio"
@playAudio="playAudio"
@setAudioSource="setAudioSource"
:set-song-info="setSongInfo"
/>
</div>
</div>
</div>
<audio ref="audioRef" @timeupdate="timeUpdateHandler" @loadedmetadata="timeUpdateHandler" />
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import MusicPlayer from './components/MusicPlayer.vue'
import MusicSong from './components/MusicSong.vue'
import LibrarySong from './components/LibrarySong.vue'
import chillHop from './data'
export default {
name: 'App',
components: {
MusicPlayer,
MusicSong,
LibrarySong
},
setup() {
const audioRef = ref(null)
const isPlaying = ref(false)
const libraryStatus = ref(false)
const currentSong = ref(chillHop()[0])
const songs = reactive(chillHop())
const songInfo = reactive({
currentTime: 0,
duration: 0,
animationPercentage: 0
})
const toggleLibraryStatus = () => {
libraryStatus.value = !libraryStatus.value // Toggle libraryStatus between true and false
}
const setLibraryStatus = (status) => {
libraryStatus.value = status
}
const toggleIsPlaying = (status) => {
isPlaying.value = status
}
const setIsPlaying = (status) => {
isPlaying.value = status
}
const setCurrentSong = (song) => {
currentSong.value = song
audioRef.value.src = song.audio
}
const setSongs = (updatedSongs) => {
for (const key in updatedSongs) {
songs[key] = updatedSongs[key]
}
}
const updateSongInfo = (newInfo) => {
songInfo.currentTime = newInfo.currentTime
songInfo.duration = newInfo.duration
songInfo.animationPercentage = newInfo.animationPercentage
}
const timeUpdateHandler = (e) => {
const current = e.target.currentTime
const duration = e.target.duration
const roundedCurrent = Math.round(current)
const roundedDuration = Math.round(duration)
const animationPercentage = Math.round((roundedCurrent / roundedDuration) * 100)
updateSongInfo({
currentTime: current,
duration: duration,
animationPercentage: animationPercentage
})
}
const playAudio = () => {
audioRef.value.play()
}
const pauseAudio = () => {
audioRef.value.pause()
}
const setAudioSource = (source) => {
audioRef.value.src = source
}
const updateAudioCurrentTime = (time) => {
audioRef.value.currentTime = time
}
onMounted(() => {
audioRef.value.src = currentSong.value.audio
})
return {
audioRef,
isPlaying,
libraryStatus,
currentSong,
songs,
songInfo,
toggleLibraryStatus,
setLibraryStatus,
toggleIsPlaying,
setIsPlaying,
setCurrentSong,
setSongs,
updateSongInfo,
timeUpdateHandler,
playAudio,
pauseAudio,
setAudioSource,
updateAudioCurrentTime
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', sans-serif;
background-color: #f1f1f1;
color: #333;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.navbar {
background-color: #2ab3bf;
color: #fff;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar__title {
font-size: 2rem;
font-weight: 600;
}
.content {
flex-grow: 1;
display: flex;
position: relative;
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
}
.btn {
background-color: #205950;
color: #fff;
border: none;
padding: 0.5rem 1rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
}
.btn:hover {
background-color: #18463e;
}
.btn--library {
background-color: #2ab3bf;
}
.btn--library:hover {
background-color: #23a2ad;
}
.library-container {
position: absolute;
top: 0;
left: 0;
width: 20%;
height: 100%;
background-color: #fff;
border-right: 1px solid #ddd;
padding: 2rem;
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.library-container--open {
transform: translateX(0);
}
.library {
height: 100%;
overflow-y: auto;
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
}
.main-content--with-library {
justify-content: flex-start; /* Align content to the left when library is active */
}
.library {
height: 100%;
overflow-y: auto;
}
/* Adjustments for smaller screens */
@media only screen and (max-width: 768px) {
.music-player {
padding: 1rem;
}
.time-control {
flex-direction: column;
align-items: stretch;
}
.time-control__current,
.time-control__total {
margin: 0.5rem 0;
text-align: center;
}
.track {
margin: 0.5rem 0;
}
.play-control {
margin-top: 1rem;
}
}
/* Adjustments for even smaller screens */
@media only screen and (max-width: 768px) {
.content {
flex-direction: column;
align-items: center;
}
.main-content {
padding: 1rem;
}
.library {
width: 100%;
max-width: 100%;
margin-top: 1rem;
}
}
</style>
JavaScript
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPlay, faAngleLeft, faAngleRight, faPause } from '@fortawesome/free-solid-svg-icons';
library.add(faPlay, faAngleLeft, faAngleRight, faPause);
const app = createApp(App);
app.component('FontAwesomeIcon', FontAwesomeIcon);
app.mount('#app');
Output:
|