Magren

Magren

Idealist & Garbage maker 🛸
twitter
jike

About CloudMusic

Preface#

This project is something I came up with one day while listening to music in my dormitory. It has been five years since I registered for Netease Cloud in 2016. Although this platform has been increasingly disappointing, I have developed an emotional attachment to it after listening for five years. So, I spent nearly a month during this vacation to write this project. As for why I wrote a mobile page, it's because I used my phone to listen to music at that time... So, I imitated the UI of the Android version of Netease Cloud Music. Although not all functions have been implemented, the core playback page has been created. This blog also records the implementation process and some pitfalls of this project.

The project is implemented based on Vue + Typescript + Vuetify UI.
Project address: CloudMusic
Implemented features:

  • Login
  • Get playlist
  • Create playlist
  • Delete/unfavorite playlist
  • Play songs
  • Rankings
  • Daily recommendations
  • Recommended playlists

Project screenshots:
1.png
2.png
3.png
4.png
5.png

Playback#

This can be considered as the core function, after all, a music platform cannot be considered good if it cannot play music.
First of all, the song selection can come from playlists, the carousel on the discovery page, or the selection on the playback page. The control of playing and pausing songs can be done through the playback tab at the bottom of other pages or on the playback page itself. Since using Prop and Emit would be too messy and complicated, I used Vuex here.

Vuex is a state management pattern library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.

In essence, it extracts the shared state of the components we need and manages it as a global singleton. We can modify the state or value of any component through Vuex.
So, I passed the selected song ID and the current playlist information ID into Vuex. In the playback page, I only need to listen for changes in the song ID in Vuex to play a new song. I also listen for changes in the pause status in Vuex to control the playback or pause of the audio component.

Playback Progress#

I used the slider component from Vuetify UI for the progress bar, and set its value using v-model.
In the audio component, I used @timeupdate to listen to the current playback timestamp of the audio. Then, I divided it by the total playback time and multiplied it by 100 to get the percentage of the current playback progress. Finally, I assigned this value to the slider component. The time on the left side of the progress bar is also converted from the current playback timestamp to time.

However, there is a problem here. I need to modify the playback progress by moving the slider. In the method bound to @timeupdate, I only assign the progress to the slider component unidirectionally. And this method keeps running while the music is playing. So, no matter how I move the slider, it will immediately jump to the current playback position. Therefore, I need to add a method to listen for whether the slider is being moved. When the slider is being moved, I won't assign the current playback progress to the slider. Instead, when I release the slider, I will convert the current progress of the slider to a timestamp and assign it back to the playback time of the audio.

My solution is to introduce @mousedown and @mouseup to the slider component. When the mouse is pressed, I assign a variable as true, and when it is released, I assign it as false. I modify the method bound to @timeupdate so that it only assigns the value to the slider component when this variable is false. However, I still need to modify the current playback time when sliding the slider. So, I introduced @change to the slider component. When the value of the slider is manually changed, this method is triggered. In this method, I modify the playback progress of the audio.

6.png

Lyrics Implementation#

The lyrics obtained from the backend are in the form of a string, with each line of lyrics separated by \n. So, by splitting the string using \n, I can get each line of lyrics. Each line of lyrics is divided by square brackets, and finally, the time is converted to a timestamp. The lyrics and timestamp are stored as a class in an array.

I set an index to record which line of lyrics it is. I use the offsetHeight of the lyrics component to determine the position of the line in the lyrics. I use the index multiplied by the height of each line of lyrics to determine whether to scroll.

In fact, it is to set the outer div to overflow hidden, and then use margin-top to achieve a scrolling effect. The value of margin-top is determined by subtracting the height of the line from the index multiplied by the height of each line of lyrics. At the same time, the transition property can be used to set the duration of the change.

For example: transition: margin-top 1s;
This means that the margin-top property will be completed within one second.

About Vue Custom Directives#

During the development process, I wanted to implement a feature where clicking outside a certain component would hide it. In other words, when the click is not on the component, a certain method would be executed.

When I searched for information, I found that Vue does not have such a directive. However, I can customize a directive to achieve this functionality.

First, let's take a look at the hook functions of a custom directive object:

  • bind: Called only once, when the directive is first bound to the element. It can be used for one-time initialization.
  • inserted: Called when the bound element is inserted into its parent node (only guaranteed to exist, but not necessarily inserted into the document).
  • update: Called when the VNode of the component that the directive belongs to is updated, but may occur before its child VNodes are updated. The value of the directive may or may not have changed. However, unnecessary template updates can be ignored by comparing the values before and after the update (see detailed hook function parameters below).
  • componentUpdated: Called after all the VNodes of the component that the directive belongs to, as well as its child VNodes, have been updated.
  • unbind: Called only once, when the directive is unbound from the element.

The hook functions above are from the Vue documentation. Although there are many hook functions and variables, they are all optional. Only select the ones that are needed. In implementing this feature, I only used bind and unbind, and only used el and binding variables.

import { DirectiveOptions } from "vue";
// Custom directive clickoutside, executes the bound method when clicking outside the current element
const clickoutside: DirectiveOptions = {
  // Initialize the directive
  bind(el: any, binding: any, vnode) {
    function documentHandler(e: any) {
      // Check if the clicked element is itself, if so, return
      if (el.contains(e.target)) {
        return false;
      }
      // Check if a function is bound in the directive
      if (binding.expression) {
        // If a function is bound, call that function
        binding.value(e);
      }
    }
    // Bind a private variable to the current element to facilitate event unbinding in unbind
    el.vueClickOutside = documentHandler;
    document.addEventListener("click", documentHandler);
  },
  unbind(el: any, binding) {
    // Unbind the event listener
    document.removeEventListener("click", el.vueClickOutside);
    delete el.vueClickOutside;
  },
};

export default clickoutside;

Import where needed:

@Component({
  directives: {
    clickoutside,
  },
})

Usage (when clicking outside the div, call the outside method):

<div v-clickoutside="outside"></div>

After I implemented this, I found that Vuetify already integrated such a directive, which I could use directly. This is the consequence of not reading the documentation carefully.

Finally⭐️#

🙏 Thanks to the Netease Cloud Music API for providing the interface and documentation.
Also, this project is made for personal learning purposes. For normal use, please go to Netease Cloud Music👈

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.