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)"),
|
("color", "var(--button-basic-hover-color)"),
|
||||||
("box-shadow", "var(--button-basic-hover-box-shadow)"),
|
("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": {
|
"card": {
|
||||||
"skip": [
|
"skip": [
|
||||||
|
|
|
@ -8,15 +8,13 @@
|
||||||
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
|
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
|
||||||
<template v-if="!compact">
|
<template v-if="!compact">
|
||||||
<a href
|
<a href
|
||||||
v-if="page !== 'skip'"
|
|
||||||
v-for="page in pages"
|
v-for="page in pages"
|
||||||
|
:key="page"
|
||||||
@click.prevent.stop="selectPage(page)"
|
@click.prevent.stop="selectPage(page)"
|
||||||
:class="[{'active': page === current}, 'item']">
|
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']">
|
||||||
{{ page }}
|
<span v-if="page !== 'skip'">{{ page }}</span>
|
||||||
|
<span v-else>…</span>
|
||||||
</a>
|
</a>
|
||||||
<div v-else class="disabled item">
|
|
||||||
…
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<a href
|
<a href
|
||||||
:disabled="current + 1 > maxPage"
|
: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 v-if="isLoading" class="ui inverted active dimmer">
|
||||||
<div class="ui loader"></div>
|
<div class="ui loader"></div>
|
||||||
</div>
|
</div>
|
||||||
<channel-entry-card v-for="entry in objects" :default-cover="defaultCover" :entry="entry" :key="entry.id" />
|
<podcast-table
|
||||||
<template v-if="count > limit">
|
v-if="isPodcast"
|
||||||
<div class="ui hidden divider"></div>
|
:default-cover="defaultCover"
|
||||||
<div class = "ui center aligned basic segment">
|
:is-podcast="isPodcast"
|
||||||
<pagination
|
:show-art="true"
|
||||||
@page-changed="updatePage"
|
:show-position="false"
|
||||||
:current="page"
|
:tracks="objects"
|
||||||
:paginate-by="limit"
|
:show-artist="false"
|
||||||
:total="count"
|
:show-album="false"
|
||||||
></pagination>
|
:paginate-results="true"
|
||||||
</div>
|
:total="count"
|
||||||
</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">
|
<template v-if="!isLoading && objects.length === 0">
|
||||||
<empty-state @refresh="fetchData('tracks/')" :refresh="true">
|
<empty-state @refresh="fetchData('tracks/')" :refresh="true">
|
||||||
<p>
|
<p>
|
||||||
|
@ -30,19 +46,19 @@
|
||||||
<script>
|
<script>
|
||||||
import _ from '@/lodash'
|
import _ from '@/lodash'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ChannelEntryCard from '@/components/audio/ChannelEntryCard'
|
import PodcastTable from '@/components/audio/podcast/Table'
|
||||||
import Pagination from "@/components/Pagination"
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
import PaginationMixin from "@/components/mixins/Pagination"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
filters: {type: Object, required: true},
|
filters: {type: Object, required: true},
|
||||||
limit: {type: Number, default: 10},
|
limit: {type: Number, default: 10},
|
||||||
defaultCover: {type: Object},
|
defaultCover: {type: Object},
|
||||||
|
isPodcast: {type: Boolean, required: true},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ChannelEntryCard,
|
PodcastTable,
|
||||||
Pagination
|
TrackTable,
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -58,7 +74,7 @@ export default {
|
||||||
this.fetchData('tracks/')
|
this.fetchData('tracks/')
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData (url) {
|
async fetchData (url) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -68,16 +84,17 @@ export default {
|
||||||
params.page_size = this.limit
|
params.page_size = this.limit
|
||||||
params.page = this.page
|
params.page = this.page
|
||||||
params.include_channels = true
|
params.include_channels = true
|
||||||
axios.get(url, {params: params}).then((response) => {
|
try {
|
||||||
self.nextPage = response.data.next
|
let channelsPromise = await axios.get(url, {params: params})
|
||||||
self.isLoading = false
|
self.nextPage = channelsPromise.data.next
|
||||||
self.objects = response.data.results
|
self.objects = channelsPromise.data.results
|
||||||
self.count = response.data.count
|
self.count = channelsPromise.data.count
|
||||||
self.$emit('fetched', response.data)
|
self.$emit('fetched', channelsPromise.data)
|
||||||
}, error => {
|
self.isLoading = false
|
||||||
|
} catch(e) {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.errors = error.backendErrors
|
self.errors = error.backendErrors
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
updatePage: function(page) {
|
updatePage: function(page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
:disabled="!playable"
|
:disabled="!playable"
|
||||||
:aria-label="labels.replacePlay"
|
:aria-label="labels.replacePlay"
|
||||||
:class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])">
|
: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>
|
<template v-if="!discrete && !iconOnly"> <slot><translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate></slot></template>
|
||||||
</button>
|
</button>
|
||||||
<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">
|
<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>
|
<i class="feed icon"></i><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate>
|
||||||
</button>
|
</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}/`)">
|
<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>
|
</button>
|
||||||
<div class="divider"></div>
|
<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">
|
<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 jQuery from 'jquery'
|
||||||
|
|
||||||
import ReportMixin from '@/components/mixins/Report'
|
import ReportMixin from '@/components/mixins/Report'
|
||||||
|
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [ReportMixin],
|
mixins: [ReportMixin, PlayOptionsMixin],
|
||||||
props: {
|
props: {
|
||||||
// we can either have a single or multiple tracks to play when clicked
|
// we can either have a single or multiple tracks to play when clicked
|
||||||
tracks: {type: Array, required: false},
|
tracks: {type: Array, required: false},
|
||||||
|
@ -71,7 +79,9 @@ export default {
|
||||||
album: {type: Object, required: false},
|
album: {type: Object, required: false},
|
||||||
library: {type: Object, required: false},
|
library: {type: Object, required: false},
|
||||||
channel: {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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -100,6 +110,7 @@ export default {
|
||||||
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
|
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
|
||||||
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
|
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
|
||||||
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
|
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
|
||||||
|
addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…'),
|
||||||
replacePlay,
|
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: {
|
watch: {
|
||||||
clicked () {
|
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>
|
<template>
|
||||||
<tr>
|
<div
|
||||||
<td>
|
:class="[
|
||||||
<play-button :class="['basic', {vibrant: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']"
|
{ active: currentTrack && track.id === currentTrack.id },
|
||||||
:discrete="true"
|
'track-row row',
|
||||||
:is-playable="playable"
|
]"
|
||||||
:track="track"
|
@mouseover="hover = track.id"
|
||||||
:track-index="trackIndex"
|
@mouseleave="hover = null"
|
||||||
:tracks="tracks"></play-button>
|
@dblclick="activateTrack(track, index)"
|
||||||
</td>
|
>
|
||||||
<td>
|
<div
|
||||||
<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)">
|
class="actions one wide left floated column"
|
||||||
<img alt="" class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
role="button"
|
||||||
</td>
|
@click.prevent.exact="activateTrack(track, index)"
|
||||||
<td colspan="6">
|
>
|
||||||
<button class="track" @click.stop="playSong()">
|
<play-indicator
|
||||||
<template v-if="displayPosition && track.position">
|
v-if="
|
||||||
{{ track.position }}.
|
!$store.state.player.isLoadingAudio &&
|
||||||
</template>
|
currentTrack &&
|
||||||
{{ track.title|truncate(40) }}
|
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>
|
</button>
|
||||||
</td>
|
<button
|
||||||
<td colspan="4">
|
v-else-if="
|
||||||
<router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
|
currentTrack &&
|
||||||
{{ track.artist.name|truncate(40) }}
|
isPlaying &&
|
||||||
</router-link>
|
track.id === currentTrack.id &&
|
||||||
</td>
|
track.id == hover
|
||||||
<td colspan="4">
|
"
|
||||||
<router-link v-if="track.album" class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
|
class="ui really tiny basic icon button play-button"
|
||||||
{{ track.album.title|truncate(40) }}
|
>
|
||||||
</router-link>
|
<i class="pause icon" />
|
||||||
</td>
|
</button>
|
||||||
<td colspan="4" v-if="track.uploads && track.uploads.length > 0">
|
<button
|
||||||
<human-duration :duration="track.uploads[0].duration"></human-duration>
|
v-else-if="track.id == hover"
|
||||||
</td>
|
class="ui really tiny basic icon button play-button"
|
||||||
<td colspan="4" v-else>
|
>
|
||||||
<translate translate-context="*/*/*">N/A</translate>
|
<i class="play icon" />
|
||||||
</td>
|
</button>
|
||||||
<td colspan="2" v-if="displayActions" class="align right">
|
<span class="track-position" v-else-if="showPosition">
|
||||||
<track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon>
|
{{ prettyPosition(track.position) }}
|
||||||
<track-playlist-icon
|
</span>
|
||||||
v-if="$store.state.auth.authenticated"
|
</div>
|
||||||
:track="track"></track-playlist-icon>
|
<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"
|
||||||
|
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
|
<play-button
|
||||||
|
id="playmenu"
|
||||||
class="play-button basic icon"
|
class="play-button basic icon"
|
||||||
:dropdown-only="true"
|
:dropdown-only="true"
|
||||||
:is-playable="track.is_playable"
|
:is-playable="track.is_playable"
|
||||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
|
:dropdown-icon-classes="[
|
||||||
|
'ellipsis',
|
||||||
|
'vertical',
|
||||||
|
'large really discrete',
|
||||||
|
]"
|
||||||
:track="track"
|
:track="track"
|
||||||
></play-button>
|
></play-button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from "vuex"
|
import PlayIndicator from "@/components/audio/track/PlayIndicator";
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import { mapActions, mapGetters } from "vuex";
|
||||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
|
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from "@/components/audio/PlayButton";
|
||||||
|
import PlayOptions from "@/components/mixins/PlayOptions";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [PlayOptions],
|
||||||
props: {
|
props: {
|
||||||
track: {type: Object, required: true},
|
tracks: Array,
|
||||||
trackIndex: {type: Number, required: true},
|
showAlbum: { type: Boolean, required: false, default: true },
|
||||||
tracks: {type: Array, required: false},
|
showArtist: { type: Boolean, required: false, default: true },
|
||||||
artist: {type: Object, required: false},
|
showPosition: { type: Boolean, required: false, default: false },
|
||||||
displayPosition: {type: Boolean, default: false},
|
showArt: { type: Boolean, required: false, default: true },
|
||||||
displayActions: {type: Boolean, default: true},
|
search: { type: Boolean, required: false, default: false },
|
||||||
playable: {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 },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hover: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
PlayIndicator,
|
||||||
TrackFavoriteIcon,
|
TrackFavoriteIcon,
|
||||||
TrackPlaylistIcon,
|
PlayButton,
|
||||||
PlayButton
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: "queue/currentTrack",
|
currentTrack: "queue/currentTrack",
|
||||||
}),
|
}),
|
||||||
isPlaying () {
|
|
||||||
return this.$store.state.player.playing
|
isPlaying() {
|
||||||
},
|
return this.$store.state.player.playing;
|
||||||
albumArtist () {
|
|
||||||
if (this.artist) {
|
|
||||||
return this.artist
|
|
||||||
} else {
|
|
||||||
return this.track.album.artist
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
playSong () {
|
|
||||||
this.$store.dispatch('queue/clean')
|
prettyPosition(position, size) {
|
||||||
this.$store.dispatch('queue/appendMany', {
|
var s = String(position);
|
||||||
tracks: this.tracks
|
while (s.length < (size || 2)) {
|
||||||
}).then(() => {
|
s = "0" + s;
|
||||||
this.$store.dispatch('queue/currentIndex', this.trackIndex)
|
}
|
||||||
})
|
return s;
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
...mapActions({
|
||||||
|
resumePlayback: "player/resumePlayback",
|
||||||
|
pausePlayback: "player/pausePlayback",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,94 +1,209 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="table-wrapper component-track-table">
|
<div>
|
||||||
<inline-search-bar v-model="query" v-if="search" @search="additionalTracks = []; loadMore()"></inline-search-bar>
|
<!-- 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">
|
<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>
|
</slot>
|
||||||
<table v-else :class="['ui', 'compact', 'very', 'basic', {loading: isLoading}, 'unstackable', 'table']">
|
<div v-else>
|
||||||
<thead>
|
<div
|
||||||
<tr>
|
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
||||||
<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>
|
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||||
<th colspan="6"><translate translate-context="*/*/*/Noun">Title</translate></th>
|
<div class="ui loader"></div>
|
||||||
<th colspan="4"><translate translate-context="*/*/*/Noun">Artist</translate></th>
|
</div>
|
||||||
<th colspan="4"><translate translate-context="*/*/*">Album</translate></th>
|
<div class="track-table row">
|
||||||
<th colspan="4"><translate translate-context="Content/*/*">Duration</translate></th>
|
<div v-if="showPosition" class="actions left floated column">
|
||||||
<th colspan="2" v-if="displayActions"><span class="visually hidden"><translate translate-context="*/*/*/Noun">Actions</translate></span></th>
|
<i class="hashtag icon"></i>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<div v-else class="actions left floated column"></div>
|
||||||
<tbody>
|
<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
|
<track-row
|
||||||
:playable="playable"
|
v-for="(track, index) in allTracks"
|
||||||
:display-position="displayPosition"
|
|
||||||
:display-actions="displayActions"
|
|
||||||
:track="track"
|
:track="track"
|
||||||
:track-index="index"
|
:key="track.id"
|
||||||
|
:index="index"
|
||||||
:tracks="allTracks"
|
:tracks="allTracks"
|
||||||
:artist="artist"
|
:show-album="showAlbum"
|
||||||
:key="index + '-' + track.id"
|
:show-artist="showArtist"
|
||||||
v-for="(track, index) in allTracks"></track-row>
|
:show-position="showPosition"
|
||||||
</tbody>
|
:show-art="showArt"
|
||||||
</table>
|
:display-actions="displayActions"
|
||||||
<button :class="['ui', {loading: isLoading}, 'button']" v-if="loadMoreUrl" @click="loadMore(loadMoreUrl)" :disabled="isLoading">
|
:show-duration="showDuration"
|
||||||
<translate translate-context="Content/*/Button.Label">Load more…</translate>
|
:is-podcast="isPodcast"
|
||||||
</button>
|
></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import _ from "@/lodash";
|
||||||
|
import axios from "axios";
|
||||||
import TrackRow from '@/components/audio/track/Row'
|
import TrackRow from "@/components/audio/track/Row";
|
||||||
import Modal from '@/components/semantic/Modal'
|
import TrackMobileRow from "@/components/audio/track/MobileRow";
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
|
|
||||||
export default {
|
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: {
|
components: {
|
||||||
Modal,
|
TrackRow,
|
||||||
TrackRow
|
TrackMobileRow,
|
||||||
|
Pagination,
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
if (!this.tracks) {
|
props: {
|
||||||
this.loadMore('tracks/')
|
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 () {
|
|
||||||
|
data() {
|
||||||
return {
|
return {
|
||||||
loadMoreUrl: this.nextUrl,
|
fetchDataUrl: this.nextUrl,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
additionalTracks: [],
|
additionalTracks: [],
|
||||||
query: '',
|
query: "",
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
allTracks () {
|
allTracks() {
|
||||||
return (this.tracks || []).concat(this.additionalTracks)
|
return (this.tracks || []).concat(this.additionalTracks);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
labels() {
|
||||||
|
return {
|
||||||
|
title: this.$pgettext("*/*/*/Noun", "Title"),
|
||||||
|
album: this.$pgettext("*/*/*/Noun", "Album"),
|
||||||
|
artist: this.$pgettext("*/*/*/Noun", "Artist"),
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadMore (url) {
|
async fetchData(url) {
|
||||||
url = url || 'tracks/'
|
if (!url) {
|
||||||
let self = this
|
return;
|
||||||
let params = {q: this.query, ...this.filters}
|
}
|
||||||
self.isLoading = true
|
this.isLoading = true;
|
||||||
axios.get(url, {params}).then((response) => {
|
let self = this;
|
||||||
self.additionalTracks = self.additionalTracks.concat(response.data.results)
|
let params = _.clone(this.filters);
|
||||||
self.loadMoreUrl = response.data.next
|
let tracksPromise = axios.get(url, { params: params })
|
||||||
self.isLoading = false
|
params.page_size = this.limit;
|
||||||
}, (error) => {
|
params.page = this.page;
|
||||||
self.isLoading = false
|
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.fetchData("tracks/");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="favorites-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
<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">
|
<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]] }}
|
{{ sharedLabels.filters[option[1]] }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="ui center aligned basic segment">
|
||||||
<pagination
|
<pagination
|
||||||
v-if="results && results.count > paginateBy"
|
v-if="results && results.count > paginateBy"
|
||||||
|
@ -76,21 +76,21 @@
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import $ from "jquery"
|
import $ from "jquery"
|
||||||
import logger from "@/logging"
|
import logger from "@/logging"
|
||||||
import TrackTable from "@/components/audio/track/Table"
|
|
||||||
import RadioButton from "@/components/radios/Button"
|
import RadioButton from "@/components/radios/Button"
|
||||||
import Pagination from "@/components/Pagination"
|
import Pagination from "@/components/Pagination"
|
||||||
import OrderingMixin from "@/components/mixins/Ordering"
|
import OrderingMixin from "@/components/mixins/Ordering"
|
||||||
import PaginationMixin from "@/components/mixins/Pagination"
|
import PaginationMixin from "@/components/mixins/Pagination"
|
||||||
import TranslationsMixin from "@/components/mixins/Translations"
|
import TranslationsMixin from "@/components/mixins/Translations"
|
||||||
import {checkRedirectToLogin} from '@/utils'
|
import {checkRedirectToLogin} from '@/utils'
|
||||||
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
const FAVORITES_URL = "tracks/"
|
const FAVORITES_URL = "tracks/"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
|
||||||
components: {
|
components: {
|
||||||
TrackTable,
|
|
||||||
RadioButton,
|
RadioButton,
|
||||||
Pagination
|
Pagination,
|
||||||
|
TrackTable
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -84,7 +84,8 @@
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
:is-serie="isSerie"
|
:is-serie="isSerie"
|
||||||
:is-channel="isChannel"
|
: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 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 small hidden divider"></div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
@ -128,7 +129,6 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import lodash from "@/lodash"
|
import lodash from "@/lodash"
|
||||||
import backend from "@/audio/backend"
|
|
||||||
import PlayButton from "@/components/audio/PlayButton"
|
import PlayButton from "@/components/audio/PlayButton"
|
||||||
import TagsList from "@/components/tags/List"
|
import TagsList from "@/components/tags/List"
|
||||||
import ArtistLabel from '@/components/audio/ArtistLabel'
|
import ArtistLabel from '@/components/audio/ArtistLabel'
|
||||||
|
@ -172,7 +172,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
this.isLoading = true
|
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 albumResponse = await axios.get(`albums/${this.id}/`, {params: {refresh: 'true'}})
|
||||||
let artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
|
let artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
|
||||||
this.artist = artistResponse.data
|
this.artist = artistResponse.data
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<translate key="1" v-if="isSerie" translate-context="Content/Channels/*">Episodes</translate>
|
<translate key="1" v-if="isSerie" translate-context="Content/Channels/*">Episodes</translate>
|
||||||
<translate key="2" v-else translate-context="*/*/*">Tracks</translate>
|
<translate key="2" v-else translate-context="*/*/*">Tracks</translate>
|
||||||
</h2>
|
</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>
|
</channel-entries>
|
||||||
<template v-else-if="discs && discs.length > 1">
|
<template v-else-if="discs && discs.length > 1">
|
||||||
<div v-for="tracks in discs" :key="tracks.disc_number">
|
<div v-for="tracks in discs" :key="tracks.disc_number">
|
||||||
|
@ -15,21 +15,36 @@
|
||||||
:translate-params="{number: tracks[0].disc_number}"
|
:translate-params="{number: tracks[0].disc_number}"
|
||||||
translate-context="Content/Album/"
|
translate-context="Content/Album/"
|
||||||
>Volume %{ number }</translate>
|
>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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<album-entries :tracks="object.tracks"></album-entries>
|
<track-table
|
||||||
</template>
|
:is-album="true"
|
||||||
<div class="ui center aligned basic segment">
|
:tracks="object.tracks"
|
||||||
<pagination
|
:show-position="true"
|
||||||
v-if="!isSerie && object.tracks && totalTracks > paginateBy"
|
:show-art="false"
|
||||||
@page-changed="updatePage"
|
:show-album="false"
|
||||||
:current="page"
|
:show-artist="false"
|
||||||
:paginate-by="paginateBy"
|
:paginate-results="true"
|
||||||
:total="totalTracks"
|
:total="totalTracks"
|
||||||
></pagination>
|
:paginate-by="paginateBy"
|
||||||
</div>
|
:page="page"
|
||||||
|
@page-changed="updatePage">
|
||||||
|
</track-table>
|
||||||
|
</template>
|
||||||
<template v-if="!artist.channel && !isSerie">
|
<template v-if="!artist.channel && !isSerie">
|
||||||
<h2>
|
<h2>
|
||||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
||||||
|
@ -44,25 +59,17 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import time from "@/utils/time"
|
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 LibraryWidget from "@/components/federation/LibraryWidget"
|
||||||
import TrackTable from "@/components/audio/track/Table"
|
|
||||||
import ChannelEntries from '@/components/audio/ChannelEntries'
|
import ChannelEntries from '@/components/audio/ChannelEntries'
|
||||||
import AlbumEntries from '@/components/audio/AlbumEntries'
|
import TrackTable from '@/components/audio/track/Table'
|
||||||
import Pagination from "@/components/Pagination"
|
|
||||||
import PaginationMixin from "@/components/mixins/Pagination"
|
|
||||||
import PlayButton from "@/components/audio/PlayButton"
|
import PlayButton from "@/components/audio/PlayButton"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"],
|
props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"],
|
||||||
components: {
|
components: {
|
||||||
LibraryWidget,
|
LibraryWidget,
|
||||||
AlbumEntries,
|
|
||||||
ChannelEntries,
|
|
||||||
TrackTable,
|
TrackTable,
|
||||||
Pagination,
|
ChannelEntries,
|
||||||
PlayButton
|
PlayButton
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -195,7 +195,7 @@ export default {
|
||||||
if (!self.object) {
|
if (!self.object) {
|
||||||
return
|
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.tracks = response.data.results
|
||||||
self.nextTracksUrl = response.data.next
|
self.nextTracksUrl = response.data.next
|
||||||
self.totalTracks = response.data.count
|
self.totalTracks = response.data.count
|
||||||
|
|
|
@ -14,6 +14,16 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -29,12 +39,6 @@
|
||||||
<translate translate-context="Content/*/Button.Label">Load more…</translate>
|
<translate translate-context="Content/*/Button.Label">Load more…</translate>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</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">
|
<section class="ui vertical stripe segment">
|
||||||
<h2>
|
<h2>
|
||||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
||||||
|
|
|
@ -85,7 +85,13 @@
|
||||||
translate-context="Content/Radio/Table.Paragraph/Short">
|
translate-context="Content/Radio/Table.Paragraph/Short">
|
||||||
%{ count } track matching combined filters
|
%{ count } track matching combined filters
|
||||||
</h3>
|
</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>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,10 +21,10 @@
|
||||||
<input :id="f.name" v-if="f.type === 'list' && config[f.name]" :value="config[f.name].join(',')" type="hidden">
|
<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="config[f.name]" class="ui menu">
|
||||||
<div
|
<div
|
||||||
v-if="f.type === 'list'"
|
|
||||||
v-for="(v, index) in config[f.name]"
|
v-for="(v, index) in config[f.name]"
|
||||||
class="ui item"
|
class="ui item"
|
||||||
:data-value="v">
|
:data-value="v"
|
||||||
|
:key="v">
|
||||||
<template v-if="config.names">
|
<template v-if="config.names">
|
||||||
{{ config.names[index] }}
|
{{ config.names[index] }}
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<div :class="['ui', {'active': show}, {'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal']">
|
<div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])">
|
||||||
<i class="close inside icon"></i>
|
<i tabindex=0 class="close inside icon"></i>
|
||||||
<slot v-if="show">
|
<slot v-if="show">
|
||||||
|
|
||||||
</slot>
|
</slot>
|
||||||
|
@ -15,6 +15,8 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
show: {type: Boolean, required: true},
|
show: {type: Boolean, required: true},
|
||||||
fullscreen: {type: Boolean, default: true},
|
fullscreen: {type: Boolean, default: true},
|
||||||
|
scrolling: {type: Boolean, required: false, default: false},
|
||||||
|
additionalClasses: {type: Array, required: false, default: () => []}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -61,6 +63,7 @@ export default {
|
||||||
this.control.modal('show')
|
this.control.modal('show')
|
||||||
this.focusTrap.activate()
|
this.focusTrap.activate()
|
||||||
this.focusTrap.unpause()
|
this.focusTrap.unpause()
|
||||||
|
document.body.classList.add('scrolling')
|
||||||
} else {
|
} else {
|
||||||
if (this.control) {
|
if (this.control) {
|
||||||
this.$emit('hide')
|
this.$emit('hide')
|
||||||
|
@ -68,6 +71,7 @@ export default {
|
||||||
this.control.remove()
|
this.control.remove()
|
||||||
this.focusTrap.deactivate()
|
this.focusTrap.deactivate()
|
||||||
this.focusTrap.pause()
|
this.focusTrap.pause()
|
||||||
|
document.body.classList.remove('scrolling')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ $bottom-player-height: 4rem;
|
||||||
@import "./components/_content_form.scss";
|
@import "./components/_content_form.scss";
|
||||||
@import "./components/_copy_input.scss";
|
@import "./components/_copy_input.scss";
|
||||||
@import "./components/_empty_state.scss";
|
@import "./components/_empty_state.scss";
|
||||||
|
@import "./components/_favorite.scss";
|
||||||
@import "./components/_form.scss";
|
@import "./components/_form.scss";
|
||||||
@import "./components/_file_upload.scss";
|
@import "./components/_file_upload.scss";
|
||||||
@import "./components/_fs_browser.scss";
|
@import "./components/_fs_browser.scss";
|
||||||
|
@ -34,6 +35,7 @@ $bottom-player-height: 4rem;
|
||||||
@import "./components/_pagination.scss";
|
@import "./components/_pagination.scss";
|
||||||
@import "./components/_placeholder.scss";
|
@import "./components/_placeholder.scss";
|
||||||
@import "./components/_play_button.scss";
|
@import "./components/_play_button.scss";
|
||||||
|
@import "./components/_play_indicator.scss";
|
||||||
@import "./components/_player.scss";
|
@import "./components/_player.scss";
|
||||||
@import "./components/_playlist_editor.scss";
|
@import "./components/_playlist_editor.scss";
|
||||||
@import "./components/_queue.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-background: rgba(255, 255, 255, 0.85) !default;
|
||||||
$dimmer-color: var(--text-color) !default;
|
$dimmer-color: var(--text-color) !default;
|
||||||
|
|
||||||
|
$border-color: rgba(34, 36, 38, 0.15) !default;
|
||||||
|
|
|
@ -98,4 +98,4 @@ button.reset {
|
||||||
.ui.inverted.buttons .button:focus,
|
.ui.inverted.buttons .button:focus,
|
||||||
.ui.inverted.button:focus {
|
.ui.inverted.button:focus {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
.track-table {
|
||||||
pre {
|
> div {
|
||||||
overflow-x: scroll;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 3.5rem;
|
||||||
}
|
}
|
||||||
&.table-wrapper {
|
.content {
|
||||||
overflow: visible;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
tr:not(:hover) {
|
.row:not(.mobile) {
|
||||||
.favorite-icon:not(.favorited),
|
border-bottom: 1px solid var(--border-color);
|
||||||
.playlist-icon {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.track {
|
|
||||||
display: block;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
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 {
|
.tablet-and-below {
|
||||||
@include media(">=desktop") {
|
@include media(">=tablet") {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,3 +104,10 @@ span.diff.removed {
|
||||||
.really.discrete {
|
.really.discrete {
|
||||||
color: var(--really-discrete-text-color);
|
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>
|
<template>
|
||||||
<section>
|
<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>
|
</channel-entries>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
:can-update="false"></rendered-description>
|
:can-update="false"></rendered-description>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
</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">
|
<h2 class="ui header">
|
||||||
<translate key="1" v-if="isPodcast" translate-context="Content/Channel/Paragraph">Latest episodes</translate>
|
<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>
|
<translate key="2" v-else translate-context="Content/Channel/Paragraph">Latest tracks</translate>
|
||||||
|
|
|
@ -42,7 +42,7 @@ module.exports = {
|
||||||
appleMobileWebAppStatusBarStyle: 'black',
|
appleMobileWebAppStatusBarStyle: 'black',
|
||||||
workboxPluginMode: 'InjectManifest',
|
workboxPluginMode: 'InjectManifest',
|
||||||
manifestOptions: {
|
manifestOptions: {
|
||||||
display: 'minimal-ui',
|
display: 'standalone',
|
||||||
start_url: '.',
|
start_url: '.',
|
||||||
description: 'A social platform to enjoy and share music',
|
description: 'A social platform to enjoy and share music',
|
||||||
scope: "/",
|
scope: "/",
|
||||||
|
|
Loading…
Reference in New Issue