Update track table
This commit is contained in:
parent
da33ec0241
commit
44394275ec
|
@ -0,0 +1 @@
|
|||
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"
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
<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"
|
||||
<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"
|
||||
></pagination>
|
||||
</div>
|
||||
</template>
|
||||
@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
|
||||
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
|
||||
self.objects = response.data.results
|
||||
self.count = response.data.count
|
||||
self.$emit('fetched', response.data)
|
||||
}, error => {
|
||||
} 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"> <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 () {
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
<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>·</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>·</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,
|
||||
TrackFavoriteIcon,
|
||||
TrackModal,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing;
|
||||
},
|
||||
actionsButtonLabel () {
|
||||
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,304 @@
|
|||
<template>
|
||||
<modal
|
||||
@update:show="$emit('update:show', $event)"
|
||||
:show="show"
|
||||
:scrolling="true"
|
||||
:additionalClasses="['scrolling-track-options']"
|
||||
>
|
||||
<div class="header">
|
||||
<div class="ui large centered rounded image">
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered 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 centered image"
|
||||
v-else-if="track.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
v-else-if="track.artist.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.artist.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
v-else
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="track-modal-title">{{ track.title }}</h3>
|
||||
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="content">
|
||||
<div class="ui one column unstackable grid">
|
||||
<div
|
||||
class="row"
|
||||
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="favoriteButton"
|
||||
@click.stop="$store.dispatch('favorites/toggle', track.id)"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'heart',
|
||||
'favorite-icon',
|
||||
{ favorited: isFavorite },
|
||||
{ pink: isFavorite },
|
||||
'icon',
|
||||
'track-modal',
|
||||
'list-icon',
|
||||
]"
|
||||
/>
|
||||
<span class="track-modal list-item">{{ favoriteButton }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop.prevent="
|
||||
add();
|
||||
closeModal();
|
||||
"
|
||||
:aria-label="labels.addToQueue"
|
||||
>
|
||||
<i class="plus icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop.prevent="
|
||||
addNext(true);
|
||||
closeModal();
|
||||
"
|
||||
:aria-label="labels.playNext"
|
||||
>
|
||||
<i class="step forward icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop.prevent="
|
||||
$store.dispatch('radios/start', {
|
||||
type: 'similar',
|
||||
objectId: track.id,
|
||||
});
|
||||
closeModal();
|
||||
"
|
||||
:aria-label="labels.startRadio"
|
||||
>
|
||||
<i class="rss icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||
:aria-label="labels.addToPlaylist"
|
||||
>
|
||||
<i class="list icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
labels.addToPlaylist
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div v-if="!isAlbum && track.album" class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="albumDetailsButton"
|
||||
@click.prevent.exact="
|
||||
$router.push({
|
||||
name: 'library.albums.detail',
|
||||
params: { id: track.album.id },
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="compact disc icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
albumDetailsButton
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isArtist" class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="artistDetailsButton"
|
||||
@click.prevent.exact="
|
||||
$router.push({
|
||||
name: 'library.artists.detail',
|
||||
params: { id: track.artist.id },
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="user icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
artistDetailsButton
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="trackDetailsButton"
|
||||
@click.prevent.exact="
|
||||
$router.push({
|
||||
name: 'library.tracks.detail',
|
||||
params: { id: track.id },
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="info icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
trackDetailsButton
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div
|
||||
v-for="obj in getReportableObjs({
|
||||
track,
|
||||
album,
|
||||
artist,
|
||||
})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
class="row"
|
||||
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||
>
|
||||
<div class="column">
|
||||
<i class="share icon track-modal list-icon" /><span
|
||||
class="track-modal list-item"
|
||||
>{{ obj.label }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/semantic/Modal";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
mixins: [ReportMixin, PlayOptionsMixin],
|
||||
props: {
|
||||
show: { type: Boolean, required: true, default: false },
|
||||
track: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
components: {
|
||||
Modal,
|
||||
TrackFavoriteIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShowing: this.show,
|
||||
tracks: [this.track],
|
||||
album: this.track.album,
|
||||
artist: this.track.artist,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFavorite() {
|
||||
return this.$store.getters["favorites/isFavorite"](this.track.id);
|
||||
},
|
||||
favoriteButton() {
|
||||
if (this.isFavorite) {
|
||||
return this.$pgettext(
|
||||
"Content/Track/Icon.Tooltip/Verb",
|
||||
"Remove from favorites"
|
||||
);
|
||||
} else {
|
||||
return this.$pgettext("Content/Track/*/Verb", "Add to favorites");
|
||||
}
|
||||
},
|
||||
trackDetailsButton() {
|
||||
if (this.track.artist.content_category === 'podcast') {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Episode details")
|
||||
} else {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details")
|
||||
}
|
||||
},
|
||||
albumDetailsButton() {
|
||||
if (this.track.artist.content_category === 'podcast') {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View series")
|
||||
} else {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album")
|
||||
}
|
||||
},
|
||||
artistDetailsButton() {
|
||||
if (this.track.artist.content_category === 'podcast') {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View channel")
|
||||
} else {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist")
|
||||
}
|
||||
},
|
||||
labels() {
|
||||
return {
|
||||
startRadio: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Play radio"
|
||||
),
|
||||
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"),
|
||||
addToQueue: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Add to queue"
|
||||
),
|
||||
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"),
|
||||
addToPlaylist: this.$pgettext(
|
||||
"Sidebar/Player/Icon.Tooltip/Verb",
|
||||
"Add to playlist…"
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.$emit("update:show", false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,154 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[
|
||||
{ active: currentTrack && track.id === currentTrack.id },
|
||||
'track-row podcast row',
|
||||
]"
|
||||
@mouseover="hover = track.id"
|
||||
@mouseleave="hover = null"
|
||||
@dblclick="activateTrack(track, index)"
|
||||
>
|
||||
<div
|
||||
v-if="showArt"
|
||||
class="image left floated column"
|
||||
role="button"
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
v-if="
|
||||
track.cover && track.cover.urls.original
|
||||
"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
v-else-if="
|
||||
defaultCover
|
||||
"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
defaultCover.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 class="content left floated column">
|
||||
<a
|
||||
class="podcast-episode-title ellipsis"
|
||||
@click.prevent.exact="activateTrack(track, index)">{{ track.title }}</a>
|
||||
<p class="podcast-episode-meta">{{ description.text }}</p>
|
||||
</div>
|
||||
<div v-if="displayActions" class="meta 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import PlayButton from "@/components/audio/PlayButton";
|
||||
import PlayOptions from "@/components/mixins/PlayOptions";
|
||||
|
||||
export default {
|
||||
mixins: [PlayOptions],
|
||||
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 },
|
||||
defaultCover: { type: Object, required: false },
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
hover: null,
|
||||
errors: null,
|
||||
description: null,
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData('tracks/' + this.track.id + '/' )
|
||||
},
|
||||
|
||||
components: {
|
||||
PlayIndicator,
|
||||
PlayButton,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchData (url) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
this.isLoading = true
|
||||
let self = this
|
||||
try {
|
||||
let channelsPromise = await axios.get(url)
|
||||
self.description = channelsPromise.data.description
|
||||
self.isLoading = false
|
||||
} catch(e) {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
}
|
||||
},
|
||||
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="ui hidden divider"></div>
|
||||
|
||||
<!-- Add a header if needed -->
|
||||
|
||||
<slot name="header"></slot>
|
||||
|
||||
<div>
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
||||
>
|
||||
<!-- For each item, build a row -->
|
||||
<podcast-row
|
||||
v-for="(track, index) in tracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
:index="index"
|
||||
:tracks="tracks"
|
||||
:display-actions="displayActions"
|
||||
:show-duration="showDuration"
|
||||
:is-podcast="isPodcast"
|
||||
></podcast-row>
|
||||
</div>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up">
|
||||
<pagination
|
||||
:total="total"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
v-on="$listeners">
|
||||
</pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
||||
>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
</div>
|
||||
|
||||
<!-- For each item, build a row -->
|
||||
|
||||
<track-mobile-row
|
||||
v-for="(track, index) in tracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
:index="index"
|
||||
:tracks="tracks"
|
||||
:show-position="showPosition"
|
||||
:show-art="showArt"
|
||||
:show-duration="showDuration"
|
||||
:is-artist="isArtist"
|
||||
:is-album="isAlbum"
|
||||
:is-podcast="isPodcast"
|
||||
></track-mobile-row>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below">
|
||||
<pagination
|
||||
v-if="paginateResults"
|
||||
:total="total"
|
||||
:current="page"
|
||||
:compact="true"
|
||||
v-on="$listeners"></pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "@/lodash";
|
||||
import TrackRow from "@/components/audio/track/Row";
|
||||
import PodcastRow from "@/components/audio/podcast/Row";
|
||||
import TrackMobileRow from "@/components/audio/track/MobileRow";
|
||||
import Pagination from "@/components/Pagination";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrackRow,
|
||||
TrackMobileRow,
|
||||
Pagination,
|
||||
PodcastRow,
|
||||
},
|
||||
|
||||
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 },
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
paginateResults: { type: Boolean, required: false, default: true},
|
||||
total: { type: Number, required: false},
|
||||
page: {type: Number, required: false, default: 1},
|
||||
paginateBy: {type: Number, required: false, default: 25},
|
||||
isPodcast: {type: Boolean, required: true},
|
||||
defaultCover: {type: Object, required: false},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
labels() {
|
||||
return {
|
||||
title: this.$pgettext("*/*/*/Noun", "Title"),
|
||||
album: this.$pgettext("*/*/*/Noun", "Album"),
|
||||
artist: this.$pgettext("*/*/*/Noun", "Artist"),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updatePage: function(page) {
|
||||
this.$emit('page-changed', page)
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,187 @@
|
|||
<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 class="track-meta mobile">
|
||||
{{ track.artist.name }} <span>·</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"
|
||||
: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,
|
||||
TrackFavoriteIcon,
|
||||
TrackModal,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing;
|
||||
},
|
||||
actionsButtonLabel () {
|
||||
return this.$pgettext('Content/Track/Icon.Tooltip/Verb', 'Show track actions')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,304 @@
|
|||
<template>
|
||||
<modal
|
||||
@update:show="$emit('update:show', $event)"
|
||||
:show="show"
|
||||
:scrolling="true"
|
||||
:additionalClasses="['scrolling-track-options']"
|
||||
>
|
||||
<div class="header">
|
||||
<div class="ui large centered rounded image">
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered 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 centered image"
|
||||
v-else-if="track.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
v-else-if="track.artist.cover"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.artist.cover.urls.medium_square_crop
|
||||
)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
class="ui centered image"
|
||||
v-else
|
||||
src="../../../assets/audio/default-cover.png"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="track-modal-title">{{ track.title }}</h3>
|
||||
<h4 class="track-modal-subtitle">{{ track.artist.name }}</h4>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="content">
|
||||
<div class="ui one column unstackable grid">
|
||||
<div
|
||||
class="row"
|
||||
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="favoriteButton"
|
||||
@click.stop="$store.dispatch('favorites/toggle', track.id)"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'heart',
|
||||
'favorite-icon',
|
||||
{ favorited: isFavorite },
|
||||
{ pink: isFavorite },
|
||||
'icon',
|
||||
'track-modal',
|
||||
'list-icon',
|
||||
]"
|
||||
/>
|
||||
<span class="track-modal list-item">{{ favoriteButton }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop.prevent="
|
||||
add();
|
||||
closeModal();
|
||||
"
|
||||
:aria-label="labels.addToQueue"
|
||||
>
|
||||
<i class="plus icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop.prevent="
|
||||
addNext(true);
|
||||
closeModal();
|
||||
"
|
||||
:aria-label="labels.playNext"
|
||||
>
|
||||
<i class="step forward icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop.prevent="
|
||||
$store.dispatch('radios/start', {
|
||||
type: 'similar',
|
||||
objectId: track.id,
|
||||
});
|
||||
closeModal();
|
||||
"
|
||||
:aria-label="labels.startRadio"
|
||||
>
|
||||
<i class="rss icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
@click.stop="$store.commit('playlists/chooseTrack', track)"
|
||||
:aria-label="labels.addToPlaylist"
|
||||
>
|
||||
<i class="list icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
labels.addToPlaylist
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div v-if="!isAlbum && track.album" class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="albumDetailsButton"
|
||||
@click.prevent.exact="
|
||||
$router.push({
|
||||
name: 'library.albums.detail',
|
||||
params: { id: track.album.id },
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="compact disc icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
albumDetailsButton
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isArtist" class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="artistDetailsButton"
|
||||
@click.prevent.exact="
|
||||
$router.push({
|
||||
name: 'library.artists.detail',
|
||||
params: { id: track.artist.id },
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="user icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
artistDetailsButton
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="column"
|
||||
role="button"
|
||||
:aria-label="trackDetailsButton"
|
||||
@click.prevent.exact="
|
||||
$router.push({
|
||||
name: 'library.tracks.detail',
|
||||
params: { id: track.id },
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="info icon track-modal list-icon" />
|
||||
<span class="track-modal list-item">{{
|
||||
trackDetailsButton
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div
|
||||
v-for="obj in getReportableObjs({
|
||||
track,
|
||||
album,
|
||||
artist,
|
||||
})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
class="row"
|
||||
:ref="`report${obj.target.type}${obj.target.id}`"
|
||||
:data-ref="`report${obj.target.type}${obj.target.id}`"
|
||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)"
|
||||
>
|
||||
<div class="column">
|
||||
<i class="share icon track-modal list-icon" /><span
|
||||
class="track-modal list-item"
|
||||
>{{ obj.label }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/semantic/Modal";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import ReportMixin from '@/components/mixins/Report'
|
||||
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||
|
||||
export default {
|
||||
mixins: [ReportMixin, PlayOptionsMixin],
|
||||
props: {
|
||||
show: { type: Boolean, required: true, default: false },
|
||||
track: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
components: {
|
||||
Modal,
|
||||
TrackFavoriteIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShowing: this.show,
|
||||
tracks: [this.track],
|
||||
album: this.track.album,
|
||||
artist: this.track.artist,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFavorite() {
|
||||
return this.$store.getters["favorites/isFavorite"](this.track.id);
|
||||
},
|
||||
favoriteButton() {
|
||||
if (this.isFavorite) {
|
||||
return this.$pgettext(
|
||||
"Content/Track/Icon.Tooltip/Verb",
|
||||
"Remove from favorites"
|
||||
);
|
||||
} else {
|
||||
return this.$pgettext("Content/Track/*/Verb", "Add to favorites");
|
||||
}
|
||||
},
|
||||
trackDetailsButton() {
|
||||
if (this.track.artist.content_category === 'podcast') {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Episode details")
|
||||
} else {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "Track details")
|
||||
}
|
||||
},
|
||||
albumDetailsButton() {
|
||||
if (this.track.artist.content_category === 'podcast') {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View series")
|
||||
} else {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View album")
|
||||
}
|
||||
},
|
||||
artistDetailsButton() {
|
||||
if (this.track.artist.content_category === 'podcast') {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View channel")
|
||||
} else {
|
||||
return this.$pgettext("*/Queue/Dropdown/Button/Label/Short", "View artist")
|
||||
}
|
||||
},
|
||||
labels() {
|
||||
return {
|
||||
startRadio: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Play radio"
|
||||
),
|
||||
playNow: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play now"),
|
||||
addToQueue: this.$pgettext(
|
||||
"*/Queue/Dropdown/Button/Title",
|
||||
"Add to queue"
|
||||
),
|
||||
playNext: this.$pgettext("*/Queue/Dropdown/Button/Title", "Play next"),
|
||||
addToPlaylist: this.$pgettext(
|
||||
"Sidebar/Player/Icon.Tooltip/Verb",
|
||||
"Add to playlist…"
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.$emit("update:show", false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -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>
|
|
@ -1,102 +1,225 @@
|
|||
<template>
|
||||
<tr>
|
||||
<td>
|
||||
<play-button :class="['basic', {vibrant: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']"
|
||||
:discrete="true"
|
||||
:is-playable="playable"
|
||||
:track="track"
|
||||
:track-index="trackIndex"
|
||||
:tracks="tracks"></play-button>
|
||||
</td>
|
||||
<td>
|
||||
<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">
|
||||
</td>
|
||||
<td colspan="6">
|
||||
<button class="track" @click.stop="playSong()">
|
||||
<template v-if="displayPosition && track.position">
|
||||
{{ track.position }}.
|
||||
</template>
|
||||
{{ track.title|truncate(40) }}
|
||||
<div
|
||||
:class="[
|
||||
{ active: currentTrack && track.id === currentTrack.id },
|
||||
'track-row row',
|
||||
]"
|
||||
@mouseover="hover = track.id"
|
||||
@mouseleave="hover = null"
|
||||
@dblclick="activateTrack(track, index)"
|
||||
>
|
||||
<div
|
||||
class="actions one wide left floated column"
|
||||
role="button"
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<play-indicator
|
||||
v-if="
|
||||
!$store.state.player.isLoadingAudio &&
|
||||
currentTrack &&
|
||||
isPlaying &&
|
||||
track.id === currentTrack.id &&
|
||||
!(track.id == hover)
|
||||
"
|
||||
>
|
||||
</play-indicator>
|
||||
<button
|
||||
v-else-if="
|
||||
currentTrack &&
|
||||
!isPlaying &&
|
||||
track.id === currentTrack.id &&
|
||||
!track.id == hover
|
||||
"
|
||||
class="ui really tiny basic icon button play-button paused"
|
||||
>
|
||||
<i class="pause icon" />
|
||||
</button>
|
||||
</td>
|
||||
<td colspan="4">
|
||||
<router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
|
||||
{{ track.artist.name|truncate(40) }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td colspan="4">
|
||||
<router-link v-if="track.album" class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
|
||||
{{ track.album.title|truncate(40) }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td colspan="4" v-if="track.uploads && track.uploads.length > 0">
|
||||
<human-duration :duration="track.uploads[0].duration"></human-duration>
|
||||
</td>
|
||||
<td colspan="4" v-else>
|
||||
<translate translate-context="*/*/*">N/A</translate>
|
||||
</td>
|
||||
<td colspan="2" v-if="displayActions" class="align right">
|
||||
<track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon>
|
||||
<track-playlist-icon
|
||||
<button
|
||||
v-else-if="
|
||||
currentTrack &&
|
||||
isPlaying &&
|
||||
track.id === currentTrack.id &&
|
||||
track.id == hover
|
||||
"
|
||||
class="ui really tiny basic icon button play-button"
|
||||
>
|
||||
<i class="pause icon" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="track.id == hover"
|
||||
class="ui really tiny basic icon button play-button"
|
||||
>
|
||||
<i class="play icon" />
|
||||
</button>
|
||||
<span class="track-position" v-else-if="showPosition">
|
||||
{{ prettyPosition(track.position) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="showArt"
|
||||
class="image left floated column"
|
||||
role="button"
|
||||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<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 && track.cover.urls.original
|
||||
"
|
||||
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 && track.artist.cover && track.album.cover.urls.original
|
||||
"
|
||||
v-lazy="
|
||||
$store.getters['instance/absoluteUrl'](
|
||||
track.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 class="content ellipsis left floated column">
|
||||
<a
|
||||
@click="activateTrack(track, index)"
|
||||
>
|
||||
{{ track.title }}
|
||||
</a>
|
||||
</div>
|
||||
<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 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
|
||||
v-if="$store.state.auth.authenticated"
|
||||
:track="track"></track-playlist-icon>
|
||||
class="meta right floated column"
|
||||
>
|
||||
<track-favorite-icon
|
||||
class="tiny"
|
||||
:border="false"
|
||||
:track="track"
|
||||
></track-favorite-icon>
|
||||
</div>
|
||||
<div v-if="showDuration" 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 v-if="displayActions" class="meta 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']"
|
||||
:dropdown-icon-classes="[
|
||||
'ellipsis',
|
||||
'vertical',
|
||||
'large really discrete',
|
||||
]"
|
||||
:track="track"
|
||||
></play-button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex"
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||
import PlayButton from "@/components/audio/PlayButton";
|
||||
import PlayOptions from "@/components/mixins/PlayOptions";
|
||||
|
||||
export default {
|
||||
mixins: [PlayOptions],
|
||||
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 },
|
||||
trackIndex: {type: Number, required: true},
|
||||
tracks: {type: Array, required: false},
|
||||
artist: {type: Object, required: false},
|
||||
displayPosition: {type: Boolean, default: false},
|
||||
displayActions: {type: Boolean, default: true},
|
||||
playable: {type: Boolean, required: false, default: false},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
hover: null,
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
PlayIndicator,
|
||||
TrackFavoriteIcon,
|
||||
TrackPlaylistIcon,
|
||||
PlayButton
|
||||
PlayButton,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
}),
|
||||
|
||||
isPlaying() {
|
||||
return this.$store.state.player.playing
|
||||
},
|
||||
albumArtist () {
|
||||
if (this.artist) {
|
||||
return this.artist
|
||||
} else {
|
||||
return this.track.album.artist
|
||||
}
|
||||
return this.$store.state.player.playing;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
playSong () {
|
||||
this.$store.dispatch('queue/clean')
|
||||
this.$store.dispatch('queue/appendMany', {
|
||||
tracks: this.tracks
|
||||
}).then(() => {
|
||||
this.$store.dispatch('queue/currentIndex', this.trackIndex)
|
||||
})
|
||||
|
||||
prettyPosition(position, size) {
|
||||
var s = String(position);
|
||||
while (s.length < (size || 2)) {
|
||||
s = "0" + s;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
...mapActions({
|
||||
resumePlayback: "player/resumePlayback",
|
||||
pausePlayback: "player/pausePlayback",
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,94 +1,209 @@
|
|||
<template>
|
||||
<div class="table-wrapper component-track-table">
|
||||
<inline-search-bar v-model="query" v-if="search" @search="additionalTracks = []; loadMore()"></inline-search-bar>
|
||||
<div>
|
||||
<!-- Show the search bar if search is true -->
|
||||
<inline-search-bar
|
||||
v-model="query"
|
||||
v-if="search"
|
||||
@search="
|
||||
additionalTracks = [];
|
||||
fetchData();
|
||||
"
|
||||
></inline-search-bar>
|
||||
<div class="ui hidden divider"></div>
|
||||
|
||||
<!-- Add a header if needed -->
|
||||
|
||||
<slot name="header"></slot>
|
||||
|
||||
<!-- Show a message if no tracks are available -->
|
||||
|
||||
<slot v-if="!isLoading && allTracks.length === 0" name="empty-state">
|
||||
<empty-state @refresh="fetchData" :refresh="true"></empty-state>
|
||||
<empty-state
|
||||
@refresh="fetchData('tracks/')"
|
||||
:refresh="true"
|
||||
></empty-state>
|
||||
</slot>
|
||||
<table v-else :class="['ui', 'compact', 'very', 'basic', {loading: isLoading}, 'unstackable', 'table']">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span class="visually-hidden"><translate translate-context="*/*/*/Noun">Play</translate></span></th>
|
||||
<th><span class="visually-hidden"><translate translate-context="*/*/*/Noun">Track Art</translate></span></th>
|
||||
<th colspan="6"><translate translate-context="*/*/*/Noun">Title</translate></th>
|
||||
<th colspan="4"><translate translate-context="*/*/*/Noun">Artist</translate></th>
|
||||
<th colspan="4"><translate translate-context="*/*/*">Album</translate></th>
|
||||
<th colspan="4"><translate translate-context="Content/*/*">Duration</translate></th>
|
||||
<th colspan="2" v-if="displayActions"><span class="visually hidden"><translate translate-context="*/*/*/Noun">Actions</translate></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div v-else>
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
||||
>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
</div>
|
||||
<div class="track-table row">
|
||||
<div v-if="showPosition" class="actions left floated column">
|
||||
<i class="hashtag icon"></i>
|
||||
</div>
|
||||
<div v-else class="actions left floated column"></div>
|
||||
<div v-if="showArt" class="image left floated column"></div>
|
||||
<div class="content ellipsis left floated column">
|
||||
<b>{{ labels.title }}</b>
|
||||
</div>
|
||||
<div v-if="showAlbum" class="content ellipsisleft floated column">
|
||||
<b>{{ labels.album }}</b>
|
||||
</div>
|
||||
<div v-if="showArtist" class="content ellipsis left floated column">
|
||||
<b>{{ labels.artist }}</b>
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.auth.authenticated"
|
||||
class="meta right floated column"
|
||||
></div>
|
||||
<div v-if="showDuration" class="meta right floated column">
|
||||
<i class="clock outline icon" style="padding: 0.5rem" />
|
||||
</div>
|
||||
<div v-if="displayActions" class="meta right floated column"></div>
|
||||
</div>
|
||||
|
||||
<!-- For each item, build a row -->
|
||||
|
||||
<track-row
|
||||
:playable="playable"
|
||||
:display-position="displayPosition"
|
||||
:display-actions="displayActions"
|
||||
v-for="(track, index) in allTracks"
|
||||
:track="track"
|
||||
:track-index="index"
|
||||
:key="track.id"
|
||||
:index="index"
|
||||
:tracks="allTracks"
|
||||
:artist="artist"
|
||||
:key="index + '-' + track.id"
|
||||
v-for="(track, index) in allTracks"></track-row>
|
||||
</tbody>
|
||||
</table>
|
||||
<button :class="['ui', {loading: isLoading}, 'button']" v-if="loadMoreUrl" @click="loadMore(loadMoreUrl)" :disabled="isLoading">
|
||||
<translate translate-context="Content/*/Button.Label">Load more…</translate>
|
||||
</button>
|
||||
:show-album="showAlbum"
|
||||
:show-artist="showArtist"
|
||||
:show-position="showPosition"
|
||||
:show-art="showArt"
|
||||
:display-actions="displayActions"
|
||||
:show-duration="showDuration"
|
||||
:is-podcast="isPodcast"
|
||||
></track-row>
|
||||
</div>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up">
|
||||
<pagination
|
||||
:total="total"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
v-on="$listeners">
|
||||
</pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
||||
>
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
</div>
|
||||
|
||||
<!-- For each item, build a row -->
|
||||
|
||||
<track-mobile-row
|
||||
v-for="(track, index) in allTracks"
|
||||
:track="track"
|
||||
:key="track.id"
|
||||
:index="index"
|
||||
:tracks="allTracks"
|
||||
:show-position="showPosition"
|
||||
:show-art="showArt"
|
||||
:show-duration="showDuration"
|
||||
:is-artist="isArtist"
|
||||
:is-album="isAlbum"
|
||||
:is-podcast="isPodcast"
|
||||
></track-mobile-row>
|
||||
<div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below">
|
||||
<pagination
|
||||
v-if="paginateResults"
|
||||
:total="total"
|
||||
:current="page"
|
||||
:compact="true"
|
||||
v-on="$listeners"></pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
import TrackRow from '@/components/audio/track/Row'
|
||||
import Modal from '@/components/semantic/Modal'
|
||||
import _ from "@/lodash";
|
||||
import axios from "axios";
|
||||
import TrackRow from "@/components/audio/track/Row";
|
||||
import TrackMobileRow from "@/components/audio/track/MobileRow";
|
||||
import Pagination from "@/components/Pagination";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tracks: {type: Array, required: false},
|
||||
playable: {type: Boolean, required: false, default: false},
|
||||
search: {type: Boolean, required: false, default: false},
|
||||
nextUrl: {type: String, required: false, default: null},
|
||||
artist: {type: Object, required: false},
|
||||
filters: {type: Object, required: false, default: () => { return {}}},
|
||||
displayPosition: {type: Boolean, default: false},
|
||||
displayActions: {type: Boolean, default: true},
|
||||
},
|
||||
components: {
|
||||
Modal,
|
||||
TrackRow
|
||||
TrackRow,
|
||||
TrackMobileRow,
|
||||
Pagination,
|
||||
},
|
||||
|
||||
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 },
|
||||
isArtist: { type: Boolean, required: false, default: false },
|
||||
isAlbum: { type: Boolean, required: false, default: false },
|
||||
isPodcast: { type: Boolean, required: false, default: false },
|
||||
paginateResults: { type: Boolean, required: false, default: true},
|
||||
total: { type: Number, required: false},
|
||||
page: {type: Number, required: false, default: 1},
|
||||
paginateBy: {type: Number, required: false, default: 25}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
fetchDataUrl: this.nextUrl,
|
||||
isLoading: false,
|
||||
additionalTracks: [],
|
||||
query: "",
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
allTracks() {
|
||||
return (this.tracks || []).concat(this.additionalTracks);
|
||||
},
|
||||
|
||||
labels() {
|
||||
return {
|
||||
title: this.$pgettext("*/*/*/Noun", "Title"),
|
||||
album: this.$pgettext("*/*/*/Noun", "Album"),
|
||||
artist: this.$pgettext("*/*/*/Noun", "Artist"),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async fetchData(url) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
let self = this;
|
||||
let params = _.clone(this.filters);
|
||||
let tracksPromise = axios.get(url, { params: params })
|
||||
params.page_size = this.limit;
|
||||
params.page = this.page;
|
||||
params.include_channels = true;
|
||||
try {
|
||||
await tracksPromise
|
||||
self.nextPage = tracksPromise.data.next;
|
||||
self.objects = tracksPromise.data.results;
|
||||
self.count = tracksPromise.data.count;
|
||||
self.$emit("fetched", tracksPromise.data);
|
||||
self.isLoading = false;
|
||||
} catch(e) {
|
||||
self.isLoading = false;
|
||||
self.errors = error.backendErrors;
|
||||
}
|
||||
},
|
||||
updatePage: function(page) {
|
||||
this.$emit('page-changed', page)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.tracks) {
|
||||
this.loadMore('tracks/')
|
||||
this.fetchData("tracks/");
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loadMoreUrl: this.nextUrl,
|
||||
isLoading: false,
|
||||
additionalTracks: [],
|
||||
query: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allTracks () {
|
||||
return (this.tracks || []).concat(this.additionalTracks)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadMore (url) {
|
||||
url = url || 'tracks/'
|
||||
let self = this
|
||||
let params = {q: this.query, ...this.filters}
|
||||
self.isLoading = true
|
||||
axios.get(url, {params}).then((response) => {
|
||||
self.additionalTracks = self.additionalTracks.concat(response.data.results)
|
||||
self.loadMoreUrl = response.data.next
|
||||
self.isLoading = false
|
||||
}, (error) => {
|
||||
self.isLoading = false
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -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>
|
||||
<track-table :show-artist="true" :show-album="true" v-if="results" :tracks="results.results"></track-table>
|
||||
<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 TrackTable from '@/components/audio/track/Table'
|
||||
const FAVORITES_URL = "tracks/"
|
||||
|
||||
export default {
|
||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||
components: {
|
||||
TrackTable,
|
||||
RadioButton,
|
||||
Pagination
|
||||
Pagination,
|
||||
TrackTable
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -84,7 +84,8 @@
|
|||
:is-album="isAlbum"
|
||||
:is-serie="isSerie"
|
||||
:is-channel="isChannel"
|
||||
:artist="artist"></album-dropdown>
|
||||
:artist="artist"
|
||||
></album-dropdown>
|
||||
<div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local">
|
||||
<div class="ui small hidden divider"></div>
|
||||
<div class="ui divider"></div>
|
||||
|
@ -128,7 +129,6 @@
|
|||
<script>
|
||||
import axios from "axios"
|
||||
import lodash from "@/lodash"
|
||||
import backend from "@/audio/backend"
|
||||
import PlayButton from "@/components/audio/PlayButton"
|
||||
import TagsList from "@/components/tags/List"
|
||||
import ArtistLabel from '@/components/audio/ArtistLabel'
|
||||
|
@ -172,7 +172,7 @@ export default {
|
|||
methods: {
|
||||
async fetchData() {
|
||||
this.isLoading = true
|
||||
let tracksResponse = axios.get(`tracks/`, {params: {ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page:this.page, include_channels: 'true'}})
|
||||
let tracksResponse = axios.get(`tracks/`, {params: {ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page:this.page, include_channels: 'true', playable: 'true'}})
|
||||
let albumResponse = await axios.get(`albums/${this.id}/`, {params: {refresh: 'true'}})
|
||||
let artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
|
||||
this.artist = artistResponse.data
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<translate key="1" v-if="isSerie" translate-context="Content/Channels/*">Episodes</translate>
|
||||
<translate key="2" v-else translate-context="*/*/*">Tracks</translate>
|
||||
</h2>
|
||||
<channel-entries v-if="artist.channel && isSerie" :limit="50" :filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}">
|
||||
<channel-entries v-if="artist.channel && isSerie" :is-podcast="isSerie" :limit="50" :filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}">
|
||||
</channel-entries>
|
||||
<template v-else-if="discs && discs.length > 1">
|
||||
<div v-for="tracks in discs" :key="tracks.disc_number">
|
||||
|
@ -15,21 +15,36 @@
|
|||
:translate-params="{number: tracks[0].disc_number}"
|
||||
translate-context="Content/Album/"
|
||||
>Volume %{ number }</translate>
|
||||
<album-entries :tracks="tracks"></album-entries>
|
||||
<track-table
|
||||
:is-album="true"
|
||||
:tracks="object.tracks"
|
||||
:show-position="true"
|
||||
:show-art="false"
|
||||
:show-album="false"
|
||||
:show-artist="false"
|
||||
:paginate-results="true"
|
||||
:total="totalTracks"
|
||||
:paginate-by="paginateBy"
|
||||
:page="page"
|
||||
@page-changed="updatePage">
|
||||
</track-table>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<album-entries :tracks="object.tracks"></album-entries>
|
||||
</template>
|
||||
<div class="ui center aligned basic segment">
|
||||
<pagination
|
||||
v-if="!isSerie && object.tracks && totalTracks > paginateBy"
|
||||
@page-changed="updatePage"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
<track-table
|
||||
:is-album="true"
|
||||
:tracks="object.tracks"
|
||||
:show-position="true"
|
||||
:show-art="false"
|
||||
:show-album="false"
|
||||
:show-artist="false"
|
||||
:paginate-results="true"
|
||||
:total="totalTracks"
|
||||
></pagination>
|
||||
</div>
|
||||
:paginate-by="paginateBy"
|
||||
:page="page"
|
||||
@page-changed="updatePage">
|
||||
</track-table>
|
||||
</template>
|
||||
<template v-if="!artist.channel && !isSerie">
|
||||
<h2>
|
||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
||||
|
@ -44,25 +59,17 @@
|
|||
<script>
|
||||
|
||||
import time from "@/utils/time"
|
||||
import axios from "axios"
|
||||
import url from "@/utils/url"
|
||||
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 Pagination from "@/components/Pagination"
|
||||
import PaginationMixin from "@/components/mixins/Pagination"
|
||||
import TrackTable from '@/components/audio/track/Table'
|
||||
import PlayButton from "@/components/audio/PlayButton"
|
||||
|
||||
export default {
|
||||
props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"],
|
||||
components: {
|
||||
LibraryWidget,
|
||||
AlbumEntries,
|
||||
ChannelEntries,
|
||||
TrackTable,
|
||||
Pagination,
|
||||
ChannelEntries,
|
||||
PlayButton
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,6 +14,16 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
|
||||
<track-table :is-artist="true" :show-position="false" :track-only="true" :tracks="tracks.slice(0,5)">
|
||||
<template slot="header">
|
||||
<h2>
|
||||
<translate translate-context="Content/Artist/Title">New tracks by this artist</translate>
|
||||
</h2>
|
||||
<div class="ui hidden divider"></div>
|
||||
</template>
|
||||
</track-table>
|
||||
</section>
|
||||
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||
</section>
|
||||
|
@ -29,12 +39,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="true" :tracks="tracks" :next-url="nextTracksUrl"></track-table>
|
||||
</section>
|
||||
<section class="ui vertical stripe segment">
|
||||
<h2>
|
||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
||||
|
|
|
@ -85,7 +85,13 @@
|
|||
translate-context="Content/Radio/Table.Paragraph/Short">
|
||||
%{ count } track matching combined filters
|
||||
</h3>
|
||||
<track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample" :playable="true"></track-table>
|
||||
<track-table
|
||||
v-if="checkResult.candidates.sample"
|
||||
:tracks="checkResult.candidates.sample"
|
||||
:playable="true"
|
||||
:show-position="false"
|
||||
:show-duration="false"
|
||||
:display-actions="false"></track-table>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
<input :id="f.name" v-if="f.type === 'list' && config[f.name]" :value="config[f.name].join(',')" type="hidden">
|
||||
<div v-if="config[f.name]" class="ui menu">
|
||||
<div
|
||||
v-if="f.type === 'list'"
|
||||
v-for="(v, index) in config[f.name]"
|
||||
class="ui item"
|
||||
:data-value="v">
|
||||
:data-value="v"
|
||||
:key="v">
|
||||
<template v-if="config.names">
|
||||
{{ config.names[index] }}
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
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})
|
||||
},
|
||||
activateTrack(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);
|
||||
}
|
||||
},
|
||||
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()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div :class="['ui', {'active': show}, {'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal']">
|
||||
<i class="close inside icon"></i>
|
||||
<div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])">
|
||||
<i tabindex=0 class="close inside icon"></i>
|
||||
<slot v-if="show">
|
||||
|
||||
</slot>
|
||||
|
@ -15,6 +15,8 @@ export default {
|
|||
props: {
|
||||
show: {type: Boolean, required: true},
|
||||
fullscreen: {type: Boolean, default: true},
|
||||
scrolling: {type: Boolean, required: false, default: false},
|
||||
additionalClasses: {type: Array, required: false, default: () => []}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -61,6 +63,7 @@ export default {
|
|||
this.control.modal('show')
|
||||
this.focusTrap.activate()
|
||||
this.focusTrap.unpause()
|
||||
document.body.classList.add('scrolling')
|
||||
} else {
|
||||
if (this.control) {
|
||||
this.$emit('hide')
|
||||
|
@ -68,6 +71,7 @@ export default {
|
|||
this.control.remove()
|
||||
this.focusTrap.deactivate()
|
||||
this.focusTrap.pause()
|
||||
document.body.classList.remove('scrolling')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ $bottom-player-height: 4rem;
|
|||
@import "./components/_content_form.scss";
|
||||
@import "./components/_copy_input.scss";
|
||||
@import "./components/_empty_state.scss";
|
||||
@import "./components/_favorite.scss";
|
||||
@import "./components/_form.scss";
|
||||
@import "./components/_file_upload.scss";
|
||||
@import "./components/_fs_browser.scss";
|
||||
|
@ -34,6 +35,7 @@ $bottom-player-height: 4rem;
|
|||
@import "./components/_pagination.scss";
|
||||
@import "./components/_placeholder.scss";
|
||||
@import "./components/_play_button.scss";
|
||||
@import "./components/_play_indicator.scss";
|
||||
@import "./components/_player.scss";
|
||||
@import "./components/_playlist_editor.scss";
|
||||
@import "./components/_queue.scss";
|
||||
|
|
|
@ -117,3 +117,5 @@ $card-box-shadow: 0 1px 3px 0 #D4D4D5, 0 0 0 1px #D4D4D5 !default;
|
|||
|
||||
$dimmer-background: rgba(255, 255, 255, 0.85) !default;
|
||||
$dimmer-color: var(--text-color) !default;
|
||||
|
||||
$border-color: rgba(34, 36, 38, 0.15) !default;
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
.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;
|
||||
}
|
|
@ -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; }
|
|
@ -1,19 +1,251 @@
|
|||
.component-track-table {
|
||||
pre {
|
||||
overflow-x: scroll;
|
||||
.track-table {
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 3.5rem;
|
||||
}
|
||||
&.table-wrapper {
|
||||
overflow: visible;
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
tr:not(:hover) {
|
||||
.favorite-icon:not(.favorited),
|
||||
.playlist-icon {
|
||||
visibility: hidden;
|
||||
.row:not(.mobile) {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.track {
|
||||
.track-row.row {
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.track-row,
|
||||
.track-table.row {
|
||||
.ui.really.tiny.button.play-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
.ui.icon.really.tiny.button.play-button.paused {
|
||||
color: var(--main-color);
|
||||
visibility: visible;
|
||||
display: contents;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.ui.floating.dropdown {
|
||||
visibility: hidden;
|
||||
}
|
||||
.ui.favorite-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
.ui.favorite-icon.pink {
|
||||
visibility: visible;
|
||||
}
|
||||
.actions {
|
||||
display: block;
|
||||
line-height: 2;
|
||||
max-width: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
.actions.left.floated.column {
|
||||
width: 3% !important;
|
||||
}
|
||||
.meta.right.floated.column:not(.mobile) {
|
||||
width: 45px;
|
||||
}
|
||||
.content,
|
||||
.meta,
|
||||
.image {
|
||||
user-select: none;
|
||||
}
|
||||
.helper {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ui.artist-track.mini.image {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.image.left.floated.column {
|
||||
width: 51px;
|
||||
}
|
||||
}
|
||||
|
||||
.track-row {
|
||||
&.active {
|
||||
background: rgba(155, 155, 155, 0.2);
|
||||
}
|
||||
&:hover:not(.mobile) {
|
||||
background: rgba(155, 155, 155, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.track-table.mobile,
|
||||
.track-row.mobile {
|
||||
height: 75px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.modal-button,
|
||||
.meta {
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
margin: 10% 0;
|
||||
width: 10vw !important;
|
||||
}
|
||||
.meta.with-art {
|
||||
align-items: right;
|
||||
}
|
||||
.modal-button.with-art {
|
||||
align-items: center;
|
||||
}
|
||||
.actions div {
|
||||
height: 75px;
|
||||
line-height: 75px;
|
||||
width: 10vw;
|
||||
}
|
||||
.ui.favorite-icon.button {
|
||||
pointer-events: none;
|
||||
}
|
||||
.ui.artist-track.mini.image {
|
||||
width: 45px;
|
||||
}
|
||||
.track-title.mobile {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
.track-title.mobile.play-indicator {
|
||||
color: var(--vibrant-color);
|
||||
}
|
||||
.image.left.floated.column {
|
||||
width: 61px;
|
||||
}
|
||||
}
|
||||
|
||||
.track-row:hover:not(.mobile) {
|
||||
cursor: pointer;
|
||||
|
||||
// explicitly style the button as if it was hovered itself
|
||||
|
||||
.ui.icon.really.tiny.button.play-button {
|
||||
color: var(--main-color);
|
||||
visibility: visible;
|
||||
display: contents;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.ui.floating.dropdown {
|
||||
visibility: visible;
|
||||
}
|
||||
.ui.favorite-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.track-row,
|
||||
.track-table {
|
||||
padding: 0.5em;
|
||||
> div:not(.mobile) {
|
||||
padding: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
.favorite-icon.tiny.button {
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 0.5em;
|
||||
transition: all ease-in-out;
|
||||
}
|
||||
.mobile {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.track-position {
|
||||
cursor: pointer;
|
||||
display: contents;
|
||||
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;
|
||||
user-select: none;
|
||||
|
||||
.mobile span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.ui.overlay.fullscreen.modal {
|
||||
.track-modal-title,
|
||||
.track-modal-subtitle {
|
||||
margin: 0.1em;
|
||||
}
|
||||
.track-modal-subtitle {
|
||||
font-weight: normal;
|
||||
}
|
||||
.track-modal.list-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.track-modal.list-item {
|
||||
font-weight: bold;
|
||||
font-size: large;
|
||||
}
|
||||
}
|
||||
|
||||
.scrolling.dimmable.dimmed {
|
||||
> .dimmer {
|
||||
overflow: auto;
|
||||
--webkit-overflow-scrolling: touch;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.track-table.podcast.row,
|
||||
.track-row.podcast.row {
|
||||
height: 20vh;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
.ui.artist-track.mini.image {
|
||||
height: 15vh;
|
||||
width: auto;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
.image.left.floated.column {
|
||||
width: (15vh);
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content.left.floated.column {
|
||||
margin-left: 26px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
|
||||
.podcast-episode-title {
|
||||
font-weight: bold;
|
||||
font-size: medium;
|
||||
display: block;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@ a {
|
|||
}
|
||||
}
|
||||
.tablet-and-below {
|
||||
@include media(">=desktop") {
|
||||
@include media(">=tablet") {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
@ -104,3 +104,10 @@ span.diff.removed {
|
|||
.really.discrete {
|
||||
color: var(--really-discrete-text-color);
|
||||
}
|
||||
|
||||
.ui.inverted.dimmer {
|
||||
background-color: var(--dimmer-background);
|
||||
> .ui.dimmer {
|
||||
color: var(--dimmer-color);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<section>
|
||||
<channel-entries :limit="25" :filters="{channel: object.uuid, ordering: 'creation_date'}">
|
||||
<channel-entries :default-cover="object.artist.cover" :is-podcast="object.artist.content_category === 'podcast'" :limit="25" :filters="{channel: object.uuid, ordering: 'creation_date'}">
|
||||
</channel-entries>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
:can-update="false"></rendered-description>
|
||||
<div class="ui hidden divider"></div>
|
||||
</div>
|
||||
<channel-entries :key="String(episodesKey) + 'entries'" :default-cover='object.artist.cover' :limit='25' :filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}">
|
||||
<channel-entries :is-podcast="isPodcast" :key="String(episodesKey) + 'entries'" :default-cover='object.artist.cover' :limit='25' :filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}">
|
||||
<h2 class="ui header">
|
||||
<translate key="1" v-if="isPodcast" translate-context="Content/Channel/Paragraph">Latest episodes</translate>
|
||||
<translate key="2" v-else translate-context="Content/Channel/Paragraph">Latest tracks</translate>
|
||||
|
|
|
@ -42,7 +42,7 @@ module.exports = {
|
|||
appleMobileWebAppStatusBarStyle: 'black',
|
||||
workboxPluginMode: 'InjectManifest',
|
||||
manifestOptions: {
|
||||
display: 'minimal-ui',
|
||||
display: 'standalone',
|
||||
start_url: '.',
|
||||
description: 'A social platform to enjoy and share music',
|
||||
scope: "/",
|
||||
|
|
Loading…
Reference in New Issue