...
 
Commits (6)
......@@ -22,4 +22,8 @@
.application--wrap {
padding-bottom: 120px;
}
*:focus:not(:focus-visible) {
outline: none !important;
}
</style>
<template>
<div class="progress" role="progressbar" @click="seek"
:aria-valuemin="min" :aria-valuemax="max"
:aria-valuenow="value" :aria-valuetext="label">
<div class="progress-track" ref="track" role="presentation">
<div class="progress" :class="isDragging ? 'is-dragging': null" role="slider" tabindex="0"
:aria-valuemin="min" :aria-valuemax="max" :aria-valuenow="value" :aria-valuetext="label"
@click="seekFromMousePosition"
@keyup.home.exact="seek(0)" @keydown.home.exact.prevent
@keyup.end.exact="seek(max)" @keydown.end.exact.prevent
@keyup.up.exact="seekBy(step)" @keydown.up.exact.prevent
@keyup.down.exact="seekBy(-step)" @keydown.down.exact.prevent
@keyup.page-up.exact="seekBy(step * stepFactor)" @keydown.page-up.exact.prevent
@keyup.page-down.exact="seekBy(-step * stepFactor)" @keydown.page-down.exact.prevent>
<div class="progress-track" role="presentation">
<div class="progress-value" :style="barValue"></div>
</div>
<button type="button" class="progress-knob" :style="knobValue"></button>
<button type="button" class="progress-knob" :style="knobValue" :aria-label="knobLabel"
@mousedown.exact="isDragging = true"></button>
</div>
</template>
<script>
// TODO implement knob drag & drop
import Vue from 'vue'
const globalState = Vue.observable({
mouseX: null,
lastMouseRelease: null,
currentWindowWidth: null
})
function updateWindowSize () {
globalState.currentWindowWidth =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
}
if (document && window) {
// TODO: Do we want to do cleanup? What are the criteria?
document.addEventListener('mousemove', function mouseMoveListener (event) {
globalState.mouseX = event.clientX
}, { passive: true })
document.addEventListener('mouseup', function mouseUpListener () {
globalState.lastMouseRelease = new Date()
}, { passive: true })
window.addEventListener('resize', updateWindowSize, { passive: true })
updateWindowSize()
}
export default {
props: {
min: {
......@@ -25,30 +58,82 @@
type: Number,
required: true
},
label: String
step: {
value: Number,
default: 1
},
stepFactor: {
value: Number,
default: 10
},
label: String,
knobLabel: String
},
data () {
return {
isDragging: false,
trackRect: null,
globalState
}
},
computed: {
progress () {
const { min, max } = this
if (!this.isDragging) {
return this.value
} else {
const { left, width } = this.trackRect
const { mouseX } = this.globalState
const right = left + width
return mouseX < left ? min : mouseX > right ? max : ((mouseX - left) / width * max)
}
},
barValue () {
return {
transform: `translateX(${this.value / this.max * 100}%)`
transform: `translateX(${this.progress / this.max * 100}%)`
}
},
knobValue () {
const { value, max } = this
const trackWidth = this.$refs.track
? this.$refs.track.getBoundingClientRect().width
: 0
if (!this.trackRect) { return 0 }
const { progress, max } = this
const { width } = this.trackRect
return {
transform: `translateX(calc(${trackWidth * (value / max)}px + 50%))`
transform: `translateX(calc(${width * (progress / max)}px + 50%))`
}
}
},
methods: {
seek (e) {
const rect = e.target.getBoundingClientRect()
const relativeValue = (e.clientX - rect.left) / rect.width
seekFromMousePosition (e) {
const { left, width } = this.trackRect
const relativeValue = (e.clientX - left) / width
this.$emit('input', relativeValue * this.max)
},
seek (value) {
this.$emit('input', value)
},
seekBy (value) {
const { min, max } = this
this.$emit('input', Math.min(max, Math.max(min, this.value + value)))
},
updateTrackRect () {
if (this.$el) {
this.trackRect = this.$el.getBoundingClientRect()
}
}
},
watch: {
'globalState.lastMouseRelease' () {
if (this.isDragging) {
this.$emit('input', this.progress)
setTimeout(() => { this.isDragging = false }, 150)
}
},
// TODO: debounce
'globalState.currentWindowWidth': 'updateTrackRect'
},
mounted () {
this.updateTrackRect()
}
}
</script>
......@@ -82,7 +167,8 @@
z-index: 1;
}
.progress:hover .progress-value {
.progress:hover .progress-value,
.progress.is-dragging .progress-value {
color: #ff9800;
}
......@@ -102,7 +188,8 @@
z-index: 2;
}
.progress:hover .progress-knob {
.progress:hover .progress-knob,
.progress.is-dragging .progress-knob {
opacity: 1;
pointer-events: all;
}
......
<template>
<div class="player">
<div class="player-level">
<PlayerTrackInfo/>
<div class="player-level is-left">
<div>
<PlayerTrackInfo/>
</div>
</div>
<div class="player-level" style="width: 30%">
<PlayerControls/>
<PlayerProgress/>
<div class="player-level is-centered">
<div>
<PlayerControls/>
<PlayerProgress/>
</div>
</div>
<div class="player-level">
<PlayerExtraControls/>
<div class="player-level is-right">
<div>
<PlayerExtraControls/>
</div>
</div>
</div>
</template>
......@@ -41,5 +47,29 @@
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.player-level {
display: flex;
align-items: center;
flex: 1 0;
}
.player-level.is-left {
justify-content: flex-start;
}
.player-level.is-centered {
flex-grow: 1.75;
justify-content: center;
}
.player-level.is-centered > div {
width: 100%;
}
.player-level.is-right {
justify-content: flex-end;
}
</style>
<template>
<div class="player-controls">
<button type="button" class="player-control" :disabled="true" title="Vorheriger Titel">
<button type="button" class="player-control" :disabled="true" :title="$t('player_prev_track')">
<feather type="skip-back" size="19"/>
</button>
......@@ -9,7 +9,7 @@
<feather :type="playIcon" stroke-width="1" size="38"/>
</button>
<button type="button" class="player-control" :disabled="true" title="Nächster Titel">
<button type="button" class="player-control" :disabled="true" :title="$t('player_next_track')">
<feather type="skip-forward" size="19"/>
</button>
</div>
......@@ -24,9 +24,11 @@
: 'play-circle'
},
playLabel () {
return this.$player.isPlaying
? 'Wiedergabe pausieren'
: 'Wiedergabe fortsetzen'
return this.$t(
this.$player.isPlaying
? 'player_pause'
: 'player_play'
)
}
}
}
......@@ -74,3 +76,16 @@
transform: scale(1.06);
}
</style>
<i18n>
en:
player_play: 'Play'
player_pause: 'Pause'
player_next_track: 'Next Track'
player_prev_track: 'Previous Track'
de:
player_play: 'Wiedergabe fortsetzen'
player_pause: 'Wiedergabe pausieren'
player_next_track: 'Nächster Titel'
player_prev_track: 'Vorheriger Titel'
</i18n>
<template>
<div></div>
<div>
<div class="player-volume">
<button type="button" class="player-control" :title="muteLabel" @click.exact="$player.mute">
<feather :type="volumeIcon" size="19"/>
</button>
<SeekableProgress v-model="$player.volume" :aria-label="$t('player_volume')"
:knob-label="$t('player_set_volume')"/>
</div>
</div>
</template>
<script>
import SeekableProgress from '../SeekableProgress'
export default {
components: { SeekableProgress },
computed: {
muteLabel () {
return this.$t(this.$player.isMuted ? 'player_unmute' : 'player_mute')
},
volumeIcon () {
if (this.$player.isMuted) { return 'volume-x' }
if (this.$player.volume < (100 / 3)) { return 'volume' }
if (this.$player.volume < (100 / 3 * 2)) { return 'volume-1' }
return 'volume-2'
}
}
}
</script>
<style>
.player-volume {
display: flex;
align-items: center;
width: 125px;
}
.player-volume .player-control {
margin-right: 12px;
}
</style>
<i18n>
en:
player_mute: Mute
player_unmute: Unmute
player_volume: Volume
player_set_volume: Set Volume
de:
player_mute: Ton aus
player_unmute: Ton an
player_volume: Lautstärke
player_set_volume: Lautstärke einstellen
</i18n>
......@@ -3,6 +3,7 @@
<span class="player-progress-label">{{ timeLabels.progress }}</span>
<div class="player-progress-bar">
<SeekableProgress :min="progress.min" :max="progress.max" v-model="value"
:step="5" :step-factor="6" :knob-label="$t('player_set_seek')"
:label="`${timeLabels.progress} / ${timeLabels.duration}`"/>
</div>
<span class="player-progress-label">{{ timeLabels.duration }}</span>
......@@ -64,3 +65,11 @@
width: 100%;
}
</style>
<i18n>
en:
player_set_seek: Jump to position in track
de:
player_set_seek: Zu Position im Track springen
</i18n>
......@@ -8,6 +8,13 @@ import i18n from './i18n'
import Player from './player'
Vue.config.productionTip = false
Vue.config.keyCodes = {
'page-up': 33,
'page-down': 34,
'end': 35,
'home': 36
}
Vue.use(Player)
Vue.use(VueFeather)
......
......@@ -114,7 +114,7 @@ function createPlayer (Vue) {
return this
},
mute (state = null) {
const newState = state !== null ? Boolean(state) : !this.isMuted
const newState = typeof state === 'boolean' ? state : !this.isMuted
this.isMuted = newState
Howler.mute(newState)
this.$emit('mute', newState)
......@@ -131,6 +131,11 @@ function createPlayer (Vue) {
this.pause()
}
}
},
watch: {
volume (newVolume) {
Howler.volume(newVolume / 100)
}
}
})
......