diff --git a/changes/changelog.d/262.enhancement b/changes/changelog.d/262.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..cad67e939528317c60c989a1d6493c9fa5cbbc6f --- /dev/null +++ b/changes/changelog.d/262.enhancement @@ -0,0 +1 @@ +Added feedback on shuffle button (#262) diff --git a/front/src/App.vue b/front/src/App.vue index 673f8386460ecba32737c129e3421adc06881f04..91cf29843429eb06e55d1aefd861b7eb3f81f1b4 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,6 +1,7 @@ <template> <div id="app"> <sidebar></sidebar> + <service-messages v-if="messages.length > 0" /> <router-view :key="$route.fullPath"></router-view> <div class="ui fitted divider"></div> <div id="footer" class="ui vertical footer segment"> @@ -44,9 +45,11 @@ <script> import axios from 'axios' import _ from 'lodash' +import {mapState} from 'vuex' import Sidebar from '@/components/Sidebar' import Raven from '@/components/Raven' +import ServiceMessages from '@/components/ServiceMessages' import PlaylistModal from '@/components/playlists/PlaylistModal' @@ -55,7 +58,8 @@ export default { components: { Sidebar, Raven, - PlaylistModal + PlaylistModal, + ServiceMessages }, data () { return { @@ -80,6 +84,9 @@ export default { } }, computed: { + ...mapState({ + messages: state => state.ui.messages + }), version () { if (!this.nodeinfo) { return null @@ -115,6 +122,14 @@ html, body { } transform: none !important; } +.service-messages { + position: fixed; + bottom: 1em; + left: 1em; + @include media(">desktop") { + left: 350px; + } +} .main-pusher { padding: 1.5rem 0; } diff --git a/front/src/components/ServiceMessages.vue b/front/src/components/ServiceMessages.vue new file mode 100644 index 0000000000000000000000000000000000000000..0a3be99e5becb1f0efd13ec60d212a5f32233068 --- /dev/null +++ b/front/src/components/ServiceMessages.vue @@ -0,0 +1,82 @@ +<template> + <div class="service-messages"> + <message v-for="message in displayedMessages" :key="String(message.date)" :class="['large', getLevel(message)]"> + <p>{{ message.content }}</p> + </message> + </div> +</template> + +<script> +import {mapState} from 'vuex' + +export default { + data () { + return { + date: new Date(), + interval: null + } + }, + created () { + this.setupInterval() + }, + destroyed () { + if (this.interval) { + clearInterval(this.interval) + } + }, + computed: { + ...mapState({ + messages: state => state.ui.messages, + displayDuration: state => state.ui.messageDisplayDuration + }), + displayedMessages () { + let now = this.date + let interval = this.displayDuration + let toDisplay = this.messages.filter(m => { + return now - m.date <= interval + }) + return toDisplay.slice(0, 3) + } + }, + methods: { + setupInterval () { + if (this.interval) { + return + } + let self = this + this.interval = setInterval(() => { + if (self.displayedMessages.length === 0) { + clearInterval(self.interval) + this.interval = null + } + self.date = new Date() + }, 1000) + }, + getLevel (message) { + return message.level || 'info' + } + }, + watch: { + messages: { + handler (v) { + if (v.length > 0 && !this.interval) { + this.setupInterval() + } + }, + deep: true + } + } +} +</script> + +<style> +.service-messages { + z-index: 9999; + margin-left: 1em; + min-width: 20em; + max-width: 40em; +} +.service-messages .message:last-child { + margin-bottom: 0; +} +</style> diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index 28a8900841afc29fdefe844b3b8c473420f7809c..9777fa83ca2d52605e39d37219e25694e49b614c 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -124,19 +124,28 @@ export default { add () { let self = this this.getPlayableTracks().then((tracks) => { - self.$store.dispatch('queue/appendMany', {tracks: tracks}) + self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks)) }) }, addNext (next) { let self = this let wasEmpty = this.$store.state.queue.tracks.length === 0 this.getPlayableTracks().then((tracks) => { - self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1}) + self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1}).then(() => self.addMessage(tracks)) let goNext = next && !wasEmpty if (goNext) { self.$store.dispatch('queue/next') } }) + }, + addMessage (tracks) { + if (tracks.length < 1) { + return + } + this.$store.commit('ui/addMessage', { + content: this.$t('{% tracks %} tracks were added to your queue.', {tracks: tracks.length}), + date: new Date() + }) } } } diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index c475ec684a008ec46392361523eefc77ab38b0e6..3c922e14ad323d75413d969ba7c033add19e92e0 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -113,7 +113,8 @@ :disabled="queue.tracks.length === 0" :title="$t('Shuffle your queue')" class="two wide column control"> - <i @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i> + <div v-if="isShuffling" class="ui inline shuffling inverted small active loader"></div> + <i v-else @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i> </div> <div class="one wide column"></div> <div @@ -158,6 +159,7 @@ export default { data () { let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]] return { + isShuffling: false, renderAudio: true, sliderVolume: this.volume, Track: Track, @@ -173,9 +175,24 @@ export default { ...mapActions({ togglePlay: 'player/togglePlay', clean: 'queue/clean', - shuffle: 'queue/shuffle', updateProgress: 'player/updateProgress' }), + shuffle () { + if (this.isShuffling) { + return + } + let self = this + this.isShuffling = true + setTimeout(() => { + self.$store.dispatch('queue/shuffle', () => { + self.isShuffling = false + self.$store.commit('ui/addMessage', { + content: self.$t('Queue shuffled!'), + date: new Date() + }) + }) + }, 100) + }, next () { let self = this this.$store.dispatch('queue/next').then(() => { @@ -402,5 +419,8 @@ export default { .ui.feed.icon { margin: 0; } +.shuffling.loader.inline { + margin: 0; +} </style> diff --git a/front/src/components/common/Message.vue b/front/src/components/common/Message.vue new file mode 100644 index 0000000000000000000000000000000000000000..772071db78c98881dab84405b2bc0903beff8106 --- /dev/null +++ b/front/src/components/common/Message.vue @@ -0,0 +1,36 @@ +<template> + <div class="ui message"> + <div class="content"> + <slot></slot> + </div> + <i class="close icon"></i> + </div> +</template> +<script> +import $ from 'jquery' + +export default { + mounted () { + let self = this + $(this.$el).find('.close.icon').on('click', function () { + $(self.$el).transition('fade', 125) + }) + $(this.$el).on('click', function () { + $(self.$el).transition('fade', 125) + }) + } +} +</script> +<style scoped> +.ui.message .content { + padding-right: 0.5em; + cursor: pointer; +} +.ui.message .content :first-child { + margin-top: 0; +} + +.ui.message .content :last-child { + margin-bottom: 0; +} +</style> diff --git a/front/src/components/globals.js b/front/src/components/globals.js index 79bbcf1b93a4a74ecf3c3b3d3d4e724870d7120c..4ad09f70425a987fd99e86f6e4277cd07d797605 100644 --- a/front/src/components/globals.js +++ b/front/src/components/globals.js @@ -12,4 +12,8 @@ import DangerousButton from '@/components/common/DangerousButton' Vue.component('dangerous-button', DangerousButton) +import Message from '@/components/common/Message' + +Vue.component('message', Message) + export default {} diff --git a/front/src/store/queue.js b/front/src/store/queue.js index 23e074a80c36fb849eb306ea59da68756a05282b..2d6c667b29ba5e87623f3cc60e7be31e0d5d3fc4 100644 --- a/front/src/store/queue.js +++ b/front/src/store/queue.js @@ -72,16 +72,20 @@ export default { } }, - appendMany ({state, dispatch}, {tracks, index}) { + appendMany ({state, dispatch}, {tracks, index, callback}) { logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title })) if (state.tracks.length === 0) { index = 0 } else { index = index || state.tracks.length } - tracks.forEach((t) => { - dispatch('append', {track: t, index: index, skipPlay: true}) + let total = tracks.length + tracks.forEach((t, i) => { + let p = dispatch('append', {track: t, index: index, skipPlay: true}) index += 1 + if (callback && i + 1 === total) { + p.then(callback) + } }) dispatch('resume') }, @@ -148,13 +152,17 @@ export default { // so we replay automatically on next track append commit('ended', true) }, - shuffle ({dispatch, commit, state}) { + shuffle ({dispatch, commit, state}, callback) { let toKeep = state.tracks.slice(0, state.currentIndex + 1) let toShuffle = state.tracks.slice(state.currentIndex + 1) let shuffled = toKeep.concat(_.shuffle(toShuffle)) commit('player/currentTime', 0, {root: true}) commit('tracks', []) - dispatch('appendMany', {tracks: shuffled}) + let params = {tracks: shuffled} + if (callback) { + params.callback = callback + } + dispatch('appendMany', params) } } } diff --git a/front/src/store/ui.js b/front/src/store/ui.js index f0935e491bb1a48abc5f90d3482e3d5eab7ee9a5..be744afe51ad954a4bae722f9442a9d71ad85730 100644 --- a/front/src/store/ui.js +++ b/front/src/store/ui.js @@ -2,11 +2,20 @@ export default { namespaced: true, state: { - lastDate: new Date() + lastDate: new Date(), + maxMessages: 100, + messageDisplayDuration: 10000, + messages: [] }, mutations: { computeLastDate: (state) => { state.lastDate = new Date() + }, + addMessage (state, message) { + state.messages.push(message) + if (state.messages.length > state.maxMessages) { + state.messages.shift() + } } } } diff --git a/front/test/unit/specs/store/ui.spec.js b/front/test/unit/specs/store/ui.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..adcfa87d8f34bd600f113fc4100c0c6318bc730e --- /dev/null +++ b/front/test/unit/specs/store/ui.spec.js @@ -0,0 +1,18 @@ +import store from '@/store/ui' + +import { testAction } from '../../utils' + +describe('store/ui', () => { + describe('mutations', () => { + it('addMessage', () => { + const state = {maxMessages: 100, messages: []} + store.mutations.addMessage(state, 'hello') + expect(state.messages).to.deep.equal(['hello']) + }) + it('addMessage', () => { + const state = {maxMessages: 1, messages: ['hello']} + store.mutations.addMessage(state, 'world') + expect(state.messages).to.deep.equal(['world']) + }) + }) +})