chore(front): Update track detail views with new components

This commit is contained in:
ArneBo 2025-01-21 21:21:15 +01:00
parent 1f0ebb3367
commit c57b47bdd3
6 changed files with 492 additions and 552 deletions

View File

@ -10,6 +10,7 @@ import axios from 'axios'
import AlbumCard from '~/components/album/Card.vue' import AlbumCard from '~/components/album/Card.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Spacer from '~/components/ui/Spacer.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
@ -70,54 +71,52 @@ watch(
</script> </script>
<template> <template>
<div class="wrapper"> <h3
front/src/components/album/Widget.vue v-if="!!$slots.title"
<h3 class="ui header"
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" /> <div class="ui loader" />
<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> </div>
<slot <album-card
v-if="!isLoading && albums.length === 0" v-for="album in albums"
name="empty-state" :key="album.id"
> :album="album"
<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>
</div> </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> </template>

View File

@ -130,6 +130,7 @@ watch(
</script> </script>
<template> <template>
<!-- TODO: Use activity.vue -->
<div class="component-track-widget"> <div class="component-track-widget">
<h3 v-if="!!$slots.title"> <h3 v-if="!!$slots.title">
<slot name="title" /> <slot name="title" />
@ -164,11 +165,7 @@ watch(
v-else v-else
class="bi bi-vinyl-fill" class="bi bi-vinyl-fill"
/> />
<PlayButton <!-- TODO: Add Playbutton overlay -->
class="play-overlay"
:icon-only="true"
:track="object.track"
/>
</div> </div>
<div class="activity-content"> <div class="activity-content">
<div class="track-title"> <div class="track-title">

View File

@ -13,11 +13,17 @@ import axios from 'axios'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue' import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import EmbedWizard from '~/components/audio/EmbedWizard.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 PlayButton from '~/components/audio/PlayButton.vue'
import Button from '~/components/ui/Button.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 Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.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 updateQueryString from '~/composables/updateQueryString'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
@ -40,6 +46,7 @@ const { report, getReportableObjects } = useReport()
const track = ref<Track | null>(null) const track = ref<Track | null>(null)
const artist = ref<Artist | null>(null) const artist = ref<Artist | null>(null)
const showEmbedModal = ref(false) const showEmbedModal = ref(false)
const showDeleteModal = ref(false)
const libraries = ref([] as Library[]) const libraries = ref([] as Library[])
const logger = useLogger() const logger = useLogger()
@ -106,215 +113,252 @@ const remove = async () => {
} }
const open = ref(false) const open = ref(false)
watch(showDeleteModal, (newValue) => {
if (newValue) {
// NOTE: Explicitly close the popover when delete modal opens
open.value = false
}
})
</script> </script>
<template> <template>
<main> <Layout stack main>
<div <Loader
v-if="isLoading" v-if="isLoading"
v-title="labels.title" v-title="labels.title"
class="ui vertical segment" />
> <template v-if="track">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> <Layout flex>
</div> <img
<template v-if="track"> v-if="track.album && track.album.cover"
<section v-lazy="store.getters['instance/absoluteUrl'](track.album.cover.urls.medium_square_crop)"
v-title="track.title" alt=""
:class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" 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 <Layout stack style="flex: 1; gap: 8px;">
v-if="upload" <Layout flex no-gap style="align-items: baseline; margin-bottom: 24px;">
role="button" <h1>{{ track.title }}</h1>
:aria-label="labels.download" <Spacer grow />
:href="downloadUrl" <Button
target="_blank" v-if="upload"
class="ui basic circular icon button" :aria-label="labels.download"
:title="labels.download" :to="downloadUrl"
> target="_blank"
<i class="download icon" /> primary
</a> icon="bi-download"
<semantic-modal :title="labels.download"
v-if="isEmbedable" >
v-model:show="showEmbedModal" {{ labels.download }}
> </Button>
<h4 class="header"> </Layout>
{{ t('components.library.TrackBase.modal.embed.header') }} <div class="meta">
</h4> <template v-if="track.attributed_to">
<div class="scrolling content"> <Link
<div class="description"> :to="attributedToUrl"
<embed-wizard >
:id="track.id" <i class="bi bi-at" />
type="track" {{ track.attributed_to.full_username }}
/> </Link>
</div> <i class="bi bi-dot" />
</div> </template>
<div class="actions">
<Button color="secondary">
{{ t('components.library.TrackBase.button.cancel') }}
</Button>
</div>
</semantic-modal>
<Popover v-model:open="open"> <time
<template #default="{ toggleOpen }"> :title="track.creation_date"
<OptionsButton @click="toggleOpen" /> :datetime="track.creation_date"
</template> >
<template #items> {{ momentFormat(new Date(track.creation_date), 'LL') }}
<PopoverItem> </time>
<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>
<PopoverItem> <template v-if="track.album">
<RouterLink <i class="bi bi-dot" />
v-if="track.is_local" <span>{{ track.album.title }}</span>
:to="{name: 'library.tracks.edit', params: {id: track.id }}" </template>
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>
</div> </div>
</section>
<router-view <Layout flex>
v-if="track" <PlayButton
:key="route.fullPath" class="vibrant"
:track="track" split
:object="track" :track="track"
object-type="track" />
@libraries-loaded="libraries = $event"
<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> </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> </template>
<style scoped>
.channel-image {
width: 300px;
height: 300px;
border: none;
}
.meta {
line-height: 48px;
}
</style>

View File

@ -12,6 +12,8 @@ import axios from 'axios'
import LibraryWidget from '~/components/federation/LibraryWidget.vue' import LibraryWidget from '~/components/federation/LibraryWidget.vue'
import PlaylistWidget from '~/components/playlists/Widget.vue' import PlaylistWidget from '~/components/playlists/Widget.vue'
import TagsList from '~/components/tags/List.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' import useErrorHandler from '~/composables/useErrorHandler'
@ -57,240 +59,103 @@ watchEffect(() => {
</script> </script>
<template> <template>
<div v-if="track"> <Layout stack v-if="track">
<section class="ui vertical stripe segment"> <template v-if="track.tags && track.tags.length > 0">
<div class="ui stackable grid row container"> <TagsList :tags="track.tags" />
<div class="six wide column"> </template>
<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>
<rendered-description <Layout flex style="gap: 24px;">
:content="track.description" <Layout stack style="flex: 1; gap: 0;">
:can-update="false" <h2>Release Details</h2>
/> <Activity
<h2 class="ui header"> :label="t('components.library.TrackDetail.table.release.artist')"
{{ t('components.library.TrackDetail.header.release') }} :value="track.artist_credit.map(ac => ac.credit).join(', ')"
</h2> :link="track.artist_credit.length > 0
<table class="ui basic table ellipsis-rows"> ? {
<tbody> name: 'library.artists.detail',
<tr> params: { id: track.artist_credit[0].artist.id }
<td> }
{{ t('components.library.TrackDetail.table.release.artist') }} : undefined"
</td> :is-first="true"
<td class="right aligned"> />
<template <Activity
v-for="ac in track.artist_credit" :label="track.album?.artist_credit?.[0].artist.content_category === 'music'
:key="ac.artist.id" ? t('components.library.TrackDetail.table.release.album')
> : t('components.library.TrackDetail.table.release.series')"
<router-link :value="track.album?.title || t('components.library.TrackDetail.notApplicable')"
class="discrete link" :link="track.album
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id }}" ? {
style="display: inline;" name: 'library.albums.detail',
> params: { id: track.album.id }
{{ ac.credit }} }
</router-link> : undefined"
<span style="display: inline;">{{ ac.joinphrase }}</span> />
</template> <Activity
</td> :label="t('components.library.TrackDetail.table.release.year')"
</tr> :value="track.album?.release_date
<tr v-if="track.album"> ? momentFormat(new Date(track.album.release_date), 'Y')
<td> : t('components.library.TrackDetail.notApplicable')"
<span v-if="track.album.artist_credit?.[0].artist.content_category === 'music'"> />
{{ t('components.library.TrackDetail.table.release.album') }} <Activity
</span> :label="t('components.library.TrackDetail.table.release.copyright')"
<span v-else> :value="track.copyright || t('components.library.TrackDetail.notApplicable')"
{{ t('components.library.TrackDetail.table.release.series') }} />
</span> <Activity
</td> :label="t('components.library.TrackDetail.table.release.license')"
<td class="right aligned"> :value="license?.name || t('components.library.TrackDetail.notApplicable')"
<router-link :to="{name: 'library.albums.detail', params: {id: track.album.id}}"> :is-last="true"
{{ track.album.title }} />
</router-link> </Layout>
</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'}"
/>
<h2 class="ui header"> <Layout stack style="flex: 1; gap: 0;">
{{ t('components.library.TrackDetail.header.library') }} <h2>Track Details</h2>
</h2> <Activity
<library-widget :label="t('components.library.TrackDetail.table.track.duration')"
:url="`tracks/${track.id}/libraries/`" :value="upload?.duration ? time.parse(upload.duration) : t('components.library.TrackDetail.notApplicable')"
@loaded="emit('libraries-loaded', $event)" :is-first="true"
> />
{{ t('components.library.TrackDetail.description.library') }} <Activity
</library-widget> :label="t('components.library.TrackDetail.table.track.size')"
</div> :value="upload?.size ? humanSize(upload.size) : t('components.library.TrackDetail.notApplicable')"
</div> />
</section> <Activity
</div> :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> </template>
<style scoped>
.channel-image {
width: 300px;
height: 300px;
border: none;
}
</style>

View File

@ -7,6 +7,8 @@ import { computed } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Card from '~/components/ui/Card.vue'
import ActorLink from '~/components/common/ActorLink.vue'
interface Props { interface Props {
playlist: Playlist playlist: Playlist
@ -26,53 +28,82 @@ const images = computed(() => {
return urls return urls
}) })
const goToPlaylist = () => {
router.push({name: 'library.playlists.detail', params: {id: props.playlist.id}})
}
</script> </script>
<template> <template>
<div class="ui app-card card"> <Card
<div :title="playlist.name"
:class="['ui', 'head-image', 'squares']" :to="{ name: 'library.playlists.detail', params: { id: playlist.id } }"
@click="router.push({name: 'library.playlists.detail', params: {id: playlist.id }})" >
> <template #image>
<img <div class="playlist-grid">
v-for="(url, idx) in images" <img
:key="idx" v-for="(url, idx) in images"
v-lazy="url" :key="idx"
alt="" 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"
/> />
</div> </div>
</div> </template>
<div class="extra content">
{{ t('components.playlists.Card.meta.tracks', playlist.tracks_count) }} <template #topright>
<play-button <PlayButton
class="right floated basic icon" iconOnly
:dropdown-only="true"
:is-playable="playlist.is_playable" :is-playable="playlist.is_playable"
:dropdown-icon-classes="['ellipsis', 'horizontal', 'large really discrete']"
:playlist="playlist" :playlist="playlist"
/> />
</div> </template>
</div>
<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> </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>

View File

@ -12,6 +12,10 @@ import useErrorHandler from '~/composables/useErrorHandler'
import PlaylistCard from '~/components/playlists/Card.vue' import PlaylistCard from '~/components/playlists/Card.vue'
import Button from '~/components/ui/Button.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 { interface Props {
filters: Record<string, unknown> filters: Record<string, unknown>
@ -54,53 +58,53 @@ watch(
</script> </script>
<template> <template>
<div> <Layout stack>
<h3 <h3 v-if="!!$slots.title">
v-if="!!$slots.title"
class="ui header"
>
<slot name="title" /> <slot name="title" />
</h3> </h3>
<div
v-if="isLoading" <Loader v-if="isLoading"/>
class="ui inverted active dimmer"
<Layout
v-else-if="objects.length > 0"
flex
style="gap: 16px; flex-wrap: wrap;"
> >
<div class="ui loader" /> <PlaylistCard
</div>
<div
v-if="objects.length > 0"
class="ui cards app-cards"
>
<playlist-card
v-for="playlist in objects" v-for="playlist in objects"
:key="playlist.id" :key="playlist.id"
:playlist="playlist" :playlist="playlist"
/> />
</div> </Layout>
<div
<Alert
v-else v-else
class="ui placeholder segment" blue
> >
<div class="ui icon header"> <div>
<i class="list icon" /> <i class="bi bi-list" />
{{ t('components.playlists.Widget.placeholder.noPlaylists') }} {{ t('components.playlists.Widget.placeholder.noPlaylists') }}
</div> </div>
<Spacer />
<Button <Button
v-if="store.state.auth.authenticated" v-if="store.state.auth.authenticated"
icon="bi-card-list" icon="bi-card-list"
primary
@click="store.commit('playlists/chooseTrack', null)" @click="store.commit('playlists/chooseTrack', null)"
> >
{{ t('components.playlists.Widget.button.create') }} {{ t('components.playlists.Widget.button.create') }}
</Button> </Button>
</div> </Alert>
<template v-if="nextPage"> <template v-if="nextPage">
<div class="ui hidden divider" /> <Spacer v grow/>
<Button <Button
v-if="nextPage" v-if="nextPage"
primary
@click="fetchData(nextPage)" @click="fetchData(nextPage)"
> >
{{ t('components.playlists.Widget.button.more') }} {{ t('components.playlists.Widget.button.more') }}
</Button> </Button>
</template> </template>
</div> </Layout>
</template> </template>