Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
Show changes
Commits on Source (23)
Showing
with 1749 additions and 436 deletions
Made changes to the track table to make it more visibly pleasing.
......@@ -21,7 +21,7 @@
"diff": "^4.0.1",
"django-channels": "1.1.6",
"focus-trap": "^5.1.0",
"fomantic-ui-css": "^2.8.3",
"fomantic-ui-css": "^2.8.7",
"howler": "^2.2.1",
"js-logger": "^1.4.1",
"lodash": "^4.17.21",
......
......@@ -309,6 +309,11 @@ REPLACEMENTS = {
("color", "var(--button-basic-hover-color)"),
("box-shadow", "var(--button-basic-hover-box-shadow)"),
],
(".ui.basic.button:focus",): [
("background", "var(--button-basic-hover-background)"),
("color", "var(--button-basic-hover-color)"),
("box-shadow", "var(--button-basic-hover-box-shadow)"),
],
},
"card": {
"skip": [
......
......@@ -8,15 +8,13 @@
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
<template v-if="!compact">
<a href
v-if="page !== 'skip'"
v-for="page in pages"
:key="page"
@click.prevent.stop="selectPage(page)"
:class="[{'active': page === current}, 'item']">
{{ page }}
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']">
<span v-if="page !== 'skip'">{{ page }}</span>
<span v-else></span>
</a>
<div v-else class="disabled item">
</div>
</template>
<a href
:disabled="current + 1 > maxPage"
......
<template>
<div class="album-entries">
<div :class="[{active: currentTrack && isPlaying && track.id === currentTrack.id}, 'album-entry']" @click.prevent="replacePlay(tracks, index)" v-for="(track, index) in tracks" :key="track.id">
<div class="actions">
<play-button class="basic circular icon" :button-classes="['circular inverted vibrant icon button']" :discrete="true" :icon-only="true" :track="track" :tracks="tracks"></play-button>
</div>
<div class="position">{{ prettyPosition(track.position) }}</div>
<div class="content ellipsis">
<strong>{{ track.title }}</strong><br>
</div>
<div class="meta">
<template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](track.id)">
<track-favorite-icon class="tiny" :track="track"></track-favorite-icon>
</template>
<human-duration v-if="track.uploads[0] && track.uploads[0].duration" :duration="track.uploads[0].duration"></human-duration>
</div>
<div class="actions">
<play-button class="play-button basic icon" :dropdown-only="true" :is-playable="track.is_playable" :dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']" :track="track"></play-button>
</div>
</div>
</div>
</template>
<script>
import _ from '@/lodash'
import axios from 'axios'
import ChannelEntryCard from '@/components/audio/ChannelEntryCard'
import PlayButton from '@/components/audio/PlayButton'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import { mapGetters } from "vuex"
export default {
props: {
tracks: Array,
},
components: {
PlayButton,
TrackFavoriteIcon
},
computed: {
...mapGetters({
currentTrack: "queue/currentTrack",
}),
isPlaying () {
return this.$store.state.player.playing
},
},
methods: {
prettyPosition (position, size) {
var s = String(position);
while (s.length < (size || 2)) {s = "0" + s;}
return s;
},
replacePlay (tracks, trackIndex) {
this.$store.dispatch('queue/clean')
this.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => {
this.$store.dispatch('queue/currentIndex', trackIndex)
})
},
}
}
</script>
......@@ -5,18 +5,34 @@
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<channel-entry-card v-for="entry in objects" :default-cover="defaultCover" :entry="entry" :key="entry.id" />
<template v-if="count > limit">
<div class="ui hidden divider"></div>
<div class = "ui center aligned basic segment">
<pagination
<podcast-table
v-if="isPodcast"
:default-cover="defaultCover"
:is-podcast="isPodcast"
:show-art="true"
:show-position="false"
:tracks="objects"
:show-artist="false"
:show-album="false"
:paginate-results="true"
:total="count"
@page-changed="updatePage"
:current="page"
:paginate-by="limit"
: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"
></pagination>
</div>
</template>
@page-changed="updatePage"
:page="page"
:paginate-by="limit"></track-table>
<template v-if="!isLoading && objects.length === 0">
<empty-state @refresh="fetchData('tracks/')" :refresh="true">
<p>
......@@ -30,19 +46,19 @@
<script>
import _ from '@/lodash'
import axios from 'axios'
import ChannelEntryCard from '@/components/audio/ChannelEntryCard'
import Pagination from "@/components/Pagination"
import PaginationMixin from "@/components/mixins/Pagination"
import PodcastTable from '@/components/audio/podcast/Table'
import TrackTable from '@/components/audio/track/Table'
export default {
props: {
filters: {type: Object, required: true},
limit: {type: Number, default: 10},
defaultCover: {type: Object},
isPodcast: {type: Boolean, required: true},
},
components: {
ChannelEntryCard,
Pagination
PodcastTable,
TrackTable,
},
data () {
return {
......@@ -58,7 +74,7 @@ export default {
this.fetchData('tracks/')
},
methods: {
fetchData (url) {
async fetchData (url) {
if (!url) {
return
}
......@@ -68,16 +84,17 @@ export default {
params.page_size = this.limit
params.page = this.page
params.include_channels = true
axios.get(url, {params: params}).then((response) => {
self.nextPage = response.data.next
try {
let channelsPromise = await axios.get(url, {params: params})
self.nextPage = channelsPromise.data.next
self.objects = channelsPromise.data.results
self.count = channelsPromise.data.count
self.$emit('fetched', channelsPromise.data)
self.isLoading = false
self.objects = response.data.results
self.count = response.data.count
self.$emit('fetched', response.data)
}, error => {
} catch(e) {
self.isLoading = false
self.errors = error.backendErrors
})
}
},
updatePage: function(page) {
this.page = page
......
......@@ -6,7 +6,8 @@
:disabled="!playable"
:aria-label="labels.replacePlay"
:class="buttonClasses.concat(['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}])">
<i :class="[playIconClass, 'icon']"></i>
<i v-if="playing" class="pause icon"></i>
<i v-else :class="[playIconClass, 'icon']"></i>
<template v-if="!discrete && !iconOnly">&nbsp;<slot><translate translate-context="*/Queue/Button.Label/Short, Verb">Play</translate></slot></template>
</button>
<button
......@@ -27,8 +28,14 @@
<button v-if="track" class="item basic" :disabled="!playable" @click.stop.prevent="$store.dispatch('radios/start', {type: 'similar', objectId: track.id})" :title="labels.startRadio">
<i class="feed icon"></i><translate translate-context="*/Queue/Button.Label/Short, Verb">Play radio</translate>
</button>
<button v-if="track" class="item basic" :disabled="!playable" @click.stop="$store.commit('playlists/chooseTrack', track)">
<i class="list icon"></i>
<translate translate-context="Sidebar/Player/Icon.Tooltip/Verb">Add to playlist…</translate>
</button>
<button v-if="track" class="item basic" @click.stop.prevent="$router.push(`/library/tracks/${track.id}/`)">
<i class="info icon"></i><translate translate-context="*/Queue/Dropdown/Button/Label/Short">Track details</translate>
<i class="info icon"></i>
<translate v-if="track.artist.content_category === 'podcast'" translate-context="*/Queue/Dropdown/Button/Label/Short">Episode details</translate>
<translate v-else translate-context="*/Queue/Dropdown/Button/Label/Short">Track details</translate>
</button>
<div class="divider"></div>
<button v-if="filterableArtist" ref="filterArtist" data-ref="filterArtist" class="item basic" :disabled="!filterableArtist" @click.stop.prevent="filterArtist" :title="labels.hideArtist">
......@@ -52,9 +59,10 @@ import axios from 'axios'
import jQuery from 'jquery'
import ReportMixin from '@/components/mixins/Report'
import PlayOptionsMixin from '@/components/mixins/PlayOptions'
export default {
mixins: [ReportMixin],
mixins: [ReportMixin, PlayOptionsMixin],
props: {
// we can either have a single or multiple tracks to play when clicked
tracks: {type: Array, required: false},
......@@ -71,7 +79,9 @@ export default {
album: {type: Object, required: false},
library: {type: Object, required: false},
channel: {type: Object, required: false},
isPlayable: {type: Boolean, required: false, default: null}
isPlayable: {type: Boolean, required: false, default: null},
playing: {type: Boolean, required: false, default: false},
paused: {type: Boolean, required: false, default: false}
},
data () {
return {
......@@ -100,6 +110,7 @@ export default {
playNext: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play next'),
startRadio: this.$pgettext('*/Queue/Dropdown/Button/Title', 'Play similar songs'),
report: this.$pgettext('*/Moderation/*/Button/Label,Verb', 'Report…'),
addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…'),
replacePlay,
}
},
......@@ -112,165 +123,6 @@ export default {
}
}
},
playable () {
if (this.isPlayable) {
return true
}
if (this.track) {
return this.track.uploads && this.track.uploads.length > 0
} else if (this.artist && this.artist.tracks_count) {
return this.artist.tracks_count > 0
} else if (this.artist && this.artist.albums) {
return this.artist.albums.filter((a) => {
return a.is_playable === true
}).length > 0
} else if (this.album) {
return true
} else if (this.tracks) {
return this.tracks.filter((t) => {
return t.uploads && t.uploads.length > 0
}).length > 0
}
return false
},
filterableArtist () {
if (this.track) {
return this.track.artist
}
if (this.album) {
return this.album.artist
}
if (this.artist) {
return this.artist
}
},
},
methods: {
filterArtist () {
this.$store.dispatch('moderation/hide', {type: 'artist', target: this.filterableArtist})
},
getTracksPage (page, params, resolve, tracks) {
if (page > 10) {
// it's 10 * 100 tracks already, let's stop here
resolve(tracks)
}
// when fetching artists/or album tracks, sometimes, we may have to fetch
// multiple pages
let self = this
params['page_size'] = 100
params['page'] = page
params['hidden'] = ''
params['playable'] = 'true'
tracks = tracks || []
axios.get('tracks/', {params: params}).then((response) => {
response.data.results.forEach(t => {
tracks.push(t)
})
if (response.data.next) {
self.getTracksPage(page + 1, params, resolve, tracks)
} else {
resolve(tracks)
}
})
},
getPlayableTracks () {
let self = this
this.isLoading = true
let getTracks = new Promise((resolve, reject) => {
if (self.tracks) {
resolve(self.tracks)
} else if (self.track) {
if (!self.track.uploads || self.track.uploads.length === 0) {
// fetch uploads from api
axios.get(`tracks/${self.track.id}/`).then((response) => {
resolve([response.data])
})
} else {
resolve([self.track])
}
} else if (self.playlist) {
let url = 'playlists/' + self.playlist.id + '/'
axios.get(url + 'tracks/').then((response) => {
let artistIds = self.$store.getters['moderation/artistFilters']().map((f) => {
return f.target.id
})
let tracks = response.data.results.map(plt => {
return plt.track
})
if (artistIds.length > 0) {
// skip tracks from hidden artists
tracks = tracks.filter((t) => {
let matchArtist = artistIds.indexOf(t.artist.id) > -1
return !(matchArtist || t.album && artistIds.indexOf(t.album.artist.id) > -1)
})
}
resolve(tracks)
})
} else if (self.artist) {
let params = {'artist': self.artist.id, include_channels: 'true', 'ordering': 'album__release_date,disc_number,position'}
self.getTracksPage(1, params, resolve)
} else if (self.album) {
let params = {'album': self.album.id, include_channels: 'true', 'ordering': 'disc_number,position'}
self.getTracksPage(1, params, resolve)
} else if (self.library) {
let params = {'library': self.library.uuid, 'ordering': '-creation_date'}
self.getTracksPage(1, params, resolve)
}
})
return getTracks.then((tracks) => {
setTimeout(e => {
self.isLoading = false
}, 250)
return tracks.filter(e => {
return e.uploads && e.uploads.length > 0
})
})
},
add () {
let self = this
this.getPlayableTracks().then((tracks) => {
self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks))
})
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
},
replacePlay () {
let self = this
self.$store.dispatch('queue/clean')
this.getPlayableTracks().then((tracks) => {
self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => {
if (self.track) {
// set queue position to selected track
const trackIndex = self.tracks.findIndex(track => track.id === self.track.id)
self.$store.dispatch('queue/currentIndex', trackIndex)
}
self.addMessage(tracks)
})
})
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
},
addNext (next) {
let self = this
let wasEmpty = this.$store.state.queue.tracks.length === 0
this.getPlayableTracks().then((tracks) => {
self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1}).then(() => self.addMessage(tracks))
let goNext = next && !wasEmpty
if (goNext) {
self.$store.dispatch('queue/next')
}
})
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
},
addMessage (tracks) {
if (tracks.length < 1) {
return
}
let msg = this.$npgettext('*/Queue/Message', '%{ count } track was added to your queue', '%{ count } tracks were added to your queue', tracks.length)
this.$store.commit('ui/addMessage', {
content: this.$gettextInterpolate(msg, {count: tracks.length}),
date: new Date()
})
},
},
watch: {
clicked () {
......
<template>
<div
:class="[
{ active: currentTrack && track.id === currentTrack.id },
'track-row row mobile',
]"
>
<div
v-if="showArt"
@click.prevent.exact="activateTrack(track, index)"
class="image left floated column"
>
<img
alt=""
class="ui artist-track mini image"
v-if="
track.album && track.album.cover && track.album.cover.urls.original
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.album.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else-if="
track.cover
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else-if="
track.artist.cover
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.artist.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else
src="../../../assets/audio/default-cover.png"
/>
</div>
<div
tabindex=0
@click="activateTrack(track, index)"
role="button"
class="content ellipsis left floated column"
>
<p
:class="[
'track-title',
'mobile',
{ 'play-indicator': isPlaying && currentTrack && track.id === currentTrack.id },
]"
>
{{ track.title }}
</p>
<p v-if="track.artist.content_category === 'podcast'" class="track-meta mobile">
<human-date class="really discrete" :date="track.creation_date"></human-date>
<span>&#183;</span>
<human-duration
v-if="track.uploads[0] && track.uploads[0].duration"
:duration="track.uploads[0].duration"
></human-duration>
</p>
<p v-else class="track-meta mobile">
{{ track.artist.name }} <span>&#183;</span>
<human-duration
v-if="track.uploads[0] && track.uploads[0].duration"
:duration="track.uploads[0].duration"
></human-duration>
</p>
</div>
<div
v-if="$store.state.auth.authenticated && this.track.artist.content_category !== 'podcast'"
:class="[
'meta',
'right',
'floated',
'column',
'mobile',
{ 'with-art': showArt },
]"
role="button"
>
<track-favorite-icon
class="tiny"
:border="false"
:track="track"
></track-favorite-icon>
</div>
<div
role="button"
:aria-label="actionsButtonLabel"
@click.prevent.exact="showTrackModal = !showTrackModal"
:class="[
'modal-button',
'right',
'floated',
'column',
'mobile',
{ 'with-art': showArt },
]"
>
<i class="ellipsis large vertical icon" />
</div>
<track-modal
@update:show="showTrackModal = $event;"
:show="showTrackModal"
:track="track"
:index="index"
:is-artist="isArtist"
:is-album="isAlbum"
></track-modal>
</div>
</template>
<script>
import PlayIndicator from "@/components/audio/track/PlayIndicator";
import { mapActions, mapGetters } from "vuex";
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
import TrackModal from "@/components/audio/track/Modal";
import PlayOptionsMixin from "@/components/mixins/PlayOptions"
export default {
mixins: [PlayOptionsMixin],
data() {
return {
showTrackModal: false,
}
},
props: {
tracks: Array,
showAlbum: { type: Boolean, required: false, default: true },
showArtist: { type: Boolean, required: false, default: true },
showPosition: { type: Boolean, required: false, default: false },
showArt: { type: Boolean, required: false, default: true },
search: { type: Boolean, required: false, default: false },
filters: { type: Object, required: false, default: null },
nextUrl: { type: String, required: false, default: null },
displayActions: { type: Boolean, required: false, default: true },
showDuration: { type: Boolean, required: false, default: true },
index: { type: Number, required: true },
track: { type: Object, required: true },
isArtist: {type: Boolean, required: false, default: false},
isAlbum: {type: Boolean, required: false, default: false},
},
components: {
PlayIndicator,
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>
<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>
<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">
An episode description, with all its twists and turns!
This episode focuses on something I'm sure, but nobody really knows what it's focusing on.</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 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,
}
},
components: {
PlayIndicator,
PlayButton,
},
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;
},
...mapActions({
resumePlayback: "player/resumePlayback",
pausePlayback: "player/pausePlayback",
}),
},
};
</script>
<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"
:default-cover="defaultCover"
: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>
<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>&#183;</span>
<human-duration
v-if="track.uploads[0] && track.uploads[0].duration"
:duration="track.uploads[0].duration"
></human-duration>
</p>
</div>
<div
v-if="$store.state.auth.authenticated"
: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>
<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>
<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>
\ No newline at end of file
<template>
<tr>
<td>
<play-button :class="['basic', {vibrant: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']"
:discrete="true"
:is-playable="playable"
:track="track"
:track-index="trackIndex"
:tracks="tracks"></play-button>
</td>
<td>
<img alt="" class="ui mini image" v-if="track.album && track.album.cover && track.album.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)">
<img alt="" class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
</td>
<td colspan="6">
<button class="track" @click.stop="playSong()">
<template v-if="displayPosition && track.position">
{{ track.position }}.
</template>
{{ track.title|truncate(40) }}
<div
:class="[
{ active: currentTrack && track.id === currentTrack.id },
'track-row row',
]"
@mouseover="hover = track.id"
@mouseleave="hover = null"
@dblclick="activateTrack(track, index)"
>
<div
class="actions one wide left floated column"
role="button"
@click.prevent.exact="activateTrack(track, index)"
>
<play-indicator
v-if="
!$store.state.player.isLoadingAudio &&
currentTrack &&
isPlaying &&
track.id === currentTrack.id &&
!(track.id == hover)
"
>
</play-indicator>
<button
v-else-if="
currentTrack &&
!isPlaying &&
track.id === currentTrack.id &&
!track.id == hover
"
class="ui really tiny basic icon button play-button paused"
>
<i class="pause icon" />
</button>
<button
v-else-if="
currentTrack &&
isPlaying &&
track.id === currentTrack.id &&
track.id == hover
"
class="ui really tiny basic icon button play-button"
>
<i class="pause icon" />
</button>
</td>
<td colspan="4">
<router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
{{ track.artist.name|truncate(40) }}
</router-link>
</td>
<td colspan="4">
<router-link v-if="track.album" class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
{{ track.album.title|truncate(40) }}
</router-link>
</td>
<td colspan="4" v-if="track.uploads && track.uploads.length > 0">
<human-duration :duration="track.uploads[0].duration"></human-duration>
</td>
<td colspan="4" v-else>
<translate translate-context="*/*/*">N/A</translate>
</td>
<td colspan="2" v-if="displayActions" class="align right">
<track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon>
<track-playlist-icon
<button
v-else-if="track.id == hover"
class="ui really tiny basic icon button play-button"
>
<i class="play icon" />
</button>
<span class="track-position" v-else-if="showPosition">
{{ prettyPosition(track.position) }}
</span>
</div>
<div
v-if="showArt"
class="image left floated column"
role="button"
@click.prevent.exact="activateTrack(track, index)"
>
<img
alt=""
class="ui artist-track mini image"
v-if="
track.album && track.album.cover && track.album.cover.urls.original
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.album.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else-if="
track.cover && track.cover.urls.original
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else-if="
track.artist && track.artist.cover && track.album.cover.urls.original
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else
src="../../../assets/audio/default-cover.png"
/>
</div>
<div tabindex=0 class="content ellipsis left floated column">
<a
@click="activateTrack(track, index)"
>
{{ track.title }}
</a>
</div>
<div v-if="showAlbum" class="content ellipsis left floated column">
<router-link
:to="{ name: 'library.albums.detail', params: { id: track.album.id } }"
>{{ track.album.title }}</router-link
>
</div>
<div v-if="showArtist" class="content ellipsis left floated column">
<router-link
class="artist link"
:to="{
name: 'library.artists.detail',
params: { id: track.artist.id },
}"
>{{ track.artist.name }}</router-link
>
</div>
<div
v-if="$store.state.auth.authenticated"
:track="track"></track-playlist-icon>
class="meta right floated column"
>
<track-favorite-icon
class="tiny"
:border="false"
:track="track"
></track-favorite-icon>
</div>
<div v-if="showDuration" class="meta right floated column">
<human-duration
v-if="track.uploads[0] && track.uploads[0].duration"
:duration="track.uploads[0].duration"
></human-duration>
</div>
<div v-if="displayActions" class="meta right floated column">
<play-button
id="playmenu"
class="play-button basic icon"
:dropdown-only="true"
:is-playable="track.is_playable"
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
:dropdown-icon-classes="[
'ellipsis',
'vertical',
'large really discrete',
]"
:track="track"
></play-button>
</td>
</tr>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex"
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
import PlayButton from '@/components/audio/PlayButton'
import PlayIndicator from "@/components/audio/track/PlayIndicator";
import { mapActions, mapGetters } from "vuex";
import TrackFavoriteIcon from "@/components/favorites/TrackFavoriteIcon";
import PlayButton from "@/components/audio/PlayButton";
import PlayOptions from "@/components/mixins/PlayOptions";
export default {
mixins: [PlayOptions],
props: {
tracks: Array,
showAlbum: { type: Boolean, required: false, default: true },
showArtist: { type: Boolean, required: false, default: true },
showPosition: { type: Boolean, required: false, default: false },
showArt: { type: Boolean, required: false, default: true },
search: { type: Boolean, required: false, default: false },
filters: { type: Object, required: false, default: null },
nextUrl: { type: String, required: false, default: null },
displayActions: { type: Boolean, required: false, default: true },
showDuration: { type: Boolean, required: false, default: true },
index: { type: Number, required: true },
track: { type: Object, required: true },
trackIndex: {type: Number, required: true},
tracks: {type: Array, required: false},
artist: {type: Object, required: false},
displayPosition: {type: Boolean, default: false},
displayActions: {type: Boolean, default: true},
playable: {type: Boolean, required: false, default: false},
},
data() {
return {
hover: null,
}
},
components: {
PlayIndicator,
TrackFavoriteIcon,
TrackPlaylistIcon,
PlayButton
PlayButton,
},
computed: {
...mapGetters({
currentTrack: "queue/currentTrack",
}),
isPlaying() {
return this.$store.state.player.playing
},
albumArtist () {
if (this.artist) {
return this.artist
} else {
return this.track.album.artist
}
return this.$store.state.player.playing;
},
},
methods: {
playSong () {
this.$store.dispatch('queue/clean')
this.$store.dispatch('queue/appendMany', {
tracks: this.tracks
}).then(() => {
this.$store.dispatch('queue/currentIndex', this.trackIndex)
})
},
}
prettyPosition(position, size) {
var s = String(position);
while (s.length < (size || 2)) {
s = "0" + s;
}
return s;
},
...mapActions({
resumePlayback: "player/resumePlayback",
pausePlayback: "player/pausePlayback",
}),
},
};
</script>
<template>
<div class="table-wrapper component-track-table">
<inline-search-bar v-model="query" v-if="search" @search="additionalTracks = []; loadMore()"></inline-search-bar>
<div>
<!-- Show the search bar if search is true -->
<inline-search-bar
v-model="query"
v-if="search"
@search="
additionalTracks = [];
fetchData();
"
></inline-search-bar>
<div class="ui hidden divider"></div>
<!-- Add a header if needed -->
<slot name="header"></slot>
<!-- Show a message if no tracks are available -->
<slot v-if="!isLoading && allTracks.length === 0" name="empty-state">
<empty-state @refresh="fetchData" :refresh="true"></empty-state>
<empty-state
@refresh="fetchData('tracks/')"
:refresh="true"
></empty-state>
</slot>
<table v-else :class="['ui', 'compact', 'very', 'basic', {loading: isLoading}, 'unstackable', 'table']">
<thead>
<tr>
<th><span class="visually-hidden"><translate translate-context="*/*/*/Noun">Play</translate></span></th>
<th><span class="visually-hidden"><translate translate-context="*/*/*/Noun">Track Art</translate></span></th>
<th colspan="6"><translate translate-context="*/*/*/Noun">Title</translate></th>
<th colspan="4"><translate translate-context="*/*/*/Noun">Artist</translate></th>
<th colspan="4"><translate translate-context="*/*/*">Album</translate></th>
<th colspan="4"><translate translate-context="Content/*/*">Duration</translate></th>
<th colspan="2" v-if="displayActions"><span class="visually hidden"><translate translate-context="*/*/*/Noun">Actions</translate></span></th>
</tr>
</thead>
<tbody>
<div v-else>
<div
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
>
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<div class="track-table row">
<div v-if="showPosition" class="actions left floated column">
<i class="hashtag icon"></i>
</div>
<div v-else class="actions left floated column"></div>
<div v-if="showArt" class="image left floated column"></div>
<div class="content ellipsis left floated column">
<b>{{ labels.title }}</b>
</div>
<div v-if="showAlbum" class="content ellipsisleft floated column">
<b>{{ labels.album }}</b>
</div>
<div v-if="showArtist" class="content ellipsis left floated column">
<b>{{ labels.artist }}</b>
</div>
<div
v-if="$store.state.auth.authenticated"
class="meta right floated column"
></div>
<div v-if="showDuration" class="meta right floated column">
<i class="clock outline icon" style="padding: 0.5rem" />
</div>
<div v-if="displayActions" class="meta right floated column"></div>
</div>
<!-- For each item, build a row -->
<track-row
:playable="playable"
:display-position="displayPosition"
v-for="(track, index) in allTracks"
:track="track"
:key="track.id"
:index="index"
:tracks="allTracks"
:show-album="showAlbum"
:show-artist="showArtist"
:show-position="showPosition"
:show-art="showArt"
:display-actions="displayActions"
:show-duration="showDuration"
:is-podcast="isPodcast"
></track-row>
</div>
<div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up">
<pagination
:total="total"
:current="page"
:paginate-by="paginateBy"
v-on="$listeners">
</pagination>
</div>
</div>
<div
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
>
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<!-- For each item, build a row -->
<track-mobile-row
v-for="(track, index) in allTracks"
:track="track"
:track-index="index"
:key="track.id"
:index="index"
:tracks="allTracks"
:artist="artist"
:key="index + '-' + track.id"
v-for="(track, index) in allTracks"></track-row>
</tbody>
</table>
<button :class="['ui', {loading: isLoading}, 'button']" v-if="loadMoreUrl" @click="loadMore(loadMoreUrl)" :disabled="isLoading">
<translate translate-context="Content/*/Button.Label">Load more…</translate>
</button>
:show-position="showPosition"
:show-art="showArt"
:show-duration="showDuration"
:is-artist="isArtist"
:is-album="isAlbum"
:is-podcast="isPodcast"
></track-mobile-row>
<div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below">
<pagination
v-if="paginateResults"
:total="total"
:current="page"
:compact="true"
v-on="$listeners"></pagination>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import TrackRow from '@/components/audio/track/Row'
import Modal from '@/components/semantic/Modal'
import _ from "@/lodash";
import axios from "axios";
import TrackRow from "@/components/audio/track/Row";
import TrackMobileRow from "@/components/audio/track/MobileRow";
import Pagination from "@/components/Pagination";
export default {
components: {
TrackRow,
TrackMobileRow,
Pagination,
},
props: {
tracks: {type: Array, required: false},
playable: {type: Boolean, required: false, default: false},
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 },
artist: {type: Object, required: false},
filters: {type: Object, required: false, default: () => { return {}}},
displayPosition: {type: Boolean, default: false},
displayActions: {type: Boolean, default: true},
},
components: {
Modal,
TrackRow
},
created () {
if (!this.tracks) {
this.loadMore('tracks/')
}
displayActions: { type: Boolean, required: false, default: true },
showDuration: { type: Boolean, required: false, default: true },
isArtist: { type: Boolean, required: false, default: false },
isAlbum: { type: Boolean, required: false, default: false },
isPodcast: { type: Boolean, required: false, default: false },
paginateResults: { type: Boolean, required: false, default: true},
total: { type: Number, required: false},
page: {type: Number, required: false, default: 1},
paginateBy: {type: Number, required: false, default: 25}
},
data() {
return {
loadMoreUrl: this.nextUrl,
fetchDataUrl: this.nextUrl,
isLoading: false,
additionalTracks: [],
query: '',
}
query: "",
};
},
computed: {
allTracks() {
return (this.tracks || []).concat(this.additionalTracks)
}
return (this.tracks || []).concat(this.additionalTracks);
},
methods: {
loadMore (url) {
url = url || 'tracks/'
let self = this
let params = {q: this.query, ...this.filters}
self.isLoading = true
axios.get(url, {params}).then((response) => {
self.additionalTracks = self.additionalTracks.concat(response.data.results)
self.loadMoreUrl = response.data.next
self.isLoading = false
}, (error) => {
self.isLoading = false
})
labels() {
return {
title: this.$pgettext("*/*/*/Noun", "Title"),
album: this.$pgettext("*/*/*/Noun", "Album"),
artist: this.$pgettext("*/*/*/Noun", "Artist"),
};
},
},
methods: {
async fetchData(url) {
if (!url) {
return;
}
this.isLoading = true;
let self = this;
let params = _.clone(this.filters);
let tracksPromise = axios.get(url, { params: params })
params.page_size = this.limit;
params.page = this.page;
params.include_channels = true;
try {
await tracksPromise
self.nextPage = tracksPromise.data.next;
self.objects = tracksPromise.data.results;
self.count = tracksPromise.data.count;
self.$emit("fetched", tracksPromise.data);
self.isLoading = false;
} catch(e) {
self.isLoading = false;
self.errors = error.backendErrors;
}
},
updatePage: function(page) {
this.$emit('page-changed', page)
}
},
created() {
if (!this.tracks) {
this.fetchData("tracks/");
}
},
};
</script>
......@@ -24,7 +24,7 @@
<div class="field">
<label for="favorites-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
<select id="favorites-ordering" class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
<option v-for="option in orderingOptions" :value="option[0]" :key="option[0]">
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
......@@ -46,7 +46,7 @@
</div>
</div>
</div>
<track-table v-if="results" :tracks="results.results"></track-table>
<track-table :show-artist="true" :show-album="true" v-if="results" :tracks="results.results"></track-table>
<div class="ui center aligned basic segment">
<pagination
v-if="results && results.count > paginateBy"
......@@ -76,21 +76,21 @@
import axios from "axios"
import $ from "jquery"
import logger from "@/logging"
import TrackTable from "@/components/audio/track/Table"
import RadioButton from "@/components/radios/Button"
import Pagination from "@/components/Pagination"
import OrderingMixin from "@/components/mixins/Ordering"
import PaginationMixin from "@/components/mixins/Pagination"
import TranslationsMixin from "@/components/mixins/Translations"
import {checkRedirectToLogin} from '@/utils'
import TrackTable from '@/components/audio/track/Table'
const FAVORITES_URL = "tracks/"
export default {
mixins: [OrderingMixin, PaginationMixin, TranslationsMixin],
components: {
TrackTable,
RadioButton,
Pagination
Pagination,
TrackTable
},
data() {
return {
......
......@@ -84,7 +84,8 @@
:is-album="isAlbum"
:is-serie="isSerie"
:is-channel="isChannel"
:artist="artist"></album-dropdown>
:artist="artist"
></album-dropdown>
<div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local">
<div class="ui small hidden divider"></div>
<div class="ui divider"></div>
......@@ -128,7 +129,6 @@
<script>
import axios from "axios"
import lodash from "@/lodash"
import backend from "@/audio/backend"
import PlayButton from "@/components/audio/PlayButton"
import TagsList from "@/components/tags/List"
import ArtistLabel from '@/components/audio/ArtistLabel'
......@@ -172,7 +172,7 @@ export default {
methods: {
async fetchData() {
this.isLoading = true
let tracksResponse = axios.get(`tracks/`, {params: {ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page:this.page, include_channels: 'true'}})
let tracksResponse = axios.get(`tracks/`, {params: {ordering: 'disc_number,position', album: this.id, page_size: this.paginateBy, page:this.page, include_channels: 'true', playable: 'true'}})
let albumResponse = await axios.get(`albums/${this.id}/`, {params: {refresh: 'true'}})
let artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
this.artist = artistResponse.data
......
......@@ -4,7 +4,7 @@
<translate key="1" v-if="isSerie" translate-context="Content/Channels/*">Episodes</translate>
<translate key="2" v-else translate-context="*/*/*">Tracks</translate>
</h2>
<channel-entries v-if="artist.channel && isSerie" :limit="50" :filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}">
<channel-entries v-if="artist.channel && isSerie" :is-podcast="isSerie" :limit="50" :filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}">
</channel-entries>
<template v-else-if="discs && discs.length > 1">
<div v-for="tracks in discs" :key="tracks.disc_number">
......@@ -15,21 +15,36 @@
:translate-params="{number: tracks[0].disc_number}"
translate-context="Content/Album/"
>Volume %{ number }</translate>
<album-entries :tracks="tracks"></album-entries>
<track-table
:is-album="true"
:tracks="object.tracks"
:show-position="true"
:show-art="false"
:show-album="false"
:show-artist="false"
:paginate-results="true"
:total="totalTracks"
:paginate-by="paginateBy"
:page="page"
@page-changed="updatePage">
</track-table>
</div>
</template>
<template v-else>
<album-entries :tracks="object.tracks"></album-entries>
</template>
<div class="ui center aligned basic segment">
<pagination
v-if="!isSerie && object.tracks && totalTracks > paginateBy"
@page-changed="updatePage"
:current="page"
:paginate-by="paginateBy"
<track-table
:is-album="true"
:tracks="object.tracks"
:show-position="true"
:show-art="false"
:show-album="false"
:show-artist="false"
:paginate-results="true"
:total="totalTracks"
></pagination>
</div>
:paginate-by="paginateBy"
:page="page"
@page-changed="updatePage">
</track-table>
</template>
<template v-if="!artist.channel && !isSerie">
<h2>
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
......@@ -44,25 +59,17 @@
<script>
import time from "@/utils/time"
import axios from "axios"
import url from "@/utils/url"
import logger from "@/logging"
import LibraryWidget from "@/components/federation/LibraryWidget"
import TrackTable from "@/components/audio/track/Table"
import ChannelEntries from '@/components/audio/ChannelEntries'
import AlbumEntries from '@/components/audio/AlbumEntries'
import Pagination from "@/components/Pagination"
import PaginationMixin from "@/components/mixins/Pagination"
import TrackTable from '@/components/audio/track/Table'
import PlayButton from "@/components/audio/PlayButton"
export default {
props: ["object", "libraries", "discs", "isSerie", "artist", "page", "paginateBy", "totalTracks"],
components: {
LibraryWidget,
AlbumEntries,
ChannelEntries,
TrackTable,
Pagination,
ChannelEntries,
PlayButton
},
data() {
......
......@@ -195,7 +195,7 @@ export default {
if (!self.object) {
return
}
let trackPromise = axios.get("tracks/", { params: { artist: this.id, hidden: '' } }).then(response => {
let trackPromise = axios.get("tracks/", { params: { artist: this.id, hidden: '', ordering: "-creation_date" } }).then(response => {
self.tracks = response.data.results
self.nextTracksUrl = response.data.next
self.totalTracks = response.data.count
......