From fb962a6a6ac7b58f8fab27f47db1e386035bcbc2 Mon Sep 17 00:00:00 2001 From: Ciaran Ainsworth <ciaranainsworth@outlook.com> Date: Sun, 2 May 2021 22:03:12 +0100 Subject: [PATCH] Add basic support for new table layout --- front/scripts/fix-fomantic-css.py | 5 + front/src/components/audio/AlbumEntries.vue | 54 ++++++++-- front/src/components/audio/ArtistEntries.vue | 101 ++++++++++++++++++ front/src/components/audio/PlayButton.vue | 7 +- front/src/components/library/ArtistBase.vue | 2 +- front/src/components/library/ArtistDetail.vue | 16 +-- front/src/style/components/_button.scss | 52 ++++++++- front/src/style/globals/_channels.scss | 52 +++++++-- 8 files changed, 260 insertions(+), 29 deletions(-) create mode 100644 front/src/components/audio/ArtistEntries.vue diff --git a/front/scripts/fix-fomantic-css.py b/front/scripts/fix-fomantic-css.py index 3ab13001fe..80b8644166 100755 --- a/front/scripts/fix-fomantic-css.py +++ b/front/scripts/fix-fomantic-css.py @@ -309,6 +309,11 @@ REPLACEMENTS = { ("color", "var(--button-basic-hover-color)"), ("box-shadow", "var(--button-basic-hover-box-shadow)"), ], + (".ui.basic.button:focus",): [ + ("background", "var(--button-basic-hover-background)"), + ("color", "var(--button-basic-hover-color)"), + ("box-shadow", "var(--button-basic-hover-box-shadow)"), + ], }, "card": { "skip": [ diff --git a/front/src/components/audio/AlbumEntries.vue b/front/src/components/audio/AlbumEntries.vue index c0d1726e22..fb97c97617 100644 --- a/front/src/components/audio/AlbumEntries.vue +++ b/front/src/components/audio/AlbumEntries.vue @@ -1,17 +1,42 @@ <template> <div class="album-entries"> - <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="[{active: currentTrack && track.id === currentTrack.id}, 'album-entry']" @mouseover="track.hover = true" @mouseleave="track.hover = false" @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" :tracks="tracks"></play-button> + <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> + <span class="trackPosition" v-else>{{ prettyPosition(track.position) }}</span> </div> - <div class="position">{{ prettyPosition(track.position) }}</div> <div class="content ellipsis"> <strong>{{ track.title }}</strong><br> </div> <div class="meta"> - <template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](track.id)"> - <track-favorite-icon class="tiny" :track="track"></track-favorite-icon> - </template> + <track-favorite-icon class="tiny" :border="false" :track="track"></track-favorite-icon> <human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration"></human-duration> </div> <div class="actions"> @@ -23,8 +48,6 @@ <script> import _ from '@/lodash' -import axios from 'axios' -import ChannelEntryCard from '@/components/audio/ChannelEntryCard' import PlayButton from '@/components/audio/PlayButton' import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' import { mapGetters } from "vuex" @@ -36,7 +59,13 @@ export default { }, components: { PlayButton, - TrackFavoriteIcon + TrackFavoriteIcon, + }, + data() { + return { + playingButtonClasses: ['really', 'tiny', 'basic', 'icon', 'button', 'play-button'], + pausedButtonClasses: ['really', 'tiny', 'basic', 'icon', 'button', 'play-button', 'paused'], + } }, computed: { ...mapGetters({ @@ -59,6 +88,11 @@ export default { this.$store.dispatch('queue/currentIndex', trackIndex) }) }, - } + }, + created () { + this.tracks.forEach((track) => { + this.$set(track, 'hover', false) + }) +} } </script> diff --git a/front/src/components/audio/ArtistEntries.vue b/front/src/components/audio/ArtistEntries.vue new file mode 100644 index 0000000000..26c24c87dd --- /dev/null +++ b/front/src/components/audio/ArtistEntries.vue @@ -0,0 +1,101 @@ +<template> + <div class="artist-entries"> + <div :class="[{active: currentTrack && track.id === currentTrack.id}, 'artist-entry']" @mouseover="track.hover = true" @mouseleave="track.hover = false" @click.prevent="replacePlay(tracks, index)" v-for="(track, index) in tracks" :key="track.id"> + <span> + <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"> + </span> + <div class="actions"> + <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> + </div> + <div class="content ellipsis"> + <strong>{{ track.title }}</strong><br> + </div> + <div class="meta"> + <track-favorite-icon class="tiny" :border="false" :track="track"></track-favorite-icon> + <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> + </div> + </div> + </div> +</template> + +<script> +import _ from '@/lodash' +import PlayButton from '@/components/audio/PlayButton' +import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' +import { mapGetters } from "vuex" + + +export default { + props: { + tracks: Array, + }, + components: { + PlayButton, + TrackFavoriteIcon, + }, + data() { + return { + playingButtonClasses: ['really', 'tiny', 'basic', 'icon', 'button', 'play-button'], + pausedButtonClasses: ['really', 'tiny', 'basic', 'icon', 'button', 'play-button', 'paused'], + } + }, + computed: { + ...mapGetters({ + currentTrack: "queue/currentTrack", + }), + + isPlaying () { + return this.$store.state.player.playing + }, + }, + methods: { + prettyPosition (position, size) { + 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) + }) + }, + }, + created () { + this.tracks.forEach((track) => { + this.$set(track, 'hover', false) + }) +} +} +</script> diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index c595f07c85..78b1b36b05 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -6,7 +6,8 @@ :disabled="!playable" :aria-label="labels.replacePlay" :class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])"> - <i :class="[playIconClass, 'icon']"></i> + <i v-if="playing" class="pause icon"></i> + <i v-else :class="[playIconClass, 'icon']"></i> <template v-if="!discrete && !iconOnly"> <slot><translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate></slot></template> </button> <button @@ -71,7 +72,9 @@ export default { album: {type: Object, required: false}, library: {type: Object, required: false}, channel: {type: Object, required: false}, - isPlayable: {type: Boolean, required: false, default: null} + isPlayable: {type: Boolean, required: false, default: null}, + playing: {type: Boolean, required: false, default: false}, + paused: {type: Boolean, required: false, default: false} }, data () { return { diff --git a/front/src/components/library/ArtistBase.vue b/front/src/components/library/ArtistBase.vue index f23b1311ed..ebc7fe5c1d 100644 --- a/front/src/components/library/ArtistBase.vue +++ b/front/src/components/library/ArtistBase.vue @@ -195,7 +195,7 @@ export default { if (!self.object) { return } - let trackPromise = axios.get("tracks/", { params: { artist: this.id, hidden: '' } }).then(response => { + let trackPromise = axios.get("tracks/", { params: { artist: this.id, hidden: '', ordering: "-creation_date" } }).then(response => { self.tracks = response.data.results self.nextTracksUrl = response.data.next self.totalTracks = response.data.count diff --git a/front/src/components/library/ArtistDetail.vue b/front/src/components/library/ArtistDetail.vue index e43eebf8ca..d64e0eb731 100644 --- a/front/src/components/library/ArtistDetail.vue +++ b/front/src/components/library/ArtistDetail.vue @@ -14,6 +14,12 @@ </button> </div> </div> + <section v-if="tracks.length > 0" class="ui vertical stripe segment"> + <h2> + <translate translate-context="Content/Artist/Title">New tracks by this artist</translate> + </h2> + <artist-entries :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> </section> @@ -29,12 +35,6 @@ <translate translate-context="Content/*/Button.Label">Load moreā¦</translate> </button> </section> - <section v-if="tracks.length > 0" class="ui vertical stripe segment"> - <h2> - <translate translate-context="Content/Artist/Title">Tracks by this artist</translate> - </h2> - <track-table :display-position="false" :tracks="tracks" :next-url="nextTracksUrl"></track-table> - </section> <section class="ui vertical stripe segment"> <h2> <translate translate-context="Content/*/Title/Noun">User libraries</translate> @@ -51,14 +51,14 @@ import _ from "@/lodash" import axios from "axios" import logger from "@/logging" import AlbumCard from "@/components/audio/album/Card" -import TrackTable from "@/components/audio/track/Table" +import ArtistEntries from "@/components/audio/ArtistEntries" import LibraryWidget from "@/components/federation/LibraryWidget" export default { props: ["object", "tracks", "albums", "isLoadingAlbums", "nextTracksUrl", "nextAlbumsUrl"], components: { AlbumCard, - TrackTable, + ArtistEntries, LibraryWidget, }, data () { diff --git a/front/src/style/components/_button.scss b/front/src/style/components/_button.scss index eae2c1ebe6..d47d804bb3 100644 --- a/front/src/style/components/_button.scss +++ b/front/src/style/components/_button.scss @@ -98,4 +98,54 @@ button.reset { .ui.inverted.buttons .button:focus, .ui.inverted.button:focus { color: white; -} \ No newline at end of file +} + +.ui.favorite-icon.favorited { + animation: .5s linear burst; + outline-color: transparent; + @keyframes burst{ + 0%,10%{ + transform: scale(1); + opacity: .5; + color:lavender; + box-shadow: none; + } + 45%{ + transform: scale(.2) rotate(30deg); + opacity: .75; + box-shadow: none; + } + 50%{ + transform: scale(2) rotate(-37.5deg); + opacity: 1; + color: #E03997; + text-shadow: 2px 2px 6px rgba(235, 9, 9, 0.5); + box-shadow: none; + } + 90%,95%{ + transform: scale(1) rotate(10deg); + text-shadow: none; + } + 100% { + transform: rotate(-2.5deg); + } + + } +} + +.ui.basic.button.really.favorite-icon { + box-shadow: none; +} + +.trackPosition { + cursor: pointer; + display: inline-block; + min-height: 1em; + outline: none; + border: none; + vertical-align: baseline; + font-family: var(--font-family); + margin: 0 0.25em 0 0; + line-height: 1em; + padding: 0.5rem; +} diff --git a/front/src/style/globals/_channels.scss b/front/src/style/globals/_channels.scss index 424b80efb3..b7febeed24 100644 --- a/front/src/style/globals/_channels.scss +++ b/front/src/style/globals/_channels.scss @@ -51,11 +51,13 @@ flex-grow: 1; } } -.album-entries { +.album-entries, +.artist-entries { > div { display: flex; align-items: center; justify-content: space-between; + height: 3.5rem; } .content { flex-grow: 1; @@ -76,18 +78,53 @@ } } -.album-entry:hover { +.album-entry, +.artist-entry { + .ui.really.tiny.button.play-button { + visibility: hidden; + } + .ui.floating.dropdown { + visibility: hidden; + } + .ui.favorite-icon { + visibility: hidden; + } + .ui.favorite-icon.pink { + visibility: visible; + } + .actions { + display: block; + max-width: 2rem; + width: 100%; + } + .ui.really.tiny.button.play-button.playing { + color: var(--vibrant-color); + visibility: visible; + } + .ui.really.tiny.button.play-button.paused { + color: var(--vibrant-color); + visibility: visible; + } +} + +.album-entry:hover, +.artist-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; + .ui.really.tiny.button.play-button { + color: var(--main-color); + visibility: visible; + } + .ui.floating.dropdown { + visibility: visible; + } + .ui.favorite-icon { + visibility: visible; } } -.album-entry, .channel-entry-card { +.album-entry, .artist-entry, .channel-entry-card { border-radius: 5px; padding: 0.5em; .meta { @@ -110,6 +147,7 @@ border: none !important; padding: 0 !important; margin: 0 0.5em; + transition: all ease-in-out; } } .channel-image { -- GitLab