...
 
Commits (5)
......@@ -80,7 +80,7 @@ export default [
{
'id': 1,
'license': licenseCcBy4,
'description': null,
'description': 'Bad Behaviour',
'audio': [
audioBadBehaviour
]
......@@ -88,7 +88,7 @@ export default [
{
'id': 2,
'license': licenseCcZero1,
'description': null,
'description': 'Hospitalized',
'audio': [
audioHospitalized
]
......@@ -96,7 +96,7 @@ export default [
{
'id': 3,
'license': licenseCcByNcSa4,
'description': null,
'description': 'Jazz Mezzo & Hospitalized',
'audio': [
audioJazzMezzo,
audioHospitalized
......@@ -105,7 +105,7 @@ export default [
{
'id': 4,
'license': licenseCcBySa4,
'description': null,
'description': 'Hospitalized 2',
'audio': [
audioHospitalized
]
......
This diff is collapsed.
<template>
<v-app dark>
<toolbar/>
<Toolbar/>
<v-content>
<router-view :key="$route.fullPath"></router-view>
</v-content>
<Player/>
</v-app>
</template>
<script>
import Toolbar from './components/Toolbar'
import Player from './components/player/Player'
export default {
name: 'App',
components: { Toolbar }
components: { Player, Toolbar }
}
</script>
<style>
.application--wrap {
padding-bottom: 120px;
}
</style>
<template>
<div class="player">
<div class="player-level">
<PlayerTrackInfo/>
</div>
<div class="player-level" style="width: 30%">
<PlayerControls/>
<PlayerProgress/>
</div>
<div class="player-level">
<PlayerExtraControls/>
</div>
</div>
</template>
<script>
import PlayerControls from './PlayerControls'
import PlayerExtraControls from './PlayerExtraControls'
import PlayerProgress from './PlayerProgress'
import PlayerTrackInfo from './PlayerTrackInfo'
export default {
components: {
PlayerControls,
PlayerExtraControls,
PlayerProgress,
PlayerTrackInfo
}
}
</script>
<style>
.player {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: #181818;
height: 98px;
padding: 0 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
<template>
<div class="player-controls">
<button type="button" class="player-control" :disabled="true" title="Vorheriger Titel">
<feather type="skip-back" size="19"/>
</button>
<button type="button" class="player-control is-primary" @click="$player.togglePlay"
:title="playLabel">
<feather :type="playIcon" stroke-width="1" size="38"/>
</button>
<button type="button" class="player-control" :disabled="true" title="Nächster Titel">
<feather type="skip-forward" size="19"/>
</button>
</div>
</template>
<script>
export default {
computed: {
playIcon () {
return this.$player.isPlaying
? 'pause-circle'
: 'play-circle'
},
playLabel () {
return this.$player.isPlaying
? 'Wiedergabe pausieren'
: 'Wiedergabe fortsetzen'
}
}
}
</script>
<style>
.player-controls {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.player-control {
border-radius: 50%;
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
color: #b3b3b3;
transition: none 33ms cubic-bezier(.3, 0, 0, 1);
transition-property: color, transform;
transform-origin: 50% 50%;
padding: 0;
}
.player-control:disabled {
color: #404040;
pointer-events: none;
}
.player-control:hover,
.player-control:focus {
color: #fff;
outline: none;
}
.player-control:not(:last-child) {
margin-right: 24px;
}
.player-control.is-primary:hover,
.player-control.is-primary:focus {
transform: scale(1.06);
}
</style>
<template>
<div></div>
</template>
<template>
<div class="player-progress">
<span class="player-progress-label">{{ timeLabels.progress }}</span>
<div class="player-progress-bar">
<div class="player-progress-track" ref="track">
<div class="player-progress-value" :style="barValue"></div>
</div>
<button type="button" class="player-progress-knob" :style="knobValue"></button>
</div>
<span class="player-progress-label">{{ timeLabels.duration }}</span>
</div>
</template>
<script>
function secondsToDuration (seconds, max = null) {
max = max === null ? seconds : max
const start = max <= 599 ? 15 : max <= 3599 ? 14 : 11
const date = new Date(null)
date.setSeconds(seconds)
return date.toISOString().substring(start, 19)
}
// TODO implement knob drag & drop
export default {
computed: {
progress () {
return this.$player.currentTrack
? { min: 0, max: this.$player.currentTrack.duration, value: this.$player.currentTrack.progress }
: { min: 0, max: 1, value: 0 }
},
timeLabels () {
const { duration, progress } = this.$player.currentTrack || { duration: 0, progress: 0 }
return {
duration: secondsToDuration(duration),
progress: secondsToDuration(progress, duration)
}
},
barValue () {
return {
transform: `translateX(${this.progress.value / this.progress.max * 100}%)`
}
},
knobValue () {
const { value, max } = this.progress
const trackWidth = this.$refs.track
? this.$refs.track.getBoundingClientRect().width
: 0
return {
transform: `translateX(calc(${trackWidth * (value / max)}px + 50%))`
}
}
}
}
</script>
<style>
.player-progress {
display: flex;
align-items: center;
justify-content: center;
}
.player-progress-label {
color: #b3b3b3;
font-size: 12px;
}
.player-progress-bar {
width: 100%;
margin: 0 16px;
padding: 6px 0;
position: relative;
overflow: visible;
}
.player-progress-track {
width: 100%;
height: 4px;
border-radius: 2px;
background-color: #404040;
overflow: hidden;
position: relative;
}
.player-progress-value {
width: 100%;
height: 100%;
position: absolute;
right: 100%;
transition: color .1s cubic-bezier(.3, 0, 0, 1);
transition-delay: .1s;
background-color: currentColor;
color: #b3b3b3;
z-index: 1;
}
.player-progress-bar:hover .player-progress-value {
color: #ff9800;
}
.player-progress-knob {
border-radius: 50%;
background-color: #fff;
width: 12px;
height: 12px;
border: none;
opacity: 0;
pointer-events: none;
position: absolute;
right: 100%;
top: calc(50% - 6px);
transition: opacity .1s cubic-bezier(.3, 0, 0, 1);
transition-delay: .1s;
z-index: 2;
}
.player-progress-bar:hover .player-progress-knob {
opacity: 1;
pointer-events: all;
}
</style>
<template>
<div></div>
</template>
import Vue from 'vue'
import VueFeather from 'vue-feather'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'
import store from './store'
import i18n from './i18n'
import Player from './player'
Vue.config.productionTip = false
Vue.use(Player)
Vue.use(VueFeather)
new Vue({
router,
......
import { Howl, Howler } from 'howler'
function ensureNumber (value, _default = 0) {
if (typeof value === 'number') {
return value
}
return _default
}
function createPlayer (Vue) {
let currentHowler
let trackControl
const { setInterval, clearInterval } = window
const state = new Vue({
data: {
currentTrack: null,
isPaused: false,
isStopped: true,
isMuted: false,
volume: 100,
playlist: []
},
computed: {
isPlaying () {
return this.isStopped === false && this.isPaused === false
}
},
methods: {
playTrack (trackData) {
if (trackControl) {
trackControl.destroy()
}
const proxyEvent = (name, handler) => (...args) => {
if (handler) {
handler(...args)
}
this.$emit(name, ...args)
}
currentHowler = new Howl({
// TODO implement proper src order and codec detection
src: trackData.src,
onload: proxyEvent('load', () => {
this.currentTrack.duration = ensureNumber(currentHowler.duration())
}),
onloaderror: proxyEvent('loadError'),
onplayerror: proxyEvent('playError'),
onplay: proxyEvent('play', () => {
this.isStopped = false
this.isPaused = false
}),
onend: proxyEvent('end', () => {
this.isStopped = true
this.isPaused = false
}),
onpause: proxyEvent('pause', () => {
this.isStopped = false
this.isPaused = true
}),
onstop: proxyEvent('stop', () => {
this.isStopped = true
this.isPaused = false
}),
onvolume: proxyEvent('volume'),
onrate: proxyEvent('rate'),
onseek: proxyEvent('seek'),
onfade: proxyEvent('fade'),
onunlock: proxyEvent('unlock')
})
trackControl = {
destroy () {
currentHowler.unload()
clearInterval(seekUpdater)
currentHowler = null
trackControl = null
}
}
const seekUpdater = setInterval(() => {
if (!this.isPaused) {
this.currentTrack.progress = ensureNumber(currentHowler.seek())
}
}, 250)
this.currentTrack = {
duration: 0,
progress: 0,
metadata: trackData
}
currentHowler.play()
},
play () {
if (currentHowler && this.isPaused) {
currentHowler.play()
}
return this
},
pause () {
if (this.isPlaying) {
currentHowler.pause()
}
return this
},
mute (state = null) {
const newState = state !== null ? Boolean(state) : !this.isMuted
this.isMuted = newState
Howler.mute(newState)
this.$emit('mute', newState)
return this
},
stop () {
trackControl.destroy()
return this
},
togglePlay () {
if (this.isPaused) {
this.play()
} else {
this.pause()
}
}
}
})
return state
}
export default {
install (Vue, options) {
Vue.prototype.$player = createPlayer(Vue)
}
}
......@@ -2,7 +2,7 @@ import Vapi from 'vuex-rest-api'
export default ({ axios }) => {
const storeConfig = new Vapi({
baseURL: '/api/v1/broadcasts',
baseURL: '/api/v1/broadcasts/',
axios,
state: {
broadcasts: []
......
......@@ -2,7 +2,7 @@ import Vapi from 'vuex-rest-api'
export default ({ axios }) => {
const storeConfig = new Vapi({
baseURL: '/api/v1/recordings',
baseURL: '/api/v1/recordings/',
axios,
state: {
recordings: []
......
......@@ -2,7 +2,7 @@ import Vapi from 'vuex-rest-api'
export default ({ axios }) => {
const storeConfig = new Vapi({
baseURL: '/api/v1/series',
baseURL: '/api/v1/series/',
axios,
state: {
series: []
......
......@@ -8,6 +8,18 @@
</v-flex>
</v-layout>
</v-container>
<v-container grid-list-xl>
<v-layout row wrap>
<v-flex xs12 sm6 lg4 xl3 grow
v-for="recording in recordings" :key="recording.id">
<div>
{{ recording.description }}
<v-btn @click="play(recording)">Abspielen</v-btn>
</div>
</v-flex>
</v-layout>
</v-container>
</div>
</template>
......@@ -20,8 +32,17 @@
components: { SeriesPreview },
computed: {
...mapState('api', {
series: state => state.series.series
series: state => state.series.series,
recordings: state => state.recordings.recordings
})
},
methods: {
play (recording) {
this.$player.playTrack({
recording,
src: recording.audio.flatMap(audio => audio.sources.map(asrc => asrc.src))
})
}
}
}
</script>