Open In App

Build a Music app using VueJS

Last Updated : 21 Jun, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

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

Screenshot-2024-06-01-223222


Prerequisites

Approach

  • 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:


Screenshot-2024-06-01-223521


  • 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:


Next Article

Similar Reads