From a1fd0d828ec3fba881d5b22a93033fcfdf5cfd02 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 17 Dec 2017 15:38:40 +0100 Subject: [PATCH 01/22] Fixed #53: f shortcut for favorite and avoiding collisions with 'exact' modifier --- CHANGELOG | 4 ++++ front/src/components/audio/Player.vue | 15 +++++++++------ .../components/favorites/TrackFavoriteIcon.vue | 7 +------ front/src/favorites/tracks.js | 4 ++++ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index aed490eaa..d9d896035 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,10 @@ Changelog 0.2.7 (Unreleased) ------------------ +- Shortcuts: can now use the ``f`` shortcut to toggle the currently playing track + as a favorite (#53) +- Shortcuts: avoid collisions between shortcuts by using the exact modifier (#53) + 0.2.6 (2017-12-15) ------------------ diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 423c9d12f..aff3e65bd 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -57,11 +57,12 @@ @@ -70,10 +71,11 @@ diff --git a/front/src/components/mixins/Pagination.vue b/front/src/components/mixins/Pagination.vue new file mode 100644 index 000000000..532faaaa3 --- /dev/null +++ b/front/src/components/mixins/Pagination.vue @@ -0,0 +1,8 @@ + diff --git a/front/src/router/index.js b/front/src/router/index.js index f6653e73d..7db5da6ba 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -47,7 +47,11 @@ export default new Router({ }, { path: '/favorites', - component: Favorites + component: Favorites, + props: (route) => ({ + defaultOrdering: route.query.ordering, + defaultPage: route.query.page + }) }, { path: '/library', From f3c914779d814b894073cecda970060413b4284b Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 17 Dec 2017 20:08:49 +0100 Subject: [PATCH 06/22] Changelog --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index aa2b67dfa..4f17b8519 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,8 @@ Changelog - Shortcuts: avoid collisions between shortcuts by using the exact modifier (#53) - Player: Added looping controls and shortcuts (#52) - Player: Added shuffling controls and shortcuts (#52) +- Favorites: can now modify the ordering of track list (#50) +- Library: can now search/reorder results on artist browsing view (#50) 0.2.6 (2017-12-15) From 0c5f151fc11e19f7a9c16c8b292d99c9c12b80d7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 17 Dec 2017 20:24:48 +0100 Subject: [PATCH 07/22] Fixed some debouncing issues --- front/src/components/library/Artists.vue | 4 ++-- front/src/components/utils/global-events.vue | 2 +- front/src/router/index.js | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue index 8d0a4f552..23a30e771 100644 --- a/front/src/components/library/Artists.vue +++ b/front/src/components/library/Artists.vue @@ -103,7 +103,7 @@ export default { $('.ui.dropdown').dropdown() }, methods: { - updateQueryString: function () { + updateQueryString: _.debounce(function () { this.$router.replace({ query: { query: this.query, @@ -112,7 +112,7 @@ export default { ordering: this.getOrderingAsString() } }) - }, + }, 500), fetchData: _.debounce(function () { var self = this this.isLoading = true diff --git a/front/src/components/utils/global-events.vue b/front/src/components/utils/global-events.vue index 2905e3a7d..dd25865c9 100644 --- a/front/src/components/utils/global-events.vue +++ b/front/src/components/utils/global-events.vue @@ -27,7 +27,7 @@ export default { let wrapper = function (event) { // we check here the event is not triggered from an input // to avoid collisions - if (!$(event.target).is(':input, [contenteditable]')) { + if (!$(event.target).is('.field, :input, [contenteditable]')) { handler(event) } } diff --git a/front/src/router/index.js b/front/src/router/index.js index 7db5da6ba..c7c46a275 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -50,7 +50,8 @@ export default new Router({ component: Favorites, props: (route) => ({ defaultOrdering: route.query.ordering, - defaultPage: route.query.page + defaultPage: route.query.page, + defaultPaginateBy: route.query.paginateBy }) }, { From 7492395c9b10dec8cdef60c3f94ba05618d347a2 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 23 Dec 2017 16:40:40 +0100 Subject: [PATCH 08/22] removed node_modules volume, you should rebuild on dependency change --- dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/dev.yml b/dev.yml index c71298cfc..44e38e326 100644 --- a/dev.yml +++ b/dev.yml @@ -13,7 +13,6 @@ services: - "8080:8080" volumes: - './front:/app' - - /app/node_modules postgres: env_file: .env.dev From 254996453f66c20aeebc478ec473c352449d9491 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 23 Dec 2017 16:40:52 +0100 Subject: [PATCH 09/22] Added vuex to dependencies --- front/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/front/package.json b/front/package.json index bad90430f..5bec01602 100644 --- a/front/package.json +++ b/front/package.json @@ -22,7 +22,8 @@ "vue-lazyload": "^1.1.4", "vue-resource": "^1.3.4", "vue-router": "^2.3.1", - "vuedraggable": "^2.14.1" + "vuedraggable": "^2.14.1", + "vuex": "^3.0.1" }, "devDependencies": { "autoprefixer": "^6.7.2", From df94ae37bf2c3f6f5fe090cf9a06a6746529d024 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 23 Dec 2017 16:41:19 +0100 Subject: [PATCH 10/22] Now use vuex to manage state for player/queue/radios --- front/src/audio/index.js | 184 ------------ front/src/audio/queue.js | 332 ---------------------- front/src/components/Sidebar.vue | 36 +-- front/src/components/audio/PlayButton.vue | 11 +- front/src/components/audio/Player.vue | 160 ++++++----- front/src/components/audio/Search.vue | 4 +- front/src/components/audio/Track.vue | 104 +++++++ front/src/components/audio/album/Card.vue | 2 - front/src/components/radios/Button.vue | 17 +- front/src/components/radios/Card.vue | 3 +- front/src/main.js | 2 + front/src/radios/index.js | 64 ----- front/src/store/index.js | 16 ++ front/src/store/player.js | 91 ++++++ front/src/store/queue.js | 153 ++++++++++ front/src/store/radios.js | 78 +++++ 16 files changed, 563 insertions(+), 694 deletions(-) delete mode 100644 front/src/audio/index.js delete mode 100644 front/src/audio/queue.js create mode 100644 front/src/components/audio/Track.vue delete mode 100644 front/src/radios/index.js create mode 100644 front/src/store/index.js create mode 100644 front/src/store/player.js create mode 100644 front/src/store/queue.js create mode 100644 front/src/store/radios.js diff --git a/front/src/audio/index.js b/front/src/audio/index.js deleted file mode 100644 index 4896b83b0..000000000 --- a/front/src/audio/index.js +++ /dev/null @@ -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 diff --git a/front/src/audio/queue.js b/front/src/audio/queue.js deleted file mode 100644 index 4273fb9a6..000000000 --- a/front/src/audio/queue.js +++ /dev/null @@ -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 diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 68927a37b..9112c2588 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -51,7 +51,7 @@
- +
{{ index + 1}} @@ -63,23 +63,23 @@ + - +
-
+
You have a radio playing

New tracks will be appended here automatically.

-
Stop radio
+
Stop radio
@@ -87,24 +87,19 @@
-
+ + + diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue index ce5e832e2..4c803b29c 100644 --- a/front/src/components/audio/album/Card.vue +++ b/front/src/components/audio/album/Card.vue @@ -51,7 +51,6 @@ diff --git a/front/src/components/auth/Profile.vue b/front/src/components/auth/Profile.vue index 2aaae9e2d..607fa8ff2 100644 --- a/front/src/components/auth/Profile.vue +++ b/front/src/components/auth/Profile.vue @@ -3,17 +3,17 @@
-