Commit 44394275 authored by Ciarán Ainsworth's avatar Ciarán Ainsworth Committed by Georg Krause
Browse files

Update track table

parent da33ec02
Made changes to the track table to make it more visibly pleasing.
......@@ -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": [
......
......@@ -8,15 +8,13 @@
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
<template v-if="!compact">
<a href
v-if="page !== 'skip'"
v-for="page in pages"
:key="page"
@click.prevent.stop="selectPage(page)"
:class="[{'active': page === current}, 'item']">
{{ page }}
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']">
<span v-if="page !== 'skip'">{{ page }}</span>
<span v-else></span>
</a>
<div v-else class="disabled item">
</div>
</template>
<a href
:disabled="current + 1 > maxPage"
......
<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="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>
</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>
<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 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"
export default {
props: {
tracks: Array,
},
components: {
PlayButton,
TrackFavoriteIcon
},
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)
})
},
}
}
</script>
......@@ -5,18 +5,34 @@
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<channel-entry-card v-for="entry in objects" :default-cover="defaultCover" :entry="entry" :key="entry.id" />
<template v-if="count > limit">
<div class="ui hidden divider"></div>
<div class = "ui center aligned basic segment">
<pagination
@page-changed="updatePage"
:current="page"
:paginate-by="limit"
:total="count"
></pagination>
</div>
</template>
<podcast-table
v-if="isPodcast"
:default-cover="defaultCover"
:is-podcast="isPodcast"
:show-art="true"
:show-position="false"
:tracks="objects"
:show-artist="false"
:show-album="false"
:paginate-results="true"
:total="count"
@page-changed="updatePage"
:page="page"
:paginate-by="limit"></podcast-table>
<track-table
v-else
:default-cover="defaultCover"
:is-podcast="isPodcast"
:show-art="true"
:show-position="false"
:tracks="objects"
:show-artist="false"
:show-album="false"
:paginate-results="true"
:total="count"
@page-changed="updatePage"
:page="page"
:paginate-by="limit"></track-table>
<template v-if="!isLoading && objects.length === 0">
<empty-state @refresh="fetchData('tracks/')" :refresh="true">
<p>
......@@ -30,19 +46,19 @@
<script>
import _ from '@/lodash'
import axios from 'axios'
import ChannelEntryCard from '@/components/audio/ChannelEntryCard'
import Pagination from "@/components/Pagination"
import PaginationMixin from "@/components/mixins/Pagination"
import PodcastTable from '@/components/audio/podcast/Table'
import TrackTable from '@/components/audio/track/Table'
export default {
props: {
filters: {type: Object, required: true},
limit: {type: Number, default: 10},
defaultCover: {type: Object},
isPodcast: {type: Boolean, required: true},
},
components: {
ChannelEntryCard,
Pagination
PodcastTable,
TrackTable,
},
data () {
return {
......@@ -58,7 +74,7 @@ export default {
this.fetchData('tracks/')
},
methods: {
fetchData (url) {
async fetchData (url) {
if (!url) {
return
}
......@@ -68,16 +84,17 @@ export default {
params.page_size = this.limit
params.page = this.page
params.include_channels = true
axios.get(url, {params: params}).then((response) => {
self.nextPage = response.data.next
self.isLoading = false
self.objects = response.data.results
self.count = response.data.count
self.$emit('fetched', response.data)
}, error => {
try {
let channelsPromise = await axios.get(url, {params: params})
self.nextPage = channelsPromise.data.next
self.objects = channelsPromise.data.results
self.count = channelsPromise.data.count
self.$emit('fetched', channelsPromise.data)
self.isLoading = false
} catch(e) {
self.isLoading = false
self.errors = error.backendErrors
})
}
},
updatePage: function(page) {
this.page = page
......
......@@ -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
......@@ -27,8 +28,14 @@
<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" :disabled="!playable" @click.stop="$store.commit('playlists/chooseTrack', track)">
<i class="list icon"></i>
<translate translate-context="Sidebar/Player/Icon.Tooltip/Verb">Add to playlist…</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>
<i class="info icon"></i>
<translate v-if="track.artist.content_category === 'podcast'" translate-context="*/Queue/Dropdown/Button/Label/Short">Episode details</translate>
<translate v-else 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">
......@@ -52,9 +59,10 @@ import axios from 'axios'
import jQuery from 'jquery'
import ReportMixin from '@/components/mixins/Report'
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
export default {
mixins: [ReportMixin],
mixins: [ReportMixin, PlayOptionsMixin],
props: {
// we can either have a single or multiple tracks to play when clicked
tracks: {type: Array, required: false},
......@@ -71,7 +79,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 {
......@@ -100,6 +110,7 @@ export default {
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…'),
replacePlay,
}
},
......@@ -112,165 +123,6 @@ export default {
}
}
},
playable () {
if (this.isPlayable) {
return true
}
if (this.track) {
return this.track.uploads && this.track.uploads.length > 0
} else if (this.artist && this.artist.tracks_count) {
return this.artist.tracks_count > 0
} else if (this.artist && this.artist.albums) {
return this.artist.albums.filter((a) => {
return a.is_playable === true
}).length > 0
} else if (this.album) {
return true
} else if (this.tracks) {
return this.tracks.filter((t) => {
return t.uploads && t.uploads.length > 0
}).length > 0
}
return false
},
filterableArtist () {
if (this.track) {
return this.track.artist
}
if (this.album) {
return this.album.artist
}
if (this.artist) {
return this.artist
}
},
},
methods: {
filterArtist () {
this.$store.dispatch('moderation/hide', {type: 'artist', target: this.filterableArtist})
},
getTracksPage (page, params, resolve, tracks) {
if (page > 10) {
// it's 10 * 100 tracks already, let's stop here
resolve(tracks)
}
// when fetching artists/or album tracks, sometimes, we may have to fetch
// multiple pages
let self = this
params['page_size'] = 100
params['page'] = page
params['hidden'] = ''
params['playable'] = 'true'
tracks = tracks || []
axios.get('tracks/', {params: params}).then((response) => {
response.data.results.forEach(t => {
tracks.push(t)
})
if (response.data.next) {
self.getTracksPage(page + 1, params, resolve, tracks)
} else {
resolve(tracks)
}
})
},
getPlayableTracks () {
let self = this
this.isLoading = true
let getTracks = new Promise((resolve, reject) => {
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) => {
resolve([response.data])
})
} else {
resolve([self.track])
}
} else if (self.playlist) {
let url = 'playlists/' + self.playlist.id + '/'
axios.get(url + 'tracks/').then((response) => {
let artistIds = self.$store.getters['moderation/artistFilters']().map((f) => {
return f.target.id
})
let tracks = response.data.results.map(plt => {
return plt.track
})
if (artistIds.length > 0) {
// skip tracks from hidden artists
tracks = tracks.filter((t) => {
let matchArtist = artistIds.indexOf(t.artist.id) > -1
return !(matchArtist || t.album && artistIds.indexOf(t.album.artist.id) > -1)
})
}
resolve(tracks)
})
} else if (self.artist) {
let params = {'artist': self.artist.id, include_channels: 'true', 'ordering': 'album__release_date,disc_number,position'}
self.getTracksPage(1, params, resolve)
} else if (self.album) {
let params = {'album': self.album.id, include_channels: 'true', 'ordering': 'disc_number,position'}
self.getTracksPage(1, params, resolve)
} else if (self.library) {
let params = {'library': self.library.uuid, 'ordering': '-creation_date'}
self.getTracksPage(1, params, resolve)
}
})
return getTracks.then((tracks) => {
setTimeout(e => {
self.isLoading = false
}, 250)
return tracks.filter(e => {
return e.uploads && e.uploads.length > 0
})
})
},
add () {
let self = this
this.getPlayableTracks().then((tracks) => {
self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks))
})
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
},
replacePlay () {
let self = this
self.$store.dispatch('queue/clean')
this.getPlayableTracks().then((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')
},
addNext (next) {
let self = this
let wasEmpty = this.$store.state.queue.tracks.length === 0
this.getPlayableTracks().then((tracks) => {
self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1}).then(() => self.addMessage(tracks))
let goNext = next && !wasEmpty
if (goNext) {
self.$store.dispatch('queue/next')
}
})
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
},
addMessage (tracks) {
if (tracks.length < 1) {
return
}
let msg = this.$npgettext('*/Queue/Message', '%{ count } track was added to your queue', '%{ count } tracks were added to your queue', tracks.length)
this.$store.commit('ui/addMessage', {
content: this.$gettextInterpolate(msg, {count: tracks.length}),
date: new Date()
})
},
},
watch: {
clicked () {
......
<template>
<div
:class="[
{ active: currentTrack && track.id === currentTrack.id },
'track-row row mobile',
]"
>
<div
v-if="showArt"
@click.prevent.exact="activateTrack(track, index)"
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-if="
track.cover
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else-if="
track.artist.cover
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.artist.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else
src="../../../assets/audio/default-cover.png"
/>
</div>
<div
tabindex=0
@click="activateTrack(track, index)"
role="button"
class="content ellipsis left floated column"
>
<p
:class="[
'track-title',
'mobile',
{ 'play-indicator': isPlaying && currentTrack && track.id === currentTrack.id },
]"
>
{{ track.title }}
</p>
<p v-if="track.artist.content_category === 'podcast'" class="track-meta mobile">
<human-date class="really discrete" :date="track.creation_date"></human-date>
<span>&#183;</span>
<human-duration
v-if="track.uploads[0] && track.uploads[0].duration"
:duration="track.uploads[0].duration"
></human-duration>
</p>
<p v-else class="track-meta mobile">
{{ track.artist.name }} <span>&#183;</span>
<human-duration
v-if="track.uploads[0] && track.uploads[0].duration"
:duration="track.uploads[0].duration"
></human-duration>
</p>
</div>
<div
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'"
:class="[
'meta',
'right',
'floated',
'column',
'mobile',
{ 'with-art': showArt },
]"
role="button"
>
<track-favorite-icon
class="tiny"
:border="false"
:track="track"
></track-favorite-icon>
</div>
<div
role="button"
:aria-label="actionsButtonLabel"
@click.prevent.exact="showTrackModal = !showTrackModal"
:class="[
'modal-button',
'right',
'floated',
'column',
'mobile',
{ 'with-art': showArt },
]"
>
<i class="ellipsis large vertical icon" />
</div>
<track-modal
@update:show="showTrackModal = $event;"
:show="showTrackModal"
:track="track"
:index="index"
:is-artist="isArtist"
:is-album="isAlbum"
></track-modal>
</div>
</template>
<script>
import PlayIndicator from "@/components/audio/track/PlayIndicator";
import { mapActions, mapGetters } from "vuex";
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
import TrackModal from "@/components/audio/track/Modal";
import PlayOptionsMixin from "@/components/mixins/PlayOptions"
export default {
mixins: [PlayOptionsMixin],
data() {
return {
showTrackModal: false,
}
},
props: {
tracks: Array,
showAlbum: { type: Boolean, required: false, default: true },
showArtist: { type: Boolean, required: false, default: true },
showPosition: { type: Boolean, required: false, default: false },
showArt: { type: Boolean, required: false, default: true },
search: { type: Boolean, required: false, default: false },
filters: { type: Object, required: false, default: null },
nextUrl: { type: String, required: false, default: null },
displayActions: { type: Boolean, required: false, default: true },
showDuration: { type: Boolean, required: false, default: true },
index: { type: Number, required: true },
track: { type: Object, required: true },
isArtist: {type: Boolean, required: false, default: false},
isAlbum: {type: Boolean, required: false, default: false},
},
components: {
PlayIndicator,