See #170: UI for albums / series
This commit is contained in:
parent
2f0c01df1a
commit
9e447ab523
|
@ -210,6 +210,7 @@ def serialize_album_track(track):
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||||
|
# XXX: remove in 1.0, it's expensive and can work with a filter/api call
|
||||||
tracks = serializers.SerializerMethodField()
|
tracks = serializers.SerializerMethodField()
|
||||||
artist = serializers.SerializerMethodField()
|
artist = serializers.SerializerMethodField()
|
||||||
cover = cover_field
|
cover = cover_field
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div class="album-entries">
|
||||||
|
<div :class="[{active: currentTrack && isPlaying && track.id === currentTrack.id}, 'album-entry']" v-for="track in tracks" :key="track.id">
|
||||||
|
<div class="actions">
|
||||||
|
<play-button class="basic circular icon" :button-classes="['circular inverted orange icon button']" :discrete="true" :icon-only="true" :track="track"></play-button>
|
||||||
|
</div>
|
||||||
|
<div class="position">{{ prettyPosition(track.position) }}</div>
|
||||||
|
<div class="content ellipsis">
|
||||||
|
<router-link :to="{name: 'library.tracks.detail', params: {id: track.id}}" class="discrete link">
|
||||||
|
<strong>{{ track.title }}</strong><br>
|
||||||
|
</router-link>
|
||||||
|
</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>
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<router-link class="artist-label ui image label" :to="route">
|
||||||
|
<img :class="[{circular: artist.content_category != 'podcast'}]" v-if="artist.cover && artist.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](artist.cover.small_square_crop)" />
|
||||||
|
<i :class="[artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']" v-else />
|
||||||
|
{{ artist.name }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {momentFormat} from '@/filters'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
artist: Object,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
route () {
|
||||||
|
if (this.artist.channel) {
|
||||||
|
return {name: 'channels.detail', params: {id: this.artist.channel.uuid}}
|
||||||
|
}
|
||||||
|
return {name: 'library.artists.detail', params: {id: this.artist.id}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -15,7 +15,7 @@
|
||||||
<template v-if="!isLoading && objects.length === 0">
|
<template v-if="!isLoading && objects.length === 0">
|
||||||
<div class="ui placeholder segment">
|
<div class="ui placeholder segment">
|
||||||
<div class="ui icon header">
|
<div class="ui icon header">
|
||||||
<i class="compact disc icon"></i>
|
<i class="music icon"></i>
|
||||||
No results matching your query
|
No results matching your query
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@ import ChannelEntryCard from '@/components/audio/ChannelEntryCard'
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
filters: {type: Object, required: true},
|
filters: {type: Object, required: true},
|
||||||
limit: {type: Number, default: 5},
|
limit: {type: Number, default: 10},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ChannelEntryCard
|
ChannelEntryCard
|
||||||
|
|
|
@ -1,33 +1,67 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="channel-entry-card">
|
<div :class="[{active: currentTrack && isPlaying && entry.id === currentTrack.id}, 'channel-entry-card']">
|
||||||
<img @click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" class="channel-image image" v-if="cover && cover.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.square_crop)">
|
<div class="controls">
|
||||||
<img @click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})" class="channel-image image" v-else src="../../assets/audio/default-cover.png">
|
<play-button class="basic circular icon" :discrete="true" :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'inverted orange', 'icon', 'button']" :track="entry"></play-button>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||||
|
class="channel-image image"
|
||||||
|
v-if="cover && cover.original"
|
||||||
|
v-lazy="$store.getters['instance/absoluteUrl'](cover.square_crop)">
|
||||||
|
<span
|
||||||
|
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||||
|
class="channel-image image"
|
||||||
|
v-else-if="entry.artist.content_category === 'podcast'">#{{ entry.position }}</span>
|
||||||
|
<img
|
||||||
|
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||||
|
class="channel-image image"
|
||||||
|
v-else-if="entry.album && entry.album.cover && entry.album.cover.original"
|
||||||
|
v-lazy="$store.getters['instance/absoluteUrl'](entry.album.cover.square_crop)">
|
||||||
|
<img
|
||||||
|
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||||
|
class="channel-image image"
|
||||||
|
v-else
|
||||||
|
src="../../assets/audio/default-cover.png">
|
||||||
<div class="ellipsis content">
|
<div class="ellipsis content">
|
||||||
<strong>
|
<strong>
|
||||||
<router-link class="discrete link" :title="entry.title" :to="{name: 'library.tracks.detail', params: {id: entry.id}}">
|
<router-link class="discrete link" :title="entry.title" :to="{name: 'library.tracks.detail', params: {id: entry.id}}">
|
||||||
{{ entry.title }}
|
{{ entry.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
<div class="description">
|
<br>
|
||||||
<human-date :date="entry.creation_date"></human-date><template v-if="duration"> ·
|
<human-date class="really discrete" :date="entry.creation_date"></human-date>
|
||||||
<human-duration :duration="duration"></human-duration></template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="meta">
|
||||||
<play-button :icon-only="true" :is-playable="true" :button-classes="['ui', 'circular', 'orange', 'icon', 'button']" :track="entry"></play-button>
|
<template v-if="$store.state.auth.authenticated && $store.getters['favorites/isFavorite'](entry.id)">
|
||||||
|
<track-favorite-icon class="tiny" :track="entry"></track-favorite-icon>
|
||||||
|
</template>
|
||||||
|
<human-duration v-if="duration" :duration="duration"></human-duration>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
|
import { mapGetters } from "vuex"
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['entry'],
|
props: ['entry'],
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
|
TrackFavoriteIcon,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
...mapGetters({
|
||||||
|
currentTrack: "queue/currentTrack",
|
||||||
|
}),
|
||||||
|
|
||||||
|
isPlaying () {
|
||||||
|
return this.$store.state.player.playing
|
||||||
|
},
|
||||||
imageUrl () {
|
imageUrl () {
|
||||||
let url = '../../assets/audio/default-cover.png'
|
let url = '../../assets/audio/default-cover.png'
|
||||||
let cover = this.cover
|
let cover = this.cover
|
||||||
|
@ -42,9 +76,6 @@ export default {
|
||||||
if (this.entry.cover) {
|
if (this.entry.cover) {
|
||||||
return this.entry.cover
|
return this.entry.cover
|
||||||
}
|
}
|
||||||
if (this.entry.album && this.entry.album.cover) {
|
|
||||||
return this.entry.album.cover
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
duration () {
|
duration () {
|
||||||
let uploads = this.entry.uploads.filter((e) => {
|
let uploads = this.entry.uploads.filter((e) => {
|
||||||
|
@ -60,7 +91,4 @@ export default {
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.default-cover {
|
|
||||||
background-image: url("../../assets/audio/default-cover.png") !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="channel-serie-card">
|
<div class="channel-serie-card">
|
||||||
<div class="two-images">
|
<div class="two-images">
|
||||||
<img @click="$router.push({name: 'library.tracks.detail', params: {id: serie.id}})" class="channel-image" v-if="cover.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.square_crop)">
|
<img @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-if="cover.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.square_crop)">
|
||||||
<img @click="$router.push({name: 'library.tracks.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
<img @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||||
<img @click="$router.push({name: 'library.tracks.detail', params: {id: serie.id}})" class="channel-image" v-if="cover.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.square_crop)">
|
<img @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-if="cover.original" v-lazy="$store.getters['instance/absoluteUrl'](cover.square_crop)">
|
||||||
<img @click="$router.push({name: 'library.tracks.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
<img @click="$router.push({name: 'library.albums.detail', params: {id: serie.id}})" class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<strong>
|
<strong>
|
||||||
|
|
|
@ -5,7 +5,12 @@
|
||||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||||
<div class="ui loader"></div>
|
<div class="ui loader"></div>
|
||||||
</div>
|
</div>
|
||||||
<channel-serie-card v-for="serie in objects" :serie="serie" :key="serie.id" />
|
<template v-if="isPodcast">
|
||||||
|
<channel-serie-card v-for="serie in objects" :serie="serie" :key="serie.id" />
|
||||||
|
</template>
|
||||||
|
<div v-else class="ui app-cards cards">
|
||||||
|
<album-card v-for="album in objects" :album="album" :key="album.id" />
|
||||||
|
</div>
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
<button v-if="nextPage" @click="fetchData(nextPage)" :class="['ui', 'basic', 'button']">
|
||||||
|
@ -27,14 +32,18 @@
|
||||||
import _ from '@/lodash'
|
import _ from '@/lodash'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ChannelSerieCard from '@/components/audio/ChannelSerieCard'
|
import ChannelSerieCard from '@/components/audio/ChannelSerieCard'
|
||||||
|
import AlbumCard from '@/components/audio/album/Card'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
filters: {type: Object, required: true},
|
filters: {type: Object, required: true},
|
||||||
|
isPodcast: {type: Boolean, default: true},
|
||||||
limit: {type: Number, default: 5},
|
limit: {type: Number, default: 5},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ChannelSerieCard
|
ChannelSerieCard,
|
||||||
|
AlbumCard,
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4" v-if="track.uploads && track.uploads.length > 0">
|
<td colspan="4" v-if="track.uploads && track.uploads.length > 0">
|
||||||
{{ time.parse(track.uploads[0].duration) }}
|
<human-duration :duration="track.uploads[0].duration"></human-duration>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4" v-else>
|
<td colspan="4" v-else>
|
||||||
<translate translate-context="*/*/*">N/A</translate>
|
<translate translate-context="*/*/*">N/A</translate>
|
||||||
|
@ -49,7 +49,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from "vuex"
|
import { mapGetters } from "vuex"
|
||||||
import time from '@/utils/time'
|
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
|
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
|
||||||
import PlayButton from '@/components/audio/PlayButton'
|
import PlayButton from '@/components/audio/PlayButton'
|
||||||
|
@ -67,11 +66,6 @@ export default {
|
||||||
TrackPlaylistIcon,
|
TrackPlaylistIcon,
|
||||||
PlayButton
|
PlayButton
|
||||||
},
|
},
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentTrack: "queue/currentTrack",
|
currentTrack: "queue/currentTrack",
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<time :datetime="`${duration}s`">
|
<time :datetime="`${duration}s`">
|
||||||
<template v-if="durationObj.hours">{{ durationObj.hours|padDuration }}:</template>{{ durationObj.minutes|padDuration }}:{{ durationObj.seconds|padDuration }}
|
{{ duration | duration}}
|
||||||
</time>
|
</time>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import {secondsToObject} from '@/filters'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
duration: {required: true},
|
duration: {required: true},
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
durationObj () {
|
|
||||||
return secondsToObject(this.duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,131 +4,127 @@
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="object">
|
<template v-if="object">
|
||||||
<section :class="['ui', 'head', {'with-background': object.cover.original}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :style="headerStyle" v-title="object.title">
|
<section class="ui vertical stripe segment channel-serie">
|
||||||
<div class="segment-content">
|
<div class="ui stackable grid container">
|
||||||
<h2 class="ui center aligned icon header">
|
<div class="ui seven wide column">
|
||||||
<i class="circular inverted sound yellow icon"></i>
|
<div v-if="isSerie" class="padded basic segment">
|
||||||
<div class="content">
|
<div class="ui two column grid" v-if="isSerie">
|
||||||
{{ object.title }}
|
<div class="column">
|
||||||
<div v-html="subtitle"></div>
|
<div class="large two-images">
|
||||||
</div>
|
<img class="channel-image" v-if="object.cover && object.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.square_crop)">
|
||||||
</h2>
|
<img class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||||
<tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
|
<img class="channel-image" v-if="object.cover && object.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.square_crop)">
|
||||||
<div class="ui hidden divider"></div>
|
<img class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||||
<div class="header-buttons">
|
|
||||||
|
|
||||||
<div class="ui buttons">
|
|
||||||
<play-button class="orange" :tracks="object.tracks">
|
|
||||||
<translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
|
|
||||||
</play-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<modal v-if="publicLibraries.length > 0" :show.sync="showEmbedModal">
|
|
||||||
<div class="header">
|
|
||||||
<translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="description">
|
|
||||||
<embed-wizard type="album" :id="object.id" />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<div class="ui basic deny button">
|
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
<div class="ui buttons">
|
|
||||||
<button class="ui button" @click="$refs.dropdown.click()">
|
|
||||||
<translate translate-context="*/*/Button.Label/Noun">More…</translate>
|
|
||||||
</button>
|
|
||||||
<div class="ui floating dropdown icon button" ref="dropdown" v-dropdown>
|
|
||||||
<i class="dropdown icon"></i>
|
|
||||||
<div class="menu">
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
v-if="publicLibraries.length > 0"
|
|
||||||
@click="showEmbedModal = !showEmbedModal"
|
|
||||||
class="basic item">
|
|
||||||
<i class="code icon"></i>
|
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
|
|
||||||
</div>
|
</div>
|
||||||
<a :href="wikipediaUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
|
||||||
<i class="wikipedia w icon"></i>
|
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Search on Wikipedia</translate>
|
|
||||||
</a>
|
|
||||||
<a v-if="musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
|
||||||
<i class="external icon"></i>
|
|
||||||
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
|
|
||||||
</a>
|
|
||||||
<a :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
|
||||||
<i class="external icon"></i>
|
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate>
|
|
||||||
</a>
|
|
||||||
<router-link
|
|
||||||
v-if="object.is_local"
|
|
||||||
:to="{name: 'library.albums.edit', params: {id: object.id }}"
|
|
||||||
class="basic item">
|
|
||||||
<i class="edit icon"></i>
|
|
||||||
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
|
||||||
</router-link>
|
|
||||||
<dangerous-button
|
|
||||||
:class="['ui', {loading: isLoading}, 'item']"
|
|
||||||
v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername"
|
|
||||||
@confirm="remove()">
|
|
||||||
<i class="ui trash icon"></i>
|
|
||||||
<translate translate-context="*/*/*/Verb">Delete…</translate>
|
|
||||||
<p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this album?</translate></p>
|
|
||||||
<div slot="modal-content">
|
|
||||||
<p><translate translate-context="Content/Moderation/Paragraph">The album will be deleted, as well as any related files and data. This action is irreversible.</translate></p>
|
|
||||||
</div>
|
|
||||||
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
|
||||||
</dangerous-button>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
class="basic item"
|
|
||||||
v-for="obj in getReportableObjs({album: object})"
|
|
||||||
:key="obj.target.type + obj.target.id"
|
|
||||||
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
|
||||||
<i class="share icon" /> {{ obj.label }}
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}">
|
|
||||||
<i class="wrench icon"></i>
|
|
||||||
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
|
||||||
</router-link>
|
|
||||||
<a
|
|
||||||
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
|
||||||
class="basic item"
|
|
||||||
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)"
|
|
||||||
target="_blank" rel="noopener noreferrer">
|
|
||||||
<i class="wrench icon"></i>
|
|
||||||
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui column right aligned">
|
||||||
|
<tags-list v-if="object.tags && object.tags.length > 0" :tags="object.tags"></tags-list>
|
||||||
|
<div class="ui small hidden divider"></div>
|
||||||
|
<human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration>
|
||||||
|
<template v-if="totalTracks > 0">
|
||||||
|
<div class="ui hidden very small divider"></div>
|
||||||
|
<translate key="1" v-if="isSerie" translate-context="Content/Channel/Paragraph"
|
||||||
|
translate-plural="%{ count } episodes"
|
||||||
|
:translate-n="totalTracks"
|
||||||
|
:translate-params="{count: totalTracks}">
|
||||||
|
%{ count } episode
|
||||||
|
</translate>
|
||||||
|
<translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||||
|
</template>
|
||||||
|
<div class="ui small hidden divider"></div>
|
||||||
|
<play-button class="orange" :tracks="object.tracks"></play-button>
|
||||||
|
<div class="ui hidden horizontal divider"></div>
|
||||||
|
<album-dropdown
|
||||||
|
:object="object"
|
||||||
|
:public-libraries="publicLibraries"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:is-album="isAlbum"
|
||||||
|
:is-serie="isSerie"
|
||||||
|
:is-channel="isChannel"
|
||||||
|
:artist="artist"></album-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui small hidden divider"></div>
|
||||||
|
<header>
|
||||||
|
<h2 class="ui header" :title="object.title">
|
||||||
|
{{ object.title }}
|
||||||
|
</h2>
|
||||||
|
<artist-label :artist="artist"></artist-label>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div v-else class="ui center aligned text padded basic segment">
|
||||||
|
<img class="channel-image" v-if="object.cover && object.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](object.cover.square_crop)">
|
||||||
|
<img class="channel-image" v-else src="../../assets/audio/default-cover.png">
|
||||||
|
<div class="ui hidden divider"></div>
|
||||||
|
<header>
|
||||||
|
<h2 class="ui header" :title="object.title">
|
||||||
|
{{ object.title }}
|
||||||
|
</h2>
|
||||||
|
<artist-label class="rounded" :artist="artist"></artist-label>
|
||||||
|
</header>
|
||||||
|
<div class="ui small hidden divider"></div>
|
||||||
|
<template v-if="totalTracks > 0">
|
||||||
|
<div class="ui hidden very small divider"></div>
|
||||||
|
<translate key="1" v-if="isSerie" translate-context="Content/Channel/Paragraph"
|
||||||
|
translate-plural="%{ count } episodes"
|
||||||
|
:translate-n="totalTracks"
|
||||||
|
:translate-params="{count: totalTracks}">
|
||||||
|
%{ count } episode
|
||||||
|
</translate>
|
||||||
|
<translate v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate> ·
|
||||||
|
</template>
|
||||||
|
<human-duration v-if="totalDuration > 0" :duration="totalDuration"></human-duration>
|
||||||
|
<div class="ui small hidden divider"></div>
|
||||||
|
<play-button class="orange" :tracks="object.tracks"></play-button>
|
||||||
|
<div class="ui horizontal hidden divider"></div>
|
||||||
|
<album-dropdown
|
||||||
|
:object="object"
|
||||||
|
:public-libraries="publicLibraries"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:is-album="isAlbum"
|
||||||
|
:is-serie="isSerie"
|
||||||
|
:is-channel="isChannel"
|
||||||
|
: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>
|
||||||
|
<div class="ui small hidden divider"></div>
|
||||||
|
<template v-if="object.tags && object.tags.length > 0" >
|
||||||
|
<tags-list :tags="object.tags"></tags-list>
|
||||||
|
<div class="ui small hidden divider"></div>
|
||||||
|
</template>
|
||||||
|
<rendered-description
|
||||||
|
v-if="object.description"
|
||||||
|
:content="object.description"
|
||||||
|
:can-update="false"></rendered-description>
|
||||||
|
<router-link v-else-if="$store.state.auth.authenticated && object.is_local" :to="{name: 'library.albums.edit', params: {id: object.id }}">
|
||||||
|
<i class="pencil icon"></i>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">Add a description…</translate>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<rendered-description
|
||||||
|
v-if="isSerie"
|
||||||
|
:content="object.description"
|
||||||
|
:can-update="false"></rendered-description>
|
||||||
|
<div class="nine wide column">
|
||||||
|
<router-view v-if="object" :is-serie="isSerie" :artist="artist" :discs="discs" @libraries-loaded="libraries = $event" :object="object" object-type="album" :key="$route.fullPath"></router-view>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<router-view v-if="object" :discs="discs" @libraries-loaded="libraries = $event" :object="object" object-type="album" :key="$route.fullPath"></router-view>
|
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import logger from "@/logging"
|
import lodash from "@/lodash"
|
||||||
import backend from "@/audio/backend"
|
import backend from "@/audio/backend"
|
||||||
import PlayButton from "@/components/audio/PlayButton"
|
import PlayButton from "@/components/audio/PlayButton"
|
||||||
import EmbedWizard from "@/components/audio/EmbedWizard"
|
|
||||||
import Modal from '@/components/semantic/Modal'
|
|
||||||
import TagsList from "@/components/tags/List"
|
import TagsList from "@/components/tags/List"
|
||||||
import ReportMixin from '@/components/mixins/Report'
|
import ArtistLabel from '@/components/audio/ArtistLabel'
|
||||||
|
import AlbumDropdown from './AlbumDropdown'
|
||||||
const FETCH_URL = "albums/"
|
|
||||||
|
|
||||||
|
|
||||||
function groupByDisc(acc, track) {
|
function groupByDisc(acc, track) {
|
||||||
|
@ -143,13 +139,12 @@ function groupByDisc(acc, track) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [ReportMixin],
|
|
||||||
props: ["id"],
|
props: ["id"],
|
||||||
components: {
|
components: {
|
||||||
PlayButton,
|
PlayButton,
|
||||||
EmbedWizard,
|
|
||||||
Modal,
|
|
||||||
TagsList,
|
TagsList,
|
||||||
|
ArtistLabel,
|
||||||
|
AlbumDropdown,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -158,26 +153,24 @@ export default {
|
||||||
artist: null,
|
artist: null,
|
||||||
discs: [],
|
discs: [],
|
||||||
libraries: [],
|
libraries: [],
|
||||||
showEmbedModal: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
async created() {
|
||||||
this.fetchData()
|
await this.fetchData()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchData() {
|
async fetchData() {
|
||||||
var self = this
|
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let url = FETCH_URL + this.id + "/"
|
let albumResponse = await axios.get(`albums/${this.id}/`, {params: {refresh: 'true'}})
|
||||||
logger.default.debug('Fetching album "' + this.id + '"')
|
let artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
|
||||||
axios.get(url, {params: {refresh: 'true'}}).then(response => {
|
this.artist = artistResponse.data
|
||||||
self.object = backend.Album.clean(response.data)
|
if (this.artist.channel) {
|
||||||
self.discs = self.object.tracks.reduce(groupByDisc, [])
|
this.artist.channel.artist = this.artist
|
||||||
axios.get(`artists/${response.data.artist.id}/`).then(response => {
|
}
|
||||||
self.artist = response.data
|
this.object = backend.Album.clean(albumResponse.data)
|
||||||
})
|
this.discs = this.object.tracks.reduce(groupByDisc, [])
|
||||||
self.isLoading = false
|
this.isLoading = false
|
||||||
})
|
|
||||||
},
|
},
|
||||||
remove () {
|
remove () {
|
||||||
let self = this
|
let self = this
|
||||||
|
@ -193,9 +186,30 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
totalTracks () {
|
||||||
|
return this.object.tracks.length
|
||||||
|
},
|
||||||
|
isChannel () {
|
||||||
|
return this.object.artist.channel
|
||||||
|
},
|
||||||
|
isSerie () {
|
||||||
|
return this.object.artist.content_category === 'podcast'
|
||||||
|
},
|
||||||
|
isAlbum () {
|
||||||
|
return this.object.artist.content_category === 'music'
|
||||||
|
},
|
||||||
|
totalDuration () {
|
||||||
|
let durations = [0]
|
||||||
|
this.object.tracks.forEach((t) => {
|
||||||
|
if (t.uploads[0] && t.uploads[0].duration) {
|
||||||
|
durations.push(t.uploads[0].duration)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return lodash.sum(durations)
|
||||||
|
},
|
||||||
labels() {
|
labels() {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('*/*/*', 'Album')
|
title: this.$pgettext('*/*/*', 'Album'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
publicLibraries () {
|
publicLibraries () {
|
||||||
|
@ -203,39 +217,6 @@ export default {
|
||||||
return l.privacy_level === 'everyone'
|
return l.privacy_level === 'everyone'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
wikipediaUrl() {
|
|
||||||
return (
|
|
||||||
"https://en.wikipedia.org/w/index.php?search=" +
|
|
||||||
encodeURI(this.object.title + " " + this.object.artist.name)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
musicbrainzUrl() {
|
|
||||||
if (this.object.mbid) {
|
|
||||||
return "https://musicbrainz.org/release/" + this.object.mbid
|
|
||||||
}
|
|
||||||
},
|
|
||||||
discogsUrl() {
|
|
||||||
return (
|
|
||||||
"https://discogs.com/search/?type=release&title=" +
|
|
||||||
encodeURI(this.object.title) + "&artist=" +
|
|
||||||
encodeURI(this.object.artist.name)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headerStyle() {
|
|
||||||
if (!this.object.cover.original) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
"background-image: url(" +
|
|
||||||
this.$store.getters["instance/absoluteUrl"](this.object.cover.original) +
|
|
||||||
")"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
subtitle () {
|
|
||||||
let route = this.$router.resolve({name: 'library.artists.detail', params: {id: this.object.artist.id }})
|
|
||||||
let msg = this.$npgettext('Content/Album/Header.Title', 'Album containing %{ count } track, by <a class="internal" href="%{ artistUrl }">%{ artist }</a>', 'Album containing %{ count } tracks, by <a class="internal" href="%{ artistUrl }">%{ artist }</a>', this.object.tracks.length)
|
|
||||||
return this.$gettextInterpolate(msg, {count: this.object.tracks.length, artist: this.object.artist.name, artistUrl: route.href})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
id() {
|
id() {
|
||||||
|
|
|
@ -1,35 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="object">
|
<div v-if="object">
|
||||||
<template v-if="discs && discs.length > 1">
|
<h2 class="ui header">
|
||||||
<section v-for="(tracks, disc_number) in discs" class="ui vertical stripe segment">
|
<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, ordering: '-creation_date'}">
|
||||||
|
</channel-entries>
|
||||||
|
<template v-else-if="discs && discs.length > 1">
|
||||||
|
<div v-for="(tracks, discNumber) in discs" :key="discNumber">
|
||||||
|
<div class="ui hidden divider"></div>
|
||||||
<translate
|
<translate
|
||||||
tag="h2"
|
tag="h3"
|
||||||
class="left floated"
|
:translate-params="{number: discNumber + 1}"
|
||||||
:translate-params="{number: disc_number + 1}"
|
|
||||||
translate-context="Content/Album/"
|
translate-context="Content/Album/"
|
||||||
>Volume %{ number }</translate>
|
>Volume %{ number }</translate>
|
||||||
<play-button class="right floated orange" :tracks="tracks">
|
<album-entries :tracks="tracks"></album-entries>
|
||||||
<translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
|
</div>
|
||||||
</play-button>
|
|
||||||
<track-table :artist="object.artist" :display-position="true" :tracks="tracks"></track-table>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section class="ui vertical stripe segment">
|
<album-entries :tracks="object.tracks"></album-entries>
|
||||||
<h2>
|
|
||||||
<translate translate-context="*/*/*">Tracks</translate>
|
|
||||||
</h2>
|
|
||||||
<track-table v-if="object" :artist="object.artist" :display-position="true" :tracks="object.tracks"></track-table>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
<section class="ui vertical stripe segment">
|
<template v-if="!artist.channel && !isSerie">
|
||||||
<h2>
|
<h2>
|
||||||
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
<translate translate-context="Content/*/Title/Noun">User libraries</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'albums/' + object.id + '/libraries/'">
|
<library-widget @loaded="$emit('libraries-loaded', $event)" :url="'albums/' + object.id + '/libraries/'">
|
||||||
<translate slot="subtitle" translate-context="Content/Album/Paragraph">This album is present in the following libraries:</translate>
|
<translate slot="subtitle" translate-context="Content/Album/Paragraph">This album is present in the following libraries:</translate>
|
||||||
</library-widget>
|
</library-widget>
|
||||||
</section>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -41,11 +39,15 @@ import url from "@/utils/url"
|
||||||
import logger from "@/logging"
|
import logger from "@/logging"
|
||||||
import LibraryWidget from "@/components/federation/LibraryWidget"
|
import LibraryWidget from "@/components/federation/LibraryWidget"
|
||||||
import TrackTable from "@/components/audio/track/Table"
|
import TrackTable from "@/components/audio/track/Table"
|
||||||
|
import ChannelEntries from '@/components/audio/ChannelEntries'
|
||||||
|
import AlbumEntries from '@/components/audio/AlbumEntries'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["object", "libraries", "discs"],
|
props: ["object", "libraries", "discs", "isSerie", "artist"],
|
||||||
components: {
|
components: {
|
||||||
LibraryWidget,
|
LibraryWidget,
|
||||||
|
AlbumEntries,
|
||||||
|
ChannelEntries,
|
||||||
TrackTable
|
TrackTable
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
<modal v-if="isEmbedable" :show.sync="showEmbedModal">
|
||||||
|
<div class="header">
|
||||||
|
<translate translate-context="Popup/Album/Title/Verb">Embed this album on your website</translate>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="description">
|
||||||
|
<embed-wizard type="album" :id="object.id" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="ui basic deny button">
|
||||||
|
<translate translate-context="*/*/Button.Label/Verb">Cancel</translate>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
<div role="button" class="ui floating dropdown circular icon basic button" :title="labels.more" v-dropdown="{direction: 'downward'}">
|
||||||
|
<i class="ellipsis vertical icon"></i>
|
||||||
|
<div class="menu">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
v-if="isEmbedable"
|
||||||
|
@click="showEmbedModal = !showEmbedModal"
|
||||||
|
class="basic item">
|
||||||
|
<i class="code icon"></i>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">Embed</translate>
|
||||||
|
</div>
|
||||||
|
<a v-if="isAlbum && musicbrainzUrl" :href="musicbrainzUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
||||||
|
<i class="external icon"></i>
|
||||||
|
<translate translate-context="Content/*/*/Clickable, Verb">View on MusicBrainz</translate>
|
||||||
|
</a>
|
||||||
|
<a v-if="!isChannel && isAlbum" :href="discogsUrl" target="_blank" rel="noreferrer noopener" class="basic item">
|
||||||
|
<i class="external icon"></i>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">Search on Discogs</translate>
|
||||||
|
</a>
|
||||||
|
<router-link
|
||||||
|
v-if="object.is_local"
|
||||||
|
:to="{name: 'library.albums.edit', params: {id: object.id }}"
|
||||||
|
class="basic item">
|
||||||
|
<i class="edit icon"></i>
|
||||||
|
<translate translate-context="Content/*/Button.Label/Verb">Edit</translate>
|
||||||
|
</router-link>
|
||||||
|
<dangerous-button
|
||||||
|
:class="['ui', {loading: isLoading}, 'item']"
|
||||||
|
v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername"
|
||||||
|
@confirm="remove()">
|
||||||
|
<i class="ui trash icon"></i>
|
||||||
|
<translate translate-context="*/*/*/Verb">Delete…</translate>
|
||||||
|
<p slot="modal-header"><translate translate-context="Popup/Channel/Title">Delete this album?</translate></p>
|
||||||
|
<div slot="modal-content">
|
||||||
|
<p><translate translate-context="Content/Moderation/Paragraph">The album will be deleted, as well as any related files and data. This action is irreversible.</translate></p>
|
||||||
|
</div>
|
||||||
|
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
||||||
|
</dangerous-button>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
class="basic item"
|
||||||
|
v-for="obj in getReportableObjs({album: object, channel: artist.channel})"
|
||||||
|
:key="obj.target.type + obj.target.id"
|
||||||
|
@click.stop.prevent="$store.dispatch('moderation/report', obj.target)">
|
||||||
|
<i class="share icon" /> {{ obj.label }}
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<router-link class="basic item" v-if="$store.state.auth.availablePermissions['library']" :to="{name: 'manage.library.albums.detail', params: {id: object.id}}">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
|
||||||
|
</router-link>
|
||||||
|
<a
|
||||||
|
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
|
||||||
|
class="basic item"
|
||||||
|
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)"
|
||||||
|
target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import EmbedWizard from "@/components/audio/EmbedWizard"
|
||||||
|
import Modal from '@/components/semantic/Modal'
|
||||||
|
import ReportMixin from '@/components/mixins/Report'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ReportMixin],
|
||||||
|
props: {
|
||||||
|
isLoading: Boolean,
|
||||||
|
artist: Object,
|
||||||
|
object: Object,
|
||||||
|
publicLibraries: Array,
|
||||||
|
isAlbum: Boolean,
|
||||||
|
isChannel: Boolean,
|
||||||
|
isSerie: Boolean,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
EmbedWizard,
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showEmbedModal: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels() {
|
||||||
|
return {
|
||||||
|
more: this.$pgettext('*/*/Button.Label/Noun', "More…"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEmbedable () {
|
||||||
|
return (this.isChannel && this.artist.channel.actor) || this.publicLibraries.length > 0
|
||||||
|
},
|
||||||
|
|
||||||
|
musicbrainzUrl() {
|
||||||
|
if (this.object.mbid) {
|
||||||
|
return "https://musicbrainz.org/release/" + this.object.mbid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
discogsUrl() {
|
||||||
|
return (
|
||||||
|
"https://discogs.com/search/?type=release&title=" +
|
||||||
|
encodeURI(this.object.title) + "&artist=" +
|
||||||
|
encodeURI(this.object.artist.name)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -16,7 +16,7 @@
|
||||||
<translate translate-context="Content/*/*">Duration</translate>
|
<translate translate-context="Content/*/*">Duration</translate>
|
||||||
</td>
|
</td>
|
||||||
<td class="right aligned">
|
<td class="right aligned">
|
||||||
<template v-if="upload.duration">{{ time.parse(upload.duration) }}</template>
|
<template v-if="upload.duration">{{ upload.duration | duration }}</template>
|
||||||
<translate v-else translate-context="*/*/*">N/A</translate>
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<rendered-description
|
<rendered-description
|
||||||
:content="track.description"
|
:content="track.description"
|
||||||
can-update="false"></rendered-description>
|
:can-update="false"></rendered-description>
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<translate translate-context="Content/*/*">Release Details</translate>
|
<translate translate-context="Content/*/*">Release Details</translate>
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -154,7 +154,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import time from "@/utils/time"
|
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import url from "@/utils/url"
|
import url from "@/utils/url"
|
||||||
import logger from "@/logging"
|
import logger from "@/logging"
|
||||||
|
@ -173,7 +172,6 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
time,
|
|
||||||
id: this.track.id,
|
id: this.track.id,
|
||||||
licenseData: null
|
licenseData: null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="ui card">
|
|
||||||
<div class="content">
|
|
||||||
<div v-if="isLoading" class="ui vertical segment">
|
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
|
||||||
</div>
|
|
||||||
<template v-if="data.id">
|
|
||||||
<header class="header">
|
|
||||||
<a :href="getMusicbrainzUrl('artist', data.id)" target="_blank" :title="labels.musicbrainz">{{ data.name }}</a>
|
|
||||||
</header>
|
|
||||||
<div class="description">
|
|
||||||
<table class="ui very basic fixed single line compact table">
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="group in releasesGroups">
|
|
||||||
<td>
|
|
||||||
{{ group['first-release-date'] }}
|
|
||||||
</td>
|
|
||||||
<td colspan="3">
|
|
||||||
<a :href="getMusicbrainzUrl('release-group', group.id)" class="discrete link" target="_blank" :title="labels.musicbrainz">
|
|
||||||
{{ group.title }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Vue from "vue"
|
|
||||||
import CardMixin from "./CardMixin"
|
|
||||||
import time from "@/utils/time"
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
mixins: [CardMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
time
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels() {
|
|
||||||
return {
|
|
||||||
musicbrainz: this.$pgettext('Content/*/*/Clickable, Verb', "View on MusicBrainz")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type() {
|
|
||||||
return "artist"
|
|
||||||
},
|
|
||||||
releasesGroups() {
|
|
||||||
return this.data["release-group-list"].filter(r => {
|
|
||||||
return r.type === "Album"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.ui.card {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,47 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import logger from '@/logging'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
mbId: {type: String, required: true}
|
|
||||||
},
|
|
||||||
created: function () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
isLoading: false,
|
|
||||||
data: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
let self = this
|
|
||||||
this.isLoading = true
|
|
||||||
let url = 'providers/musicbrainz/' + this.type + 's/' + this.mbId + '/'
|
|
||||||
axios.get(url).then((response) => {
|
|
||||||
logger.default.info('successfully fetched', self.type, self.mbId)
|
|
||||||
self.data = response.data[self.type]
|
|
||||||
this.$emit('metadata-changed', self.data)
|
|
||||||
self.isLoading = false
|
|
||||||
}, (response) => {
|
|
||||||
logger.default.error('error while fetching', self.type, self.mbId)
|
|
||||||
self.isLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getMusicbrainzUrl (type, id) {
|
|
||||||
return 'https://musicbrainz.org/' + type + '/' + id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,71 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="ui card">
|
|
||||||
<div class="content">
|
|
||||||
<div v-if="isLoading" class="ui vertical segment">
|
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
|
||||||
</div>
|
|
||||||
<template v-if="data.id">
|
|
||||||
<div class="header">
|
|
||||||
<a :href="getMusicbrainzUrl('release', data.id)" target="_blank" :title="labels.musicbrainz">{{ data.title }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="meta">
|
|
||||||
<a :href="getMusicbrainzUrl('artist', data['artist-credit'][0]['artist']['id'])" target="_blank" :title="labels.musicbrainz">{{ data['artist-credit-phrase'] }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
<table class="ui very basic fixed single line compact table">
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="track in tracks">
|
|
||||||
<td>
|
|
||||||
{{ track.position }}
|
|
||||||
</td>
|
|
||||||
<td colspan="3">
|
|
||||||
<a :href="getMusicbrainzUrl('recording', track.id)" class="discrete link" target="_blank" :title="labels.musicbrainz">
|
|
||||||
{{ track.recording.title }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ time.parse(parseInt(track.length) / 1000) }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Vue from 'vue'
|
|
||||||
import CardMixin from './CardMixin'
|
|
||||||
import time from '@/utils/time'
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
mixins: [CardMixin],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
musicbrainz: this.$pgettext('Content/*/*/Clickable, Verb', 'View on MusicBrainz')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type () {
|
|
||||||
return 'release'
|
|
||||||
},
|
|
||||||
tracks () {
|
|
||||||
return this.data['medium-list'][0]['track-list']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.ui.card {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,158 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="ui form">
|
|
||||||
<div class="inline fields">
|
|
||||||
<div v-for="type in types" class="field">
|
|
||||||
<div class="ui radio checkbox">
|
|
||||||
<input type="radio" :value="type.value" v-model="currentType">
|
|
||||||
<label >{{ type.label }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui fluid search">
|
|
||||||
<div class="ui icon input">
|
|
||||||
<input class="prompt" :placeholder="labels.placeholder" type="text">
|
|
||||||
<i class="search icon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="results"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import jQuery from 'jquery'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
mbType: {type: String, required: false},
|
|
||||||
mbId: {type: String, required: false}
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
currentType: this.mbType || 'artist',
|
|
||||||
currentId: this.mbId || ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted: function () {
|
|
||||||
jQuery(this.$el).find('.ui.checkbox').checkbox()
|
|
||||||
this.setUpSearch()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
setUpSearch () {
|
|
||||||
var self = this
|
|
||||||
jQuery(this.$el).search({
|
|
||||||
minCharacters: 3,
|
|
||||||
onSelect (result, response) {
|
|
||||||
self.currentId = result.id
|
|
||||||
},
|
|
||||||
apiSettings: {
|
|
||||||
beforeXHR: function (xhrObject, s) {
|
|
||||||
xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
|
|
||||||
return xhrObject
|
|
||||||
},
|
|
||||||
onResponse: function (initialResponse) {
|
|
||||||
let category = self.currentTypeObject.value
|
|
||||||
let results = initialResponse[category + '-list'].map(r => {
|
|
||||||
let description = []
|
|
||||||
if (category === 'artist') {
|
|
||||||
if (r.type) {
|
|
||||||
description.push(r.type)
|
|
||||||
}
|
|
||||||
if (r.area) {
|
|
||||||
description.push(r.area.name)
|
|
||||||
} else if (r['begin-area']) {
|
|
||||||
description.push(r['begin-area'].name)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title: r.name,
|
|
||||||
id: r.id,
|
|
||||||
description: description.join(' - ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (category === 'release') {
|
|
||||||
if (r['medium-track-count']) {
|
|
||||||
description.push(
|
|
||||||
r['medium-track-count'] + ' tracks'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (r['artist-credit-phrase']) {
|
|
||||||
description.push(r['artist-credit-phrase'])
|
|
||||||
}
|
|
||||||
if (r['date']) {
|
|
||||||
description.push(r['date'])
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title: r.title,
|
|
||||||
id: r.id,
|
|
||||||
description: description.join(' - ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (category === 'recording') {
|
|
||||||
if (r['artist-credit-phrase']) {
|
|
||||||
description.push(r['artist-credit-phrase'])
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title: r.title,
|
|
||||||
id: r.id,
|
|
||||||
description: description.join(' - ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return {results: results}
|
|
||||||
},
|
|
||||||
url: this.searchUrl
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
placeholder: this.$pgettext('Content/Library/Input.Placeholder/Verb', 'Enter your search query…')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentTypeObject: function () {
|
|
||||||
let self = this
|
|
||||||
return this.types.filter(t => {
|
|
||||||
return t.value === self.currentType
|
|
||||||
})[0]
|
|
||||||
},
|
|
||||||
searchUrl: function () {
|
|
||||||
return this.$store.getters['instance/absoluteUrl']('api/v1/providers/musicbrainz/search/' + this.currentTypeObject.value + 's/?query={query}')
|
|
||||||
},
|
|
||||||
types: function () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
value: 'artist',
|
|
||||||
label: this.$pgettext('*/*/*/Noun', 'Artist')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'release',
|
|
||||||
label: this.$pgettext('*/*/*', 'Album')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'recording',
|
|
||||||
label: this.$pgettext('*/*/*/Noun', 'Track')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
currentType (newValue) {
|
|
||||||
this.setUpSearch()
|
|
||||||
this.$emit('type-changed', newValue)
|
|
||||||
},
|
|
||||||
currentId (newValue) {
|
|
||||||
this.$emit('id-changed', newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,5 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import time from '@/utils/time'
|
||||||
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
export function truncate (str, max, ellipsis, middle) {
|
export function truncate (str, max, ellipsis, middle) {
|
||||||
|
@ -89,6 +91,12 @@ export function padDuration (duration) {
|
||||||
|
|
||||||
Vue.filter('padDuration', padDuration)
|
Vue.filter('padDuration', padDuration)
|
||||||
|
|
||||||
|
export function duration (seconds) {
|
||||||
|
return time.parse(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.filter('duration', duration)
|
||||||
|
|
||||||
export function momentFormat (date, format) {
|
export function momentFormat (date, format) {
|
||||||
format = format || 'lll'
|
format = format || 'lll'
|
||||||
return moment(date).format(format)
|
return moment(date).format(format)
|
||||||
|
|
|
@ -420,6 +420,10 @@ input + .help {
|
||||||
.ui.very.small.divider {
|
.ui.very.small.divider {
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
.ui.horizontal.divider {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.queue.segment.player-focused #queue-grid #player {
|
.queue.segment.player-focused #queue-grid #player {
|
||||||
@include media("<desktop") {
|
@include media("<desktop") {
|
||||||
|
@ -562,39 +566,106 @@ input + .help {
|
||||||
}
|
}
|
||||||
|
|
||||||
// channels stuff
|
// channels stuff
|
||||||
|
@mixin two-images {
|
||||||
|
margin-right: 1em;
|
||||||
|
position: relative;
|
||||||
|
width: 3.5em;
|
||||||
|
height: 3.5em;
|
||||||
|
&.large {
|
||||||
|
width: 15em;
|
||||||
|
height: 15em;
|
||||||
|
img {
|
||||||
|
width: 11em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 2.5em;
|
||||||
|
position: absolute;
|
||||||
|
&:last-child {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
&:first-child {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.two-images {
|
||||||
|
@include two-images;
|
||||||
|
}
|
||||||
.channel-entry-card, .channel-serie-card {
|
.channel-entry-card, .channel-serie-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 auto 1em;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
.image {
|
.controls {
|
||||||
width: 3.5em;
|
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
.two-images {
|
.image {
|
||||||
width: 3.5em;
|
width: 3em;
|
||||||
height: 3.5em;
|
height: 3em;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
position: relative;
|
line-height: 3em;
|
||||||
img {
|
text-align: center;
|
||||||
width: 2.5em;
|
font-weight: bold;
|
||||||
position: absolute;
|
}
|
||||||
&:last-child {
|
.two-images {
|
||||||
bottom: 0;
|
@include two-images;
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
&:first-child {
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.album-entries {
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ui.artist-label {
|
||||||
|
.icon {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
&.rounded {
|
||||||
|
border-radius: 5em;
|
||||||
|
padding: 0.2em 0.75em 0.2em 0.2em;
|
||||||
|
line-height: 2em;
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.album-entry, .channel-entry-card {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.5em;
|
||||||
|
.meta {
|
||||||
|
text-align: right;
|
||||||
|
min-width: 7em;
|
||||||
|
}
|
||||||
|
> div {
|
||||||
|
padding: 0.25em;
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background: rgba(155, 155, 155, 0.2);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: rgba(155, 155, 155, 0.1);
|
||||||
|
}
|
||||||
|
.favorite-icon.tiny.button {
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
.channel-image {
|
.channel-image {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
.discrete {
|
.discrete {
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
.really.discrete {
|
||||||
|
color: rgba(0, 0, 0, 0.57);
|
||||||
|
}
|
||||||
.playlist.card {
|
.playlist.card {
|
||||||
.attached.button {
|
.attached.button {
|
||||||
background-color: rgb(243, 244, 245);
|
background-color: rgb(243, 244, 245);
|
||||||
|
|
|
@ -9,9 +9,16 @@ function pad (val) {
|
||||||
export default {
|
export default {
|
||||||
parse: function (sec) {
|
parse: function (sec) {
|
||||||
let min = 0
|
let min = 0
|
||||||
|
let hours = Math.floor(sec/3600)
|
||||||
|
if (hours >= 1) {
|
||||||
|
sec = sec % 3600
|
||||||
|
}
|
||||||
min = Math.floor(sec / 60)
|
min = Math.floor(sec / 60)
|
||||||
sec = sec - min * 60
|
sec = sec - min * 60
|
||||||
return pad(min) + ':' + pad(sec)
|
if (hours >= 1) {
|
||||||
|
return hours + ':' + pad(min) + ':' + pad(sec)
|
||||||
|
}
|
||||||
|
return min + ':' + pad(sec)
|
||||||
},
|
},
|
||||||
durationFormatted (v) {
|
durationFormatted (v) {
|
||||||
let duration = parseInt(v)
|
let duration = parseInt(v)
|
||||||
|
|
|
@ -240,7 +240,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="object.duration">
|
<template v-if="object.duration">
|
||||||
{{ time.parse(object.duration) }}
|
{{ object.duration | duration }}
|
||||||
</template>
|
</template>
|
||||||
<translate v-else translate-context="*/*/*">N/A</translate>
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -206,7 +206,8 @@
|
||||||
<translate translate-context="Content/Channels/Link">Overview</translate>
|
<translate translate-context="Content/Channels/Link">Overview</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="item" :exact="true" :to="{name: 'channels.detail.episodes', params: {id: id}}">
|
<router-link class="item" :exact="true" :to="{name: 'channels.detail.episodes', params: {id: id}}">
|
||||||
<translate translate-context="Content/Channels/*">Episodes</translate>
|
<translate key="1" v-if="isPodcast" translate-context="Content/Channels/*">Episodes</translate>
|
||||||
|
<translate key="2" v-else translate-context="*/*/*">Tracks</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
|
@ -313,6 +314,9 @@ export default {
|
||||||
isOwner () {
|
isOwner () {
|
||||||
return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername
|
return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername
|
||||||
},
|
},
|
||||||
|
isPodcast () {
|
||||||
|
return this.object.artist.content_category === 'podcast'
|
||||||
|
},
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('*/*/*', 'Channel')
|
title: this.$pgettext('*/*/*', 'Channel')
|
||||||
|
|
|
@ -51,13 +51,16 @@
|
||||||
</div>
|
</div>
|
||||||
<channel-entries :key="String(episodesKey) + 'entries'" :filters="{channel: object.uuid, ordering: '-creation_date'}">
|
<channel-entries :key="String(episodesKey) + 'entries'" :filters="{channel: object.uuid, ordering: '-creation_date'}">
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
<translate translate-context="Content/Channel/Paragraph">Latest episodes</translate>
|
<translate key="1" v-if="isPodcast" translate-context="Content/Channel/Paragraph">Latest episodes</translate>
|
||||||
|
<translate key="2" v-else translate-context="Content/Channel/Paragraph">Latest tracks</translate>
|
||||||
</h2>
|
</h2>
|
||||||
</channel-entries>
|
</channel-entries>
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
<channel-series :key="String(seriesKey) + 'series'" :filters="seriesFilters">
|
<channel-series :key="String(seriesKey) + 'series'" :filters="seriesFilters" :is-podcast="isPodcast">
|
||||||
<h2 class="ui with-actions header">
|
<h2 class="ui with-actions header">
|
||||||
<translate translate-context="Content/Channel/Paragraph">Series</translate>
|
|
||||||
|
<translate key="1" v-if="isPodcast" translate-context="Content/Channel/Paragraph">Series</translate>
|
||||||
|
<translate key="2" v-else translate-context="*/*/*">Albums</translate>
|
||||||
<div class="actions" v-if="isOwner">
|
<div class="actions" v-if="isOwner">
|
||||||
<a @click.stop.prevent="$refs.albumModal.show = true">
|
<a @click.stop.prevent="$refs.albumModal.show = true">
|
||||||
<i class="plus icon"></i>
|
<i class="plus icon"></i>
|
||||||
|
@ -114,6 +117,9 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isPodcast () {
|
||||||
|
return this.object.artist.content_category === 'podcast'
|
||||||
|
},
|
||||||
isOwner () {
|
isOwner () {
|
||||||
return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername
|
return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername
|
||||||
},
|
},
|
||||||
|
|
|
@ -165,7 +165,7 @@
|
||||||
<i class="question circle outline icon"></i>
|
<i class="question circle outline icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="scope.obj.duration">{{ time.parse(scope.obj.duration) }}</td>
|
<td v-if="scope.obj.duration">{{ scope.obj.duration | duration }}</td>
|
||||||
<td v-else>
|
<td v-else>
|
||||||
<translate translate-context="*/*/*">N/A</translate>
|
<translate translate-context="*/*/*">N/A</translate>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -90,11 +90,11 @@ describe('store/player', () => {
|
||||||
describe('getters', () => {
|
describe('getters', () => {
|
||||||
it('durationFormatted', () => {
|
it('durationFormatted', () => {
|
||||||
const state = { duration: 12.51 }
|
const state = { duration: 12.51 }
|
||||||
expect(store.getters['durationFormatted'](state)).to.equal('00:13')
|
expect(store.getters['durationFormatted'](state)).to.equal('0:13')
|
||||||
})
|
})
|
||||||
it('currentTimeFormatted', () => {
|
it('currentTimeFormatted', () => {
|
||||||
const state = { currentTime: 12.51 }
|
const state = { currentTime: 12.51 }
|
||||||
expect(store.getters['currentTimeFormatted'](state)).to.equal('00:13')
|
expect(store.getters['currentTimeFormatted'](state)).to.equal('0:13')
|
||||||
})
|
})
|
||||||
it('progress', () => {
|
it('progress', () => {
|
||||||
const state = { currentTime: 4, duration: 10 }
|
const state = { currentTime: 4, duration: 10 }
|
||||||
|
|
Loading…
Reference in New Issue