Horje
Build a Music app using VueJS

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:




Reffered: https://www.geeksforgeeks.org


Project

Related
Word Cloud Generator from Given Passage Word Cloud Generator from Given Passage
Notes Maker App using MEAN Stack Notes Maker App using MEAN Stack
30 Scratch Project Ideas for Kids (Age 5 - 15) &amp; Beginners 30 Scratch Project Ideas for Kids (Age 5 - 15) &amp; Beginners
TIF to JPG Converter TIF to JPG Converter
Build a Flip the Card &amp; Match Done Game using React Build a Flip the Card &amp; Match Done Game using React

Type:
Geek
Category:
Coding
Sub Category:
Tutorial
Uploaded by:
Admin
Views:
17