Skip to content
Snippets Groups Projects
Commit f5a4faa5 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'feature/store-persistence' into 'develop'

Feature/store persistence

See merge request funkwhale/funkwhale!35
parents c9ffd51b 62a7d909
No related branches found
No related tags found
No related merge requests found
...@@ -3,3 +3,4 @@ DJANGO_SETTINGS_MODULE=config.settings.test ...@@ -3,3 +3,4 @@ DJANGO_SETTINGS_MODULE=config.settings.test
# -- recommended but optional: # -- recommended but optional:
python_files = tests.py test_*.py *_tests.py python_files = tests.py test_*.py *_tests.py
testpatsh = tests
...@@ -23,7 +23,8 @@ ...@@ -23,7 +23,8 @@
"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" "vuex": "^3.0.1",
"vuex-persistedstate": "^2.4.2"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.7.2", "autoprefixer": "^6.7.2",
......
import logger from '@/logging'
export default {
get (key, d) {
let v = localStorage.getItem(key)
if (v === null) {
return d
} else {
try {
return JSON.parse(v).value
} catch (e) {
logger.default.error('Removing unparsable cached value for key ' + key)
this.remove(key)
return d
}
}
},
set (key, value) {
return localStorage.setItem(key, JSON.stringify({value: value}))
},
remove (key) {
return localStorage.removeItem(key)
},
clear () {
localStorage.clear()
}
}
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
{{ track.artist.name }} {{ track.artist.name }}
</td> </td>
<td> <td>
<template v-if="favoriteTracks.objects[track.id]"> <template v-if="$store.getters['favorites/isFavorite'](track.id)">
<i class="pink heart icon"></i> <i class="pink heart icon"></i>
</template </template
</td> </td>
...@@ -94,7 +94,6 @@ ...@@ -94,7 +94,6 @@
import {mapState, mapActions} from 'vuex' import {mapState, mapActions} from 'vuex'
import Player from '@/components/audio/Player' import Player from '@/components/audio/Player'
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 backend from '@/audio/backend' import backend from '@/audio/backend'
...@@ -112,8 +111,7 @@ export default { ...@@ -112,8 +111,7 @@ export default {
}, },
data () { data () {
return { return {
backend: backend, backend: backend
favoriteTracks
} }
}, },
mounted () { mounted () {
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
v-if="currentTrack" v-if="currentTrack"
:key="(currentIndex, currentTrack.id)" :key="(currentIndex, currentTrack.id)"
:is-current="true" :is-current="true"
:start-time="$store.state.player.currentTime"
:autoplay="$store.state.player.playing"
:track="currentTrack"> :track="currentTrack">
</audio-track> </audio-track>
...@@ -127,7 +129,7 @@ ...@@ -127,7 +129,7 @@
@keydown.ctrl.right.prevent.exact="next" @keydown.ctrl.right.prevent.exact="next"
@keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)" @keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
@keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)" @keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
@keydown.f.prevent.exact="favoriteTracks.toggle(currentTrack.id)" @keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
@keydown.l.prevent.exact="$store.commit('player/toggleLooping')" @keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
@keydown.s.prevent.exact="shuffle" @keydown.s.prevent.exact="shuffle"
/> />
...@@ -139,7 +141,6 @@ ...@@ -139,7 +141,6 @@
import {mapState, mapGetters, mapActions} from 'vuex' 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 Track from '@/audio/track' import Track from '@/audio/track'
import AudioTrack from '@/components/audio/Track' import AudioTrack from '@/components/audio/Track'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
...@@ -154,8 +155,7 @@ export default { ...@@ -154,8 +155,7 @@ export default {
data () { data () {
return { return {
sliderVolume: this.volume, sliderVolume: this.volume,
Track: Track, Track: Track
favoriteTracks
} }
}, },
mounted () { mounted () {
......
...@@ -18,6 +18,7 @@ const SEARCH_URL = config.API_URL + 'search?query={query}' ...@@ -18,6 +18,7 @@ const SEARCH_URL = config.API_URL + 'search?query={query}'
export default { export default {
mounted () { mounted () {
let self = this
jQuery(this.$el).search({ jQuery(this.$el).search({
type: 'category', type: 'category',
minCharacters: 3, minCharacters: 3,
...@@ -26,7 +27,7 @@ export default { ...@@ -26,7 +27,7 @@ export default {
}, },
apiSettings: { apiSettings: {
beforeXHR: function (xhrObject) { beforeXHR: function (xhrObject) {
xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header']) xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
return xhrObject return xhrObject
}, },
onResponse: function (initialResponse) { onResponse: function (initialResponse) {
......
...@@ -22,7 +22,9 @@ import url from '@/utils/url' ...@@ -22,7 +22,9 @@ import url from '@/utils/url'
export default { export default {
props: { props: {
track: {type: Object}, track: {type: Object},
isCurrent: {type: Boolean, default: false} isCurrent: {type: Boolean, default: false},
startTime: {type: Number, default: 0},
autoplay: {type: Boolean, default: false}
}, },
computed: { computed: {
...mapState({ ...mapState({
...@@ -57,8 +59,11 @@ export default { ...@@ -57,8 +59,11 @@ export default {
}, },
loaded: function () { loaded: function () {
this.$store.commit('player/duration', this.$refs.audio.duration) if (this.isCurrent && this.autoplay) {
if (this.isCurrent) { this.$store.commit('player/duration', this.$refs.audio.duration)
if (this.startTime) {
this.setCurrentTime(this.startTime)
}
this.$store.commit('player/playing', true) this.$store.commit('player/playing', true)
this.$refs.audio.play() this.$refs.audio.play()
} }
...@@ -72,8 +77,9 @@ export default { ...@@ -72,8 +77,9 @@ export default {
if (this.looping === 1) { if (this.looping === 1) {
this.setCurrentTime(0) this.setCurrentTime(0)
this.$refs.audio.play() this.$refs.audio.play()
} else {
this.$store.dispatch('player/trackEnded', this.track)
} }
this.$store.dispatch('player/trackEnded', this.track)
}, },
setCurrentTime (t) { setCurrentTime (t) {
if (t < 0 | t > this.duration) { if (t < 0 | t > this.duration) {
......
...@@ -119,7 +119,6 @@ export default { ...@@ -119,7 +119,6 @@ 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
self.$store.commit('favorites/count', response.data.count)
self.results.results.forEach((track) => { self.results.results.forEach((track) => {
self.$store.commit('favorites/track', {id: track.id, value: true}) self.$store.commit('favorites/track', {id: track.id, value: true})
}) })
......
<template> <template>
<button @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']"> <button @click="$store.dispatch('favorites/toggle', track.id)" 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,16 @@ ...@@ -8,23 +8,16 @@
Add to favorites Add to favorites
</template> </template>
</button> </button>
<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> <i v-else @click="$store.dispatch('favorites/toggle', track.id)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
</template> </template>
<script> <script>
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}
}, },
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'
......
...@@ -65,7 +65,7 @@ export default { ...@@ -65,7 +65,7 @@ export default {
}, },
apiSettings: { apiSettings: {
beforeXHR: function (xhrObject, s) { beforeXHR: function (xhrObject, s) {
xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header']) xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
return xhrObject return xhrObject
}, },
onResponse: function (initialResponse) { onResponse: function (initialResponse) {
......
import Vue from 'vue' import Vue from 'vue'
import config from '@/config' import config from '@/config'
import logger from '@/logging' import logger from '@/logging'
import cache from '@/cache'
import router from '@/router' import router from '@/router'
// import favoriteTracks from '@/favorites/tracks'
const LOGIN_URL = config.API_URL + 'token/' const LOGIN_URL = config.API_URL + 'token/'
const USER_PROFILE_URL = config.API_URL + 'users/users/me/' const USER_PROFILE_URL = config.API_URL + 'users/users/me/'
...@@ -46,9 +44,7 @@ export default { ...@@ -46,9 +44,7 @@ export default {
return resource.save({}, credentials).then(response => { return resource.save({}, credentials).then(response => {
logger.default.info('Successfully logged in as', credentials.username) logger.default.info('Successfully logged in as', credentials.username)
commit('token', response.data.token) commit('token', response.data.token)
cache.set('token', response.data.token)
commit('username', credentials.username) commit('username', credentials.username)
cache.set('username', credentials.username)
commit('authenticated', true) commit('authenticated', true)
dispatch('fetchProfile') dispatch('fetchProfile')
// Redirect to a specified route // Redirect to a specified route
...@@ -59,7 +55,6 @@ export default { ...@@ -59,7 +55,6 @@ export default {
}) })
}, },
logout ({commit}) { logout ({commit}) {
cache.clear()
commit('authenticated', false) commit('authenticated', false)
commit('profile', null) commit('profile', null)
logger.default.info('Log out, goodbye!') logger.default.info('Log out, goodbye!')
...@@ -67,8 +62,8 @@ export default { ...@@ -67,8 +62,8 @@ export default {
}, },
check ({commit, dispatch, state}) { check ({commit, dispatch, state}) {
logger.default.info('Checking authentication...') logger.default.info('Checking authentication...')
var jwt = cache.get('token') var jwt = state.token
var username = cache.get('username') var username = state.username
if (jwt) { if (jwt) {
commit('authenticated', true) commit('authenticated', true)
commit('username', username) commit('username', username)
......
...@@ -14,16 +14,16 @@ export default { ...@@ -14,16 +14,16 @@ export default {
mutations: { mutations: {
track: (state, {id, value}) => { track: (state, {id, value}) => {
if (value) { if (value) {
state.tracks.push(id) if (state.tracks.indexOf(id) === -1) {
state.tracks.push(id)
}
} else { } else {
let i = state.tracks.indexOf(id) let i = state.tracks.indexOf(id)
if (i > -1) { if (i > -1) {
state.tracks.splice(i, 1) state.tracks.splice(i, 1)
} }
} }
}, state.count = state.tracks.length
count: (state, value) => {
state.count = value
} }
}, },
getters: { getters: {
...@@ -35,29 +35,25 @@ export default { ...@@ -35,29 +35,25 @@ export default {
set ({commit, state}, {id, value}) { set ({commit, state}, {id, value}) {
commit('track', {id, value}) commit('track', {id, value})
if (value) { if (value) {
commit('count', state.count + 1)
let resource = Vue.resource(FAVORITES_URL) let resource = Vue.resource(FAVORITES_URL)
resource.save({}, {'track': id}).then((response) => { resource.save({}, {'track': id}).then((response) => {
logger.default.info('Successfully added track to favorites') logger.default.info('Successfully added track to favorites')
}, (response) => { }, (response) => {
logger.default.info('Error while adding track to favorites') logger.default.info('Error while adding track to favorites')
commit('track', {id, value: !value}) commit('track', {id, value: !value})
commit('count', state.count - 1)
}) })
} else { } else {
commit('count', state.count - 1)
let resource = Vue.resource(REMOVE_URL) let resource = Vue.resource(REMOVE_URL)
resource.delete({}, {'track': id}).then((response) => { resource.delete({}, {'track': id}).then((response) => {
logger.default.info('Successfully removed track from favorites') logger.default.info('Successfully removed track from favorites')
}, (response) => { }, (response) => {
logger.default.info('Error while removing track from favorites') logger.default.info('Error while removing track from favorites')
commit('track', {id, value: !value}) commit('track', {id, value: !value})
commit('count', state.count + 1)
}) })
} }
}, },
toggle ({getters, dispatch}, id) { toggle ({getters, dispatch}, id) {
dispatch('set', {id, value: getters['isFavorite'](id)}) dispatch('set', {id, value: !getters['isFavorite'](id)})
}, },
fetch ({dispatch, state, commit}, url) { fetch ({dispatch, state, commit}, url) {
// will fetch favorites by batches from API to have them locally // will fetch favorites by batches from API to have them locally
...@@ -68,7 +64,6 @@ export default { ...@@ -68,7 +64,6 @@ export default {
response.data.results.forEach(result => { response.data.results.forEach(result => {
commit('track', {id: result.track, value: true}) commit('track', {id: result.track, value: true})
}) })
commit('count', state.tracks.length)
if (response.data.next) { if (response.data.next) {
dispatch('fetch', response.data.next) dispatch('fetch', response.data.next)
} }
......
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import favorites from './favorites' import favorites from './favorites'
import auth from './auth' import auth from './auth'
...@@ -16,5 +17,84 @@ export default new Vuex.Store({ ...@@ -16,5 +17,84 @@ export default new Vuex.Store({
queue, queue,
radios, radios,
player player
} },
plugins: [
createPersistedState({
key: 'auth',
paths: ['auth'],
filter: (mutation) => {
return mutation.type.startsWith('auth/')
}
}),
createPersistedState({
key: 'radios',
paths: ['radios'],
filter: (mutation) => {
return mutation.type.startsWith('radios/')
}
}),
createPersistedState({
key: 'player',
paths: [
'player.looping',
'player.playing',
'player.volume',
'player.duration',
'player.errored'],
filter: (mutation) => {
return mutation.type.startsWith('player/') && mutation.type !== 'player/currentTime'
}
}),
createPersistedState({
key: 'progress',
paths: ['player.currentTime'],
filter: (mutation) => {
let delay = 10
return mutation.type === 'player/currentTime' && parseInt(mutation.payload) % delay === 0
},
reducer: (state) => {
return {
player: {
currentTime: state.player.currentTime
}
}
}
}),
createPersistedState({
key: 'queue',
filter: (mutation) => {
return mutation.type.startsWith('queue/')
},
reducer: (state) => {
return {
queue: {
currentIndex: state.queue.currentIndex,
tracks: state.queue.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
}
})
}
}
}
})
]
}) })
...@@ -111,11 +111,6 @@ export default { ...@@ -111,11 +111,6 @@ export default {
} }
}, },
next ({state, dispatch, commit, rootState}) { 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) { if (rootState.player.looping === 2 && state.currentIndex >= state.tracks.length - 1) {
logger.default.info('Going back to the beginning of the queue') logger.default.info('Going back to the beginning of the queue')
return dispatch('currentIndex', 0) return dispatch('currentIndex', 0)
...@@ -130,6 +125,8 @@ export default { ...@@ -130,6 +125,8 @@ export default {
}, },
currentIndex ({commit, state, rootState, dispatch}, index) { currentIndex ({commit, state, rootState, dispatch}, index) {
commit('ended', false) commit('ended', false)
commit('player/currentTime', 0, {root: true})
commit('player/playing', true, {root: true})
commit('player/errored', false, {root: true}) commit('player/errored', false, {root: true})
commit('currentIndex', index) commit('currentIndex', index)
if (state.tracks.length - index <= 2 && rootState.radios.running) { if (state.tracks.length - index <= 2 && rootState.radios.running) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment