2015-12-13 04:42:28 +00:00
|
|
|
|
<template>
|
|
|
|
|
<footer id="mainFooter">
|
|
|
|
|
<div class="side player-controls" id="playerControls">
|
2015-12-14 04:51:52 +00:00
|
|
|
|
<i class="prev fa fa-step-backward control" @click.prevent="playPrev"></i>
|
2015-12-13 04:42:28 +00:00
|
|
|
|
|
|
|
|
|
<span class="play control" v-show="!playing" @click.prevent="resume">
|
|
|
|
|
<i class="fa fa-play"></i>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="pause control" v-show="playing" @click.prevent="pause">
|
|
|
|
|
<i class="fa fa-pause"></i>
|
|
|
|
|
</span>
|
|
|
|
|
|
2015-12-14 04:51:52 +00:00
|
|
|
|
<i class="next fa fa-step-forward control" @click.prevent="playNext"></i>
|
2015-12-13 04:42:28 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="media-info-wrap">
|
|
|
|
|
<div class="middle-pane">
|
|
|
|
|
|
|
|
|
|
<span class="album-thumb"
|
|
|
|
|
v-if="cover"
|
|
|
|
|
:style="{ backgroundImage: 'url(' + cover + ')' }">
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<div class="progress" id="progressPane">
|
|
|
|
|
<h3 class="title">{{ song.title }}</h3>
|
|
|
|
|
<p class="meta">
|
|
|
|
|
<span class="artist">{{ song.album.artist.name }}</span> –
|
|
|
|
|
<span class="album">{{ song.album.name }}</span>
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div class="player">
|
|
|
|
|
<audio controls></audio>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<span class="other-controls" :class="{ 'with-gradient': prefs.showExtraPanel }">
|
|
|
|
|
<sound-bar v-show="playing"></sound-bar>
|
|
|
|
|
<i class="like control fa fa-heart" :class="{ 'liked': liked }"
|
|
|
|
|
@click.prevent="like"></i>
|
|
|
|
|
|
|
|
|
|
<span class="control"
|
|
|
|
|
@click.prevent="toggleLyrics"
|
|
|
|
|
:class="{ active: prefs.showExtraPanel }">Lyrics</span>
|
|
|
|
|
|
|
|
|
|
<i class="queue control fa fa-list-ol control"
|
|
|
|
|
:class="{ 'active': viewingQueue }"
|
|
|
|
|
@click.prevent="$root.loadMainView('queue')"></i>
|
|
|
|
|
|
|
|
|
|
<span class="repeat control {{ prefs.repeatMode }}" @click.prevent="changeRepeatMode">
|
|
|
|
|
<i class="fa fa-repeat"></i>
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<span class="volume control" id="volume">
|
|
|
|
|
<i class="fa fa-volume-up" @click.prevent="mute" v-show="!muted"></i>
|
|
|
|
|
<i class="fa fa-volume-off" @click.prevent="unmute" v-show="muted"></i>
|
|
|
|
|
<input type="range" id="volumeRange" max="10" step="0.1" v-el:volume-range class="player-volume">
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import soundBar from '../shared/sound-bar.vue';
|
|
|
|
|
import songStore from '../../stores/song';
|
|
|
|
|
import favoriteStore from '../../stores/favorite';
|
|
|
|
|
import preferenceStore from '../../stores/preference';
|
|
|
|
|
import config from '../../config';
|
|
|
|
|
import playback from '../../services/playback';
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
song: songStore.stub,
|
|
|
|
|
muted: false,
|
|
|
|
|
playing: false,
|
|
|
|
|
viewingQueue: false,
|
|
|
|
|
liked: false,
|
|
|
|
|
|
|
|
|
|
prefs: preferenceStore.state,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
components: { soundBar },
|
|
|
|
|
|
|
|
|
|
watch: {
|
|
|
|
|
/**
|
|
|
|
|
* Watch the current playing song and set several data attribute that will
|
|
|
|
|
* affect the interface elements.
|
|
|
|
|
*/
|
|
|
|
|
song() {
|
|
|
|
|
this.liked = this.song.liked;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
computed: {
|
|
|
|
|
/**
|
|
|
|
|
* Get the album cover for the current song.
|
|
|
|
|
*
|
|
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
cover() {
|
|
|
|
|
// don't display the default cover here
|
|
|
|
|
if (this.song.album.cover === config.unknownCover) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.song.album.cover;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the previous song in queue.
|
|
|
|
|
*
|
|
|
|
|
* @return object|null
|
|
|
|
|
*/
|
|
|
|
|
prev() {
|
|
|
|
|
return playback.prevSong();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the next song in queue.
|
|
|
|
|
*
|
|
|
|
|
* @return object|null
|
|
|
|
|
*/
|
|
|
|
|
next() {
|
|
|
|
|
return playback.nextSong();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
|
/**
|
|
|
|
|
* Set the volume level.
|
|
|
|
|
*
|
|
|
|
|
* @param integer volume Min 0, max 10.
|
|
|
|
|
* @param bool persist Whether the volume level should be store into local storage.
|
|
|
|
|
*/
|
|
|
|
|
setVolume(volume, persist = true) {
|
|
|
|
|
playback.setVolume(volume, persist);
|
|
|
|
|
this.muted = volume === '0' || volume === 0;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mute the volume.
|
|
|
|
|
*/
|
|
|
|
|
mute() {
|
|
|
|
|
return playback.mute();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unmute the volume.
|
|
|
|
|
*/
|
|
|
|
|
unmute() {
|
|
|
|
|
return playback.unmute();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Play the previous song in queue.
|
|
|
|
|
*/
|
|
|
|
|
playPrev() {
|
|
|
|
|
return playback.playPrev();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Play the next song in queue.
|
|
|
|
|
*/
|
|
|
|
|
playNext() {
|
|
|
|
|
return playback.playNext();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resume the current song.
|
|
|
|
|
* If the current song is the stub, just play the first song in the queue.
|
|
|
|
|
*/
|
|
|
|
|
resume() {
|
|
|
|
|
if (!this.song.id) {
|
|
|
|
|
return playback.playFirstInQueue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
playback.resume();
|
|
|
|
|
this.playing = true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
2015-12-14 03:28:18 +00:00
|
|
|
|
* <Oh God do I need to document all these methods?>
|
2015-12-13 04:42:28 +00:00
|
|
|
|
*/
|
|
|
|
|
pause() {
|
|
|
|
|
playback.pause();
|
|
|
|
|
this.playing = false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <Oh well…>
|
|
|
|
|
*
|
|
|
|
|
* Change the repeat mode.
|
|
|
|
|
*/
|
|
|
|
|
changeRepeatMode() {
|
|
|
|
|
return playback.changeRepeatMode();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <Look like there's no running away from this…>
|
|
|
|
|
*
|
|
|
|
|
* Like the current song.
|
|
|
|
|
*/
|
|
|
|
|
like() {
|
|
|
|
|
if (!this.song.id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
favoriteStore.toggleOne(this.song, () => {
|
|
|
|
|
this.liked = this.song.liked;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <That's it. That's it!>
|
|
|
|
|
*
|
|
|
|
|
* Toggle hide or show the lyrics panel.
|
|
|
|
|
*/
|
|
|
|
|
toggleLyrics() {
|
|
|
|
|
preferenceStore.set('showExtraPanel', !this.prefs.showExtraPanel);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* OH YISSSSSS!
|
|
|
|
|
* FINALLY!
|
|
|
|
|
*/
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
events: {
|
|
|
|
|
/**
|
|
|
|
|
* <What…>
|
|
|
|
|
*
|
|
|
|
|
* Listen to song:play event and set the current playing song.
|
|
|
|
|
*
|
|
|
|
|
* @param object song
|
|
|
|
|
* @return true
|
|
|
|
|
*/
|
|
|
|
|
'song:play': function (song) {
|
|
|
|
|
this.playing = true;
|
|
|
|
|
this.song = song;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <OK…>
|
|
|
|
|
*
|
|
|
|
|
* Listen to song:stop event to indicate that we're not playing anymore.
|
|
|
|
|
* No we're not playing anymore.
|
|
|
|
|
* We're tired.
|
|
|
|
|
*/
|
|
|
|
|
'song:stop': function () {
|
|
|
|
|
this.playing = false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <Bye cruel world…>
|
|
|
|
|
*
|
|
|
|
|
* Listen to main-content-view:load event and highlight the Queue icon if
|
|
|
|
|
* the Queue screen is being loaded.
|
|
|
|
|
*/
|
|
|
|
|
'main-content-view:load': function (view) {
|
|
|
|
|
this.viewingQueue = view === 'queue';
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="sass">
|
|
|
|
|
@import "resources/assets/sass/partials/_vars.scss";
|
|
|
|
|
@import "resources/assets/sass/partials/_mixins.scss";
|
|
|
|
|
|
|
|
|
|
@mixin hasSoftGradientOnTop($startColor) {
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
// Add a reverse gradient here to elimate the "hard cut" feel when the
|
|
|
|
|
// song list is too long.
|
|
|
|
|
&::before {
|
|
|
|
|
$gradientHeight: 2*$footerHeight/3;
|
|
|
|
|
content: " ";
|
|
|
|
|
position: absolute;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: $gradientHeight;
|
|
|
|
|
top: -$gradientHeight;
|
|
|
|
|
left: 0;
|
|
|
|
|
|
|
|
|
|
// Safari 8 won't recognize rgba(255, 255, 255, 0) and treat it as black.
|
|
|
|
|
// rgba($startColor, 0) is a workaround.
|
|
|
|
|
// Actually, why need I care?
|
|
|
|
|
// Father always told me: Don't give a fuck about what you can't change.
|
|
|
|
|
background-image: linear-gradient(to bottom, rgba($startColor, 0) 0%, rgba($startColor, 1) 100%);
|
|
|
|
|
pointer-events: none; // click-through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#mainFooter {
|
|
|
|
|
background: $color2ndBgr;
|
|
|
|
|
position: fixed;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: $footerHeight;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
border-top: 1px solid $colorMainBgr;
|
|
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
flex: 1;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
|
|
|
|
.media-info-wrap {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.other-controls {
|
|
|
|
|
@include vertical-center();
|
|
|
|
|
@include hasSoftGradientOnTop($colorMainBgr);
|
|
|
|
|
|
|
|
|
|
&.with-gradient {
|
|
|
|
|
@include hasSoftGradientOnTop($colorExtraBgr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
flex: 0 0 334px;
|
|
|
|
|
color: $colorLink;
|
|
|
|
|
|
|
|
|
|
.control {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
color: $colorMainText;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
padding-right: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.repeat {
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
&.REPEAT_ALL, &.REPEAT_ONE {
|
|
|
|
|
color: $colorHighlight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.REPEAT_ONE::after {
|
|
|
|
|
content: "1";
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-size: 50%;
|
|
|
|
|
text-align: center;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.like {
|
|
|
|
|
&:hover {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.liked {
|
|
|
|
|
color: $colorHeart;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media only screen
|
|
|
|
|
and (max-device-width : 768px)
|
|
|
|
|
and (orientation : portrait) {
|
|
|
|
|
position: absolute !important;
|
|
|
|
|
right: 0;
|
|
|
|
|
height: $footerHeight;
|
|
|
|
|
display: block;
|
|
|
|
|
text-align: right;
|
|
|
|
|
top: 0;
|
|
|
|
|
line-height: $footerHeight;
|
|
|
|
|
width: 168px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
&::before {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.queue {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#playerControls {
|
|
|
|
|
@include vertical-center();
|
|
|
|
|
flex: 0 0 256px;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
background: $colorPlayerControlsBgr;
|
|
|
|
|
|
|
|
|
|
@include hasSoftGradientOnTop($colorSidebarBgr);
|
|
|
|
|
|
|
|
|
|
.prev, .next {
|
|
|
|
|
transition: .3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.play, .pause {
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 42px;
|
|
|
|
|
height: 42px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
line-height: 40px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
border: 1px solid #a0a0a0;
|
|
|
|
|
margin: 0 16px;
|
|
|
|
|
text-indent: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pause {
|
|
|
|
|
text-indent: 0;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.enabled {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media only screen
|
|
|
|
|
and (max-device-width : 768px)
|
|
|
|
|
and (orientation : portrait) {
|
|
|
|
|
width: 50%;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
|
|
|
|
|
&::before {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.middle-pane {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
.album-thumb {
|
|
|
|
|
flex: 0 0 $footerHeight;
|
|
|
|
|
height: $footerHeight;
|
|
|
|
|
background: url(/public/img/covers/unknown-album.png);
|
|
|
|
|
background-size: $footerHeight;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@include hasSoftGradientOnTop($colorMainBgr);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media only screen
|
|
|
|
|
and (max-device-width : 768px)
|
|
|
|
|
and (orientation : portrait) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
|
|
|
|
.album-thumb {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::before {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#progressPane {
|
|
|
|
|
width: 100%;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#progressPane {
|
|
|
|
|
flex: 1;
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
background: rgba(1, 1, 1, .2);
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
.meta {
|
|
|
|
|
font-size: 90%;
|
|
|
|
|
opacity: .4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$blue: $colorHighlight;
|
|
|
|
|
$control-color: $colorHighlight;
|
|
|
|
|
$control-bg-hover: $colorHighlight;
|
|
|
|
|
$volume-track-height: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@import "resources/assets/sass/vendors/_plyr.scss";
|
|
|
|
|
|
2015-12-13 17:03:00 +00:00
|
|
|
|
// Some little tweaks here and there
|
2015-12-13 04:42:28 +00:00
|
|
|
|
.player {
|
|
|
|
|
width: 100%;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.player-controls {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.player-controls-left, .player-controls-right {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media only screen
|
|
|
|
|
and (max-device-width : 768px)
|
|
|
|
|
and (orientation : portrait) {
|
|
|
|
|
.meta, .title {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
top: -5px !important;
|
|
|
|
|
padding-top: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#volume {
|
|
|
|
|
@include vertical-center();
|
|
|
|
|
|
|
|
|
|
// More tweaks
|
|
|
|
|
input[type=range] {
|
|
|
|
|
margin-top: -3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
width: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media only screen
|
|
|
|
|
and (max-device-width : 768px)
|
|
|
|
|
and (orientation : portrait) {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|