diff --git a/changes/changelog.d/1274.enhancement b/changes/changelog.d/1274.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..7f25d5a1da7939acd80baf86c796500fbd2b2b51 --- /dev/null +++ b/changes/changelog.d/1274.enhancement @@ -0,0 +1 @@ +Make "play in list" the default when interacting with individual tracks (#1274) diff --git a/front/src/components/audio/AlbumEntries.vue b/front/src/components/audio/AlbumEntries.vue index c1725895154e1f148eb6cd6ccde010e1a3b4cf10..c0d1726e224d2ed93eeded6eae790b47afeed6de 100644 --- a/front/src/components/audio/AlbumEntries.vue +++ b/front/src/components/audio/AlbumEntries.vue @@ -1,14 +1,12 @@ <template> <div class="album-entries"> - <div :class="[{active: currentTrack && isPlaying && track.id === currentTrack.id}, 'album-entry']" v-for="track in tracks" :key="track.id"> + <div :class="[{active: currentTrack && isPlaying && track.id === currentTrack.id}, 'album-entry']" @click.prevent="replacePlay(tracks, index)" v-for="(track, index) in tracks" :key="track.id"> <div class="actions"> - <play-button class="basic circular icon" :button-classes="['circular inverted vibrant icon button']" :discrete="true" :icon-only="true" :track="track"></play-button> + <play-button class="basic circular icon" :button-classes="['circular inverted vibrant icon button']" :discrete="true" :icon-only="true" :track="track" :tracks="tracks"></play-button> </div> <div class="position">{{ prettyPosition(track.position) }}</div> <div class="content ellipsis"> - <router-link :to="{name: 'library.tracks.detail', params: {id: track.id}}" class="discrete link"> - <strong>{{ track.title }}</strong><br> - </router-link> + <strong>{{ track.title }}</strong><br> </div> <div class="meta"> <template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](track.id)"> @@ -17,7 +15,7 @@ <human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration"></human-duration> </div> <div class="actions"> - <play-button class="play-button basic icon" :dropdown-only="true" :is-playable="track.is_playable" :dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']" :track="track"></play-button> + <play-button class="play-button basic icon" :dropdown-only="true" :is-playable="track.is_playable" :dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']" :track="track"></play-button> </div> </div> </div> @@ -54,7 +52,13 @@ export default { var s = String(position); while (s.length < (size || 2)) {s = "0" + s;} return s; - } + }, + replacePlay (tracks, trackIndex) { + this.$store.dispatch('queue/clean') + this.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => { + this.$store.dispatch('queue/currentIndex', trackIndex) + }) + }, } } </script> diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index 96c9ba96e8a83c70b5abc2f2f4d199e24d830785..c595f07c855eb2cfe27ff8a159dbe929be88e2be 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -11,7 +11,7 @@ </button> <button v-if="!discrete && !iconOnly" - @click.prevent="clicked = true" + @click.stop.prevent="clicked = true" :class="['ui', {disabled: !playable && !filterableArtist}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]"> <i :class="dropdownIconClasses.concat(['icon'])" :title="title" ></i> <div class="menu" v-if="clicked"> @@ -27,6 +27,9 @@ <button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio"> <i class="feed icon"></i><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate> </button> + <button v-if="track" class="item basic" @click.stop.prevent="$router.push(`/library/tracks/${track.id}/`)"> + <i class="info icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Track details</translate> + </button> <div class="divider"></div> <button v-if="filterableArtist" ref="filterArtist" data-ref="filterArtist" class="item basic" :disabled="!filterableArtist" @click.stop.prevent="filterArtist" :title="labels.hideArtist"> <i class="eye slash outline icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Hide content from this artist</translate> @@ -35,7 +38,7 @@ v-for="obj in getReportableObjs({track, album, artist, playlist, account, channel})" :key="obj.target.type + obj.target.id" class="item basic" - :ref="`report${obj.target.type}${obj.target.id}`" :data-ref="`report${obj.target.type}${obj.target.id}`" + :ref="`report${obj.target.type}${obj.target.id}`" :data-ref="`report${obj.target.type}${obj.target.id}`" @click.stop.prevent="$store.dispatch('moderation/report', obj.target)"> <i class="share icon" /> {{ obj.label }} </button> @@ -90,7 +93,7 @@ export default { } else { replacePlay = this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play tracks') } - + return { playNow: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play now'), addToQueue: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Add to current queue'), @@ -143,7 +146,6 @@ export default { }, }, methods: { - filterArtist () { this.$store.dispatch('moderation/hide', {type: 'artist', target: this.filterableArtist}) }, @@ -175,7 +177,9 @@ export default { let self = this this.isLoading = true let getTracks = new Promise((resolve, reject) => { - if (self.track) { + if (self.tracks) { + resolve(self.tracks) + } else if (self.track) { if (!self.track.uploads || self.track.uploads.length === 0) { // fetch uploads from api axios.get(`tracks/${self.track.id}/`).then((response) => { @@ -184,8 +188,6 @@ export default { } else { resolve([self.track]) } - } else if (self.tracks) { - resolve(self.tracks) } else if (self.playlist) { let url = 'playlists/' + self.playlist.id + '/' axios.get(url + 'tracks/').then((response) => { @@ -236,7 +238,14 @@ export default { let self = this self.$store.dispatch('queue/clean') this.getPlayableTracks().then((tracks) => { - self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks)) + self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => { + if (self.track) { + // set queue position to selected track + const trackIndex = self.tracks.findIndex(track => track.id === self.track.id) + self.$store.dispatch('queue/currentIndex', trackIndex) + } + self.addMessage(tracks) + }) }) jQuery(self.$el).find('.ui.dropdown').dropdown('hide') }, diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue index 876f2d713135ae01a25af9fe12de4dfb9de61dd7..03b6ccfa34dacd2ca9736e5f8f1d9d6e8518249d 100644 --- a/front/src/components/audio/track/Row.vue +++ b/front/src/components/audio/track/Row.vue @@ -1,19 +1,24 @@ <template> <tr> <td> - <play-button :class="['basic', {vibrant: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :is-playable="playable" :track="track"></play-button> + <play-button :class="['basic', {vibrant: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']" + :discrete="true" + :is-playable="playable" + :track="track" + :track-index="trackIndex" + :tracks="tracks"></play-button> </td> <td> <img alt="" class="ui mini image" v-if="track.album && track.album.cover && track.album.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"> <img alt="" class="ui mini image" v-else src="../../../assets/audio/default-cover.png"> </td> <td colspan="6"> - <router-link class="track" :to="{name: 'library.tracks.detail', params: {id: track.id }}"> + <button class="track" @click.stop="playSong()"> <template v-if="displayPosition && track.position"> {{ track.position }}. </template> {{ track.title|truncate(40) }} - </router-link> + </button> </td> <td colspan="4"> <router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}"> @@ -56,6 +61,8 @@ import PlayButton from '@/components/audio/PlayButton' export default { props: { track: {type: Object, required: true}, + trackIndex: {type: Number, required: true}, + tracks: {type: Array, required: false}, artist: {type: Object, required: false}, displayPosition: {type: Boolean, default: false}, displayActions: {type: Boolean, default: true}, @@ -80,6 +87,16 @@ export default { return this.track.album.artist } }, + }, + methods: { + playSong () { + this.$store.dispatch('queue/clean') + this.$store.dispatch('queue/appendMany', { + tracks: this.tracks + }).then(() => { + this.$store.dispatch('queue/currentIndex', this.trackIndex) + }) + }, } } </script> diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index 43a9069f81c3c25b73850d9714bcc0bc83d2672a..36f78cc26825350a2dca44075356aad17c48c322 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -22,6 +22,8 @@ :display-position="displayPosition" :display-actions="displayActions" :track="track" + :track-index="index" + :tracks="allTracks" :artist="artist" :key="index + '-' + track.id" v-for="(track, index) in allTracks"></track-row> diff --git a/front/src/style/components/_track_table.scss b/front/src/style/components/_track_table.scss index 7fd40ff11dfcde8001769071efb687be11103959..56d518650ec0e83f2e3f0d8e7d16a72d08f36ec3 100644 --- a/front/src/style/components/_track_table.scss +++ b/front/src/style/components/_track_table.scss @@ -11,4 +11,9 @@ visibility: hidden; } } + + .track { + display: block; + line-height: 2; + } } diff --git a/front/src/style/globals/_channels.scss b/front/src/style/globals/_channels.scss index fd90cee2996e273f0d2efc7af9ed0786086239f3..424b80efb35d147331fd69d857e1409d746b21e5 100644 --- a/front/src/style/globals/_channels.scss +++ b/front/src/style/globals/_channels.scss @@ -75,6 +75,18 @@ } } } + +.album-entry:hover { + cursor: pointer; + + // explicitly style the button as if it was hovered itself + .ui.inverted.vibrant.button { + background-color: var(--vibrant-hover-color); + color: white; + box-shadow: 0 0 0 2px var(--vibrant-color) inset; + } +} + .album-entry, .channel-entry-card { border-radius: 5px; padding: 0.5em;