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

View File

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

View File

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

View File

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

View File

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

View File

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