Update track table

1218-smartplaylist_backend
Ciarán Ainsworth 2021-10-21 17:26:18 +00:00 zatwierdzone przez Georg Krause
rodzic da33ec0241
commit 44394275ec
34 zmienionych plików z 2304 dodań i 466 usunięć

Wyświetl plik

@ -0,0 +1 @@
Made changes to the track table to make it more visibly pleasing.

Wyświetl plik

@ -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": [

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -5,18 +5,34 @@
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<channel-entry-card v-for="entry in objects" :default-cover="defaultCover" :entry="entry" :key="entry.id" />
<template v-if="count > limit">
<div class="ui hidden divider"></div>
<div class = "ui center aligned basic segment">
<pagination
@page-changed="updatePage"
:current="page"
:paginate-by="limit"
:total="count"
></pagination>
</div>
</template>
<podcast-table
v-if="isPodcast"
:default-cover="defaultCover"
:is-podcast="isPodcast"
:show-art="true"
:show-position="false"
:tracks="objects"
:show-artist="false"
:show-album="false"
:paginate-results="true"
:total="count"
@page-changed="updatePage"
:page="page"
:paginate-by="limit"></podcast-table>
<track-table
v-else
:default-cover="defaultCover"
:is-podcast="isPodcast"
:show-art="true"
:show-position="false"
:tracks="objects"
:show-artist="false"
:show-album="false"
:paginate-results="true"
:total="count"
@page-changed="updatePage"
:page="page"
:paginate-by="limit"></track-table>
<template v-if="!isLoading && objects.length === 0">
<empty-state @refresh="fetchData('tracks/')" :refresh="true">
<p>
@ -30,19 +46,19 @@
<script>
import _ from '@/lodash'
import axios from 'axios'
import ChannelEntryCard from '@/components/audio/ChannelEntryCard'
import Pagination from "@/components/Pagination"
import PaginationMixin from "@/components/mixins/Pagination"
import PodcastTable from '@/components/audio/podcast/Table'
import TrackTable from '@/components/audio/track/Table'
export default {
props: {
filters: {type: Object, required: true},
limit: {type: Number, default: 10},
defaultCover: {type: Object},
isPodcast: {type: Boolean, required: true},
},
components: {
ChannelEntryCard,
Pagination
PodcastTable,
TrackTable,
},
data () {
return {
@ -58,7 +74,7 @@ export default {
this.fetchData('tracks/')
},
methods: {
fetchData (url) {
async fetchData (url) {
if (!url) {
return
}
@ -68,16 +84,17 @@ export default {
params.page_size = this.limit
params.page = this.page
params.include_channels = true
axios.get(url, {params: params}).then((response) => {
self.nextPage = response.data.next
self.isLoading = false
self.objects = response.data.results
self.count = response.data.count
self.$emit('fetched', response.data)
}, error => {
try {
let channelsPromise = await axios.get(url, {params: params})
self.nextPage = channelsPromise.data.next
self.objects = channelsPromise.data.results
self.count = channelsPromise.data.count
self.$emit('fetched', channelsPromise.data)
self.isLoading = false
} catch(e) {
self.isLoading = false
self.errors = error.backendErrors
})
}
},
updatePage: function(page) {
this.page = page

Wyświetl plik

@ -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 () {

Wyświetl plik

@ -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>&#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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>&#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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -1,102 +1,225 @@
<template>
<tr>
<td>
<play-button :class="['basic', {vibrant: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']"
:discrete="true"
:is-playable="playable"
:track="track"
:track-index="trackIndex"
:tracks="tracks"></play-button>
</td>
<td>
<img alt="" class="ui mini image" v-if="track.album && track.album.cover && track.album.cover.urls.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)">
<img alt="" class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
</td>
<td colspan="6">
<button class="track" @click.stop="playSong()">
<template v-if="displayPosition && track.position">
{{ track.position }}.
</template>
{{ track.title|truncate(40) }}
<div
:class="[
{ active: currentTrack && track.id === currentTrack.id },
'track-row row',
]"
@mouseover="hover = track.id"
@mouseleave="hover = null"
@dblclick="activateTrack(track, index)"
>
<div
class="actions one wide left floated column"
role="button"
@click.prevent.exact="activateTrack(track, index)"
>
<play-indicator
v-if="
!$store.state.player.isLoadingAudio &&
currentTrack &&
isPlaying &&
track.id === currentTrack.id &&
!(track.id == hover)
"
>
</play-indicator>
<button
v-else-if="
currentTrack &&
!isPlaying &&
track.id === currentTrack.id &&
!track.id == hover
"
class="ui really tiny basic icon button play-button paused"
>
<i class="pause icon" />
</button>
</td>
<td colspan="4">
<router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
{{ track.artist.name|truncate(40) }}
</router-link>
</td>
<td colspan="4">
<router-link v-if="track.album" class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
{{ track.album.title|truncate(40) }}
</router-link>
</td>
<td colspan="4" v-if="track.uploads && track.uploads.length > 0">
<human-duration :duration="track.uploads[0].duration"></human-duration>
</td>
<td colspan="4" v-else>
<translate translate-context="*/*/*">N/A</translate>
</td>
<td colspan="2" v-if="displayActions" class="align right">
<track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon>
<track-playlist-icon
v-if="$store.state.auth.authenticated"
:track="track"></track-playlist-icon>
<button
v-else-if="
currentTrack &&
isPlaying &&
track.id === currentTrack.id &&
track.id == hover
"
class="ui really tiny basic icon button play-button"
>
<i class="pause icon" />
</button>
<button
v-else-if="track.id == hover"
class="ui really tiny basic icon button play-button"
>
<i class="play icon" />
</button>
<span class="track-position" v-else-if="showPosition">
{{ prettyPosition(track.position) }}
</span>
</div>
<div
v-if="showArt"
class="image left floated column"
role="button"
@click.prevent.exact="activateTrack(track, index)"
>
<img
alt=""
class="ui artist-track mini image"
v-if="
track.album && track.album.cover && track.album.cover.urls.original
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.album.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else-if="
track.cover && track.cover.urls.original
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else-if="
track.artist && track.artist.cover && track.album.cover.urls.original
"
v-lazy="
$store.getters['instance/absoluteUrl'](
track.cover.urls.medium_square_crop
)
"
/>
<img
alt=""
class="ui artist-track mini image"
v-else
src="../../../assets/audio/default-cover.png"
/>
</div>
<div tabindex=0 class="content ellipsis left floated column">
<a
@click="activateTrack(track, index)"
>
{{ track.title }}
</a>
</div>
<div v-if="showAlbum" class="content ellipsis left floated column">
<router-link
:to="{ name: 'library.albums.detail', params: { id: track.album.id } }"
>{{ track.album.title }}</router-link
>
</div>
<div v-if="showArtist" class="content ellipsis left floated column">
<router-link
class="artist link"
:to="{
name: 'library.artists.detail',
params: { id: track.artist.id },
}"
>{{ track.artist.name }}</router-link
>
</div>
<div
v-if="$store.state.auth.authenticated"
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: {
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},
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 },
},
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
}
isPlaying() {
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>

Wyświetl plik

@ -1,94 +1,209 @@
<template>
<div class="table-wrapper component-track-table">
<inline-search-bar v-model="query" v-if="search" @search="additionalTracks = []; loadMore()"></inline-search-bar>
<div>
<!-- Show the search bar if search is true -->
<inline-search-bar
v-model="query"
v-if="search"
@search="
additionalTracks = [];
fetchData();
"
></inline-search-bar>
<div class="ui hidden divider"></div>
<!-- Add a header if needed -->
<slot name="header"></slot>
<!-- Show a message if no tracks are available -->
<slot v-if="!isLoading && allTracks.length === 0" name="empty-state">
<empty-state @refresh="fetchData" :refresh="true"></empty-state>
<empty-state
@refresh="fetchData('tracks/')"
:refresh="true"
></empty-state>
</slot>
<table v-else :class="['ui', 'compact', 'very', 'basic', {loading: isLoading}, 'unstackable', 'table']">
<thead>
<tr>
<th><span class="visually-hidden"><translate translate-context="*/*/*/Noun">Play</translate></span></th>
<th><span class="visually-hidden"><translate translate-context="*/*/*/Noun">Track Art</translate></span></th>
<th colspan="6"><translate translate-context="*/*/*/Noun">Title</translate></th>
<th colspan="4"><translate translate-context="*/*/*/Noun">Artist</translate></th>
<th colspan="4"><translate translate-context="*/*/*">Album</translate></th>
<th colspan="4"><translate translate-context="Content/*/*">Duration</translate></th>
<th colspan="2" v-if="displayActions"><span class="visually hidden"><translate translate-context="*/*/*/Noun">Actions</translate></span></th>
</tr>
</thead>
<tbody>
<div v-else>
<div
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
>
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<div class="track-table row">
<div v-if="showPosition" class="actions left floated column">
<i class="hashtag icon"></i>
</div>
<div v-else class="actions left floated column"></div>
<div v-if="showArt" class="image left floated column"></div>
<div class="content ellipsis left floated column">
<b>{{ labels.title }}</b>
</div>
<div v-if="showAlbum" class="content ellipsisleft floated column">
<b>{{ labels.album }}</b>
</div>
<div v-if="showArtist" class="content ellipsis left floated column">
<b>{{ labels.artist }}</b>
</div>
<div
v-if="$store.state.auth.authenticated"
class="meta right floated column"
></div>
<div v-if="showDuration" class="meta right floated column">
<i class="clock outline icon" style="padding: 0.5rem" />
</div>
<div v-if="displayActions" class="meta right floated column"></div>
</div>
<!-- For each item, build a row -->
<track-row
:playable="playable"
:display-position="displayPosition"
:display-actions="displayActions"
v-for="(track, index) in allTracks"
:track="track"
:track-index="index"
:key="track.id"
:index="index"
:tracks="allTracks"
:artist="artist"
:key="index + '-' + track.id"
v-for="(track, index) in allTracks"></track-row>
</tbody>
</table>
<button :class="['ui', {loading: isLoading}, 'button']" v-if="loadMoreUrl" @click="loadMore(loadMoreUrl)" :disabled="isLoading">
<translate translate-context="Content/*/Button.Label">Load more</translate>
</button>
:show-album="showAlbum"
:show-artist="showArtist"
:show-position="showPosition"
:show-art="showArt"
:display-actions="displayActions"
:show-duration="showDuration"
:is-podcast="isPodcast"
></track-row>
</div>
<div v-if="paginateResults" class="ui center aligned basic segment desktop-and-up">
<pagination
:total="total"
:current="page"
:paginate-by="paginateBy"
v-on="$listeners">
</pagination>
</div>
</div>
<div
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
>
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>
<!-- For each item, build a row -->
<track-mobile-row
v-for="(track, index) in allTracks"
:track="track"
:key="track.id"
:index="index"
:tracks="allTracks"
:show-position="showPosition"
:show-art="showArt"
:show-duration="showDuration"
:is-artist="isArtist"
:is-album="isAlbum"
:is-podcast="isPodcast"
></track-mobile-row>
<div v-if="paginateResults" class="ui center aligned basic segment tablet-and-below">
<pagination
v-if="paginateResults"
:total="total"
:current="page"
:compact="true"
v-on="$listeners"></pagination>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import TrackRow from '@/components/audio/track/Row'
import Modal from '@/components/semantic/Modal'
import _ from "@/lodash";
import axios from "axios";
import TrackRow from "@/components/audio/track/Row";
import TrackMobileRow from "@/components/audio/track/MobileRow";
import Pagination from "@/components/Pagination";
export default {
props: {
tracks: {type: Array, required: false},
playable: {type: Boolean, required: false, default: false},
search: {type: Boolean, required: false, default: false},
nextUrl: {type: String, required: false, default: null},
artist: {type: Object, required: false},
filters: {type: Object, required: false, default: () => { return {}}},
displayPosition: {type: Boolean, default: false},
displayActions: {type: Boolean, default: true},
},
components: {
Modal,
TrackRow
TrackRow,
TrackMobileRow,
Pagination,
},
created () {
if (!this.tracks) {
this.loadMore('tracks/')
}
props: {
tracks: Array,
showAlbum: { type: Boolean, required: false, default: true },
showArtist: { type: Boolean, required: false, default: true },
showPosition: { type: Boolean, required: false, default: false },
showArt: { type: Boolean, required: false, default: true },
search: { type: Boolean, required: false, default: false },
filters: { type: Object, required: false, default: null },
nextUrl: { type: String, required: false, default: null },
displayActions: { type: Boolean, required: false, default: true },
showDuration: { type: Boolean, required: false, default: true },
isArtist: { type: Boolean, required: false, default: false },
isAlbum: { type: Boolean, required: false, default: false },
isPodcast: { type: Boolean, required: false, default: false },
paginateResults: { type: Boolean, required: false, default: true},
total: { type: Number, required: false},
page: {type: Number, required: false, default: 1},
paginateBy: {type: Number, required: false, default: 25}
},
data () {
data() {
return {
loadMoreUrl: this.nextUrl,
fetchDataUrl: this.nextUrl,
isLoading: false,
additionalTracks: [],
query: '',
}
query: "",
};
},
computed: {
allTracks () {
return (this.tracks || []).concat(this.additionalTracks)
}
allTracks() {
return (this.tracks || []).concat(this.additionalTracks);
},
labels() {
return {
title: this.$pgettext("*/*/*/Noun", "Title"),
album: this.$pgettext("*/*/*/Noun", "Album"),
artist: this.$pgettext("*/*/*/Noun", "Artist"),
};
},
},
methods: {
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
})
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>

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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

Wyświetl plik

@ -14,6 +14,16 @@
</button>
</div>
</div>
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
<track-table :is-artist="true" :show-position="false" :track-only="true" :tracks="tracks.slice(0,5)">
<template slot="header">
<h2>
<translate translate-context="Content/Artist/Title">New tracks by this artist</translate>
</h2>
<div class="ui hidden divider"></div>
</template>
</track-table>
</section>
<section v-if="isLoadingAlbums" class="ui vertical stripe segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</section>
@ -29,12 +39,6 @@
<translate translate-context="Content/*/Button.Label">Load more</translate>
</button>
</section>
<section v-if="tracks.length > 0" class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/Artist/Title">Tracks by this artist</translate>
</h2>
<track-table :display-position="true" :tracks="tracks" :next-url="nextTracksUrl"></track-table>
</section>
<section class="ui vertical stripe segment">
<h2>
<translate translate-context="Content/*/Title/Noun">User libraries</translate>

Wyświetl plik

@ -85,7 +85,13 @@
translate-context="Content/Radio/Table.Paragraph/Short">
%{ count } track matching combined filters
</h3>
<track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample" :playable="true"></track-table>
<track-table
v-if="checkResult.candidates.sample"
:tracks="checkResult.candidates.sample"
:playable="true"
:show-position="false"
:show-duration="false"
:display-actions="false"></track-table>
</template>
</section>
</div>

Wyświetl plik

@ -21,10 +21,10 @@
<input :id="f.name" v-if="f.type === 'list' && config[f.name]" :value="config[f.name].join(',')" type="hidden">
<div v-if="config[f.name]" class="ui menu">
<div
v-if="f.type === 'list'"
v-for="(v, index) in config[f.name]"
class="ui item"
:data-value="v">
:data-value="v"
:key="v">
<template v-if="config.names">
{{ config.names[index] }}
</template>

Wyświetl plik

@ -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>

Wyświetl plik

@ -1,6 +1,6 @@
<template>
<div :class="['ui', {'active': show}, {'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal']">
<i class="close inside icon"></i>
<div :class="additionalClasses.concat(['ui', {'active': show}, {'scrolling': scrolling} ,{'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal'])">
<i tabindex=0 class="close inside icon"></i>
<slot v-if="show">
</slot>
@ -15,6 +15,8 @@ export default {
props: {
show: {type: Boolean, required: true},
fullscreen: {type: Boolean, default: true},
scrolling: {type: Boolean, required: false, default: false},
additionalClasses: {type: Array, required: false, default: () => []}
},
data () {
return {
@ -61,6 +63,7 @@ export default {
this.control.modal('show')
this.focusTrap.activate()
this.focusTrap.unpause()
document.body.classList.add('scrolling')
} else {
if (this.control) {
this.$emit('hide')
@ -68,6 +71,7 @@ export default {
this.control.remove()
this.focusTrap.deactivate()
this.focusTrap.pause()
document.body.classList.remove('scrolling')
}
}
}

Wyświetl plik

@ -24,6 +24,7 @@ $bottom-player-height: 4rem;
@import "./components/_content_form.scss";
@import "./components/_copy_input.scss";
@import "./components/_empty_state.scss";
@import "./components/_favorite.scss";
@import "./components/_form.scss";
@import "./components/_file_upload.scss";
@import "./components/_fs_browser.scss";
@ -34,6 +35,7 @@ $bottom-player-height: 4rem;
@import "./components/_pagination.scss";
@import "./components/_placeholder.scss";
@import "./components/_play_button.scss";
@import "./components/_play_indicator.scss";
@import "./components/_player.scss";
@import "./components/_playlist_editor.scss";
@import "./components/_queue.scss";

Wyświetl plik

@ -117,3 +117,5 @@ $card-box-shadow: 0 1px 3px 0 #D4D4D5, 0 0 0 1px #D4D4D5 !default;
$dimmer-background: rgba(255, 255, 255, 0.85) !default;
$dimmer-color: var(--text-color) !default;
$border-color: rgba(34, 36, 38, 0.15) !default;

Wyświetl plik

@ -98,4 +98,4 @@ button.reset {
.ui.inverted.buttons .button:focus,
.ui.inverted.button:focus {
color: white;
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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; }

Wyświetl plik

@ -1,19 +1,251 @@
.component-track-table {
pre {
overflow-x: scroll;
.track-table {
> div {
display: flex;
align-items: center;
justify-content: space-between;
height: 3.5rem;
}
&.table-wrapper {
overflow: visible;
.content {
flex-grow: 1;
}
tr:not(:hover) {
.favorite-icon:not(.favorited),
.playlist-icon {
visibility: hidden;
}
}
.track {
display: block;
line-height: 2;
.row:not(.mobile) {
border-bottom: 1px solid var(--border-color);
}
}
.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;
}
}
}

Wyświetl plik

@ -59,7 +59,7 @@ a {
}
}
.tablet-and-below {
@include media(">=desktop") {
@include media(">=tablet") {
display: none !important;
}
}
@ -104,3 +104,10 @@ span.diff.removed {
.really.discrete {
color: var(--really-discrete-text-color);
}
.ui.inverted.dimmer {
background-color: var(--dimmer-background);
> .ui.dimmer {
color: var(--dimmer-color);
}
}

Wyświetl plik

@ -1,6 +1,6 @@
<template>
<section>
<channel-entries :limit="25" :filters="{channel: object.uuid, ordering: 'creation_date'}">
<channel-entries :default-cover="object.artist.cover" :is-podcast="object.artist.content_category === 'podcast'" :limit="25" :filters="{channel: object.uuid, ordering: 'creation_date'}">
</channel-entries>
</section>
</template>

Wyświetl plik

@ -49,7 +49,7 @@
:can-update="false"></rendered-description>
<div class="ui hidden divider"></div>
</div>
<channel-entries :key="String(episodesKey) + 'entries'" :default-cover='object.artist.cover' :limit='25' :filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}">
<channel-entries :is-podcast="isPodcast" :key="String(episodesKey) + 'entries'" :default-cover='object.artist.cover' :limit='25' :filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}">
<h2 class="ui header">
<translate key="1" v-if="isPodcast" translate-context="Content/Channel/Paragraph">Latest episodes</translate>
<translate key="2" v-else translate-context="Content/Channel/Paragraph">Latest tracks</translate>

Wyświetl plik

@ -42,7 +42,7 @@ module.exports = {
appleMobileWebAppStatusBarStyle: 'black',
workboxPluginMode: 'InjectManifest',
manifestOptions: {
display: 'minimal-ui',
display: 'standalone',
start_url: '.',
description: 'A social platform to enjoy and share music',
scope: "/",