diff --git a/front/src/components/audio/ArtistEntries.vue b/front/src/components/audio/ArtistEntries.vue index e2467d03eb8f563e8e331e47727adf85a83c981c..98466d6f4925eb53dac01c98dee5d35fbd76ba54 100644 --- a/front/src/components/audio/ArtistEntries.vue +++ b/front/src/components/audio/ArtistEntries.vue @@ -2,13 +2,16 @@ <div class="artist-entries ui unstackable grid"> <div class="artist-entries row"> <div class="actions one wide left floated column"></div> - <div class="image one wide left floated column"></div> + <div v-if="showArt" class="image one wide left floated column"></div> <div class="content ellipsis two wide left floated column"> <b>{{ labels.title }}</b> </div> - <div class="content ellipsis two wide left floated column"> + <div v-if="showAlbum" class="content ellipsis two wide left floated column"> <b>{{ labels.album }}</b> </div> + <div v-if="showArtist" class="content ellipsis two wide left floated column"> + <b>{{ labels.artist }}</b> + </div> <div class="meta one wide right floated column"></div> <div class="meta one wide right floated column"> <i class="clock icon" style="padding: 0.5rem;" /> @@ -19,56 +22,58 @@ :class="[{active: currentTrack && track.id === currentTrack.id}, 'artist-entry row']" @mouseover="track.hover = true" @mouseleave="track.hover = false" - @dblclick="replacePlay(tracks, index)" + @dblclick="doubleClick(track, index)" @contextmenu.prevent="$refs.playmenu.open()" v-for="(track, index) in tracks" :key="track.id"> <div class="actions one wide left floated column"> - <play-button - v-if="currentTrack && isPlaying && track.id === currentTrack.id" - class="basic circular icon" - :playing="true" - :button-classes="pausedButtonClasses" - :discrete="true" - :icon-only="true" - :track="track" - :tracks="tracks"> - </play-button> - <play-button - v-else-if="currentTrack && !isPlaying && track.id === currentTrack.id" - class="basic circular icon" - :paused="true" - :button-classes="pausedButtonClasses" - :discrete="true" - :icon-only="true" - :track="track" - :tracks="tracks"> - </play-button> - <play-button - v-else-if="track.hover" - class="basic circular icon" - :button-classes="playingButtonClasses" - :discrete="true" :icon-only="true" - :track="track" - :tracks="tracks"> - </play-button> + <play-indicator + v-if="!isLoadingAudio && currentTrack && isPlaying && track.id === currentTrack.id && !track.hover"> + </play-indicator> + <button + v-else-if="currentTrack && isPlaying && track.id === currentTrack.id && track.hover" + class="ui really tiny basic icon button play-button" + @click.prevent.exact="pausePlayback" + > + <i class="pause icon" /> + </button> + <button + v-else-if="currentTrack && !isPlaying && track.id === currentTrack.id && track.hover" + class="ui really tiny basic icon button play-button" + @click.prevent.exact="resumePlayback" + > + <i class="play icon" /> + </button> + <button + v-else-if="track.hover" + class="ui really tiny basic icon button play-button" + @click.prevent.exact="replacePlay(tracks, index)" + > + <i class="play icon" /> + </button> + <span class="trackPosition" v-else-if="showPosition"> + {{ prettyPosition(track.position) }} + </span> </div> - <div class="image one wide left floated column"> + <div v-if="showArt" class="image left floated column"> <img alt="" class="ui artist-track 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 artist-track mini image" v-else src="../../assets/audio/default-cover.png"> </div> - <div class="content ellipsis two wide left floated column"> + <div class="content ellipsis left floated column"> <router-link :to="{name: 'library.tracks.detail', params: {id: track.id }}">{{ track.title }}</router-link> </div> - <div class="content ellipsis two wide left floated column"> + <div v-if="showAlbum" class="content ellipsis left floated column"> <router-link :to="{name: 'library.albums.detail', params: {id: track.album.id }}">{{ track.album.title }}</router-link> </div> - <div class="meta one wide right floated column"> + <div v-if="showArtist" class="content ellipsis left floated column"> + <router-link class="artist link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">{{ track.artist.name }}</router-link> + </div> + <div class="meta right floated column"> <track-favorite-icon class="tiny" :border="false" :track="track"></track-favorite-icon> </div> - <div class="meta one wide right floated column"> + <div class="meta right floated column"> <human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration"></human-duration> </div> - <div class="one wide right floated column"> + <div class="right floated column"> <play-button id="playmenu" 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> @@ -77,28 +82,29 @@ <script> import _ from '@/lodash' -import PlayButton from '@/components/audio/PlayButton' import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' -import { mapGetters } from "vuex" - +import { mapActions, mapGetters } from "vuex" +import PlayIndicator from '@/components/audio/track/PlayIndicator' +import PlayButton from '@/components/audio/PlayButton' export default { props: { tracks: Array, + showAlbum: {type: Boolean, required: false, default: true}, + showArtist: {type: Boolean, required: false, default: true}, + trackOnly: {type: Boolean, required: false, default: false}, + showPosition: {type: Boolean, required: false, default: false}, + showArt: {type: Boolean, required: false, default: true} }, components: { - PlayButton, TrackFavoriteIcon, - }, - data() { - return { - playingButtonClasses: ['really', 'tiny', 'basic', 'icon', 'button', 'play-button'], - pausedButtonClasses: ['really', 'tiny', 'basic', 'icon', 'button', 'play-button', 'paused'], - } + PlayIndicator, + PlayButton }, computed: { ...mapGetters({ currentTrack: "queue/currentTrack", + isLoadingAudio: "player/isLoadingAudio" }), isPlaying () { @@ -108,11 +114,17 @@ export default { labels() { return { title: this.$pgettext("*/*/*/Noun", "Title"), - album: this.$pgettext("*/*/*/Noun", "Album") + album: this.$pgettext("*/*/*/Noun", "Album"), + artist: this.$pgettext("*/*/*/Noun", "Artist") } } }, methods: { + ...mapActions({ + resumePlayback: "player/resumePlayback", + pausePlayback: "player/pausePlayback", + }), + prettyPosition (position, size) { var s = String(position); while (s.length < (size || 2)) {s = "0" + s;} @@ -124,6 +136,15 @@ export default { this.$store.dispatch('queue/currentIndex', trackIndex) }) }, + doubleClick(track, index) { + if (this.currentTrack && this.isPlaying && track.id === this.currentTrack.id) { + this.pausePlayback() + } else if (this.currentTrack && !this.isPlaying && track.id === this.currentTrack.id) { + this.resumePlayback() + } else { + this.replacePlay(this.tracks, index) + } + } }, created () { this.tracks.forEach((track) => { diff --git a/front/src/components/audio/track/PlayIndicator.vue b/front/src/components/audio/track/PlayIndicator.vue new file mode 100644 index 0000000000000000000000000000000000000000..c97b3c1224b35b92383403d9ca36cc864887f3dc --- /dev/null +++ b/front/src/components/audio/track/PlayIndicator.vue @@ -0,0 +1,8 @@ +<template> + <div id="audio-bars"> + <div class="audio-bar"></div> + <div class="audio-bar"></div> + <div class="audio-bar"></div> + <div class="audio-bar"></div> + </div> +</template> \ No newline at end of file diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue index b43ec41f4f1cf7bf554fdf9bd0df52ded1022a85..62e50dc142e2fefcad2c243ec41a39966df1862e 100644 --- a/front/src/components/favorites/List.vue +++ b/front/src/components/favorites/List.vue @@ -24,7 +24,7 @@ <div class="field"> <label for="favorites-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label> <select id="favorites-ordering" class="ui dropdown" v-model="ordering"> - <option v-for="option in orderingOptions" :value="option[0]"> + <option v-for="option in orderingOptions" :value="option[0]" :key="option[0]"> {{ sharedLabels.filters[option[1]] }} </option> </select> @@ -46,7 +46,7 @@ </div> </div> </div> - <track-table v-if="results" :tracks="results.results"></track-table> + <artist-entries :show-artist="true" :show-album="true" v-if="results" :tracks="results.results"></artist-entries> <div class="ui center aligned basic segment"> <pagination v-if="results && results.count > paginateBy" @@ -76,21 +76,21 @@ import axios from "axios" import $ from "jquery" import logger from "@/logging" -import TrackTable from "@/components/audio/track/Table" import RadioButton from "@/components/radios/Button" import Pagination from "@/components/Pagination" import OrderingMixin from "@/components/mixins/Ordering" import PaginationMixin from "@/components/mixins/Pagination" import TranslationsMixin from "@/components/mixins/Translations" import {checkRedirectToLogin} from '@/utils' +import ArtistEntries from '@/components/audio/ArtistEntries' const FAVORITES_URL = "tracks/" export default { mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], components: { - TrackTable, RadioButton, - Pagination + Pagination, + ArtistEntries }, data() { return { diff --git a/front/src/components/library/AlbumDetail.vue b/front/src/components/library/AlbumDetail.vue index 2a419376223bbb0e324a3e30b80e317a45ca6d6a..a51adea3e40043f20dbb5e2dfec49f7fc4e091cd 100644 --- a/front/src/components/library/AlbumDetail.vue +++ b/front/src/components/library/AlbumDetail.vue @@ -19,7 +19,13 @@ </div> </template> <template v-else> - <album-entries :tracks="object.tracks"></album-entries> + <artist-entries + :tracks="object.tracks" + :show-position="true" + :show-art="false" + :show-album="false" + :show-artist="false"> + </artist-entries> </template> <div class="ui center aligned basic segment"> <pagination @@ -50,7 +56,7 @@ import logger from "@/logging" import LibraryWidget from "@/components/federation/LibraryWidget" import TrackTable from "@/components/audio/track/Table" import ChannelEntries from '@/components/audio/ChannelEntries' -import AlbumEntries from '@/components/audio/AlbumEntries' +import ArtistEntries from '@/components/audio/ArtistEntries' import Pagination from "@/components/Pagination" import PaginationMixin from "@/components/mixins/Pagination" import PlayButton from "@/components/audio/PlayButton" @@ -59,7 +65,7 @@ export default { props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"], components: { LibraryWidget, - AlbumEntries, + ArtistEntries, ChannelEntries, TrackTable, Pagination, diff --git a/front/src/components/library/ArtistDetail.vue b/front/src/components/library/ArtistDetail.vue index cdce0685ec544521ee28cf6e06e3bbeef6c3eaba..222a75e86938024c660e4a6177cf1cb4e27c7dce 100644 --- a/front/src/components/library/ArtistDetail.vue +++ b/front/src/components/library/ArtistDetail.vue @@ -19,7 +19,7 @@ <translate translate-context="Content/Artist/Title">New tracks by this artist</translate> </h2> <div class="ui hidden divider"></div> - <artist-entries :tracks="tracks.slice(0,5)"></artist-entries> + <artist-entries :show-position="false" :track-only="true" :tracks="tracks.slice(0,5)"></artist-entries> </section> <section v-if="isLoadingAlbums" class="ui vertical stripe segment"> <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div> diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index 678205fcae39bd93cabf2d6134c0438eef5cd360..c0faa0f155fb798cb482c038f61b22ebe685d7d5 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -48,6 +48,7 @@ $bottom-player-height: 4rem; @import "./components/_user_link.scss"; @import "./components/_volume_control.scss"; @import "./components/_loaders.scss"; +@import "./components/play_indicator.scss"; @import "./pages/_about.scss"; @import "./pages/_admin_account_detail.scss"; diff --git a/front/src/style/components/_button.scss b/front/src/style/components/_button.scss index d47d804bb30d983a7d9f9288ede0fe6c4c6f9ab1..925212d9a2d89fa190fec8009871d546f2cfcab5 100644 --- a/front/src/style/components/_button.scss +++ b/front/src/style/components/_button.scss @@ -139,7 +139,7 @@ button.reset { .trackPosition { cursor: pointer; - display: inline-block; + display: contents; min-height: 1em; outline: none; border: none; diff --git a/front/src/style/components/_play_indicator.scss b/front/src/style/components/_play_indicator.scss new file mode 100644 index 0000000000000000000000000000000000000000..d56df945d6f0b0a8966c9876299cc62116dbf33a --- /dev/null +++ b/front/src/style/components/_play_indicator.scss @@ -0,0 +1,29 @@ +#audio-bars { + height: 1em; + position: relative; +} + +.audio-bar { + background: var(--main-color); + bottom: 0; + height: .1em; + position: absolute; + width: 3px; + animation: sound 1s cubic-bezier(.17,.37,.43,.67) infinite alternate; +} + +@keyframes sound { + 0% { + opacity: .35; + height: .1em; + } + 100% { + opacity: 1; + height: 1em; + } +} + +.audio-bar:nth-child(1) { left: 0em; animation-duration: 0.4s; } +.audio-bar:nth-child(2) { left: .25em; animation-duration: 0.2s; } +.audio-bar:nth-child(3) { left: .50em; animation-duration: 1.0s; } +.audio-bar:nth-child(4) { left: .75em; animation-duration: 0.3s; } diff --git a/front/src/style/globals/_channels.scss b/front/src/style/globals/_channels.scss index 6b706781ed634983c1d70186220ea201f48d02ed..f305a79d489f9f753e7bd07afeecb86b2dce99a8 100644 --- a/front/src/style/globals/_channels.scss +++ b/front/src/style/globals/_channels.scss @@ -105,6 +105,9 @@ .ui.really.tiny.button.play-button.playing { color: var(--vibrant-color); visibility: visible; + display: contents; + left: auto; + right: auto; } .ui.really.tiny.button.play-button.paused { color: var(--vibrant-color); @@ -139,6 +142,9 @@ .ui.really.tiny.button.play-button { color: var(--main-color); visibility: visible; + display: contents; + left: auto; + right: auto; } .ui.floating.dropdown { visibility: visible;