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">&nbsp;<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