<template> <div id="player" class="flex-column" v-show="selectedTrack._id || selectedRadio._id"> <input type="range" id="slider" min="0" max="100" step="0.1" v-model="selectedTrack.percent" @change="slideChanged" /> <div id="playerBar" class="flex-row"> <div class="flex-row grow"> <img class="cover pointer" :src="cover" :title="selectedTrack.parent.title" @click="gotoContainer" /> <div v-if="selectedTrack._id" class="flex-column"> <b>{{ selectedTrack.title }}</b> from <b>{{ selectedTrack.parent.title }}</b> </div> </div> <div id="playerControls" class="flex-row center"> <button @click="switchShuffle" title="Shuffle mode" v-if="selectedTrack._id"> <img src="static/icons/media-shuffle-dark.svg" v-show="$store.getters['player/shuffle']" class="small" /> <img src="static/icons/media-consecutive-dark.svg" v-show="$store.getters['player/shuffle'] == false" class="small" /> </button> <button @click="prevTrack" title="Back" v-if="selectedTrack._id"> <awesome-icon icon="backward" /> </button> <button @click="togglePlaying" :title="audio.paused ? 'Play' : 'Pause'"> <awesome-icon icon="play" size="2x" v-if="audio.paused" /> <awesome-icon icon="pause" size="2x" v-else /> </button> <button @click="nextTrack" title="Forward" v-if="selectedTrack._id"> <awesome-icon icon="forward" /> </button> <button @click="switchRepeatType" title="Repeat mode" v-if="selectedTrack._id"> <img src="static/icons/media-repeat-dark.svg" class="small" v-show="$store.getters['player/repeatType'] == 'all'" /> <img src="static/icons/media-repeat-song-dark.svg" class="small" v-show="$store.getters['player/repeatType'] == 'one'" /> <img src="static/icons/media-no-repeat-dark.svg" class="small" v-show="$store.getters['player/repeatType'] == 'none'" /> </button> </div> <div class="flex-row ma-right hideOnMobilePortrait grow right" v-show="selectedTrack.title"> {{ formatedP }} | {{ formatedD }} </div> </div> <audio preload="auto" ref="audioControl" type="audio/mpeg" @ended="nextTrack" @canplay="play" @playing="playing" @durationchange="durationChanged" @timeupdate="timeUpdate" src></audio> </div> </template> <script> export default { name: "PlayerControl", data() { return { audio: {}, duration: 0, progress: 0, intervalProgress: 0, intervalState: 0, preConvert: false, }; }, mounted() { this.$nextTick(() => { this.audio = this.$refs.audioControl; }); this.setMediaSession(); }, methods: { play() { this.audio.play(); }, pause() { this.audio.pause(); window.clearInterval(this.intervalProgress); window.clearInterval(this.intervalState); this.pushState(); }, durationChanged() { this.duration = this.audio.duration; }, playing() { window.clearInterval(this.intervalProgress); window.clearInterval(this.intervalState); this.intervalProgress = setInterval(() => { this.progress = this.audio.currentTime; this.selectedTrack.percent = (100 / this.duration) * this.progress; }, 500); if (this.currentUser._id) { this.intervalState = setInterval(() => { this.pushState(); }, 10000); } }, audioReset() { this.audio.pause(); this.audio.src = ""; this.$store.commit("tracks/resetSelectedTrack"); }, slideChanged() { this.audio.pause(); this.$store.dispatch("tracks/skip"); }, skipToPercent(percent) { let was_paused = this.audio.paused; this.audio.pause(); let currentTime = Math.floor((this.duration * percent) / 100); this.audio.currentTime = currentTime; this.progress = currentTime; if (!was_paused) { this.audio.play(); } }, skipToSecond(second) { let was_paused = this.audio.paused; this.audio.pause(); this.audio.currentTime = second; if (!was_paused) { this.audio.play(); } }, playRadio(radio) { this.$store.commit("tracks/resetSelectedTrack"); this.audio.pause(); this.audio.src = radio.url; let item = { id: this.selectedRadio._id, cover128: this.selectedRadio.cover128, name: this.selectedRadio.name, type: "radio", }; this.$store.dispatch("user/saveHistoryItem", item); }, playTrack(track) { this.preConvert = false; this.$store.commit("radios/resetSelectedRadio"); let url = this.$store.getters.server + "/api/tracks/" + track._id + "/stream/" + this.audioBpm; this.audio.pause(); this.audio.src = url; this.pushHistoryItem(); if (this.selectedTrack.parent.progress) { this.skipToSecond(this.selectedTrack.parent.progress.progress); this.selectedTrack.parent.progress = undefined; } else { // Try to fix SAFARI this.audio.play(); } }, pushHistoryItem() { if (!this.currentUser._id) { return; } let item = { id: this.currentTrackParent._id, type: this.currentTrackParentType, }; if (item.type == "album") { item.title = this.currentTrackParent.title; item.covers = { cover128: this.currentTrackParent.covers.cover128 }; } else { item.name = this.currentTrackParent.name; item.covers = { cover256: this.currentTrackParent.covers.cover256 }; } this.$store.dispatch("user/saveHistoryItem", item); item = { id: this.selectedTrack._id, type: "track", title: this.selectedTrack.title, covers: { cover32: this.selectedTrack.parent.covers.cover32 }, parent: { _id: this.selectedTrack.parent._id, title: this.selectedTrack.parent.title, }, }; this.$store.dispatch("user/saveHistoryItem", item); }, nextTrack() { if (this.$store.getters["player/repeatType"] == "one") { this.skipToPercent(0); this.audio.play(); } else { this.$store.dispatch("tracks/playNextTo", this.selectedTrack); } }, prevTrack() { this.$store.dispatch("tracks/playPrevTo", this.selectedTrack); }, togglePlaying() { if (!this.audio) { return; } if (!this.audio.paused) { this.pause(); } else if (this.audio.src != "") { this.audio.play(); } }, reset() { window.clearInterval(this.intervalProgress); window.clearInterval(this.intervalState); if (!this.audio.paused) { this.audio.pause(); } this.audio.src = ""; }, setMediaSession() { if ("mediaSession" in navigator) { let me = this; navigator.mediaSession.setActionHandler("play", function () { me.play(); }); navigator.mediaSession.setActionHandler("pause", function () { me.pause(); }); navigator.mediaSession.setActionHandler("seekto", function (details) { if (details.fastSeek && "fastSeek" in me.audio) { me.audio.fastSeek(details.seekTime); return; } me.audio.currentTime = details.seekTime; }); navigator.mediaSession.setActionHandler("previoustrack", function () { me.prevTrack(); }); navigator.mediaSession.setActionHandler("nexttrack", function () { me.nextTrack(); }); } }, gotoContainer() { if (this.currentUser._id) { switch (this.selectedTrack.parentType) { case "album": this.$router.push("/albums?id=" + this.selectedTrack.parent._id); break; case "artist": this.$router.push( "/artists?id=" + this.selectedTrack.parent.parent._id ); break; } } }, switchShuffle() { this.$store.dispatch("player/toggleShuffleMode"); this.saveUserSettings(); }, switchRepeatType() { this.$store.dispatch("player/switchPlayerRepeatMode"); if (!this.currentUser._id) { return; } this.saveUserSettings(); }, saveUserSettings() { if (!this.currentUser._id) { return; } this.$store.dispatch("user/savePlayerSettings"); }, pushState() { if (!this.currentUser._id) { return; } this.progress = this.audio.currentTime; let item = { id: this.selectedTrack._id, parentId: this.selectedTrack.parent._id, type: "track", progress: Math.round(this.progress) } this.$store.dispatch("user/saveProgress", item); }, timeUpdate(event) { let percent = (event.target.currentTime / event.target.duration) * 100; if (percent > 10 && !this.preConvert) { this.preConvert = true; this.$store.dispatch("tracks/convertNextTo", { track: this.selectedTrack, rate: this.audioBpm, }); } }, }, computed: { cover() { if (this.selectedTrack.title != "") { let res = "/static/icons/dummy/album.svg"; if (this.selectedTrack.parent.covers.cover64) { res = this.selectedTrack.parent.covers.cover64; } return res; } if (this.selectedRadio) { let res = "/static/icons/dummy/radio.svg"; if (this.selectedRadio.cover64) { res = this.selectedRadio.cover64; } return res; } return ""; }, selectedTrack() { return this.$store.getters["tracks/selectedTrack"]; }, skipTo() { return this.selectedTrack.skipTo; }, selectedRadio() { return this.$store.getters["radios/selectedRadio"]; }, currentTrackParent() { return this.$store.getters["tracks/selectedTrackContainer"]; }, currentTrackParentType() { let type = "album"; if ( this.selectedTrack.parent.parent && this.selectedTrack.parent.parent.tracks ) { type = "artist"; } return type; }, currentUser() { return this.$store.getters["user/user"]; }, formatedD() { let m = Math.floor(this.duration / 60); let s = Math.floor(this.duration - m * 60); return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s; }, formatedP() { let m = Math.floor(this.progress / 60); let s = Math.floor(this.progress - m * 60); return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s; }, requestReplayTrack() { return this.$store.getters["player/requestReplayTrack"]; }, audioBpm() { return this.$store.getters["user/settings"].desktop_bpm || "128"; }, }, watch: { requestReplayTrack() { this.skipToPercent(0); }, skipTo(newVal) { if (newVal) { this.skipToPercent(newVal); } }, selectedRadio(newVal) { if (newVal._id) { this.playRadio(newVal); } else { this.reset(); } }, selectedTrack(newVal) { if (newVal._id) { this.playTrack(newVal); } else { this.reset(); } }, }, }; </script> <style scoped> #player { background-color: var(--nav); max-height: 60px; height: 60px; z-index: 1001; box-shadow: 0 0px 4px var(--shadow); } #player .cover { width: 52px; margin-right: 4px; } #playerBar { overflow: hidden; max-height: 52px; } #playerBar>div { align-items: center; } #playerControls button { display: flex; padding: 4px 12px; align-self: stretch; border: none; } @media (max-width: 480px) { #player #playerControls { justify-content: end; } } input[type="range"] { border: none; overflow: hidden; -webkit-appearance: none; background-color: var(--secondary); height: 8px; padding: 0px; } input[type="range"]::-webkit-slider-runnable-track { height: 8px; -webkit-appearance: none; color: var(--primary); } input[type="range"]::-webkit-slider-thumb { width: 10px; -webkit-appearance: none; height: 8px; cursor: ew-resize; background: var(--dark); box-shadow: -4000px 0 0 4000px var(--primary); } </style>