Merge branch 'feature/38-vuex' into 'develop'
Feature/38 vuex, fix #38 Closes #38 See merge request funkwhale/funkwhale!33
This commit is contained in:
commit
a7758395ee
|
@ -49,7 +49,7 @@ mutagen>=1.39,<1.40
|
||||||
|
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
#django-taggit>=0.22,<0.23
|
#django-taggit>=0.22,<0.23
|
||||||
git+https://github.com/jdufresne/django-taggit.git@e8f7f216f04c9781bebc84363ab24d575f948ede
|
git+https://github.com/alex/django-taggit.git@95776ac66948ed7ba7c12e35c1170551e3be66a5
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
git+https://github.com/EliotBerriot/PyMemoize.git@django
|
||||||
# Until this is merged
|
# Until this is merged
|
||||||
|
|
1
dev.yml
1
dev.yml
|
@ -13,7 +13,6 @@ services:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- './front:/app'
|
- './front:/app'
|
||||||
- /app/node_modules
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
env_file: .env.dev
|
env_file: .env.dev
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
"vue-lazyload": "^1.1.4",
|
"vue-lazyload": "^1.1.4",
|
||||||
"vue-resource": "^1.3.4",
|
"vue-resource": "^1.3.4",
|
||||||
"vue-router": "^2.3.1",
|
"vue-router": "^2.3.1",
|
||||||
"vuedraggable": "^2.14.1"
|
"vuedraggable": "^2.14.1",
|
||||||
|
"vuex": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^6.7.2",
|
"autoprefixer": "^6.7.2",
|
||||||
|
|
|
@ -1,184 +0,0 @@
|
||||||
import logger from '@/logging'
|
|
||||||
import time from '@/utils/time'
|
|
||||||
|
|
||||||
const Cov = {
|
|
||||||
on (el, type, func) {
|
|
||||||
el.addEventListener(type, func)
|
|
||||||
},
|
|
||||||
off (el, type, func) {
|
|
||||||
el.removeEventListener(type, func)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Audio {
|
|
||||||
constructor (src, options = {}) {
|
|
||||||
let preload = true
|
|
||||||
if (options.preload !== undefined && options.preload === false) {
|
|
||||||
preload = false
|
|
||||||
}
|
|
||||||
this.tmp = {
|
|
||||||
src: src,
|
|
||||||
options: options
|
|
||||||
}
|
|
||||||
this.onEnded = function (e) {
|
|
||||||
logger.default.info('track ended')
|
|
||||||
}
|
|
||||||
if (options.onEnded) {
|
|
||||||
this.onEnded = options.onEnded
|
|
||||||
}
|
|
||||||
this.onError = options.onError
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
preload: preload,
|
|
||||||
startLoad: false,
|
|
||||||
failed: false,
|
|
||||||
try: 3,
|
|
||||||
tried: 0,
|
|
||||||
playing: false,
|
|
||||||
paused: false,
|
|
||||||
playbackRate: 1.0,
|
|
||||||
progress: 0,
|
|
||||||
currentTime: 0,
|
|
||||||
volume: 0.5,
|
|
||||||
duration: 0,
|
|
||||||
loaded: '0',
|
|
||||||
durationTimerFormat: '00:00',
|
|
||||||
currentTimeFormat: '00:00',
|
|
||||||
lastTimeFormat: '00:00'
|
|
||||||
}
|
|
||||||
if (options.volume !== undefined) {
|
|
||||||
this.state.volume = options.volume
|
|
||||||
}
|
|
||||||
this.hook = {
|
|
||||||
playState: [],
|
|
||||||
loadState: []
|
|
||||||
}
|
|
||||||
if (preload) {
|
|
||||||
this.init(src, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init (src, options = {}) {
|
|
||||||
if (!src) throw Error('src must be required')
|
|
||||||
this.state.startLoad = true
|
|
||||||
if (this.state.tried >= this.state.try) {
|
|
||||||
this.state.failed = true
|
|
||||||
logger.default.error('Cannot fetch audio', src)
|
|
||||||
if (this.onError) {
|
|
||||||
this.onError(src)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$Audio = new window.Audio(src)
|
|
||||||
Cov.on(this.$Audio, 'error', () => {
|
|
||||||
this.state.tried++
|
|
||||||
this.init(src, options)
|
|
||||||
})
|
|
||||||
if (options.autoplay) {
|
|
||||||
this.play()
|
|
||||||
}
|
|
||||||
if (options.rate) {
|
|
||||||
this.$Audio.playbackRate = options.rate
|
|
||||||
}
|
|
||||||
if (options.loop) {
|
|
||||||
this.$Audio.loop = true
|
|
||||||
}
|
|
||||||
if (options.volume) {
|
|
||||||
this.setVolume(options.volume)
|
|
||||||
}
|
|
||||||
this.loadState()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadState () {
|
|
||||||
if (this.$Audio.readyState >= 2) {
|
|
||||||
Cov.on(this.$Audio, 'progress', this.updateLoadState.bind(this))
|
|
||||||
} else {
|
|
||||||
Cov.on(this.$Audio, 'loadeddata', () => {
|
|
||||||
this.loadState()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLoadState (e) {
|
|
||||||
if (!this.$Audio) return
|
|
||||||
this.hook.loadState.forEach(func => {
|
|
||||||
func(this.state)
|
|
||||||
})
|
|
||||||
this.state.duration = Math.round(this.$Audio.duration * 100) / 100
|
|
||||||
this.state.loaded = Math.round(10000 * this.$Audio.buffered.end(0) / this.$Audio.duration) / 100
|
|
||||||
this.state.durationTimerFormat = time.parse(this.state.duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlayState (e) {
|
|
||||||
this.state.currentTime = Math.round(this.$Audio.currentTime * 100) / 100
|
|
||||||
this.state.duration = Math.round(this.$Audio.duration * 100) / 100
|
|
||||||
this.state.progress = Math.round(10000 * this.state.currentTime / this.state.duration) / 100
|
|
||||||
|
|
||||||
this.state.durationTimerFormat = time.parse(this.state.duration)
|
|
||||||
this.state.currentTimeFormat = time.parse(this.state.currentTime)
|
|
||||||
this.state.lastTimeFormat = time.parse(this.state.duration - this.state.currentTime)
|
|
||||||
|
|
||||||
this.hook.playState.forEach(func => {
|
|
||||||
func(this.state)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHook (type, func) {
|
|
||||||
if (!(type in this.hook)) throw Error('updateHook: type should be playState or loadState')
|
|
||||||
this.hook[type].push(func)
|
|
||||||
}
|
|
||||||
|
|
||||||
play () {
|
|
||||||
if (this.state.startLoad) {
|
|
||||||
if (!this.state.playing && this.$Audio.readyState >= 2) {
|
|
||||||
logger.default.info('Playing track')
|
|
||||||
this.$Audio.play()
|
|
||||||
this.state.paused = false
|
|
||||||
this.state.playing = true
|
|
||||||
Cov.on(this.$Audio, 'timeupdate', this.updatePlayState.bind(this))
|
|
||||||
Cov.on(this.$Audio, 'ended', this.onEnded)
|
|
||||||
} else {
|
|
||||||
Cov.on(this.$Audio, 'loadeddata', () => {
|
|
||||||
this.play()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.init(this.tmp.src, this.tmp.options)
|
|
||||||
Cov.on(this.$Audio, 'loadeddata', () => {
|
|
||||||
this.play()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyed () {
|
|
||||||
this.$Audio.pause()
|
|
||||||
Cov.off(this.$Audio, 'timeupdate', this.updatePlayState)
|
|
||||||
Cov.off(this.$Audio, 'progress', this.updateLoadState)
|
|
||||||
Cov.off(this.$Audio, 'ended', this.onEnded)
|
|
||||||
this.$Audio.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
pause () {
|
|
||||||
logger.default.info('Pausing track')
|
|
||||||
this.$Audio.pause()
|
|
||||||
this.state.paused = true
|
|
||||||
this.state.playing = false
|
|
||||||
this.$Audio.removeEventListener('timeupdate', this.updatePlayState)
|
|
||||||
}
|
|
||||||
|
|
||||||
setVolume (number) {
|
|
||||||
if (number > -0.01 && number <= 1) {
|
|
||||||
this.state.volume = Math.round(number * 100) / 100
|
|
||||||
this.$Audio.volume = this.state.volume
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTime (time) {
|
|
||||||
if (time < 0 && time > this.state.duration) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
this.$Audio.currentTime = time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Audio
|
|
|
@ -1,332 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import logger from '@/logging'
|
|
||||||
import cache from '@/cache'
|
|
||||||
import config from '@/config'
|
|
||||||
import Audio from '@/audio'
|
|
||||||
import backend from '@/audio/backend'
|
|
||||||
import radios from '@/radios'
|
|
||||||
import url from '@/utils/url'
|
|
||||||
import auth from '@/auth'
|
|
||||||
|
|
||||||
class Queue {
|
|
||||||
constructor (options = {}) {
|
|
||||||
logger.default.info('Instanciating queue')
|
|
||||||
this.previousQueue = cache.get('queue')
|
|
||||||
this.tracks = []
|
|
||||||
this.currentIndex = -1
|
|
||||||
this.currentTrack = null
|
|
||||||
this.ended = true
|
|
||||||
this.state = {
|
|
||||||
looping: 0, // 0 -> no, 1 -> on track, 2 -> on queue
|
|
||||||
volume: cache.get('volume', 0.5)
|
|
||||||
}
|
|
||||||
this.audio = {
|
|
||||||
state: {
|
|
||||||
startLoad: false,
|
|
||||||
failed: false,
|
|
||||||
try: 3,
|
|
||||||
tried: 0,
|
|
||||||
playing: false,
|
|
||||||
paused: false,
|
|
||||||
playbackRate: 1.0,
|
|
||||||
progress: 0,
|
|
||||||
currentTime: 0,
|
|
||||||
duration: 0,
|
|
||||||
volume: this.state.volume,
|
|
||||||
loaded: '0',
|
|
||||||
durationTimerFormat: '00:00',
|
|
||||||
currentTimeFormat: '00:00',
|
|
||||||
lastTimeFormat: '00:00'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cache () {
|
|
||||||
let cached = {
|
|
||||||
tracks: this.tracks.map(track => {
|
|
||||||
// we keep only valuable fields to make the cache lighter and avoid
|
|
||||||
// cyclic value serialization errors
|
|
||||||
let artist = {
|
|
||||||
id: track.artist.id,
|
|
||||||
mbid: track.artist.mbid,
|
|
||||||
name: track.artist.name
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: track.id,
|
|
||||||
title: track.title,
|
|
||||||
mbid: track.mbid,
|
|
||||||
album: {
|
|
||||||
id: track.album.id,
|
|
||||||
title: track.album.title,
|
|
||||||
mbid: track.album.mbid,
|
|
||||||
cover: track.album.cover,
|
|
||||||
artist: artist
|
|
||||||
},
|
|
||||||
artist: artist,
|
|
||||||
files: track.files
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
currentIndex: this.currentIndex
|
|
||||||
}
|
|
||||||
cache.set('queue', cached)
|
|
||||||
}
|
|
||||||
|
|
||||||
restore () {
|
|
||||||
let cached = cache.get('queue')
|
|
||||||
if (!cached) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
logger.default.info('Restoring previous queue...')
|
|
||||||
this.tracks = cached.tracks
|
|
||||||
this.play(cached.currentIndex)
|
|
||||||
this.previousQueue = null
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
removePrevious () {
|
|
||||||
this.previousQueue = undefined
|
|
||||||
cache.remove('queue')
|
|
||||||
}
|
|
||||||
setVolume (newValue) {
|
|
||||||
newValue = Math.min(newValue, 1)
|
|
||||||
newValue = Math.max(newValue, 0)
|
|
||||||
this.state.volume = newValue
|
|
||||||
if (this.audio.setVolume) {
|
|
||||||
this.audio.setVolume(newValue)
|
|
||||||
} else {
|
|
||||||
this.audio.state.volume = newValue
|
|
||||||
}
|
|
||||||
cache.set('volume', newValue)
|
|
||||||
}
|
|
||||||
incrementVolume (value) {
|
|
||||||
this.setVolume(this.state.volume + value)
|
|
||||||
}
|
|
||||||
reorder (oldIndex, newIndex) {
|
|
||||||
// called when the user uses drag / drop to reorder
|
|
||||||
// tracks in queue
|
|
||||||
if (oldIndex === this.currentIndex) {
|
|
||||||
this.currentIndex = newIndex
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (oldIndex < this.currentIndex && newIndex >= this.currentIndex) {
|
|
||||||
// item before was moved after
|
|
||||||
this.currentIndex -= 1
|
|
||||||
}
|
|
||||||
if (oldIndex > this.currentIndex && newIndex <= this.currentIndex) {
|
|
||||||
// item after was moved before
|
|
||||||
this.currentIndex += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
append (track, index, skipPlay) {
|
|
||||||
this.previousQueue = null
|
|
||||||
index = index || this.tracks.length
|
|
||||||
if (index > this.tracks.length - 1) {
|
|
||||||
// we simply push to the end
|
|
||||||
this.tracks.push(track)
|
|
||||||
} else {
|
|
||||||
// we insert the track at given position
|
|
||||||
this.tracks.splice(index, 0, track)
|
|
||||||
}
|
|
||||||
if (!skipPlay) {
|
|
||||||
this.resumeQueue()
|
|
||||||
}
|
|
||||||
this.cache()
|
|
||||||
}
|
|
||||||
|
|
||||||
appendMany (tracks, index) {
|
|
||||||
logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
|
|
||||||
let self = this
|
|
||||||
if (this.tracks.length === 0) {
|
|
||||||
index = 0
|
|
||||||
} else {
|
|
||||||
index = index || this.tracks.length
|
|
||||||
}
|
|
||||||
tracks.forEach((t) => {
|
|
||||||
self.append(t, index, true)
|
|
||||||
index += 1
|
|
||||||
})
|
|
||||||
this.resumeQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
resumeQueue () {
|
|
||||||
if (this.ended | this.errored) {
|
|
||||||
this.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
populateFromRadio () {
|
|
||||||
if (!radios.running) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var self = this
|
|
||||||
radios.fetch().then((response) => {
|
|
||||||
logger.default.info('Adding track to queue from radio')
|
|
||||||
self.append(response.data.track)
|
|
||||||
}, (response) => {
|
|
||||||
logger.default.error('Error while adding track to queue from radio')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
clean () {
|
|
||||||
this.stop()
|
|
||||||
radios.stop()
|
|
||||||
this.tracks = []
|
|
||||||
this.currentIndex = -1
|
|
||||||
this.currentTrack = null
|
|
||||||
// so we replay automatically on next track append
|
|
||||||
this.ended = true
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanTrack (index) {
|
|
||||||
// are we removing current playin track
|
|
||||||
let current = index === this.currentIndex
|
|
||||||
if (current) {
|
|
||||||
this.stop()
|
|
||||||
}
|
|
||||||
if (index < this.currentIndex) {
|
|
||||||
this.currentIndex -= 1
|
|
||||||
}
|
|
||||||
this.tracks.splice(index, 1)
|
|
||||||
if (current) {
|
|
||||||
// we play next track, which now have the same index
|
|
||||||
this.play(index)
|
|
||||||
}
|
|
||||||
if (this.currentIndex === this.tracks.length - 1) {
|
|
||||||
this.populateFromRadio()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop () {
|
|
||||||
if (this.audio.pause) {
|
|
||||||
this.audio.pause()
|
|
||||||
}
|
|
||||||
if (this.audio.destroyed) {
|
|
||||||
this.audio.destroyed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
play (index) {
|
|
||||||
let self = this
|
|
||||||
let currentIndex = index
|
|
||||||
let currentTrack = this.tracks[index]
|
|
||||||
|
|
||||||
if (this.audio.destroyed) {
|
|
||||||
logger.default.debug('Destroying previous audio...', index - 1)
|
|
||||||
this.audio.destroyed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentTrack) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentIndex = currentIndex
|
|
||||||
this.currentTrack = currentTrack
|
|
||||||
|
|
||||||
this.ended = false
|
|
||||||
this.errored = false
|
|
||||||
let file = this.currentTrack.files[0]
|
|
||||||
if (!file) {
|
|
||||||
this.errored = true
|
|
||||||
return this.next()
|
|
||||||
}
|
|
||||||
let path = backend.absoluteUrl(file.path)
|
|
||||||
if (auth.user.authenticated) {
|
|
||||||
// we need to send the token directly in url
|
|
||||||
// so authentication can be checked by the backend
|
|
||||||
// because for audio files we cannot use the regular Authentication
|
|
||||||
// header
|
|
||||||
path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
|
|
||||||
}
|
|
||||||
|
|
||||||
let audio = new Audio(path, {
|
|
||||||
preload: true,
|
|
||||||
autoplay: true,
|
|
||||||
rate: 1,
|
|
||||||
loop: false,
|
|
||||||
volume: this.state.volume,
|
|
||||||
onEnded: this.handleAudioEnded.bind(this),
|
|
||||||
onError: function (src) {
|
|
||||||
self.errored = true
|
|
||||||
self.next()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.audio = audio
|
|
||||||
audio.updateHook('playState', function (e) {
|
|
||||||
// in some situations, we may have a race condition, for example
|
|
||||||
// if the user spams the next / previous buttons, with multiple audios
|
|
||||||
// playing at the same time. To avoid that, we ensure the audio
|
|
||||||
// still matches de queue current audio
|
|
||||||
if (audio !== self.audio) {
|
|
||||||
logger.default.debug('Destroying duplicate audio')
|
|
||||||
audio.destroyed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (this.currentIndex === this.tracks.length - 1) {
|
|
||||||
this.populateFromRadio()
|
|
||||||
}
|
|
||||||
this.cache()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAudioEnded (e) {
|
|
||||||
this.recordListen(this.currentTrack)
|
|
||||||
if (this.state.looping === 1) {
|
|
||||||
// we loop on the same track
|
|
||||||
logger.default.info('Looping on the same track')
|
|
||||||
return this.play(this.currentIndex)
|
|
||||||
}
|
|
||||||
if (this.currentIndex < this.tracks.length - 1) {
|
|
||||||
logger.default.info('Audio track ended, playing next one')
|
|
||||||
return this.next()
|
|
||||||
} else {
|
|
||||||
logger.default.info('We reached the end of the queue')
|
|
||||||
if (this.state.looping === 2) {
|
|
||||||
logger.default.info('Going back to the beginning of the queue')
|
|
||||||
return this.play(0)
|
|
||||||
} else {
|
|
||||||
this.ended = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recordListen (track) {
|
|
||||||
let url = config.API_URL + 'history/listenings/'
|
|
||||||
let resource = Vue.resource(url)
|
|
||||||
resource.save({}, {'track': track.id}).then((response) => {}, (response) => {
|
|
||||||
logger.default.error('Could not record track in history')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
previous () {
|
|
||||||
if (this.currentIndex > 0) {
|
|
||||||
this.play(this.currentIndex - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next () {
|
|
||||||
if (this.currentIndex < this.tracks.length - 1) {
|
|
||||||
logger.default.debug('Playing next track')
|
|
||||||
this.play(this.currentIndex + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLooping () {
|
|
||||||
if (this.state.looping > 1) {
|
|
||||||
this.state.looping = 0
|
|
||||||
} else {
|
|
||||||
this.state.looping += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shuffle () {
|
|
||||||
let tracks = this.tracks
|
|
||||||
let shuffled = _.shuffle(tracks)
|
|
||||||
this.clean()
|
|
||||||
this.appendMany(shuffled)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
let queue = new Queue()
|
|
||||||
|
|
||||||
export default queue
|
|
|
@ -1,99 +0,0 @@
|
||||||
import logger from '@/logging'
|
|
||||||
import config from '@/config'
|
|
||||||
import cache from '@/cache'
|
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
import favoriteTracks from '@/favorites/tracks'
|
|
||||||
|
|
||||||
// URL and endpoint constants
|
|
||||||
const LOGIN_URL = config.API_URL + 'token/'
|
|
||||||
const USER_PROFILE_URL = config.API_URL + 'users/users/me/'
|
|
||||||
// const SIGNUP_URL = API_URL + 'users/'
|
|
||||||
|
|
||||||
let userData = {
|
|
||||||
authenticated: false,
|
|
||||||
username: '',
|
|
||||||
availablePermissions: {},
|
|
||||||
profile: {}
|
|
||||||
}
|
|
||||||
let auth = {
|
|
||||||
|
|
||||||
// Send a request to the login URL and save the returned JWT
|
|
||||||
login (context, creds, redirect, onError) {
|
|
||||||
return context.$http.post(LOGIN_URL, creds).then(response => {
|
|
||||||
logger.default.info('Successfully logged in as', creds.username)
|
|
||||||
cache.set('token', response.data.token)
|
|
||||||
cache.set('username', creds.username)
|
|
||||||
|
|
||||||
this.user.authenticated = true
|
|
||||||
this.user.username = creds.username
|
|
||||||
this.connect()
|
|
||||||
// Redirect to a specified route
|
|
||||||
if (redirect) {
|
|
||||||
context.$router.push(redirect)
|
|
||||||
}
|
|
||||||
}, response => {
|
|
||||||
logger.default.error('Error while logging in', response.data)
|
|
||||||
if (onError) {
|
|
||||||
onError(response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// To log out, we just need to remove the token
|
|
||||||
logout () {
|
|
||||||
cache.clear()
|
|
||||||
this.user.authenticated = false
|
|
||||||
logger.default.info('Log out, goodbye!')
|
|
||||||
},
|
|
||||||
|
|
||||||
checkAuth () {
|
|
||||||
logger.default.info('Checking authentication...')
|
|
||||||
var jwt = this.getAuthToken()
|
|
||||||
var username = cache.get('username')
|
|
||||||
if (jwt) {
|
|
||||||
this.user.authenticated = true
|
|
||||||
this.user.username = username
|
|
||||||
logger.default.info('Logged back in as ' + username)
|
|
||||||
this.connect()
|
|
||||||
} else {
|
|
||||||
logger.default.info('Anonymous user')
|
|
||||||
this.user.authenticated = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getAuthToken () {
|
|
||||||
return cache.get('token')
|
|
||||||
},
|
|
||||||
|
|
||||||
// The object to be passed as a header for authenticated requests
|
|
||||||
getAuthHeader () {
|
|
||||||
return 'JWT ' + this.getAuthToken()
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchProfile () {
|
|
||||||
let resource = Vue.resource(USER_PROFILE_URL)
|
|
||||||
return resource.get({}).then((response) => {
|
|
||||||
logger.default.info('Successfully fetched user profile')
|
|
||||||
return response.data
|
|
||||||
}, (response) => {
|
|
||||||
logger.default.info('Error while fetching user profile')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
connect () {
|
|
||||||
// called once user has logged in successfully / reauthenticated
|
|
||||||
// e.g. after a page refresh
|
|
||||||
let self = this
|
|
||||||
this.fetchProfile().then(data => {
|
|
||||||
Vue.set(self.user, 'profile', data)
|
|
||||||
Object.keys(data.permissions).forEach(function (key) {
|
|
||||||
// this makes it easier to check for permissions in templates
|
|
||||||
Vue.set(self.user.availablePermissions, key, data.permissions[String(key)].status)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
favoriteTracks.fetch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.set(auth, 'user', userData)
|
|
||||||
export default auth
|
|
|
@ -28,8 +28,8 @@
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="ui bottom attached active tab" data-tab="library">
|
<div class="ui bottom attached active tab" data-tab="library">
|
||||||
<div class="ui inverted vertical fluid menu">
|
<div class="ui inverted vertical fluid menu">
|
||||||
<router-link class="item" v-if="auth.user.authenticated" :to="{name: 'profile', params: {username: auth.user.username}}"><i class="user icon"></i> Logged in as {{ auth.user.username }}</router-link>
|
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><i class="user icon"></i> Logged in as {{ $store.state.auth.username }}</router-link>
|
||||||
<router-link class="item" v-if="auth.user.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i> Logout</router-link>
|
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i> Logout</router-link>
|
||||||
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> Login</router-link>
|
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> Login</router-link>
|
||||||
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>Browse library</router-link>
|
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>Browse library</router-link>
|
||||||
<router-link class="item" :to="{path: '/favorites'}"><i class="heart icon"></i> Favorites</router-link>
|
<router-link class="item" :to="{path: '/favorites'}"><i class="heart icon"></i> Favorites</router-link>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<div class="ui bottom attached tab" data-tab="queue">
|
<div class="ui bottom attached tab" data-tab="queue">
|
||||||
<table class="ui compact inverted very basic fixed single line table">
|
<table class="ui compact inverted very basic fixed single line table">
|
||||||
<draggable v-model="queue.tracks" element="tbody" @update="reorder">
|
<draggable v-model="queue.tracks" element="tbody" @update="reorder">
|
||||||
<tr @click="queue.play(index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
|
<tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
|
||||||
<td class="right aligned">{{ index + 1}}</td>
|
<td class="right aligned">{{ index + 1}}</td>
|
||||||
<td class="center aligned">
|
<td class="center aligned">
|
||||||
<img class="ui mini image" v-if="track.album.cover" :src="backend.absoluteUrl(track.album.cover)">
|
<img class="ui mini image" v-if="track.album.cover" :src="backend.absoluteUrl(track.album.cover)">
|
||||||
|
@ -63,23 +63,23 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="favoriteTracks.objects[track.id]">
|
<template v-if="favoriteTracks.objects[track.id]">
|
||||||
<i @click.stop="queue.cleanTrack(index)" class="pink heart icon"></i>
|
<i class="pink heart icon"></i>
|
||||||
</template
|
</template
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<i @click.stop="queue.cleanTrack(index)" class="circular trash icon"></i>
|
<i @click.stop="cleanTrack(index)" class="circular trash icon"></i>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</draggable>
|
</draggable>
|
||||||
</table>
|
</table>
|
||||||
<div v-if="radios.running" class="ui black message">
|
<div v-if="$store.state.radios.running" class="ui black message">
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<i class="feed icon"></i> You have a radio playing
|
<i class="feed icon"></i> You have a radio playing
|
||||||
</div>
|
</div>
|
||||||
<p>New tracks will be appended here automatically.</p>
|
<p>New tracks will be appended here automatically.</p>
|
||||||
<div @click="radios.stop()" class="ui basic inverted red button">Stop radio</div>
|
<div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button">Stop radio</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,24 +87,18 @@
|
||||||
<div class="ui inverted segment player-wrapper">
|
<div class="ui inverted segment player-wrapper">
|
||||||
<player></player>
|
<player></player>
|
||||||
</div>
|
</div>
|
||||||
<GlobalEvents
|
|
||||||
@keydown.r.stop="queue.restore"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import GlobalEvents from '@/components/utils/global-events'
|
import {mapState, mapActions} from 'vuex'
|
||||||
|
|
||||||
import Player from '@/components/audio/Player'
|
import Player from '@/components/audio/Player'
|
||||||
import favoriteTracks from '@/favorites/tracks'
|
import favoriteTracks from '@/favorites/tracks'
|
||||||
import Logo from '@/components/Logo'
|
import Logo from '@/components/Logo'
|
||||||
import SearchBar from '@/components/audio/SearchBar'
|
import SearchBar from '@/components/audio/SearchBar'
|
||||||
import auth from '@/auth'
|
|
||||||
import queue from '@/audio/queue'
|
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import radios from '@/radios'
|
|
||||||
|
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
|
@ -114,24 +108,28 @@ export default {
|
||||||
Player,
|
Player,
|
||||||
SearchBar,
|
SearchBar,
|
||||||
Logo,
|
Logo,
|
||||||
draggable,
|
draggable
|
||||||
GlobalEvents
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
auth: auth,
|
|
||||||
backend: backend,
|
backend: backend,
|
||||||
queue: queue,
|
|
||||||
radios,
|
|
||||||
favoriteTracks
|
favoriteTracks
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
$(this.$el).find('.menu .item').tab()
|
$(this.$el).find('.menu .item').tab()
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
queue: state => state.queue
|
||||||
|
})
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
reorder (e) {
|
...mapActions({
|
||||||
this.queue.reorder(e.oldIndex, e.newIndex)
|
cleanTrack: 'queue/cleanTrack'
|
||||||
|
}),
|
||||||
|
reorder: function (oldValue, newValue) {
|
||||||
|
this.$store.commit('queue/reorder', {oldValue, newValue})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import queue from '@/audio/queue'
|
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -40,19 +39,19 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
add () {
|
add () {
|
||||||
if (this.track) {
|
if (this.track) {
|
||||||
queue.append(this.track)
|
this.$store.dispatch('queue/append', {track: this.track})
|
||||||
} else {
|
} else {
|
||||||
queue.appendMany(this.tracks)
|
this.$store.dispatch('queue/appendMany', {tracks: this.tracks})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addNext (next) {
|
addNext (next) {
|
||||||
if (this.track) {
|
if (this.track) {
|
||||||
queue.append(this.track, queue.currentIndex + 1)
|
this.$store.dispatch('queue/append', {track: this.track, index: this.$store.state.queue.currentIndex + 1})
|
||||||
} else {
|
} else {
|
||||||
queue.appendMany(this.tracks, queue.currentIndex + 1)
|
this.$store.dispatch('queue/appendMany', {tracks: this.tracks, index: this.$store.state.queue.currentIndex + 1})
|
||||||
}
|
}
|
||||||
if (next) {
|
if (next) {
|
||||||
queue.next()
|
this.$store.dispatch('queue/next')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,104 +1,112 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="player">
|
<div class="player">
|
||||||
<div v-if="queue.currentTrack" class="track-area ui items">
|
<audio-track
|
||||||
|
ref="currentAudio"
|
||||||
|
v-if="currentTrack"
|
||||||
|
:key="(currentIndex, currentTrack.id)"
|
||||||
|
:is-current="true"
|
||||||
|
:track="currentTrack">
|
||||||
|
</audio-track>
|
||||||
|
|
||||||
|
<div v-if="currentTrack" class="track-area ui items">
|
||||||
<div class="ui inverted item">
|
<div class="ui inverted item">
|
||||||
<div class="ui tiny image">
|
<div class="ui tiny image">
|
||||||
<img v-if="queue.currentTrack.album.cover" :src="Track.getCover(queue.currentTrack)">
|
<img v-if="currentTrack.album.cover" :src="Track.getCover(currentTrack)">
|
||||||
<img v-else src="../../assets/audio/default-cover.png">
|
<img v-else src="../../assets/audio/default-cover.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="middle aligned content">
|
<div class="middle aligned content">
|
||||||
<router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: queue.currentTrack.id }}">
|
<router-link class="small header discrete link track" :to="{name: 'library.tracks.detail', params: {id: currentTrack.id }}">
|
||||||
{{ queue.currentTrack.title }}
|
{{ currentTrack.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<router-link class="artist" :to="{name: 'library.artists.detail', params: {id: queue.currentTrack.artist.id }}">
|
<router-link class="artist" :to="{name: 'library.artists.detail', params: {id: currentTrack.artist.id }}">
|
||||||
{{ queue.currentTrack.artist.name }}
|
{{ currentTrack.artist.name }}
|
||||||
</router-link> /
|
</router-link> /
|
||||||
<router-link class="album" :to="{name: 'library.albums.detail', params: {id: queue.currentTrack.album.id }}">
|
<router-link class="album" :to="{name: 'library.albums.detail', params: {id: currentTrack.album.id }}">
|
||||||
{{ queue.currentTrack.album.title }}
|
{{ currentTrack.album.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<track-favorite-icon :track="queue.currentTrack"></track-favorite-icon>
|
<track-favorite-icon :track="currentTrack"></track-favorite-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-area" v-if="queue.currentTrack">
|
<div class="progress-area" v-if="currentTrack">
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="left floated four wide column">
|
<div class="left floated four wide column">
|
||||||
<p class="timer start" @click="queue.audio.setTime(0)">{{queue.audio.state.currentTimeFormat}}</p>
|
<p class="timer start" @click="updateProgress(0)">{{currentTimeFormatted}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right floated four wide column">
|
<div class="right floated four wide column">
|
||||||
<p class="timer total">{{queue.audio.state.durationTimerFormat}}</p>
|
<p class="timer total">{{durationFormatted}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="progress" class="ui small orange inverted progress" @click="touchProgress">
|
<div ref="progress" class="ui small orange inverted progress" @click="touchProgress">
|
||||||
<div class="bar" :data-percent="queue.audio.state.progress" :style="{ 'width': queue.audio.state.progress + '%' }"></div>
|
<div class="bar" :data-percent="progress" :style="{ 'width': progress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="two wide column controls ui grid">
|
<div class="two wide column controls ui grid">
|
||||||
<div
|
<div
|
||||||
@click="queue.previous()"
|
@click="previous"
|
||||||
title="Previous track"
|
title="Previous track"
|
||||||
class="two wide column control"
|
class="two wide column control"
|
||||||
:disabled="!hasPrevious">
|
:disabled="!hasPrevious">
|
||||||
<i :class="['ui', {'disabled': !hasPrevious}, 'step', 'backward', 'big', 'icon']" ></i>
|
<i :class="['ui', {'disabled': !hasPrevious}, 'step', 'backward', 'big', 'icon']" ></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!queue.audio.state.playing"
|
v-if="!playing"
|
||||||
@click="pauseOrPlay"
|
@click="togglePlay"
|
||||||
title="Play track"
|
title="Play track"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
<i :class="['ui', 'play', {'disabled': !queue.currentTrack}, 'big', 'icon']"></i>
|
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@click="pauseOrPlay"
|
@click="togglePlay"
|
||||||
title="Pause track"
|
title="Pause track"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
<i :class="['ui', 'pause', {'disabled': !queue.currentTrack}, 'big', 'icon']"></i>
|
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@click="queue.next()"
|
@click="next"
|
||||||
title="Next track"
|
title="Next track"
|
||||||
class="two wide column control"
|
class="two wide column control"
|
||||||
:disabled="!hasNext">
|
:disabled="!hasNext">
|
||||||
<i :class="['ui', {'disabled': !hasPrevious}, 'step', 'forward', 'big', 'icon']" ></i>
|
<i :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="two wide column control volume-control">
|
<div class="two wide column control volume-control">
|
||||||
<i title="Unmute" @click="queue.setVolume(1)" v-if="currentVolume === 0" class="volume off secondary icon"></i>
|
<i title="Unmute" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
|
||||||
<i title="Mute" @click="queue.setVolume(0)" v-else-if="currentVolume < 0.5" class="volume down secondary icon"></i>
|
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
|
||||||
<i title="Mute" @click="queue.setVolume(0)" v-else class="volume up secondary icon"></i>
|
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
|
||||||
<input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
|
<input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
|
||||||
</div>
|
</div>
|
||||||
<div class="two wide column control looping">
|
<div class="two wide column control looping">
|
||||||
<i
|
<i
|
||||||
title="Looping disabled. Click to switch to single-track looping."
|
title="Looping disabled. Click to switch to single-track looping."
|
||||||
v-if="queue.state.looping === 0"
|
v-if="looping === 0"
|
||||||
@click="queue.state.looping = 1"
|
@click="$store.commit('player/looping', 1)"
|
||||||
:disabled="!queue.currentTrack"
|
:disabled="!currentTrack"
|
||||||
:class="['ui', {'disabled': !queue.currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
|
:class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
|
||||||
<i
|
<i
|
||||||
title="Looping on a single track. Click to switch to whole queue looping."
|
title="Looping on a single track. Click to switch to whole queue looping."
|
||||||
v-if="queue.state.looping === 1"
|
v-if="looping === 1"
|
||||||
@click="queue.state.looping = 2"
|
@click="$store.commit('player/looping', 2)"
|
||||||
:disabled="!queue.currentTrack"
|
:disabled="!currentTrack"
|
||||||
class="repeat secondary icon">
|
class="repeat secondary icon">
|
||||||
<span class="ui circular tiny orange label">1</span>
|
<span class="ui circular tiny orange label">1</span>
|
||||||
</i>
|
</i>
|
||||||
<i
|
<i
|
||||||
title="Looping on whole queue. Click to disable looping."
|
title="Looping on whole queue. Click to disable looping."
|
||||||
v-if="queue.state.looping === 2"
|
v-if="looping === 2"
|
||||||
@click="queue.state.looping = 0"
|
@click="$store.commit('player/looping', 0)"
|
||||||
:disabled="!queue.currentTrack"
|
:disabled="!currentTrack"
|
||||||
class="repeat orange secondary icon">
|
class="repeat orange secondary icon">
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@click="queue.shuffle()"
|
@click="shuffle()"
|
||||||
:disabled="queue.tracks.length === 0"
|
:disabled="queue.tracks.length === 0"
|
||||||
title="Shuffle your queue"
|
title="Shuffle your queue"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
|
@ -106,7 +114,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="one wide column"></div>
|
<div class="one wide column"></div>
|
||||||
<div
|
<div
|
||||||
@click="queue.clean()"
|
@click="clean()"
|
||||||
:disabled="queue.tracks.length === 0"
|
:disabled="queue.tracks.length === 0"
|
||||||
title="Clear your queue"
|
title="Clear your queue"
|
||||||
class="two wide column control">
|
class="two wide column control">
|
||||||
|
@ -114,79 +122,87 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GlobalEvents
|
<GlobalEvents
|
||||||
@keydown.space.prevent.exact="pauseOrPlay"
|
@keydown.space.prevent.exact="togglePlay"
|
||||||
@keydown.ctrl.left.prevent.exact="queue.previous"
|
@keydown.ctrl.left.prevent.exact="previous"
|
||||||
@keydown.ctrl.right.prevent.exact="queue.next"
|
@keydown.ctrl.right.prevent.exact="next"
|
||||||
@keydown.ctrl.down.prevent.exact="queue.incrementVolume(-0.1)"
|
@keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
|
||||||
@keydown.ctrl.up.prevent.exact="queue.incrementVolume(0.1)"
|
@keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
|
||||||
@keydown.f.prevent.exact="favoriteTracks.toggle(queue.currentTrack.id)"
|
@keydown.f.prevent.exact="favoriteTracks.toggle(currentTrack.id)"
|
||||||
@keydown.l.prevent.exact="queue.toggleLooping"
|
@keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
|
||||||
@keydown.s.prevent.exact="queue.shuffle"
|
@keydown.s.prevent.exact="shuffle"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {mapState, mapGetters, mapActions} from 'vuex'
|
||||||
import GlobalEvents from '@/components/utils/global-events'
|
import GlobalEvents from '@/components/utils/global-events'
|
||||||
|
|
||||||
import favoriteTracks from '@/favorites/tracks'
|
import favoriteTracks from '@/favorites/tracks'
|
||||||
import queue from '@/audio/queue'
|
|
||||||
import radios from '@/radios'
|
|
||||||
import Track from '@/audio/track'
|
import Track from '@/audio/track'
|
||||||
|
import AudioTrack from '@/components/audio/Track'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'player',
|
name: 'player',
|
||||||
components: {
|
components: {
|
||||||
TrackFavoriteIcon,
|
TrackFavoriteIcon,
|
||||||
GlobalEvents
|
GlobalEvents,
|
||||||
|
AudioTrack
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
sliderVolume: this.currentVolume,
|
sliderVolume: this.volume,
|
||||||
queue: queue,
|
|
||||||
Track: Track,
|
Track: Track,
|
||||||
favoriteTracks,
|
favoriteTracks
|
||||||
radios
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
// we trigger the watcher explicitely it does not work otherwise
|
// we trigger the watcher explicitely it does not work otherwise
|
||||||
this.sliderVolume = this.currentVolume
|
this.sliderVolume = this.volume
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
pauseOrPlay () {
|
...mapActions({
|
||||||
if (this.queue.audio.state.playing) {
|
pause: 'player/pause',
|
||||||
this.queue.audio.pause()
|
togglePlay: 'player/togglePlay',
|
||||||
} else {
|
clean: 'queue/clean',
|
||||||
this.queue.audio.play()
|
next: 'queue/next',
|
||||||
}
|
previous: 'queue/previous',
|
||||||
},
|
shuffle: 'queue/shuffle',
|
||||||
|
updateProgress: 'player/updateProgress'
|
||||||
|
}),
|
||||||
touchProgress (e) {
|
touchProgress (e) {
|
||||||
let time
|
let time
|
||||||
let target = this.$refs.progress
|
let target = this.$refs.progress
|
||||||
time = e.layerX / target.offsetWidth * this.queue.audio.state.duration
|
time = e.layerX / target.offsetWidth * this.duration
|
||||||
this.queue.audio.setTime(time)
|
this.$refs.currentAudio.setCurrentTime(time)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasPrevious () {
|
...mapState({
|
||||||
return this.queue.currentIndex > 0
|
currentIndex: state => state.queue.currentIndex,
|
||||||
},
|
playing: state => state.player.playing,
|
||||||
hasNext () {
|
volume: state => state.player.volume,
|
||||||
return this.queue.currentIndex < this.queue.tracks.length - 1
|
looping: state => state.player.looping,
|
||||||
},
|
duration: state => state.player.duration,
|
||||||
currentVolume () {
|
queue: state => state.queue
|
||||||
return this.queue.audio.state.volume
|
}),
|
||||||
}
|
...mapGetters({
|
||||||
|
currentTrack: 'queue/currentTrack',
|
||||||
|
hasNext: 'queue/hasNext',
|
||||||
|
hasPrevious: 'queue/hasPrevious',
|
||||||
|
durationFormatted: 'player/durationFormatted',
|
||||||
|
currentTimeFormatted: 'player/currentTimeFormatted',
|
||||||
|
progress: 'player/progress'
|
||||||
|
})
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentVolume (newValue) {
|
volume (newValue) {
|
||||||
this.sliderVolume = newValue
|
this.sliderVolume = newValue
|
||||||
},
|
},
|
||||||
sliderVolume (newValue) {
|
sliderVolume (newValue) {
|
||||||
this.queue.setVolume(parseFloat(newValue))
|
this.$store.commit('player/volume', newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import queue from '@/audio/queue'
|
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import AlbumCard from '@/components/audio/album/Card'
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
import ArtistCard from '@/components/audio/artist/Card'
|
import ArtistCard from '@/components/audio/artist/Card'
|
||||||
|
@ -54,8 +53,7 @@ export default {
|
||||||
artists: []
|
artists: []
|
||||||
},
|
},
|
||||||
backend: backend,
|
backend: backend,
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
queue: queue
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
<script>
|
<script>
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import auth from '@/auth'
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
const SEARCH_URL = config.API_URL + 'search?query={query}'
|
const SEARCH_URL = config.API_URL + 'search?query={query}'
|
||||||
|
@ -27,7 +26,7 @@ export default {
|
||||||
},
|
},
|
||||||
apiSettings: {
|
apiSettings: {
|
||||||
beforeXHR: function (xhrObject) {
|
beforeXHR: function (xhrObject) {
|
||||||
xhrObject.setRequestHeader('Authorization', auth.getAuthHeader())
|
xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header'])
|
||||||
return xhrObject
|
return xhrObject
|
||||||
},
|
},
|
||||||
onResponse: function (initialResponse) {
|
onResponse: function (initialResponse) {
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<audio
|
||||||
|
ref="audio"
|
||||||
|
:src="url"
|
||||||
|
@error="errored"
|
||||||
|
@progress="updateLoad"
|
||||||
|
@loadeddata="loaded"
|
||||||
|
@timeupdate="updateProgress"
|
||||||
|
@ended="ended"
|
||||||
|
preload>
|
||||||
|
|
||||||
|
</audio>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
import backend from '@/audio/backend'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
|
||||||
|
// import logger from '@/logging'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
track: {type: Object},
|
||||||
|
isCurrent: {type: Boolean, default: false}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
playing: state => state.player.playing,
|
||||||
|
currentTime: state => state.player.currentTime,
|
||||||
|
duration: state => state.player.duration,
|
||||||
|
volume: state => state.player.volume,
|
||||||
|
looping: state => state.player.looping
|
||||||
|
}),
|
||||||
|
url: function () {
|
||||||
|
let file = this.track.files[0]
|
||||||
|
if (!file) {
|
||||||
|
this.$store.dispatch('player/trackErrored')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let path = backend.absoluteUrl(file.path)
|
||||||
|
if (this.$store.state.auth.authenticated) {
|
||||||
|
// we need to send the token directly in url
|
||||||
|
// so authentication can be checked by the backend
|
||||||
|
// because for audio files we cannot use the regular Authentication
|
||||||
|
// header
|
||||||
|
path = url.updateQueryString(path, 'jwt', this.$store.state.auth.token)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
errored: function () {
|
||||||
|
this.$store.dispatch('player/trackErrored')
|
||||||
|
},
|
||||||
|
updateLoad: function () {
|
||||||
|
|
||||||
|
},
|
||||||
|
loaded: function () {
|
||||||
|
this.$store.commit('player/duration', this.$refs.audio.duration)
|
||||||
|
if (this.isCurrent) {
|
||||||
|
this.$store.commit('player/playing', true)
|
||||||
|
this.$refs.audio.play()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateProgress: function () {
|
||||||
|
if (this.$refs.audio) {
|
||||||
|
this.$store.dispatch('player/updateProgress', this.$refs.audio.currentTime)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ended: function () {
|
||||||
|
if (this.looping === 1) {
|
||||||
|
this.setCurrentTime(0)
|
||||||
|
this.$refs.audio.play()
|
||||||
|
}
|
||||||
|
this.$store.dispatch('player/trackEnded', this.track)
|
||||||
|
},
|
||||||
|
setCurrentTime (t) {
|
||||||
|
if (t < 0 | t > this.duration) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateProgress(t)
|
||||||
|
this.$refs.audio.currentTime = t
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
playing: function (newValue) {
|
||||||
|
if (newValue === true) {
|
||||||
|
this.$refs.audio.play()
|
||||||
|
} else {
|
||||||
|
this.$refs.audio.pause()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volume: function (newValue) {
|
||||||
|
this.$refs.audio.volume = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -51,7 +51,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import queue from '@/audio/queue'
|
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
@ -68,7 +67,6 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
backend: backend,
|
backend: backend,
|
||||||
queue: queue,
|
|
||||||
initialTracks: 4,
|
initialTracks: 4,
|
||||||
showAllTracks: false
|
showAllTracks: false
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,9 +58,9 @@
|
||||||
Keep your PRIVATE_TOKEN secret as it gives access to your account.
|
Keep your PRIVATE_TOKEN secret as it gives access to your account.
|
||||||
</div>
|
</div>
|
||||||
<pre>
|
<pre>
|
||||||
export PRIVATE_TOKEN="{{ auth.getAuthToken ()}}"
|
export PRIVATE_TOKEN="{{ $store.state.auth.token }}"
|
||||||
<template v-for="track in tracks"><template v-if="track.files.length > 0">
|
<template v-for="track in tracks"><template v-if="track.files.length > 0">
|
||||||
curl -G -o "{{ track.files[0].filename }}" <template v-if="auth.user.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ backend.absoluteUrl(track.files[0].path) }}"</template></template>
|
curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ backend.absoluteUrl(track.files[0].path) }}"</template></template>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,7 +83,6 @@ curl -G -o "{{ track.files[0].filename }}" <template v-if="auth.user.authenticat
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
import auth from '@/auth'
|
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
|
||||||
|
@ -102,7 +101,6 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
backend: backend,
|
backend: backend,
|
||||||
auth: auth,
|
|
||||||
showDownloadModal: false
|
showDownloadModal: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,12 +39,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '@/auth'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
props: {
|
props: {
|
||||||
next: {type: String}
|
next: {type: String, default: '/'}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -72,14 +71,17 @@ export default {
|
||||||
}
|
}
|
||||||
// We need to pass the component's this context
|
// We need to pass the component's this context
|
||||||
// to properly make use of http in the auth service
|
// to properly make use of http in the auth service
|
||||||
auth.login(this, credentials, {path: this.next}, function (response) {
|
this.$store.dispatch('auth/login', {
|
||||||
// error callback
|
credentials,
|
||||||
if (response.status === 400) {
|
next: this.next,
|
||||||
self.error = 'invalid_credentials'
|
onError: response => {
|
||||||
} else {
|
if (response.status === 400) {
|
||||||
self.error = 'unknown_error'
|
self.error = 'invalid_credentials'
|
||||||
|
} else {
|
||||||
|
self.error = 'unknown_error'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).then((response) => {
|
}).then(e => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<div class="ui vertical stripe segment">
|
<div class="ui vertical stripe segment">
|
||||||
<div class="ui small text container">
|
<div class="ui small text container">
|
||||||
<h2>Are you sure you want to log out?</h2>
|
<h2>Are you sure you want to log out?</h2>
|
||||||
<p>You are currently logged in as {{ auth.user.username }}</p>
|
<p>You are currently logged in as {{ $store.state.auth.username }}</p>
|
||||||
<button class="ui button" @click="logout">Yes, log me out!</button>
|
<button class="ui button" @click="$store.dispatch('auth/logout')">Yes, log me out!</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,23 +12,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '@/auth'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'logout',
|
name: 'logout'
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
// We need to initialize the component with any
|
|
||||||
// properties that will be used in it
|
|
||||||
auth: auth
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
logout () {
|
|
||||||
auth.logout()
|
|
||||||
this.$router.push({name: 'index'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,17 @@
|
||||||
<div v-if="isLoading" class="ui vertical segment">
|
<div v-if="isLoading" class="ui vertical segment">
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="profile">
|
<template v-if="$store.state.auth.profile">
|
||||||
<div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']">
|
<div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']">
|
||||||
<h2 class="ui center aligned icon header">
|
<h2 class="ui center aligned icon header">
|
||||||
<i class="circular inverted user green icon"></i>
|
<i class="circular inverted user green icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ profile.username }}
|
{{ $store.state.auth.profile.username }}
|
||||||
<div class="sub header">Registered since {{ signupDate }}</div>
|
<div class="sub header">Registered since {{ signupDate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="ui basic green label">this is you!</div>
|
<div class="ui basic green label">this is you!</div>
|
||||||
<div v-if="profile.is_staff" class="ui yellow label">
|
<div v-if="$store.state.auth.profile.is_staff" class="ui yellow label">
|
||||||
<i class="star icon"></i>
|
<i class="star icon"></i>
|
||||||
Staff member
|
Staff member
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,35 +23,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '@/auth'
|
const dateFormat = require('dateformat')
|
||||||
var dateFormat = require('dateformat')
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
props: ['username'],
|
props: ['username'],
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
profile: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
created () {
|
||||||
this.fetchProfile()
|
this.$store.dispatch('auth/fetchProfile')
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchProfile () {
|
|
||||||
let self = this
|
|
||||||
auth.fetchProfile().then(data => {
|
|
||||||
self.profile = data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
signupDate () {
|
signupDate () {
|
||||||
let d = new Date(this.profile.date_joined)
|
let d = new Date(this.$store.state.auth.profile.date_joined)
|
||||||
return dateFormat(d, 'longDate')
|
return dateFormat(d, 'longDate')
|
||||||
},
|
},
|
||||||
isLoading () {
|
isLoading () {
|
||||||
return !this.profile
|
return !this.$store.state.auth.profile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
<h2 v-if="results" class="ui center aligned icon header">
|
<h2 v-if="results" class="ui center aligned icon header">
|
||||||
<i class="circular inverted heart pink icon"></i>
|
<i class="circular inverted heart pink icon"></i>
|
||||||
{{ favoriteTracks.count }} favorites
|
{{ $store.state.favorites.count }} favorites
|
||||||
</h2>
|
</h2>
|
||||||
<radio-button type="favorites"></radio-button>
|
<radio-button type="favorites"></radio-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,10 +55,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import Vue from 'vue'
|
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import favoriteTracks from '@/favorites/tracks'
|
|
||||||
import TrackTable from '@/components/audio/track/Table'
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
import RadioButton from '@/components/radios/Button'
|
import RadioButton from '@/components/radios/Button'
|
||||||
import Pagination from '@/components/Pagination'
|
import Pagination from '@/components/Pagination'
|
||||||
|
@ -80,7 +78,6 @@ export default {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
nextLink: null,
|
nextLink: null,
|
||||||
previousLink: null,
|
previousLink: null,
|
||||||
favoriteTracks,
|
|
||||||
page: parseInt(this.defaultPage),
|
page: parseInt(this.defaultPage),
|
||||||
paginateBy: parseInt(this.defaultPaginateBy || 25),
|
paginateBy: parseInt(this.defaultPaginateBy || 25),
|
||||||
orderingDirection: defaultOrdering.direction,
|
orderingDirection: defaultOrdering.direction,
|
||||||
|
@ -122,10 +119,9 @@ export default {
|
||||||
self.results = response.data
|
self.results = response.data
|
||||||
self.nextLink = response.data.next
|
self.nextLink = response.data.next
|
||||||
self.previousLink = response.data.previous
|
self.previousLink = response.data.previous
|
||||||
Vue.set(favoriteTracks, 'count', response.data.count)
|
self.$store.commit('favorites/count', response.data.count)
|
||||||
favoriteTracks.count = response.data.count
|
|
||||||
self.results.results.forEach((track) => {
|
self.results.results.forEach((track) => {
|
||||||
Vue.set(favoriteTracks.objects, track.id, true)
|
self.$store.commit('favorites/track', {id: track.id, value: true})
|
||||||
})
|
})
|
||||||
logger.default.timeEnd('Loading user favorites')
|
logger.default.timeEnd('Loading user favorites')
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click="favoriteTracks.set(track.id, !isFavorite)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']">
|
<button @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']">
|
||||||
<i class="heart icon"></i>
|
<i class="heart icon"></i>
|
||||||
<template v-if="isFavorite">
|
<template v-if="isFavorite">
|
||||||
In favorites
|
In favorites
|
||||||
|
@ -8,23 +8,23 @@
|
||||||
Add to favorites
|
Add to favorites
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
<i v-else @click="favoriteTracks.set(track.id, !isFavorite)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
|
<i v-else @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import favoriteTracks from '@/favorites/tracks'
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
track: {type: Object},
|
track: {type: Object},
|
||||||
button: {type: Boolean, default: false}
|
button: {type: Boolean, default: false}
|
||||||
},
|
},
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
favoriteTracks
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
favorites: state => {
|
||||||
|
return state.favorites.tracks
|
||||||
|
}
|
||||||
|
}),
|
||||||
title () {
|
title () {
|
||||||
if (this.isFavorite) {
|
if (this.isFavorite) {
|
||||||
return 'Remove from favorites'
|
return 'Remove from favorites'
|
||||||
|
@ -33,7 +33,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isFavorite () {
|
isFavorite () {
|
||||||
return favoriteTracks.objects[this.track.id]
|
return this.$store.getters['favorites/isFavorite'](this.track.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<router-link class="ui item" to="/library" exact>Browse</router-link>
|
<router-link class="ui item" to="/library" exact>Browse</router-link>
|
||||||
<router-link class="ui item" to="/library/artists" exact>Artists</router-link>
|
<router-link class="ui item" to="/library/artists" exact>Artists</router-link>
|
||||||
<div class="ui secondary right menu">
|
<div class="ui secondary right menu">
|
||||||
<router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link>
|
<router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link>
|
||||||
<router-link v-if="auth.user.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link>
|
<router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<router-view :key="$route.fullPath"></router-view>
|
<router-view :key="$route.fullPath"></router-view>
|
||||||
|
@ -14,15 +14,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import auth from '@/auth'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'library',
|
name: 'library'
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import auth from '@/auth'
|
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import logger from '@/logging'
|
import logger from '@/logging'
|
||||||
import backend from '@/audio/backend'
|
import backend from '@/audio/backend'
|
||||||
|
@ -124,8 +123,8 @@ export default {
|
||||||
downloadUrl () {
|
downloadUrl () {
|
||||||
if (this.track.files.length > 0) {
|
if (this.track.files.length > 0) {
|
||||||
let u = backend.absoluteUrl(this.track.files[0].path)
|
let u = backend.absoluteUrl(this.track.files[0].path)
|
||||||
if (auth.user.authenticated) {
|
if (this.$store.state.auth.authenticated) {
|
||||||
u = url.updateQueryString(u, 'jwt', auth.getAuthToken())
|
u = url.updateQueryString(u, 'jwt', this.$store.state.auth.token)
|
||||||
}
|
}
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
<script>
|
<script>
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import auth from '@/auth'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -66,7 +65,7 @@ export default {
|
||||||
},
|
},
|
||||||
apiSettings: {
|
apiSettings: {
|
||||||
beforeXHR: function (xhrObject, s) {
|
beforeXHR: function (xhrObject, s) {
|
||||||
xhrObject.setRequestHeader('Authorization', auth.getAuthHeader())
|
xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header'])
|
||||||
return xhrObject
|
return xhrObject
|
||||||
},
|
},
|
||||||
onResponse: function (initialResponse) {
|
onResponse: function (initialResponse) {
|
||||||
|
|
|
@ -9,33 +9,28 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import radios from '@/radios'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
type: {type: String, required: true},
|
type: {type: String, required: true},
|
||||||
objectId: {type: Number, default: null}
|
objectId: {type: Number, default: null}
|
||||||
},
|
},
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
radios
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleRadio () {
|
toggleRadio () {
|
||||||
if (this.running) {
|
if (this.running) {
|
||||||
radios.stop()
|
this.$store.dispatch('radios/stop')
|
||||||
} else {
|
} else {
|
||||||
radios.start(this.type, this.objectId)
|
this.$store.dispatch('radios/start', {type: this.type, objectId: this.objectId})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
running () {
|
running () {
|
||||||
if (!radios.running) {
|
let state = this.$store.state.radios
|
||||||
|
let current = state.current
|
||||||
|
if (!state.running) {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
return radios.current.type === this.type & radios.current.objectId === this.objectId
|
return current.type === this.type & current.objectId === this.objectId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import radios from '@/radios'
|
|
||||||
import RadioButton from './Button'
|
import RadioButton from './Button'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -25,7 +24,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
radio () {
|
radio () {
|
||||||
return radios.types[this.type]
|
return this.$store.getters['radios/types'][this.type]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
const REMOVE_URL = config.API_URL + 'favorites/tracks/remove/'
|
|
||||||
const FAVORITES_URL = config.API_URL + 'favorites/tracks/'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
objects: {},
|
|
||||||
count: 0,
|
|
||||||
set (id, newValue) {
|
|
||||||
let self = this
|
|
||||||
Vue.set(self.objects, id, newValue)
|
|
||||||
if (newValue) {
|
|
||||||
Vue.set(self, 'count', self.count + 1)
|
|
||||||
let resource = Vue.resource(FAVORITES_URL)
|
|
||||||
resource.save({}, {'track': id}).then((response) => {
|
|
||||||
logger.default.info('Successfully added track to favorites')
|
|
||||||
}, (response) => {
|
|
||||||
logger.default.info('Error while adding track to favorites')
|
|
||||||
Vue.set(self.objects, id, !newValue)
|
|
||||||
Vue.set(self, 'count', self.count - 1)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Vue.set(self, 'count', self.count - 1)
|
|
||||||
let resource = Vue.resource(REMOVE_URL)
|
|
||||||
resource.delete({}, {'track': id}).then((response) => {
|
|
||||||
logger.default.info('Successfully removed track from favorites')
|
|
||||||
}, (response) => {
|
|
||||||
logger.default.info('Error while removing track from favorites')
|
|
||||||
Vue.set(self.objects, id, !newValue)
|
|
||||||
Vue.set(self, 'count', self.count + 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggle (id) {
|
|
||||||
let isFavorite = this.objects[id]
|
|
||||||
this.set(id, !isFavorite)
|
|
||||||
},
|
|
||||||
fetch (url) {
|
|
||||||
// will fetch favorites by batches from API to have them locally
|
|
||||||
var self = this
|
|
||||||
url = url || FAVORITES_URL
|
|
||||||
let resource = Vue.resource(url)
|
|
||||||
resource.get().then((response) => {
|
|
||||||
logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
|
|
||||||
Vue.set(self, 'count', response.data.count)
|
|
||||||
response.data.results.forEach(result => {
|
|
||||||
Vue.set(self.objects, result.track, true)
|
|
||||||
})
|
|
||||||
if (response.data.next) {
|
|
||||||
self.fetch(response.data.next)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -9,8 +9,8 @@ import Vue from 'vue'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import VueResource from 'vue-resource'
|
import VueResource from 'vue-resource'
|
||||||
import auth from './auth'
|
|
||||||
import VueLazyload from 'vue-lazyload'
|
import VueLazyload from 'vue-lazyload'
|
||||||
|
import store from './store'
|
||||||
|
|
||||||
window.$ = window.jQuery = require('jquery')
|
window.$ = window.jQuery = require('jquery')
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ Vue.config.productionTip = false
|
||||||
|
|
||||||
Vue.http.interceptors.push(function (request, next) {
|
Vue.http.interceptors.push(function (request, next) {
|
||||||
// modify headers
|
// modify headers
|
||||||
if (auth.user.authenticated) {
|
if (store.state.auth.authenticated) {
|
||||||
request.headers.set('Authorization', auth.getAuthHeader())
|
request.headers.set('Authorization', store.getters['auth/header'])
|
||||||
}
|
}
|
||||||
next(function (response) {
|
next(function (response) {
|
||||||
// redirect to login form when we get unauthorized response from server
|
// redirect to login form when we get unauthorized response from server
|
||||||
|
@ -37,11 +37,12 @@ Vue.http.interceptors.push(function (request, next) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
auth.checkAuth()
|
store.dispatch('auth/check')
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
router,
|
router,
|
||||||
|
store,
|
||||||
template: '<App/>',
|
template: '<App/>',
|
||||||
components: { App }
|
components: { App }
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import config from '@/config'
|
|
||||||
import logger from '@/logging'
|
|
||||||
import queue from '@/audio/queue'
|
|
||||||
|
|
||||||
const CREATE_RADIO_URL = config.API_URL + 'radios/sessions/'
|
|
||||||
const GET_TRACK_URL = config.API_URL + 'radios/tracks/'
|
|
||||||
|
|
||||||
var radios = {
|
|
||||||
types: {
|
|
||||||
random: {
|
|
||||||
name: 'Random',
|
|
||||||
description: "Totally random picks, maybe you'll discover new things?"
|
|
||||||
},
|
|
||||||
favorites: {
|
|
||||||
name: 'Favorites',
|
|
||||||
description: 'Play your favorites tunes in a never-ending happiness loop.'
|
|
||||||
},
|
|
||||||
'less-listened': {
|
|
||||||
name: 'Less listened',
|
|
||||||
description: "Listen to tracks you usually don't. It's time to restore some balance."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
start (type, objectId) {
|
|
||||||
this.current.type = type
|
|
||||||
this.current.objectId = objectId
|
|
||||||
this.running = true
|
|
||||||
let resource = Vue.resource(CREATE_RADIO_URL)
|
|
||||||
var self = this
|
|
||||||
var params = {
|
|
||||||
radio_type: type,
|
|
||||||
related_object_id: objectId
|
|
||||||
}
|
|
||||||
resource.save({}, params).then((response) => {
|
|
||||||
logger.default.info('Successfully started radio ', type)
|
|
||||||
self.current.session = response.data.id
|
|
||||||
queue.populateFromRadio()
|
|
||||||
}, (response) => {
|
|
||||||
logger.default.error('Error while starting radio', type)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
stop () {
|
|
||||||
this.current.type = null
|
|
||||||
this.current.objectId = null
|
|
||||||
this.running = false
|
|
||||||
this.session = null
|
|
||||||
},
|
|
||||||
fetch () {
|
|
||||||
let resource = Vue.resource(GET_TRACK_URL)
|
|
||||||
var self = this
|
|
||||||
var params = {
|
|
||||||
session: self.current.session
|
|
||||||
}
|
|
||||||
return resource.save({}, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.set(radios, 'running', false)
|
|
||||||
Vue.set(radios, 'current', {})
|
|
||||||
Vue.set(radios.current, 'objectId', null)
|
|
||||||
Vue.set(radios.current, 'type', null)
|
|
||||||
Vue.set(radios.current, 'session', null)
|
|
||||||
|
|
||||||
export default radios
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import config from '@/config'
|
||||||
|
import logger from '@/logging'
|
||||||
|
import cache from '@/cache'
|
||||||
|
import router from '@/router'
|
||||||
|
// import favoriteTracks from '@/favorites/tracks'
|
||||||
|
|
||||||
|
const LOGIN_URL = config.API_URL + 'token/'
|
||||||
|
const USER_PROFILE_URL = config.API_URL + 'users/users/me/'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
authenticated: false,
|
||||||
|
username: '',
|
||||||
|
availablePermissions: {},
|
||||||
|
profile: null,
|
||||||
|
token: ''
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
header: state => {
|
||||||
|
return 'JWT ' + state.token
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
profile: (state, value) => {
|
||||||
|
state.profile = value
|
||||||
|
},
|
||||||
|
authenticated: (state, value) => {
|
||||||
|
state.authenticated = value
|
||||||
|
},
|
||||||
|
username: (state, value) => {
|
||||||
|
state.username = value
|
||||||
|
},
|
||||||
|
token: (state, value) => {
|
||||||
|
state.token = value
|
||||||
|
},
|
||||||
|
permission: (state, {key, status}) => {
|
||||||
|
state.availablePermissions[key] = status
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Send a request to the login URL and save the returned JWT
|
||||||
|
login ({commit, dispatch, state}, {next, credentials, onError}) {
|
||||||
|
let resource = Vue.resource(LOGIN_URL)
|
||||||
|
return resource.save({}, credentials).then(response => {
|
||||||
|
logger.default.info('Successfully logged in as', credentials.username)
|
||||||
|
commit('token', response.data.token)
|
||||||
|
cache.set('token', response.data.token)
|
||||||
|
commit('username', credentials.username)
|
||||||
|
cache.set('username', credentials.username)
|
||||||
|
commit('authenticated', true)
|
||||||
|
dispatch('fetchProfile')
|
||||||
|
// Redirect to a specified route
|
||||||
|
router.push(next)
|
||||||
|
}, response => {
|
||||||
|
logger.default.error('Error while logging in', response.data)
|
||||||
|
onError(response)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
logout ({commit}) {
|
||||||
|
cache.clear()
|
||||||
|
commit('authenticated', false)
|
||||||
|
commit('profile', null)
|
||||||
|
logger.default.info('Log out, goodbye!')
|
||||||
|
router.push({name: 'index'})
|
||||||
|
},
|
||||||
|
check ({commit, dispatch, state}) {
|
||||||
|
logger.default.info('Checking authentication...')
|
||||||
|
var jwt = cache.get('token')
|
||||||
|
var username = cache.get('username')
|
||||||
|
if (jwt) {
|
||||||
|
commit('authenticated', true)
|
||||||
|
commit('username', username)
|
||||||
|
commit('token', jwt)
|
||||||
|
logger.default.info('Logged back in as ' + username)
|
||||||
|
dispatch('fetchProfile')
|
||||||
|
} else {
|
||||||
|
logger.default.info('Anonymous user')
|
||||||
|
commit('authenticated', false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchProfile ({commit, dispatch, state}) {
|
||||||
|
let resource = Vue.resource(USER_PROFILE_URL)
|
||||||
|
return resource.get({}).then((response) => {
|
||||||
|
logger.default.info('Successfully fetched user profile')
|
||||||
|
let data = response.data
|
||||||
|
commit('profile', data)
|
||||||
|
dispatch('favorites/fetch', null, {root: true})
|
||||||
|
Object.keys(data.permissions).forEach(function (key) {
|
||||||
|
// this makes it easier to check for permissions in templates
|
||||||
|
commit('permission', {key, status: data.permissions[String(key)].status})
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}, (response) => {
|
||||||
|
logger.default.info('Error while fetching user profile')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import config from '@/config'
|
||||||
|
import logger from '@/logging'
|
||||||
|
|
||||||
|
const REMOVE_URL = config.API_URL + 'favorites/tracks/remove/'
|
||||||
|
const FAVORITES_URL = config.API_URL + 'favorites/tracks/'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
tracks: [],
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
track: (state, {id, value}) => {
|
||||||
|
if (value) {
|
||||||
|
state.tracks.push(id)
|
||||||
|
} else {
|
||||||
|
let i = state.tracks.indexOf(id)
|
||||||
|
if (i > -1) {
|
||||||
|
state.tracks.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
count: (state, value) => {
|
||||||
|
state.count = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isFavorite: (state) => (id) => {
|
||||||
|
return state.tracks.indexOf(id) > -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
set ({commit, state}, {id, value}) {
|
||||||
|
commit('track', {id, value})
|
||||||
|
if (value) {
|
||||||
|
commit('count', state.count + 1)
|
||||||
|
let resource = Vue.resource(FAVORITES_URL)
|
||||||
|
resource.save({}, {'track': id}).then((response) => {
|
||||||
|
logger.default.info('Successfully added track to favorites')
|
||||||
|
}, (response) => {
|
||||||
|
logger.default.info('Error while adding track to favorites')
|
||||||
|
commit('track', {id, value: !value})
|
||||||
|
commit('count', state.count - 1)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
commit('count', state.count - 1)
|
||||||
|
let resource = Vue.resource(REMOVE_URL)
|
||||||
|
resource.delete({}, {'track': id}).then((response) => {
|
||||||
|
logger.default.info('Successfully removed track from favorites')
|
||||||
|
}, (response) => {
|
||||||
|
logger.default.info('Error while removing track from favorites')
|
||||||
|
commit('track', {id, value: !value})
|
||||||
|
commit('count', state.count + 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggle ({getters, dispatch}, id) {
|
||||||
|
dispatch('set', {id, value: getters['isFavorite'](id)})
|
||||||
|
},
|
||||||
|
fetch ({dispatch, state, commit}, url) {
|
||||||
|
// will fetch favorites by batches from API to have them locally
|
||||||
|
url = url || FAVORITES_URL
|
||||||
|
let resource = Vue.resource(url)
|
||||||
|
resource.get().then((response) => {
|
||||||
|
logger.default.info('Fetched a batch of ' + response.data.results.length + ' favorites')
|
||||||
|
response.data.results.forEach(result => {
|
||||||
|
commit('track', {id: result.track, value: true})
|
||||||
|
})
|
||||||
|
commit('count', state.tracks.length)
|
||||||
|
if (response.data.next) {
|
||||||
|
dispatch('fetch', response.data.next)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
|
import favorites from './favorites'
|
||||||
|
import auth from './auth'
|
||||||
|
import queue from './queue'
|
||||||
|
import radios from './radios'
|
||||||
|
import player from './player'
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
auth,
|
||||||
|
favorites,
|
||||||
|
queue,
|
||||||
|
radios,
|
||||||
|
player
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,91 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import config from '@/config'
|
||||||
|
import logger from '@/logging'
|
||||||
|
import time from '@/utils/time'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
playing: false,
|
||||||
|
volume: 0.5,
|
||||||
|
duration: 0,
|
||||||
|
currentTime: 0,
|
||||||
|
errored: false,
|
||||||
|
looping: 0 // 0 -> no, 1 -> on track, 2 -> on queue
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
volume (state, value) {
|
||||||
|
value = parseFloat(value)
|
||||||
|
value = Math.min(value, 1)
|
||||||
|
value = Math.max(value, 0)
|
||||||
|
state.volume = value
|
||||||
|
},
|
||||||
|
incrementVolume (state, value) {
|
||||||
|
value = parseFloat(state.volume + value)
|
||||||
|
value = Math.min(value, 1)
|
||||||
|
value = Math.max(value, 0)
|
||||||
|
state.volume = value
|
||||||
|
},
|
||||||
|
duration (state, value) {
|
||||||
|
state.duration = value
|
||||||
|
},
|
||||||
|
errored (state, value) {
|
||||||
|
state.errored = value
|
||||||
|
},
|
||||||
|
currentTime (state, value) {
|
||||||
|
state.currentTime = value
|
||||||
|
},
|
||||||
|
looping (state, value) {
|
||||||
|
state.looping = value
|
||||||
|
},
|
||||||
|
playing (state, value) {
|
||||||
|
state.playing = value
|
||||||
|
},
|
||||||
|
toggleLooping (state) {
|
||||||
|
if (state.looping > 1) {
|
||||||
|
state.looping = 0
|
||||||
|
} else {
|
||||||
|
state.looping += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
durationFormatted: state => {
|
||||||
|
return time.parse(Math.round(state.duration))
|
||||||
|
},
|
||||||
|
currentTimeFormatted: state => {
|
||||||
|
return time.parse(Math.round(state.currentTime))
|
||||||
|
},
|
||||||
|
progress: state => {
|
||||||
|
return Math.round(state.currentTime / state.duration * 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
incrementVolume (context, value) {
|
||||||
|
context.commit('volume', context.state.volume + value)
|
||||||
|
},
|
||||||
|
stop (context) {
|
||||||
|
},
|
||||||
|
togglePlay ({commit, state}) {
|
||||||
|
commit('playing', !state.playing)
|
||||||
|
},
|
||||||
|
trackListened ({commit}, track) {
|
||||||
|
let url = config.API_URL + 'history/listenings/'
|
||||||
|
let resource = Vue.resource(url)
|
||||||
|
resource.save({}, {'track': track.id}).then((response) => {}, (response) => {
|
||||||
|
logger.default.error('Could not record track in history')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
trackEnded ({dispatch}, track) {
|
||||||
|
dispatch('trackListened', track)
|
||||||
|
dispatch('queue/next', null, {root: true})
|
||||||
|
},
|
||||||
|
trackErrored ({commit, dispatch}) {
|
||||||
|
commit('errored', true)
|
||||||
|
dispatch('queue/next', null, {root: true})
|
||||||
|
},
|
||||||
|
updateProgress ({commit}, t) {
|
||||||
|
commit('currentTime', t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
import logger from '@/logging'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
tracks: [],
|
||||||
|
currentIndex: -1,
|
||||||
|
ended: true,
|
||||||
|
previousQueue: null
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
currentIndex (state, value) {
|
||||||
|
state.currentIndex = value
|
||||||
|
},
|
||||||
|
ended (state, value) {
|
||||||
|
state.ended = value
|
||||||
|
},
|
||||||
|
splice (state, {start, size}) {
|
||||||
|
state.tracks.splice(start, size)
|
||||||
|
},
|
||||||
|
tracks (state, value) {
|
||||||
|
state.tracks = value
|
||||||
|
},
|
||||||
|
insert (state, {track, index}) {
|
||||||
|
state.tracks.splice(index, 0, track)
|
||||||
|
},
|
||||||
|
reorder (state, {oldIndex, newIndex}) {
|
||||||
|
// called when the user uses drag / drop to reorder
|
||||||
|
// tracks in queue
|
||||||
|
if (oldIndex === state.currentIndex) {
|
||||||
|
state.currentIndex = newIndex
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (oldIndex < state.currentIndex && newIndex >= state.currentIndex) {
|
||||||
|
// item before was moved after
|
||||||
|
state.currentIndex -= 1
|
||||||
|
}
|
||||||
|
if (oldIndex > state.currentIndex && newIndex <= state.currentIndex) {
|
||||||
|
// item after was moved before
|
||||||
|
state.currentIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
currentTrack: state => {
|
||||||
|
return state.tracks[state.currentIndex]
|
||||||
|
},
|
||||||
|
hasNext: state => {
|
||||||
|
return state.currentIndex < state.tracks.length - 1
|
||||||
|
},
|
||||||
|
hasPrevious: state => {
|
||||||
|
return state.currentIndex > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
append (context, {track, index, skipPlay}) {
|
||||||
|
index = index || context.state.tracks.length
|
||||||
|
if (index > context.state.tracks.length - 1) {
|
||||||
|
// we simply push to the end
|
||||||
|
context.commit('insert', {track, index: context.state.tracks.length})
|
||||||
|
} else {
|
||||||
|
// we insert the track at given position
|
||||||
|
context.commit('insert', {track, index})
|
||||||
|
}
|
||||||
|
if (!skipPlay) {
|
||||||
|
context.dispatch('resume')
|
||||||
|
}
|
||||||
|
// this.cache()
|
||||||
|
},
|
||||||
|
|
||||||
|
appendMany (context, {tracks, index}) {
|
||||||
|
logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
|
||||||
|
if (context.state.tracks.length === 0) {
|
||||||
|
index = 0
|
||||||
|
} else {
|
||||||
|
index = index || context.state.tracks.length
|
||||||
|
}
|
||||||
|
tracks.forEach((t) => {
|
||||||
|
context.dispatch('append', {track: t, index: index, skipPlay: true})
|
||||||
|
index += 1
|
||||||
|
})
|
||||||
|
context.dispatch('resume')
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanTrack ({state, dispatch, commit}, index) {
|
||||||
|
// are we removing current playin track
|
||||||
|
let current = index === state.currentIndex
|
||||||
|
if (current) {
|
||||||
|
dispatch('player/stop', null, {root: true})
|
||||||
|
}
|
||||||
|
if (index < state.currentIndex) {
|
||||||
|
dispatch('currentIndex', state.currentIndex - 1)
|
||||||
|
}
|
||||||
|
commit('splice', {start: index, size: 1})
|
||||||
|
if (current) {
|
||||||
|
// we play next track, which now have the same index
|
||||||
|
dispatch('currentIndex', index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resume (context) {
|
||||||
|
if (context.state.ended | context.rootState.player.errored) {
|
||||||
|
context.dispatch('next')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previous (context) {
|
||||||
|
if (context.state.currentIndex > 0) {
|
||||||
|
context.dispatch('currentIndex', context.state.currentIndex - 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next ({state, dispatch, commit, rootState}) {
|
||||||
|
if (rootState.player.looping === 1) {
|
||||||
|
// we loop on the same track, this is handled directly on the track
|
||||||
|
// component, so we do nothing.
|
||||||
|
return logger.default.info('Looping on the same track')
|
||||||
|
}
|
||||||
|
if (rootState.player.looping === 2 && state.currentIndex >= state.tracks.length - 1) {
|
||||||
|
logger.default.info('Going back to the beginning of the queue')
|
||||||
|
return dispatch('currentIndex', 0)
|
||||||
|
} else {
|
||||||
|
if (state.currentIndex < state.tracks.length - 1) {
|
||||||
|
logger.default.debug('Playing next track')
|
||||||
|
return dispatch('currentIndex', state.currentIndex + 1)
|
||||||
|
} else {
|
||||||
|
commit('ended', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currentIndex ({commit, state, rootState, dispatch}, index) {
|
||||||
|
commit('ended', false)
|
||||||
|
commit('player/errored', false, {root: true})
|
||||||
|
commit('currentIndex', index)
|
||||||
|
if (state.tracks.length - index <= 2 && rootState.radios.running) {
|
||||||
|
dispatch('radios/populateQueue', null, {root: true})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clean ({dispatch, commit}) {
|
||||||
|
dispatch('player/stop', null, {root: true})
|
||||||
|
// radios.stop()
|
||||||
|
commit('tracks', [])
|
||||||
|
dispatch('currentIndex', -1)
|
||||||
|
// so we replay automatically on next track append
|
||||||
|
commit('ended', true)
|
||||||
|
},
|
||||||
|
shuffle ({dispatch, commit, state}) {
|
||||||
|
let shuffled = _.shuffle(state.tracks)
|
||||||
|
commit('tracks', [])
|
||||||
|
dispatch('appendMany', {tracks: shuffled})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import config from '@/config'
|
||||||
|
import logger from '@/logging'
|
||||||
|
|
||||||
|
const CREATE_RADIO_URL = config.API_URL + 'radios/sessions/'
|
||||||
|
const GET_TRACK_URL = config.API_URL + 'radios/tracks/'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
current: null,
|
||||||
|
running: false
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
types: state => {
|
||||||
|
return {
|
||||||
|
random: {
|
||||||
|
name: 'Random',
|
||||||
|
description: "Totally random picks, maybe you'll discover new things?"
|
||||||
|
},
|
||||||
|
favorites: {
|
||||||
|
name: 'Favorites',
|
||||||
|
description: 'Play your favorites tunes in a never-ending happiness loop.'
|
||||||
|
},
|
||||||
|
'less-listened': {
|
||||||
|
name: 'Less listened',
|
||||||
|
description: "Listen to tracks you usually don't. It's time to restore some balance."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
current: (state, value) => {
|
||||||
|
state.current = value
|
||||||
|
},
|
||||||
|
running: (state, value) => {
|
||||||
|
state.running = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
start ({commit, dispatch}, {type, objectId}) {
|
||||||
|
let resource = Vue.resource(CREATE_RADIO_URL)
|
||||||
|
var params = {
|
||||||
|
radio_type: type,
|
||||||
|
related_object_id: objectId
|
||||||
|
}
|
||||||
|
resource.save({}, params).then((response) => {
|
||||||
|
logger.default.info('Successfully started radio ', type)
|
||||||
|
commit('current', {type, objectId, session: response.data.id})
|
||||||
|
commit('running', true)
|
||||||
|
dispatch('populateQueue')
|
||||||
|
}, (response) => {
|
||||||
|
logger.default.error('Error while starting radio', type)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
stop ({commit}) {
|
||||||
|
commit('current', null)
|
||||||
|
commit('running', false)
|
||||||
|
},
|
||||||
|
populateQueue ({state, dispatch}) {
|
||||||
|
if (!state.running) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let resource = Vue.resource(GET_TRACK_URL)
|
||||||
|
var params = {
|
||||||
|
session: state.current.session
|
||||||
|
}
|
||||||
|
let promise = resource.save({}, params)
|
||||||
|
promise.then((response) => {
|
||||||
|
logger.default.info('Adding track to queue from radio')
|
||||||
|
dispatch('queue/append', {track: response.data.track}, {root: true})
|
||||||
|
}, (response) => {
|
||||||
|
logger.default.error('Error while adding track to queue from radio')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue