451 lines
12 KiB
Vue
451 lines
12 KiB
Vue
<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.currentTrackParent.progress) {
|
|
if (this.currentTrackParent.progress.id == this.selectedTrack._id) {
|
|
this.skipToSecond(this.currentTrackParent.progress.progress);
|
|
}
|
|
this.currentTrackParent.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(item) {
|
|
let parentId = item.parent._id;
|
|
if (item.parent.parent && item.parent.parent.tracks) {
|
|
parentId = item.parent.parent._id;
|
|
}
|
|
this.$store.dispatch("user/resetProgress", parentId);
|
|
|
|
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.currentTrackParent._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, oldVal) {
|
|
if (newVal._id) {
|
|
this.playTrack(newVal);
|
|
} else {
|
|
this.reset(oldVal);
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</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> |