I am working on an audio player with Vue 3 and the Napster API.
Project details
I have made the vinyl spin with the help of a CSS keyframes-based animation and the isSpinning
computed property.
I want the vinyl to stop spinning once the end of the current track is reached, which is why isSpinning
has this "formula":
isSpinning() {
return this.isPlaying && !this.player.ended;
}
const musicApp = {
data() {
return {
player: new Audio(),
trackCount: 0,
tracks: [],
muted: false,
isPlaying: false
};
},
methods: {
async getTracks() {
try {
const response = await axios
.get(
"https://api.napster.com/v2.1/tracks/top?apikey=ZTk2YjY4MjMtMDAzYy00MTg4LWE2MjYtZDIzNjJmMmM0YTdm"
)
.catch((error) => {
console.log(error);
});
this.tracks = response;
this.tracks = response.data.tracks;
} catch (error) {
console.log(error);
}
},
nextTrack() {
if (this.trackCount < this.tracks.length - 1) {
this.trackCount ;
this.setPlayerSource();
this.playPause();
}
},
prevTrack() {
if (this.trackCount >= 1) {
this.trackCount--;
this.setPlayerSource();
this.playPause();
}
},
setPlayerSource() {
this.player.src = this.tracks[this.trackCount].previewURL;
},
playPause() {
if (this.player.paused) {
this.isPlaying = true;
this.player.play();
} else {
this.isPlaying = false;
this.player.pause();
}
},
toggleMute() {
this.player.muted = !this.player.muted;
this.muted = this.player.muted;
}
},
async created() {
await this.getTracks();
this.setPlayerSource();
},
computed: {
isSpinning() {
return this.isPlaying && !this.player.ended;
}
}
};
Vue.createApp(musicApp).mount("#audioPlayer");
html,
body {
margin: 0;
padding: 0;
font-size: 16px;
}
body * {
box-sizing: border-box;
font-family: "Montserrat", sans-serif;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.player-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #2998ff;
background-image: linear-gradient(62deg, #2998ff 0%, #5966eb 100%);
}
#audioPlayer {
width: 300px;
height: 300px;
border-radius: 8px;
position: relative;
overflow: hidden;
background-color: #00ca81;
background-image: linear-gradient(160deg, #00ca81 0%, #ffffff 100%);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
display: flex;
flex-direction: column;
align-items: center;
}
.volume {
color: #ff0057;
opacity: 0.9;
display: inline-block;
width: 20px;
font-size: 20px;
position: absolute;
top: 5px;
right: 6px;
cursor: pointer;
}
.album {
width: 100%;
flex: 1;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.album-items {
padding: 0 10px;
text-align: center;
}
.cover {
width: 150px;
height: 150px;
margin: auto;
box-shadow: 0px 5px 12px 0px rgba(0, 0, 0, 0.17);
border-radius: 50%;
background: url("https://w7.pngwing.com/pngs/710/955/png-transparent-vinyl-record-artwork-phonograph-record-compact-disc-lp-record-disc-jockey-symbol-miscellaneous-classical-music-sound.png") center top transparent;
background-size: cover;
}
.cover.spinning {
webkit-animation: spin 6s linear infinite;
/* Safari */
animation: spin 6s linear infinite;
}
.info {
width: 100%;
padding-top: 5px;
color: #000;
opacity: 0.85;
}
.info h1 {
font-size: 11px;
margin: 5px 0 0 0;
}
.info h2 {
font-size: 10px;
margin: 3px 0 0 0;
}
.to-bottom {
width: 100%;
margin-top: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.track {
background-color: #ff0057;
opacity: 0.9;
height: 3px;
width: 100%;
}
.controls {
width: 150px;
display: flex;
height: 60px;
justify-content: space-between;
align-items: center;
}
.controls .navigate {
display: flex;
box-shadow: 1px 2px 7px 2px rgba(0, 0, 0, 0.09);
width: 33px;
height: 33px;
line-height: 1;
color: #ff0057;
cursor: pointer;
background: #fff;
opacity: 0.9;
border-radius: 50%;
text-align: center;
justify-content: center;
align-items: center;
}
.controls .navigate.disabled {
pointer-events: none;
color: #606060;
background-color: #f7f7f7;
}
.controls .navigate.navigate-play {
width: 38px;
height: 38px;
}
.navigate-play .fa-play {
margin-left: 3px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;500&display=swap" rel="stylesheet">
<div >
<div id="audioPlayer">
<span @click="toggleMute">
<i v-show="!muted" ></i>
<i v-show="muted" ></i>
</span>
<div >
<div >
<div :></div>
<div >
<h1>{{tracks[trackCount].name}}</h1>
<h2>{{tracks[trackCount].artistName}}</h2>
</div>
</div>
</div>
<div >
<div ></div>
<div >
<div : @click="prevTrack">
<i ></i>
</div>
<div @click="playPause">
<i v-show="!isPlaying" ></i>
<i v-show="isPlaying" ></i>
</div>
<div : @click="nextTrack">
<i ></i>
</div>
</div>
</div>
</div>
</div>
The problem
But, to my surprise, the app is unaware of the fact that the value of this.player.ended
has changed (or is supposed to).
What am I doing wrong?
CodePudding user response:
An Audio
object will not function in the same manner as normal JavaScript objects in Vue, as the internals that Vue uses to abstract away state change observation will not be maintained when the Audio
object changes state. In other words, the thing that allows Vue to detect the Audio
object switching from ended === false
to ended === true
won't work, preventing Vue from knowing that the component needs to be updated.
If you wish to observe a change in ended
state, then you'll want to add a custom event listener to the object in your created
hook to toggle the spinning state and simply remove the ended
check:
const musicApp = {
data() {
return {
player: new Audio(),
trackCount: 0,
tracks: [],
muted: false,
isPlaying: false
};
},
methods: {
async getTracks() {
try {
const response = await axios
.get(
"https://api.napster.com/v2.1/tracks/top?apikey=ZTk2YjY4MjMtMDAzYy00MTg4LWE2MjYtZDIzNjJmMmM0YTdm"
)
.catch((error) => {
console.log(error);
});
this.tracks = response;
this.tracks = response.data.tracks;
} catch (error) {
console.log(error);
}
},
nextTrack() {
if (this.trackCount < this.tracks.length - 1) {
this.trackCount ;
this.setPlayerSource();
this.playPause();
}
},
prevTrack() {
if (this.trackCount >= 1) {
this.trackCount--;
this.setPlayerSource();
this.playPause();
}
},
setPlayerSource() {
this.player.src = this.tracks[this.trackCount].previewURL;
},
playPause() {
if (this.player.paused) {
this.isPlaying = true;
this.player.play();
} else {
this.isPlaying = false;
this.player.pause();
}
},
toggleMute() {
this.player.muted = !this.player.muted;
this.muted = this.player.muted;
}
},
async created() {
await this.getTracks();
this.setPlayerSource();
this.player.addEventListener('ended', () => {
this.isPlaying = false;
});
},
computed: {
isSpinning() {
return this.isPlaying;
}
}
};
Vue.createApp(musicApp).mount("#audioPlayer");
html,
body {
margin: 0;
padding: 0;
font-size: 16px;
}
body * {
box-sizing: border-box;
font-family: "Montserrat", sans-serif;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.player-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #2998ff;
background-image: linear-gradient(62deg, #2998ff 0%, #5966eb 100%);
}
#audioPlayer {
width: 300px;
height: 300px;
border-radius: 8px;
position: relative;
overflow: hidden;
background-color: #00ca81;
background-image: linear-gradient(160deg, #00ca81 0%, #ffffff 100%);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
display: flex;
flex-direction: column;
align-items: center;
}
.volume {
color: #ff0057;
opacity: 0.9;
display: inline-block;
width: 20px;
font-size: 20px;
position: absolute;
top: 5px;
right: 6px;
cursor: pointer;
}
.album {
width: 100%;
flex: 1;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.album-items {
padding: 0 10px;
text-align: center;
}
.cover {
width: 150px;
height: 150px;
margin: auto;
box-shadow: 0px 5px 12px 0px rgba(0, 0, 0, 0.17);
border-radius: 50%;
background: url("https://w7.pngwing.com/pngs/710/955/png-transparent-vinyl-record-artwork-phonograph-record-compact-disc-lp-record-disc-jockey-symbol-miscellaneous-classical-music-sound.png") center top transparent;
background-size: cover;
}
.cover.spinning {
webkit-animation: spin 6s linear infinite;
/* Safari */
animation: spin 6s linear infinite;
}
.info {
width: 100%;
padding-top: 5px;
color: #000;
opacity: 0.85;
}
.info h1 {
font-size: 11px;
margin: 5px 0 0 0;
}
.info h2 {
font-size: 10px;
margin: 3px 0 0 0;
}
.to-bottom {
width: 100%;
margin-top: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.track {
background-color: #ff0057;
opacity: 0.9;
height: 3px;
width: 100%;
}
.controls {
width: 150px;
display: flex;
height: 60px;
justify-content: space-between;
align-items: center;
}
.controls .navigate {
display: flex;
box-shadow: 1px 2px 7px 2px rgba(0, 0, 0, 0.09);
width: 33px;
height: 33px;
line-height: 1;
color: #ff0057;
cursor: pointer;
background: #fff;
opacity: 0.9;
border-radius: 50%;
text-align: center;
justify-content: center;
align-items: center;
}
.controls .navigate.disabled {
pointer-events: none;
color: #606060;
background-color: #f7f7f7;
}
.controls .navigate.navigate-play {
width: 38px;
height: 38px;
}
.navigate-play .fa-play {
margin-left: 3px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;500&display=swap" rel="stylesheet">
<div >
<div id="audioPlayer">
<span @click="toggleMute">
<i v-show="!muted" ></i>
<i v-show="muted" ></i>
</span>
<div >
<div >
<div :></div>
<div >
<h1>{{tracks[trackCount].name}}</h1>
<h2>{{tracks[trackCount].artistName}}</h2>
</div>
</div>
</div>
<div >
<div ></div>
<div >
<div : @click="prevTrack">
<i ></i>
</div>
<div @click="playPause">
<i v-show="!isPlaying" ></i>
<i v-show="isPlaying" ></i>
</div>
<div : @click="nextTrack">
<i ></i>
</div>
</div>
</div>
</div>
</div>
CodePudding user response:
Try to add ended
event instead
https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement#events
const audioElement = new Audio('car_horn.wav');
audioElement.addEventListener('ended', () => {
this.isPlaying = false
})