chore(front): Update track detail views with new components
This commit is contained in:
parent
1f0ebb3367
commit
c57b47bdd3
|
@ -10,6 +10,7 @@ import axios from 'axios'
|
|||
|
||||
import AlbumCard from '~/components/album/Card.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
||||
|
@ -70,54 +71,52 @@ watch(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
front/src/components/album/Widget.vue
|
||||
<h3
|
||||
v-if="!!$slots.title"
|
||||
class="ui header"
|
||||
<h3
|
||||
v-if="!!$slots.title"
|
||||
class="ui header"
|
||||
>
|
||||
<slot name="title" />
|
||||
<span
|
||||
v-if="showCount"
|
||||
class="ui tiny circular label"
|
||||
>{{ count }}</span>
|
||||
</h3>
|
||||
<slot />
|
||||
<inline-search-bar
|
||||
v-if="search"
|
||||
v-model="query"
|
||||
@search="performSearch"
|
||||
/>
|
||||
<div style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<slot name="title" />
|
||||
<span
|
||||
v-if="showCount"
|
||||
class="ui tiny circular label"
|
||||
>{{ count }}</span>
|
||||
</h3>
|
||||
<slot />
|
||||
<inline-search-bar
|
||||
v-if="search"
|
||||
v-model="query"
|
||||
@search="performSearch"
|
||||
/>
|
||||
<div style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<album-card
|
||||
v-for="album in albums"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<slot
|
||||
v-if="!isLoading && albums.length === 0"
|
||||
name="empty-state"
|
||||
>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData"
|
||||
/>
|
||||
</slot>
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider" />
|
||||
<Button
|
||||
v-if="nextPage"
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
{{ t('components.audio.album.Widget.button.more') }}
|
||||
</Button>
|
||||
</template>
|
||||
<album-card
|
||||
v-for="album in albums"
|
||||
:key="album.id"
|
||||
:album="album"
|
||||
/>
|
||||
</div>
|
||||
<slot
|
||||
v-if="!isLoading && albums.length === 0"
|
||||
name="empty-state"
|
||||
>
|
||||
<empty-state
|
||||
:refresh="true"
|
||||
@refresh="fetchData"
|
||||
/>
|
||||
</slot>
|
||||
<template v-if="nextPage">
|
||||
<Spacer />
|
||||
<Button
|
||||
v-if="nextPage"
|
||||
primary
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
{{ t('components.audio.album.Widget.button.more') }}
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -130,6 +130,7 @@ watch(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<!-- TODO: Use activity.vue -->
|
||||
<div class="component-track-widget">
|
||||
<h3 v-if="!!$slots.title">
|
||||
<slot name="title" />
|
||||
|
@ -164,11 +165,7 @@ watch(
|
|||
v-else
|
||||
class="bi bi-vinyl-fill"
|
||||
/>
|
||||
<PlayButton
|
||||
class="play-overlay"
|
||||
:icon-only="true"
|
||||
:track="object.track"
|
||||
/>
|
||||
<!-- TODO: Add Playbutton overlay -->
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="track-title">
|
||||
|
|
|
@ -13,11 +13,17 @@ import axios from 'axios'
|
|||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||
import Popover from '~/components/ui/Popover.vue'
|
||||
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
|
||||
import updateQueryString from '~/composables/updateQueryString'
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
@ -40,6 +46,7 @@ const { report, getReportableObjects } = useReport()
|
|||
const track = ref<Track | null>(null)
|
||||
const artist = ref<Artist | null>(null)
|
||||
const showEmbedModal = ref(false)
|
||||
const showDeleteModal = ref(false)
|
||||
const libraries = ref([] as Library[])
|
||||
|
||||
const logger = useLogger()
|
||||
|
@ -106,215 +113,252 @@ const remove = async () => {
|
|||
}
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
watch(showDeleteModal, (newValue) => {
|
||||
if (newValue) {
|
||||
// NOTE: Explicitly close the popover when delete modal opens
|
||||
open.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
class="ui vertical segment"
|
||||
>
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||
</div>
|
||||
<template v-if="track">
|
||||
<section
|
||||
v-title="track.title"
|
||||
:class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']"
|
||||
<Layout stack main>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
/>
|
||||
<template v-if="track">
|
||||
<Layout flex>
|
||||
<img
|
||||
v-if="track.album && track.album.cover"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
<div class="ui basic padded segment">
|
||||
<div class="ui stackable grid row container">
|
||||
<div class="eight wide left aligned column">
|
||||
<h1 class="ui header">
|
||||
{{ track.title }}
|
||||
</h1>
|
||||
<span class="ui header">
|
||||
<i18n-t
|
||||
v-if="track.attributed_to"
|
||||
keypath="components.library.TrackBase.subtitle.with-uploader"
|
||||
>
|
||||
<a
|
||||
class="internal"
|
||||
:href="attributedToUrl"
|
||||
>
|
||||
<span class="symbol at" />{{ track.attributed_to.full_username }}
|
||||
</a>
|
||||
<time
|
||||
:title="track.creation_date"
|
||||
:datetime="track.creation_date"
|
||||
>
|
||||
{{ momentFormat(new Date(track.creation_date), 'LL') }}
|
||||
</time>
|
||||
</i18n-t>
|
||||
<i18n-t
|
||||
v-else
|
||||
keypath="components.library.TrackBase.subtitle.without-uploader"
|
||||
>
|
||||
<time
|
||||
:title="track.creation_date"
|
||||
:datetime="track.creation_date"
|
||||
>
|
||||
{{ momentFormat(new Date(track.creation_date), 'LL') }}
|
||||
</time>
|
||||
</i18n-t>
|
||||
</span>
|
||||
</div>
|
||||
<div class="eight wide right aligned column button-group">
|
||||
<PlayButton :track="track" />
|
||||
<TrackFavoriteIcon v-if="store.state.auth.authenticated" :track="track" />
|
||||
<TrackPlaylistIcon v-if="store.state.auth.authenticated" :track="track" />
|
||||
|
||||
<a
|
||||
v-if="upload"
|
||||
role="button"
|
||||
:aria-label="labels.download"
|
||||
:href="downloadUrl"
|
||||
target="_blank"
|
||||
class="ui basic circular icon button"
|
||||
:title="labels.download"
|
||||
>
|
||||
<i class="download icon" />
|
||||
</a>
|
||||
<semantic-modal
|
||||
v-if="isEmbedable"
|
||||
v-model:show="showEmbedModal"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.library.TrackBase.modal.embed.header') }}
|
||||
</h4>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<embed-wizard
|
||||
:id="track.id"
|
||||
type="track"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<Button color="secondary">
|
||||
{{ t('components.library.TrackBase.button.cancel') }}
|
||||
</Button>
|
||||
</div>
|
||||
</semantic-modal>
|
||||
<Layout stack style="flex: 1; gap: 8px;">
|
||||
<Layout flex no-gap style="align-items: baseline; margin-bottom: 24px;">
|
||||
<h1>{{ track.title }}</h1>
|
||||
<Spacer grow />
|
||||
<Button
|
||||
v-if="upload"
|
||||
:aria-label="labels.download"
|
||||
:to="downloadUrl"
|
||||
target="_blank"
|
||||
primary
|
||||
icon="bi-download"
|
||||
:title="labels.download"
|
||||
>
|
||||
{{ labels.download }}
|
||||
</Button>
|
||||
</Layout>
|
||||
<div class="meta">
|
||||
<template v-if="track.attributed_to">
|
||||
<Link
|
||||
:to="attributedToUrl"
|
||||
>
|
||||
<i class="bi bi-at" />
|
||||
{{ track.attributed_to.full_username }}
|
||||
</Link>
|
||||
<i class="bi bi-dot" />
|
||||
</template>
|
||||
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem>
|
||||
<Button v-if="domain != store.getters['instance/domain']" :href="track.fid" target="_blank" variant="outline" icon="bi-external-link">
|
||||
{{ t('components.library.TrackBase.link.domain', {domain: domain}) }}
|
||||
</Button>
|
||||
</PopoverItem>
|
||||
<PopoverItem>
|
||||
<Button v-if="isEmbedable" @click="showEmbedModal = !showEmbedModal" variant="outline" icon="bi-code">
|
||||
{{ t('components.library.TrackBase.button.embed') }}
|
||||
</Button>
|
||||
</PopoverItem>
|
||||
<PopoverItem>
|
||||
<Button
|
||||
:href="wikipediaUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
variant="outline"
|
||||
icon="bi-wikipedia"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.wikipedia') }}
|
||||
</Button>
|
||||
</PopoverItem>
|
||||
<PopoverItem>
|
||||
<Button v-if="discogsUrl"
|
||||
:href="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
variant="outline"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.discogs') }}
|
||||
</Button>
|
||||
</PopoverItem>
|
||||
<time
|
||||
:title="track.creation_date"
|
||||
:datetime="track.creation_date"
|
||||
>
|
||||
{{ momentFormat(new Date(track.creation_date), 'LL') }}
|
||||
</time>
|
||||
|
||||
<PopoverItem>
|
||||
<RouterLink
|
||||
v-if="track.is_local"
|
||||
:to="{name: 'library.tracks.edit', params: {id: track.id }}"
|
||||
class="basic item"
|
||||
>
|
||||
<Button v-if="track.is_local" :to="{ name: 'library.tracks.edit', params: { id: track.id } }" tag="router-link" variant="outline" icon="bi-pencil">
|
||||
{{ t('components.library.TrackBase.button.edit') }}
|
||||
</Button>
|
||||
</RouterLink>
|
||||
</PopoverItem>
|
||||
<!-- TODO: Make the following button dangerous. Btw, it's actually a modal triggered by a button! -->
|
||||
<Button
|
||||
v-if="artist && store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === store.state.auth.fullUsername"
|
||||
:class="['ui', {loading: isLoading}, 'item']"
|
||||
@confirm="remove()"
|
||||
>
|
||||
<i class="ui trash icon" />
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
<template #modal-header>
|
||||
<p>
|
||||
{{ t('components.library.TrackBase.modal.delete.header') }}
|
||||
</p>
|
||||
</template>
|
||||
<template #modal-content>
|
||||
<div>
|
||||
<p>
|
||||
{{ t('components.library.TrackBase.modal.delete.content.warning') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
<p>
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
</p>
|
||||
</template>
|
||||
</Button>
|
||||
<div class="divider" />
|
||||
<div
|
||||
v-for="obj in getReportableObjects({track})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
role="button"
|
||||
class="basic item"
|
||||
@click.stop.prevent="report(obj)"
|
||||
>
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<router-link
|
||||
v-if="store.state.auth.availablePermissions['library']"
|
||||
class="basic item"
|
||||
:to="{name: 'manage.library.tracks.detail', params: {id: track.id}}"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
{{ t('components.library.TrackBase.link.moderation') }}
|
||||
</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/track/${track.id}`)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
{{ t('components.library.TrackBase.link.django') }}
|
||||
</a>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="track.album">
|
||||
<i class="bi bi-dot" />
|
||||
<span>{{ track.album.title }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
<router-view
|
||||
v-if="track"
|
||||
:key="route.fullPath"
|
||||
:track="track"
|
||||
:object="track"
|
||||
object-type="track"
|
||||
@libraries-loaded="libraries = $event"
|
||||
|
||||
<Layout flex>
|
||||
<PlayButton
|
||||
class="vibrant"
|
||||
split
|
||||
:track="track"
|
||||
/>
|
||||
|
||||
<Spacer h grow />
|
||||
|
||||
<TrackFavoriteIcon v-if="store.state.auth.authenticated" :track="track" />
|
||||
<TrackPlaylistIcon v-if="store.state.auth.authenticated" :track="track" />
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="domain != store.getters['instance/domain']"
|
||||
:to="track.fid"
|
||||
target="_blank"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.domain', { domain }) }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="isEmbedable"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
icon="bi-code"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.embed') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
:to="wikipediaUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
icon="bi-wikipedia"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.wikipedia') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="discogsUrl"
|
||||
:to="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
icon="bi-box-arrow-up-right"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.discogs') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="track.is_local"
|
||||
icon="bi-pencil-fill"
|
||||
:to="{ name: 'library.tracks.edit', params: { id: track.id } }"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.edit') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="artist &&
|
||||
store.state.auth.authenticated &&
|
||||
artist.channel &&
|
||||
artist.attributed_to.full_username === store.state.auth.fullUsername"
|
||||
@click="showDeleteModal = true"
|
||||
icon="bi-trash"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
</PopoverItem>
|
||||
|
||||
<hr>
|
||||
|
||||
<PopoverItem
|
||||
v-for="obj in getReportableObjects({ track })"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click="report(obj)"
|
||||
icon="bi-flag"
|
||||
>
|
||||
{{ obj.label }}
|
||||
</PopoverItem>
|
||||
|
||||
<hr>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.availablePermissions['library']"
|
||||
:to="{
|
||||
name: 'manage.library.tracks.detail',
|
||||
params: { id: track.id }
|
||||
}"
|
||||
icon="bi-wrench"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.moderation') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.profile?.is_superuser"
|
||||
:to="store.getters['instance/absoluteUrl'](`/api/admin/music/track/${track.id}`)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon="bi-wrench"
|
||||
>
|
||||
{{ t('components.library.TrackBase.link.django') }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Modal
|
||||
v-if="isEmbedable"
|
||||
v-model="showEmbedModal"
|
||||
:title="t('components.library.TrackBase.modal.embed.header')"
|
||||
>
|
||||
<embed-wizard
|
||||
:id="track.id"
|
||||
type="track"
|
||||
/>
|
||||
|
||||
<template #actions>
|
||||
<Button
|
||||
secondary
|
||||
@click="showEmbedModal = false"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.cancel') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<router-view
|
||||
v-if="track"
|
||||
:key="route.fullPath"
|
||||
:track="track"
|
||||
:object="track"
|
||||
object-type="track"
|
||||
@libraries-loaded="libraries = $event"
|
||||
/>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<Modal
|
||||
v-model="showDeleteModal"
|
||||
:title="t('components.library.TrackBase.modal.delete.header')"
|
||||
destructive
|
||||
>
|
||||
<template #alert>
|
||||
<Alert red>
|
||||
{{ t('components.library.TrackBase.modal.delete.content.warning') }}
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<Button
|
||||
secondary
|
||||
@click="showDeleteModal = false"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
destructive
|
||||
:is-loading="isLoading"
|
||||
@click="remove()"
|
||||
>
|
||||
{{ t('components.library.TrackBase.button.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.channel-image {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border: none;
|
||||
}
|
||||
.meta {
|
||||
line-height: 48px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,8 @@ import axios from 'axios'
|
|||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||
import PlaylistWidget from '~/components/playlists/Widget.vue'
|
||||
import TagsList from '~/components/tags/List.vue'
|
||||
import Activity from '~/components/ui/Activity.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
||||
|
@ -57,240 +59,103 @@ watchEffect(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="track">
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui stackable grid row container">
|
||||
<div class="six wide column">
|
||||
<template v-if="upload">
|
||||
<img
|
||||
v-if="track.cover && track.cover.urls.large_square_crop"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](track.cover.urls.large_square_crop)"
|
||||
alt="Cover Image"
|
||||
class="ui fluid image track-cover-image"
|
||||
>
|
||||
<img
|
||||
v-else-if="track.album && track.album.cover && track.album.cover.urls.large_square_crop"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.large_square_crop)"
|
||||
alt="Cover Image"
|
||||
class="ui fluid image track-cover-image"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
src="../../assets/audio/default-cover.png"
|
||||
alt="Cover Image"
|
||||
class="ui fluid image track-cover-image"
|
||||
>
|
||||
<h3 class="ui header">
|
||||
<span v-if="track.artist_credit?.[0].artist?.content_category === 'music'">
|
||||
{{ t('components.library.TrackDetail.header.track') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackDetail.header.episode') }}
|
||||
</span>
|
||||
</h3>
|
||||
<table class="ui basic table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.track.duration') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<template v-if="upload.duration">
|
||||
{{ time.parse(upload.duration) }}
|
||||
</template>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackDetail.notApplicable') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.track.size') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<template v-if="upload.size">
|
||||
{{ humanSize(upload.size) }}
|
||||
</template>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackDetail.notApplicable') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.track.codec') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<template v-if="upload.extension">
|
||||
{{ upload.extension }}
|
||||
</template>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackDetail.notApplicable') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.track.bitrate.label') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<template v-if="upload.bitrate">
|
||||
{{ t('components.library.TrackDetail.table.track.bitrate.value', {bitrate: humanSize(upload.bitrate)}) }}
|
||||
</template>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackDetail.notApplicable') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.track.downloads') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
{{ track.downloads_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</div>
|
||||
<div class="ten wide column">
|
||||
<template v-if="track.tags && track.tags.length > 0">
|
||||
<TagsList :tags="track.tags" />
|
||||
<div class="ui hidden divider" />
|
||||
</template>
|
||||
<Layout stack v-if="track">
|
||||
<template v-if="track.tags && track.tags.length > 0">
|
||||
<TagsList :tags="track.tags" />
|
||||
</template>
|
||||
|
||||
<rendered-description
|
||||
:content="track.description"
|
||||
:can-update="false"
|
||||
/>
|
||||
<h2 class="ui header">
|
||||
{{ t('components.library.TrackDetail.header.release') }}
|
||||
</h2>
|
||||
<table class="ui basic table ellipsis-rows">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.release.artist') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<template
|
||||
v-for="ac in track.artist_credit"
|
||||
:key="ac.artist.id"
|
||||
>
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id }}"
|
||||
style="display: inline;"
|
||||
>
|
||||
{{ ac.credit }}
|
||||
</router-link>
|
||||
<span style="display: inline;">{{ ac.joinphrase }}</span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="track.album">
|
||||
<td>
|
||||
<span v-if="track.album.artist_credit?.[0].artist.content_category === 'music'">
|
||||
{{ t('components.library.TrackDetail.table.release.album') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackDetail.table.release.series') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<router-link :to="{name: 'library.albums.detail', params: {id: track.album.id}}">
|
||||
{{ track.album.title }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.release.year') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<template v-if="track.album && track.album.release_date">
|
||||
{{ momentFormat(new Date(track.album.release_date), 'Y') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('components.library.TrackDetail.notApplicable') }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.release.copyright') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<span
|
||||
v-if="track.copyright"
|
||||
:title="track.copyright"
|
||||
>{{ truncate(track.copyright, 50) }}</span>
|
||||
<template v-else>
|
||||
{{ t('components.library.TrackDetail.notApplicable') }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.release.license') }}
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<a
|
||||
v-if="license"
|
||||
:href="license.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ license.name }}</a>
|
||||
<span v-else>
|
||||
{{ t('components.library.TrackDetail.notApplicable') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!track.is_local">
|
||||
<td>
|
||||
{{ t('components.library.TrackDetail.table.release.url') }}
|
||||
</td>
|
||||
<td :title="track.fid">
|
||||
<a
|
||||
:href="track.fid"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ truncate(track.fid, 65) }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<a
|
||||
v-if="musicbrainzUrl"
|
||||
:href="musicbrainzUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<i class="external icon" />
|
||||
{{ t('components.library.TrackDetail.link.musicbrainz') }}
|
||||
</a>
|
||||
<h2 class="ui header">
|
||||
{{ t('components.library.TrackDetail.header.playlists') }}
|
||||
</h2>
|
||||
<playlist-widget
|
||||
:url="'playlists/'"
|
||||
:filters="{track: track.id, playable: true, ordering: '-modification_date'}"
|
||||
/>
|
||||
<Layout flex style="gap: 24px;">
|
||||
<Layout stack style="flex: 1; gap: 0;">
|
||||
<h2>Release Details</h2>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.release.artist')"
|
||||
:value="track.artist_credit.map(ac => ac.credit).join(', ')"
|
||||
:link="track.artist_credit.length > 0
|
||||
? {
|
||||
name: 'library.artists.detail',
|
||||
params: { id: track.artist_credit[0].artist.id }
|
||||
}
|
||||
: undefined"
|
||||
:is-first="true"
|
||||
/>
|
||||
<Activity
|
||||
:label="track.album?.artist_credit?.[0].artist.content_category === 'music'
|
||||
? t('components.library.TrackDetail.table.release.album')
|
||||
: t('components.library.TrackDetail.table.release.series')"
|
||||
:value="track.album?.title || t('components.library.TrackDetail.notApplicable')"
|
||||
:link="track.album
|
||||
? {
|
||||
name: 'library.albums.detail',
|
||||
params: { id: track.album.id }
|
||||
}
|
||||
: undefined"
|
||||
/>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.release.year')"
|
||||
:value="track.album?.release_date
|
||||
? momentFormat(new Date(track.album.release_date), 'Y')
|
||||
: t('components.library.TrackDetail.notApplicable')"
|
||||
/>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.release.copyright')"
|
||||
:value="track.copyright || t('components.library.TrackDetail.notApplicable')"
|
||||
/>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.release.license')"
|
||||
:value="license?.name || t('components.library.TrackDetail.notApplicable')"
|
||||
:is-last="true"
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
<h2 class="ui header">
|
||||
{{ t('components.library.TrackDetail.header.library') }}
|
||||
</h2>
|
||||
<library-widget
|
||||
:url="`tracks/${track.id}/libraries/`"
|
||||
@loaded="emit('libraries-loaded', $event)"
|
||||
>
|
||||
{{ t('components.library.TrackDetail.description.library') }}
|
||||
</library-widget>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<Layout stack style="flex: 1; gap: 0;">
|
||||
<h2>Track Details</h2>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.track.duration')"
|
||||
:value="upload?.duration ? time.parse(upload.duration) : t('components.library.TrackDetail.notApplicable')"
|
||||
:is-first="true"
|
||||
/>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.track.size')"
|
||||
:value="upload?.size ? humanSize(upload.size) : t('components.library.TrackDetail.notApplicable')"
|
||||
/>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.track.codec')"
|
||||
:value="upload?.extension || t('components.library.TrackDetail.notApplicable')"
|
||||
/>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.track.bitrate.label')"
|
||||
:value="upload?.bitrate
|
||||
? t('components.library.TrackDetail.table.track.bitrate.value', {bitrate: humanSize(upload.bitrate)})
|
||||
: t('components.library.TrackDetail.notApplicable')"
|
||||
/>
|
||||
<Activity
|
||||
:label="t('components.library.TrackDetail.table.track.downloads')"
|
||||
:value="track.downloads_count"
|
||||
:is-last="true"
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<h2>{{ t('components.library.TrackDetail.header.playlists') }}</h2>
|
||||
<playlist-widget
|
||||
:url="'playlists/'"
|
||||
:filters="{track: track.id, playable: true, ordering: '-modification_date'}"
|
||||
/>
|
||||
|
||||
<h2>{{ t('components.library.TrackDetail.header.library') }}</h2>
|
||||
<library-widget
|
||||
:url="`tracks/${track.id}/libraries/`"
|
||||
@loaded="emit('libraries-loaded', $event)"
|
||||
>
|
||||
{{ t('components.library.TrackDetail.description.library') }}
|
||||
</library-widget>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.channel-image {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,6 +7,8 @@ import { computed } from 'vue'
|
|||
import { useStore } from '~/store'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Card from '~/components/ui/Card.vue'
|
||||
import ActorLink from '~/components/common/ActorLink.vue'
|
||||
|
||||
interface Props {
|
||||
playlist: Playlist
|
||||
|
@ -26,53 +28,82 @@ const images = computed(() => {
|
|||
|
||||
return urls
|
||||
})
|
||||
|
||||
const goToPlaylist = () => {
|
||||
router.push({name: 'library.playlists.detail', params: {id: props.playlist.id}})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui app-card card">
|
||||
<div
|
||||
:class="['ui', 'head-image', 'squares']"
|
||||
@click="router.push({name: 'library.playlists.detail', params: {id: playlist.id }})"
|
||||
>
|
||||
<img
|
||||
v-for="(url, idx) in images"
|
||||
:key="idx"
|
||||
v-lazy="url"
|
||||
alt=""
|
||||
>
|
||||
<play-button
|
||||
:icon-only="true"
|
||||
:is-playable="playlist.is_playable"
|
||||
:button-classes="['ui', 'circular', 'large', 'vibrant', 'icon', 'button']"
|
||||
:playlist="playlist"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<strong>
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.playlists.detail', params: {id: playlist.id }}"
|
||||
>
|
||||
{{ playlist.name }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<div class="description">
|
||||
<actor-link
|
||||
:actor="playlist.actor"
|
||||
:avatar="false"
|
||||
class="left floated"
|
||||
<Card
|
||||
:title="playlist.name"
|
||||
:to="{ name: 'library.playlists.detail', params: { id: playlist.id } }"
|
||||
>
|
||||
<template #image>
|
||||
<div class="playlist-grid">
|
||||
<img
|
||||
v-for="(url, idx) in images"
|
||||
:key="idx"
|
||||
v-lazy="url"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
{{ t('components.playlists.Card.meta.tracks', playlist.tracks_count) }}
|
||||
<play-button
|
||||
class="right floated basic icon"
|
||||
:dropdown-only="true"
|
||||
</template>
|
||||
|
||||
<template #topright>
|
||||
<PlayButton
|
||||
iconOnly
|
||||
:is-playable="playlist.is_playable"
|
||||
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']"
|
||||
:playlist="playlist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="playlist-meta">
|
||||
<ActorLink
|
||||
:actor="playlist.actor"
|
||||
:avatar="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<div class="playlist-action">
|
||||
<span>{{ t('components.playlists.Card.meta.tracks', playlist.tracks_count) }}</span>
|
||||
<PlayButton
|
||||
dropdown-only
|
||||
:is-playable="playlist.is_playable"
|
||||
:playlist="playlist"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.playlist-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.playlist-grid img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.playlist-action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,10 @@ import useErrorHandler from '~/composables/useErrorHandler'
|
|||
|
||||
import PlaylistCard from '~/components/playlists/Card.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
|
||||
interface Props {
|
||||
filters: Record<string, unknown>
|
||||
|
@ -54,53 +58,53 @@ watch(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3
|
||||
v-if="!!$slots.title"
|
||||
class="ui header"
|
||||
>
|
||||
<Layout stack>
|
||||
<h3 v-if="!!$slots.title">
|
||||
<slot name="title" />
|
||||
</h3>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
|
||||
<Loader v-if="isLoading"/>
|
||||
|
||||
<Layout
|
||||
v-else-if="objects.length > 0"
|
||||
flex
|
||||
style="gap: 16px; flex-wrap: wrap;"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<div
|
||||
v-if="objects.length > 0"
|
||||
class="ui cards app-cards"
|
||||
>
|
||||
<playlist-card
|
||||
<PlaylistCard
|
||||
v-for="playlist in objects"
|
||||
:key="playlist.id"
|
||||
:playlist="playlist"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
</Layout>
|
||||
|
||||
<Alert
|
||||
v-else
|
||||
class="ui placeholder segment"
|
||||
blue
|
||||
>
|
||||
<div class="ui icon header">
|
||||
<i class="list icon" />
|
||||
<div>
|
||||
<i class="bi bi-list" />
|
||||
{{ t('components.playlists.Widget.placeholder.noPlaylists') }}
|
||||
</div>
|
||||
<Spacer />
|
||||
<Button
|
||||
v-if="store.state.auth.authenticated"
|
||||
icon="bi-card-list"
|
||||
primary
|
||||
@click="store.commit('playlists/chooseTrack', null)"
|
||||
>
|
||||
{{ t('components.playlists.Widget.button.create') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<template v-if="nextPage">
|
||||
<div class="ui hidden divider" />
|
||||
<Spacer v grow/>
|
||||
<Button
|
||||
v-if="nextPage"
|
||||
primary
|
||||
@click="fetchData(nextPage)"
|
||||
>
|
||||
{{ t('components.playlists.Widget.button.more') }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue