2020-12-26 15:03:12 +01:00
|
|
|
<template>
|
|
|
|
<div class="extension fade-in" :class="{hidden: !expanded}">
|
|
|
|
<div class="row">
|
|
|
|
<div class="col-3">
|
|
|
|
</div>
|
|
|
|
<div class="col-6">
|
|
|
|
<div class="buttons">
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('previous')" title="Play previous track" v-if="buttons_.previous">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-step-backward"></i>
|
|
|
|
</button>
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('stop')" v-if="buttons_.stop && status.state !== 'stop'" title="Stop playback">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-stop"></i>
|
|
|
|
</button>
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('next')" title="Play next track" v-if="buttons_.next">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-step-forward"></i>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-3">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
<div class="col-9 volume-container">
|
|
|
|
<div class="col-1">
|
|
|
|
<button :disabled="status.muted == null" @click="$emit(status.muted ? 'unmute' : 'mute')">
|
|
|
|
<i class="icon fa fa-volume-up"></i>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<div class="col-11 volume-slider">
|
|
|
|
<Slider :value="status.volume" :range="volumeRange" :disabled="status.volume == null"
|
|
|
|
@mouseup="$emit('set-volume', $event.target.value)" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="col-3 list-controls">
|
|
|
|
<button @click="$emit('consume', !status.consume)" :class="{enabled: status.consume}"
|
2021-01-05 00:50:24 +01:00
|
|
|
title="Toggle consume mode" v-if="buttons_.consume">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-utensils"></i>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button @click="$emit('random', !status.random)" :class="{enabled: status.random}"
|
2021-01-05 00:50:24 +01:00
|
|
|
title="Toggle shuffle" v-if="buttons_.random">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-random"></i>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button @click="$emit('repeat', !status.repeat)" :class="{enabled: status.repeat}"
|
2021-01-05 00:50:24 +01:00
|
|
|
title="Toggle repeat" v-if="buttons_.repeat">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-redo"></i>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
<div class="col-s-2 col-m-1 time">
|
|
|
|
<span class="elapsed-time"
|
2021-01-14 00:15:35 +01:00
|
|
|
v-text="elapsed != null && (status.state === 'play' || status.state === 'pause') ? convertTime(elapsed) : '-:--'"></span>
|
2020-12-26 15:03:12 +01:00
|
|
|
</div>
|
|
|
|
<div class="col-s-8 col-m-10">
|
|
|
|
<Slider :value="elapsed" :range="[0, duration]" :disabled="!duration || status.state === 'stop'"
|
|
|
|
@mouseup="$emit('seek', $event.target.value)" />
|
|
|
|
</div>
|
|
|
|
<div class="col-s-2 col-m-1 time">
|
|
|
|
<span class="total-time"
|
|
|
|
v-text="duration && status.state !== 'stop' ? convertTime(duration) : '-:--'"></span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
<div class="playback-controls mobile tablet col-2">
|
|
|
|
<button @click="$emit(status.state === 'play' ? 'pause' : 'play')"
|
|
|
|
:title="status.state === 'play' ? 'Pause' : 'Play'">
|
|
|
|
<i class="icon play-pause fa fa-pause" v-if="status.state === 'play'"></i>
|
|
|
|
<i class="icon play-pause fa fa-play" v-else></i>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="track-container col-s-8 col-m-8 col-l-3">
|
|
|
|
<div class="track-info" v-if="track && status?.state !== 'stop'">
|
2021-01-14 00:15:35 +01:00
|
|
|
<div class="title" v-if="status.state === 'play' || status.state === 'pause'">
|
|
|
|
<a :href="$route.fullPath" v-text="track.title?.length ? track.title : '[No Title]'"
|
2021-01-01 15:58:37 +01:00
|
|
|
@click.prevent="$emit('search', {artist: track.artist, album: track.album})" v-if="track.album"></a>
|
2021-01-14 00:15:35 +01:00
|
|
|
<a :href="track.url" v-text="track.title?.length ? track.title : '[No Title]'" v-else-if="track.url"></a>
|
|
|
|
<span v-text="track.title?.length ? track.title : '[No Title]' " v-else></span>
|
2020-12-26 15:03:12 +01:00
|
|
|
</div>
|
2021-01-14 00:15:35 +01:00
|
|
|
<div class="artist" v-if="track.artist?.length && (status.state === 'play' || status.state === 'pause')">
|
2021-01-01 15:58:37 +01:00
|
|
|
<a :href="$route.fullPath" v-text="track.artist" @click.prevent="$emit('search', {artist: track.artist})"></a>
|
2020-12-26 15:03:12 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="playback-controls desktop col-6">
|
|
|
|
<div class="row buttons">
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('previous')" title="Play previous track" v-if="buttons_.previous">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-step-backward"></i>
|
|
|
|
</button>
|
|
|
|
<button @click="$emit(status.state === 'play' ? 'pause' : 'play')"
|
|
|
|
:title="status.state === 'play' ? 'Pause' : 'Play'">
|
|
|
|
<i class="icon play-pause fa fa-pause" v-if="status.state === 'play'"></i>
|
|
|
|
<i class="icon play-pause fa fa-play" v-else></i>
|
|
|
|
</button>
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('stop')" v-if="buttons_.stop && status.state !== 'stop'" title="Stop playback">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-stop"></i>
|
|
|
|
</button>
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('next')" title="Play next track" v-if="buttons_.next">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-step-forward"></i>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
<div class="col-1 time">
|
|
|
|
<span class="elapsed-time"
|
2021-01-14 00:15:35 +01:00
|
|
|
v-text="elapsed != null && (status.state === 'play' || status.state === 'pause') ? convertTime(elapsed) : '-:--'"></span>
|
2020-12-26 15:03:12 +01:00
|
|
|
</div>
|
|
|
|
<div class="col-10">
|
|
|
|
<Slider :value="elapsed" :range="[0, duration]" :disabled="!duration || status.state === 'stop'"
|
|
|
|
@mouseup="$emit('seek', $event.target.value)" />
|
|
|
|
</div>
|
|
|
|
<div class="col-1 time">
|
|
|
|
<span class="total-time"
|
|
|
|
v-text="duration && status.state !== 'stop' ? convertTime(duration) : '-:--'"></span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="col-2 pull-right mobile tablet right-buttons">
|
|
|
|
<button @click="expanded = !expanded" :title="expanded ? 'Show more controls' : 'Hide extra controls'">
|
|
|
|
<i class="fas" :class="[`fa-chevron-${expanded ? 'down' : 'up'}`]" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="col-3 pull-right desktop">
|
|
|
|
<div class="row list-controls">
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('consume')" :class="{enabled: status.consume}" title="Toggle consume mode" v-if="buttons_.consume">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-utensils"></i>
|
|
|
|
</button>
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('random')" :class="{enabled: status.random}" title="Toggle shuffle" v-if="buttons_.random">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-random"></i>
|
|
|
|
</button>
|
2021-01-05 00:50:24 +01:00
|
|
|
<button @click="$emit('repeat')" :class="{enabled: status.repeat}" title="Toggle repeat" v-if="buttons_.repeat">
|
2020-12-26 15:03:12 +01:00
|
|
|
<i class="icon fa fa-redo"></i>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="row volume-container">
|
|
|
|
<div class="col-2">
|
|
|
|
<button :disabled="status.muted == null" @click="$emit(status.muted ? 'unmute' : 'mute')">
|
|
|
|
<i class="icon fa fa-volume-up"></i>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<div class="col-10">
|
|
|
|
<Slider :value="status.volume" :range="volumeRange" :disabled="status.volume == null"
|
|
|
|
@mouseup="$emit('set-volume', $event.target.value)" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
import Utils from "@/Utils"
|
|
|
|
import MediaUtils from "@/components/Media/Utils";
|
|
|
|
import Slider from "@/components/elements/Slider";
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: "Controls",
|
|
|
|
components: {Slider},
|
|
|
|
mixins: [Utils, MediaUtils],
|
|
|
|
emits: ['search', 'previous', 'next', 'play', 'pause', 'stop', 'seek', 'consume', 'random', 'repeat',
|
|
|
|
'set-volume', 'mute', 'unmute'],
|
|
|
|
|
|
|
|
props: {
|
|
|
|
track: {
|
|
|
|
type: Object,
|
|
|
|
},
|
|
|
|
|
|
|
|
status: {
|
|
|
|
type: Object,
|
|
|
|
default: () => {},
|
|
|
|
},
|
|
|
|
|
|
|
|
// Enabled playback buttons
|
|
|
|
buttons: {
|
|
|
|
type: Object,
|
|
|
|
default: () => {
|
|
|
|
return {
|
|
|
|
previous: true,
|
|
|
|
next: true,
|
|
|
|
stop: true,
|
|
|
|
consume: true,
|
|
|
|
random: true,
|
|
|
|
repeat: true,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
// Volume range
|
|
|
|
volumeRange: {
|
|
|
|
type: Array,
|
|
|
|
default: () => [0, 100],
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
data() {
|
2021-01-05 00:50:24 +01:00
|
|
|
const buttons = Object.keys(this.buttons)?.length ? this.buttons : {
|
|
|
|
previous: true,
|
|
|
|
next: true,
|
|
|
|
stop: true,
|
|
|
|
consume: true,
|
|
|
|
random: true,
|
|
|
|
repeat: true,
|
|
|
|
}
|
|
|
|
|
2020-12-26 15:03:12 +01:00
|
|
|
return {
|
|
|
|
expanded: false,
|
|
|
|
lastSync: 0,
|
2021-01-05 00:50:24 +01:00
|
|
|
elapsed: this.status?.elapsed || this.status?.position,
|
|
|
|
buttons_: buttons,
|
2020-12-26 15:03:12 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
computed: {
|
|
|
|
duration() {
|
|
|
|
return this.status?.duration != null ? this.status.duration : this.track?.duration
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
getTime() {
|
|
|
|
return (new Date()).getTime() / 1000
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
mounted() {
|
|
|
|
const self = this
|
2021-01-02 14:33:01 +01:00
|
|
|
this.lastSync = this.getTime()
|
2020-12-26 15:03:12 +01:00
|
|
|
|
2021-01-02 14:33:01 +01:00
|
|
|
this.$watch(() => this.track, (track) => {
|
2020-12-26 15:03:12 +01:00
|
|
|
if (!track || self.status?.state !== 'play')
|
|
|
|
self.lastSync = this.getTime()
|
|
|
|
})
|
|
|
|
|
2021-01-02 14:33:01 +01:00
|
|
|
this.$watch(() => this.status, () => {
|
2020-12-26 15:03:12 +01:00
|
|
|
self.lastSync = this.getTime()
|
|
|
|
})
|
|
|
|
|
|
|
|
setInterval(() => {
|
2021-01-05 00:50:24 +01:00
|
|
|
if (self.status?.state !== 'stop') {
|
|
|
|
self.elapsed = (self.status?.elapsed || self.status?.position || 0)
|
|
|
|
if (self.status?.state === 'play')
|
|
|
|
self.elapsed += Math.round(this.getTime() - self.lastSync)
|
|
|
|
}
|
2020-12-26 15:03:12 +01:00
|
|
|
}, 1000)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
@import 'vars.scss';
|
|
|
|
|
|
|
|
button {
|
|
|
|
border: 0;
|
|
|
|
&:hover {
|
|
|
|
border: 0;
|
|
|
|
|
|
|
|
.icon {
|
|
|
|
color: $default-hover-fg;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&.enabled {
|
|
|
|
color: $selected-fg;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.extension {
|
|
|
|
box-shadow: $border-shadow-bottom;
|
|
|
|
flex-direction: column;
|
|
|
|
display: none;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
@include until($desktop) {
|
|
|
|
display: flex;
|
2021-01-14 00:15:35 +01:00
|
|
|
padding-top: .5em;
|
2020-12-26 15:03:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
.row {
|
|
|
|
display: flex;
|
|
|
|
}
|
|
|
|
|
|
|
|
.buttons {
|
|
|
|
justify-content: center;
|
|
|
|
margin: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.volume-container,
|
|
|
|
.list-controls {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
button {
|
|
|
|
padding: 0 .25em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.list-controls {
|
|
|
|
margin-top: -.5em;
|
|
|
|
flex-flow: row-reverse;
|
|
|
|
}
|
|
|
|
|
|
|
|
.time {
|
|
|
|
&:first-child {
|
|
|
|
margin-left: .25em;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
margin-right: .25em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.volume-slider {
|
|
|
|
margin-left: 2.25em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
width: 100%;
|
|
|
|
height: $media-ctrl-panel-height;
|
|
|
|
display: flex;
|
|
|
|
padding: 1em .5em;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
.row {
|
|
|
|
width: 100%;
|
|
|
|
display: flex;
|
|
|
|
}
|
|
|
|
|
|
|
|
.track-container {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
justify-content: center;
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
|
|
@include until($tablet) {
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
a {
|
|
|
|
color: initial;
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
color: $default-hover-fg;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.artist, .title {
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
}
|
|
|
|
|
|
|
|
.artist {
|
|
|
|
opacity: 0.6;
|
|
|
|
letter-spacing: .04em;
|
|
|
|
}
|
|
|
|
|
|
|
|
.title {
|
|
|
|
font-weight: normal;
|
|
|
|
font-size: 1em;
|
|
|
|
letter-spacing: .05em;
|
|
|
|
margin-bottom: .25em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.playback-controls {
|
|
|
|
&.mobile {
|
|
|
|
display: none;
|
|
|
|
|
|
|
|
@include until($tablet) {
|
|
|
|
display: flex !important;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&.tablet {
|
|
|
|
display: none;
|
|
|
|
|
|
|
|
@media screen and (min-width: $tablet) and (max-width: $desktop - 1) {
|
|
|
|
display: flex !important;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.row {
|
|
|
|
justify-content: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.buttons {
|
|
|
|
height: 50%;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
button {
|
|
|
|
padding: 0;
|
|
|
|
margin: 0 .75em;
|
2021-02-21 02:15:27 +01:00
|
|
|
background: none;
|
2020-12-26 15:03:12 +01:00
|
|
|
|
|
|
|
.play-pause {
|
|
|
|
color: $play-btn-fg;
|
|
|
|
font-size: 1.75em;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
color: $default-hover-fg-2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.list-controls {
|
|
|
|
height: 50%;
|
|
|
|
opacity: 0.7;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
margin-bottom: 1em;
|
|
|
|
flex-flow: row-reverse;
|
|
|
|
}
|
|
|
|
|
|
|
|
.mobile.right-buttons {
|
|
|
|
@include until ($desktop) {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: right;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.pull-right {
|
|
|
|
button {
|
|
|
|
padding: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.volume-container {
|
|
|
|
button {
|
|
|
|
background: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.seek-slider {
|
|
|
|
width: 75%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.volume-slider {
|
|
|
|
width: 75%;
|
|
|
|
margin-right: 1rem;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.time {
|
|
|
|
font-size: .7em;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
|
|
|
|
.elapsed-time {
|
|
|
|
text-align: right;
|
|
|
|
float: right;
|
|
|
|
}
|
|
|
|
|
|
|
|
.mobile {
|
|
|
|
@include from($tablet) {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.tablet {
|
|
|
|
@media screen and (max-width: $tablet), screen and (min-width: $desktop - 1) {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.desktop {
|
|
|
|
@include until($desktop) {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|