From 62a7d9091e92dc88dd5cb46447a07bf49c00aad9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Sun, 24 Dec 2017 22:48:29 +0100 Subject: [PATCH] Now persist/restore queue/radio/player state automatically --- front/package.json | 3 +- front/src/cache/index.js | 29 ------- front/src/components/Sidebar.vue | 2 +- front/src/components/audio/Player.vue | 4 +- front/src/components/audio/SearchBar.vue | 3 +- front/src/components/audio/Track.vue | 14 +++- front/src/components/favorites/List.vue | 1 - .../favorites/TrackFavoriteIcon.vue | 11 +-- front/src/components/metadata/Search.vue | 2 +- front/src/store/auth.js | 8 +- front/src/store/favorites.js | 15 ++-- front/src/store/index.js | 82 ++++++++++++++++++- front/src/store/queue.js | 7 +- 13 files changed, 111 insertions(+), 70 deletions(-) delete mode 100644 front/src/cache/index.js diff --git a/front/package.json b/front/package.json index 5bec01602d..3eb5201b29 100644 --- a/front/package.json +++ b/front/package.json @@ -23,7 +23,8 @@ "vue-resource": "^1.3.4", "vue-router": "^2.3.1", "vuedraggable": "^2.14.1", - "vuex": "^3.0.1" + "vuex": "^3.0.1", + "vuex-persistedstate": "^2.4.2" }, "devDependencies": { "autoprefixer": "^6.7.2", diff --git a/front/src/cache/index.js b/front/src/cache/index.js deleted file mode 100644 index e039ee7880..0000000000 --- a/front/src/cache/index.js +++ /dev/null @@ -1,29 +0,0 @@ -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() - } - -} diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index b5c4f00462..a315aab199 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -62,7 +62,7 @@ {{ track.artist.name }} </td> <td> - <template v-if="favoriteTracks.objects[track.id]"> + <template v-if="$store.getters['favorites/isFavorite'](track.id)"> <i class="pink heart icon"></i> </template </td> diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 9701b44614..500f4dc1d2 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -5,6 +5,8 @@ v-if="currentTrack" :key="(currentIndex, currentTrack.id)" :is-current="true" + :start-time="$store.state.player.currentTime" + :autoplay="$store.state.player.playing" :track="currentTrack"> </audio-track> @@ -127,7 +129,7 @@ @keydown.ctrl.right.prevent.exact="next" @keydown.ctrl.down.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.s.prevent.exact="shuffle" /> diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue index 386e24a74f..9d8b39f870 100644 --- a/front/src/components/audio/SearchBar.vue +++ b/front/src/components/audio/SearchBar.vue @@ -18,6 +18,7 @@ const SEARCH_URL = config.API_URL + 'search?query={query}' export default { mounted () { + let self = this jQuery(this.$el).search({ type: 'category', minCharacters: 3, @@ -26,7 +27,7 @@ export default { }, apiSettings: { beforeXHR: function (xhrObject) { - xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header']) + xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header']) return xhrObject }, onResponse: function (initialResponse) { diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue index f0e1f14fa7..c8627925eb 100644 --- a/front/src/components/audio/Track.vue +++ b/front/src/components/audio/Track.vue @@ -22,7 +22,9 @@ import url from '@/utils/url' export default { props: { track: {type: Object}, - isCurrent: {type: Boolean, default: false} + isCurrent: {type: Boolean, default: false}, + startTime: {type: Number, default: 0}, + autoplay: {type: Boolean, default: false} }, computed: { ...mapState({ @@ -57,8 +59,11 @@ export default { }, loaded: function () { - this.$store.commit('player/duration', this.$refs.audio.duration) - if (this.isCurrent) { + if (this.isCurrent && this.autoplay) { + this.$store.commit('player/duration', this.$refs.audio.duration) + if (this.startTime) { + this.setCurrentTime(this.startTime) + } this.$store.commit('player/playing', true) this.$refs.audio.play() } @@ -72,8 +77,9 @@ export default { if (this.looping === 1) { this.setCurrentTime(0) this.$refs.audio.play() + } else { + this.$store.dispatch('player/trackEnded', this.track) } - this.$store.dispatch('player/trackEnded', this.track) }, setCurrentTime (t) { if (t < 0 | t > this.duration) { diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue index aef4bea93c..8577e84ca5 100644 --- a/front/src/components/favorites/List.vue +++ b/front/src/components/favorites/List.vue @@ -119,7 +119,6 @@ export default { self.results = response.data self.nextLink = response.data.next self.previousLink = response.data.previous - self.$store.commit('favorites/count', response.data.count) self.results.results.forEach((track) => { self.$store.commit('favorites/track', {id: track.id, value: true}) }) diff --git a/front/src/components/favorites/TrackFavoriteIcon.vue b/front/src/components/favorites/TrackFavoriteIcon.vue index 5abc57a952..d4838ba5f3 100644 --- a/front/src/components/favorites/TrackFavoriteIcon.vue +++ b/front/src/components/favorites/TrackFavoriteIcon.vue @@ -1,5 +1,5 @@ <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> <template v-if="isFavorite"> In favorites @@ -8,23 +8,16 @@ Add to favorites </template> </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> <script> -import {mapState} from 'vuex' - export default { props: { track: {type: Object}, button: {type: Boolean, default: false} }, computed: { - ...mapState({ - favorites: state => { - return state.favorites.tracks - } - }), title () { if (this.isFavorite) { return 'Remove from favorites' diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue index c3dc7433c5..f2dea6cab9 100644 --- a/front/src/components/metadata/Search.vue +++ b/front/src/components/metadata/Search.vue @@ -65,7 +65,7 @@ export default { }, apiSettings: { beforeXHR: function (xhrObject, s) { - xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header']) + xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header']) return xhrObject }, onResponse: function (initialResponse) { diff --git a/front/src/store/auth.js b/front/src/store/auth.js index 23b0b628dc..eea508df95 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -1,7 +1,6 @@ import Vue from 'vue' import config from '@/config' import logger from '@/logging' -import cache from '@/cache' import router from '@/router' const LOGIN_URL = config.API_URL + 'token/' @@ -45,9 +44,7 @@ export default { 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 @@ -58,7 +55,6 @@ export default { }) }, logout ({commit}) { - cache.clear() commit('authenticated', false) commit('profile', null) logger.default.info('Log out, goodbye!') @@ -66,8 +62,8 @@ export default { }, check ({commit, dispatch, state}) { logger.default.info('Checking authentication...') - var jwt = cache.get('token') - var username = cache.get('username') + var jwt = state.token + var username = state.username if (jwt) { commit('authenticated', true) commit('username', username) diff --git a/front/src/store/favorites.js b/front/src/store/favorites.js index 8bb4bb5afe..9337966fdf 100644 --- a/front/src/store/favorites.js +++ b/front/src/store/favorites.js @@ -14,16 +14,16 @@ export default { mutations: { track: (state, {id, value}) => { if (value) { - state.tracks.push(id) + if (state.tracks.indexOf(id) === -1) { + 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 + state.count = state.tracks.length } }, getters: { @@ -35,29 +35,25 @@ export default { 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)}) + dispatch('set', {id, value: !getters['isFavorite'](id)}) }, fetch ({dispatch, state, commit}, url) { // will fetch favorites by batches from API to have them locally @@ -68,7 +64,6 @@ export default { 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) } diff --git a/front/src/store/index.js b/front/src/store/index.js index 99e466e510..507f0b5876 100644 --- a/front/src/store/index.js +++ b/front/src/store/index.js @@ -1,5 +1,6 @@ import Vue from 'vue' import Vuex from 'vuex' +import createPersistedState from 'vuex-persistedstate' import favorites from './favorites' import auth from './auth' @@ -16,5 +17,84 @@ export default new Vuex.Store({ queue, radios, 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 + } + }) + } + } + } + }) + ] }) diff --git a/front/src/store/queue.js b/front/src/store/queue.js index 3a0b7dd797..5dde19bd8e 100644 --- a/front/src/store/queue.js +++ b/front/src/store/queue.js @@ -111,11 +111,6 @@ export default { } }, 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) @@ -130,6 +125,8 @@ export default { }, currentIndex ({commit, state, rootState, dispatch}, index) { commit('ended', false) + commit('player/currentTime', 0, {root: true}) + commit('player/playing', true, {root: true}) commit('player/errored', false, {root: true}) commit('currentIndex', index) if (state.tracks.length - index <= 2 && rootState.radios.running) { -- GitLab