diff --git a/front/scripts/fix-fomantic-css.py b/front/scripts/fix-fomantic-css.py
index 3ab13001fee4904975e672b614e9d503de96d9ef..80b8644166d068ee85b23095955bf95ceac9da9d 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 c0d1726e224d2ed93eeded6eae790b47afeed6de..fb97c97617664a385903d1d8d3b41faef08ed2a4 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 0000000000000000000000000000000000000000..26c24c87dd2116b852d1dd627cdfe00295d495f5
--- /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 c595f07c855eb2cfe27ff8a159dbe929be88e2be..78b1b36b051964e0042f7ecb30d8b6ea8fe59267 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 f23b1311ed65b115b2462233b7791ef7b35a0b7b..ebc7fe5c1d4a18abc3e0706f7e4feef2dd885011 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 e43eebf8cac38eb93691681b3f4c5a96d82643cf..d64e0eb7313ccd09f31a8495978430ed38b157bd 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 eae2c1ebe60c9c4a9165a4da09516bfce3eaa7d2..d47d804bb30d983a7d9f9288ede0fe6c4c6f9ab1 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 424b80efb35d147331fd69d857e1409d746b21e5..b7febeed24933b99e2c45fa55c15430110fa6e42 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 {