refactor(front): Playlist pages
This commit is contained in:
parent
9b758c1a7e
commit
8e5ddbfa1b
|
@ -13,13 +13,15 @@ interface Props {
|
|||
admin?: boolean
|
||||
displayName?: boolean
|
||||
truncateLength?: number
|
||||
discrete?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
avatar: true,
|
||||
admin: false,
|
||||
displayName: false,
|
||||
truncateLength: 30
|
||||
truncateLength: 30,
|
||||
discrete: false
|
||||
})
|
||||
|
||||
const { displayName, actor, truncateLength, admin, avatar } = toRefs(props)
|
||||
|
@ -56,9 +58,9 @@ const url = computed(() => {
|
|||
:to="url"
|
||||
:title="actor.full_username"
|
||||
class="username"
|
||||
solid
|
||||
secondary
|
||||
round
|
||||
:solid="!discrete"
|
||||
:secondary="!discrete"
|
||||
:round="!discrete"
|
||||
>
|
||||
<span class="center">
|
||||
<actor-avatar
|
||||
|
|
|
@ -46,7 +46,7 @@ const title = computed(() => isFavorite.value
|
|||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
secondary
|
||||
ghost
|
||||
icon="bi-heart-fill"
|
||||
:class="['ui', 'favorite-icon', {'pink': isFavorite}, {'favorited': isFavorite}, 'basic', 'circular', 'icon', {'really': !border}, 'button']"
|
||||
:aria-label="title"
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Playlist } from '~/types'
|
|||
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import defaultCover from '~/assets/audio/default-cover.png'
|
||||
import { computed } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
@ -29,6 +29,28 @@ const images = computed(() => {
|
|||
return urls
|
||||
})
|
||||
|
||||
|
||||
const bgcolors = ref([
|
||||
'#f2efef',
|
||||
'#eee9e9',
|
||||
'#ddd9d9',
|
||||
'#cfcaca',
|
||||
'#b3afaf',
|
||||
'#908888',
|
||||
'#605656',
|
||||
'#4d4547',
|
||||
'#292525',
|
||||
'#403a3b',
|
||||
'#322f2f'
|
||||
]);
|
||||
|
||||
function shuffleArray(array: string[]): string[] {
|
||||
return [...array].sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
const randomizedColors = computed(() => shuffleArray(bgcolors.value));
|
||||
|
||||
|
||||
const goToPlaylist = () => {
|
||||
router.push({name: 'library.playlists.detail', params: {id: props.playlist.id}})
|
||||
}
|
||||
|
@ -45,7 +67,8 @@ const goToPlaylist = () => {
|
|||
v-for="(url, idx) in images"
|
||||
:key="idx"
|
||||
v-lazy="url"
|
||||
alt=""
|
||||
:alt="playlist.name"
|
||||
:style="{ backgroundColor: randomizedColors[idx % randomizedColors.length] }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import type { Playlist } from '~/types'
|
||||
|
||||
import PlaylistCard from '~/components/playlists/Card.vue'
|
||||
import PlaylistsCard from '~/components/playlists/Card.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
|
||||
interface Props {
|
||||
playlists: Playlist[]
|
||||
|
@ -11,13 +12,11 @@ defineProps<Props>()
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="playlists.length > 0">
|
||||
<div class="ui app-cards cards">
|
||||
<playlist-card
|
||||
v-for="playlist in playlists"
|
||||
:key="playlist.id"
|
||||
:playlist="playlist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Layout grid v-if="playlists.length > 0">
|
||||
<PlaylistsCard
|
||||
v-for="playlist in playlists"
|
||||
:key="playlist.id"
|
||||
:playlist="playlist"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -4,8 +4,17 @@ import type { Playlist } from '~/types'
|
|||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useStore } from '~/store'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Popover from '~/components/ui/Popover.vue'
|
||||
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'import'): void
|
||||
(e: 'export'): void
|
||||
|
@ -53,6 +62,18 @@ const exportPlaylist = async () => {
|
|||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const deletePlaylist = async () => {
|
||||
try {
|
||||
await axios.delete(`playlists/${props.playlist.id}/`)
|
||||
store.dispatch('playlists/fetchOwn')
|
||||
return router.push({ path: '/library' })
|
||||
} catch (error) {
|
||||
useErrorHandler(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const patchPlaylist = async () => {
|
||||
const url = exportUrl.value
|
||||
|
||||
|
@ -75,37 +96,60 @@ const triggerFileInput = () => {
|
|||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const open = ref(false)
|
||||
const showEmbedModal = ref(false)
|
||||
const showDeleteModal = ref(false)
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
<button
|
||||
v-dropdown="{direction: 'downward'}"
|
||||
class="ui floating dropdown circular icon basic button"
|
||||
:title="labels.more"
|
||||
>
|
||||
<i class="ellipsis vertical icon" />
|
||||
<div class="menu">
|
||||
<div
|
||||
role="button"
|
||||
class="basic item"
|
||||
:title="t('components.playlists.PlaylistDropdown.button.export.description')"
|
||||
@click="exportPlaylist"
|
||||
>
|
||||
<i class="upload icon" />
|
||||
{{ labels.export }}
|
||||
</div>
|
||||
<div
|
||||
v-if="store.state.auth.authenticated && playlist.actor.full_username !== store.state.auth.fullUsername"
|
||||
role="button"
|
||||
class="basic item"
|
||||
:title="t('components.playlists.PlaylistDropdown.button.import.description')"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<i class="download icon" />
|
||||
{{ labels.import }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
@click="edit = !edit"
|
||||
icon="bi-pencil"
|
||||
>
|
||||
<template v-if="edit">
|
||||
{{ t('views.playlists.Detail.button.stopEdit') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('views.playlists.Detail.button.edit') }}
|
||||
</template>
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
|
||||
icon="bi-code-slash"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.embed') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem destructive
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
icon="bi-trash"
|
||||
:action="deletePlaylist"
|
||||
@click="showDeleteModal = !showDeleteModal"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.delete') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
icon="bi-download"
|
||||
@click="exportPlaylist"
|
||||
>
|
||||
{{ labels.export }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.authenticated && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
icon="bi-upload"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
{{ labels.import }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<!-- Hidden file input, triggered by the button click -->
|
||||
<input
|
||||
|
@ -115,4 +159,50 @@ const triggerFileInput = () => {
|
|||
@change="patchPlaylist"
|
||||
>
|
||||
</span>
|
||||
|
||||
<Modal
|
||||
v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
|
||||
v-model="showEmbedModal"
|
||||
title="t('views.playlists.Detail.modal.embed.header')"
|
||||
>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<embed-wizard
|
||||
:id="playlist.id"
|
||||
type="playlist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<Button variant="outline">
|
||||
{{ t('views.playlists.Detail.button.cancel') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model="showDeleteModal"
|
||||
destructive
|
||||
:title="t('views.playlists.Detail.modal.delete.header')"
|
||||
>
|
||||
<template #alert>
|
||||
<Alert red>
|
||||
<p>
|
||||
{{ t('views.playlists.Detail.modal.delete.content.warning') }}
|
||||
</p>
|
||||
</Alert>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button @click="showDeleteModal = false">
|
||||
{{ t('views.playlists.Detail.button.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
destructive
|
||||
@click="deletePlaylist"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.confirm') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
</template>
|
||||
|
|
|
@ -76,6 +76,7 @@ const attributes = computed(() =>
|
|||
|
||||
<!-- Content -->
|
||||
|
||||
<!--TODO: Pass Alert colors through to card.vue -->
|
||||
<Alert blue v-if="$slots.alert" :class="$style.alert">
|
||||
<slot name="alert" />
|
||||
</Alert>
|
||||
|
|
|
@ -9,12 +9,16 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
.row:not(.mobile) {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: var(--fw-gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.track-row.row {
|
||||
align-content: center;
|
||||
&:last-child {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.track-row,
|
||||
|
@ -39,7 +43,6 @@
|
|||
visibility: visible;
|
||||
}
|
||||
.actions {
|
||||
display: block;
|
||||
max-width: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,35 @@
|
|||
<script setup lang="ts">
|
||||
import type { PlaylistTrack, Playlist } from '~/types'
|
||||
import type { PlaylistTrack, Playlist, Library } from '~/types'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
import { momentFormat } from '~/utils/filters'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import defaultCover from '~/assets/audio/default-cover.png'
|
||||
import ActorLink from '~/components/common/ActorLink.vue'
|
||||
import PlaylistEditor from '~/components/playlists/Editor.vue'
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import HumanDate from '~/components/common/HumanDate.vue'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
|
||||
import PlaylistDropdown from '~/components/playlists/PlaylistDropdown.vue'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
||||
interface Events {
|
||||
(e: 'libraries-loaded', libraries: Library[]): void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: number
|
||||
defaultEdit?: boolean
|
||||
|
@ -65,6 +76,41 @@ const fetchData = async () => {
|
|||
|
||||
fetchData()
|
||||
|
||||
const images = computed(() => {
|
||||
const urls = playlist.value?.album_covers.slice(0, 4).map(url => store.getters['instance/absoluteUrl'](url)) || []
|
||||
|
||||
while (urls.length < 4) {
|
||||
urls.push(defaultCover)
|
||||
}
|
||||
|
||||
return urls
|
||||
})
|
||||
|
||||
const bgcolors = ref([
|
||||
'#f2efef',
|
||||
'#eee9e9',
|
||||
'#ddd9d9',
|
||||
'#cfcaca',
|
||||
'#b3afaf',
|
||||
'#908888',
|
||||
'#605656',
|
||||
'#4d4547',
|
||||
'#292525',
|
||||
'#403a3b',
|
||||
'#322f2f'
|
||||
]);
|
||||
|
||||
function shuffleArray(array: string[]): string[] {
|
||||
return [...array].sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
const randomizedColors = computed(() => shuffleArray(bgcolors.value));
|
||||
|
||||
const updatedTitle = computed(() => {
|
||||
const date = momentFormat(new Date(playlist.modification_date ?? '1970-01-01'))
|
||||
return t('components.audio.ChannelCard.title', { date })
|
||||
})
|
||||
|
||||
const deletePlaylist = async () => {
|
||||
try {
|
||||
await axios.delete(`playlists/${props.id}/`)
|
||||
|
@ -77,116 +123,130 @@ const deletePlaylist = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div
|
||||
<Layout stack main>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.playlist"
|
||||
class="ui vertical segment"
|
||||
>
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||
</div>
|
||||
<section
|
||||
/>
|
||||
<Layout flex
|
||||
v-if="!isLoading && playlist"
|
||||
v-title="playlist.name"
|
||||
class="ui head vertical center aligned stripe segment"
|
||||
>
|
||||
<div class="segment-content">
|
||||
<h2 class="ui center aligned icon header">
|
||||
<i class="circular inverted list warning icon" />
|
||||
<div class="content">
|
||||
{{ playlist.name }}
|
||||
<div class="sub header">
|
||||
{{ t('views.playlists.Detail.meta.tracks', { username: playlist.actor.name }, playlist.tracks_count) }}
|
||||
<br>
|
||||
<duration :seconds="playlist.duration" />
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="ui hidden divider" />
|
||||
<div class="header-buttons">
|
||||
<div class="ui buttons">
|
||||
<play-button
|
||||
class="vibrant"
|
||||
:is-playable="playlist.is_playable"
|
||||
:tracks="tracks"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.playAll') }}
|
||||
</play-button>
|
||||
</div>
|
||||
<div class="ui buttons">
|
||||
<Button
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
@click="edit = !edit"
|
||||
icon="bi-pencil"
|
||||
>
|
||||
<template v-if="edit">
|
||||
{{ t('views.playlists.Detail.button.stopEdit') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('views.playlists.Detail.button.edit') }}
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="ui buttons">
|
||||
<Button
|
||||
v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
|
||||
icon="bi-code-slash"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
<i class="code icon" />
|
||||
{{ t('views.playlists.Detail.button.embed') }}
|
||||
</button>
|
||||
<dangerous-button
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
class="ui labeled danger icon button"
|
||||
:action="deletePlaylist"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.delete') }}
|
||||
<template #modal-header>
|
||||
<p>
|
||||
{{ t('views.playlists.Detail.modal.delete.header', {playlist: playlist.name}) }}
|
||||
</p>
|
||||
</template>
|
||||
<template #modal-content>
|
||||
<div class="playlist-grid">
|
||||
<img
|
||||
v-for="(url, idx) in images"
|
||||
:key="idx"
|
||||
v-lazy="url"
|
||||
:alt="playlist.name"
|
||||
:style="{ backgroundColor: randomizedColors[idx % randomizedColors.length] }"
|
||||
/>
|
||||
</div>
|
||||
<Layout stack style="flex: 1; gap: 8px;">
|
||||
<h1>{{ playlist.name }}</h1>
|
||||
<div class="meta">
|
||||
{{ playlist.tracks_count }}
|
||||
{{ t('views.playlists.Detail.header.tracks') }}
|
||||
<i class="bi bi-dot" />
|
||||
<Duration :seconds="playlist.duration" />
|
||||
</div>
|
||||
<div class="meta">
|
||||
<!-- TODO: Translations -->
|
||||
by
|
||||
<ActorLink
|
||||
:actor="playlist.actor"
|
||||
:avatar="false"
|
||||
:discrete="true"
|
||||
/>
|
||||
<i class="bi bi-dot" />
|
||||
updated
|
||||
<HumanDate
|
||||
:date="playlist.modification_date"
|
||||
/>
|
||||
</div>
|
||||
<Layout flex class="header-buttons">
|
||||
<PlayButton
|
||||
split
|
||||
class="vibrant"
|
||||
:is-playable="playlist.is_playable"
|
||||
:tracks="tracks"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.playAll') }}
|
||||
</PlayButton>
|
||||
<Button
|
||||
v-if="playlist.tracks_count > 1"
|
||||
primary
|
||||
icon="bi-shuffle"
|
||||
:aria-label="t('components.audio.Player.label.shuffleQueue')"
|
||||
@click.prevent.stop="shuffle()"
|
||||
>
|
||||
{{ t('components.audio.Player.label.shuffleQueue') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
secondary
|
||||
@click="edit = !edit"
|
||||
icon="bi-pencil"
|
||||
>
|
||||
<template v-if="edit">
|
||||
{{ t('views.playlists.Detail.button.stopEdit') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('views.playlists.Detail.button.edit') }}
|
||||
</template>
|
||||
</Button>
|
||||
<DangerousButton
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
:action="deletePlaylist"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.delete') }}
|
||||
<template #alert>
|
||||
<Alert red>
|
||||
<p>
|
||||
{{ t('views.playlists.Detail.modal.delete.content.warning') }}
|
||||
</p>
|
||||
</template>
|
||||
<template #modal-confirm>
|
||||
<div>
|
||||
{{ t('views.playlists.Detail.button.confirm') }}
|
||||
</div>
|
||||
</template>
|
||||
</dangerous-button>
|
||||
<div class="ui hidden horizontal divider" />
|
||||
<playlist-dropdown
|
||||
:playlist="playlist"
|
||||
@import="fetchData"
|
||||
</Alert>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button @click="showDeleteModal = false">
|
||||
{{ t('views.playlists.Detail.button.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
destructive
|
||||
@click="deletePlaylist"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.confirm') }}
|
||||
</Button>
|
||||
</template>
|
||||
</DangerousButton>
|
||||
<Spacer h grow />
|
||||
<playlist-dropdown
|
||||
:playlist="playlist"
|
||||
@import="fetchData"
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Modal
|
||||
v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
|
||||
v-model="showEmbedModal"
|
||||
title="t('views.playlists.Detail.modal.embed.header')"
|
||||
>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<embed-wizard
|
||||
:id="playlist.id"
|
||||
type="playlist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
v-if="playlist.privacy_level === 'everyone' && playlist.is_playable"
|
||||
v-model="showEmbedModal"
|
||||
title="t('views.playlists.Detail.modal.embed.header')"
|
||||
>
|
||||
<div class="scrolling content">
|
||||
<div class="description">
|
||||
<embed-wizard
|
||||
:id="playlist.id"
|
||||
type="playlist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<Button variant="outline">
|
||||
{{ t('views.playlists.Detail.button.cancel') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</section>
|
||||
<section class="ui vertical stripe segment">
|
||||
<template #actions>
|
||||
<Button variant="outline">
|
||||
{{ t('views.playlists.Detail.button.cancel') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</Layout>
|
||||
|
||||
<Layout stack>
|
||||
<template v-if="edit">
|
||||
<playlist-editor
|
||||
v-model:playlist="playlist"
|
||||
|
@ -194,30 +254,61 @@ const deletePlaylist = async () => {
|
|||
/>
|
||||
</template>
|
||||
<template v-else-if="tracks.length > 0">
|
||||
<h2>
|
||||
{{ t('views.playlists.Detail.header.tracks') }}
|
||||
</h2>
|
||||
<track-table
|
||||
:display-position="true"
|
||||
:tracks="tracks"
|
||||
:unique="false"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
<Alert blue
|
||||
v-else
|
||||
class="ui placeholder segment"
|
||||
>
|
||||
<div class="ui icon header">
|
||||
<i class="list icon" />
|
||||
{{ t('views.playlists.Detail.empty.noTracks') }}
|
||||
</div>
|
||||
<i class="bi bi-music-note-list" />
|
||||
{{ t('views.playlists.Detail.empty.noTracks') }}
|
||||
<Button
|
||||
icon="bi-pencil"
|
||||
@click="edit = !edit"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.edit') }}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</Alert>
|
||||
<library-widget
|
||||
:url="'playlists/' + playlist?.id + '/libraries/'"
|
||||
@loaded="emit('libraries-loaded', $event)"
|
||||
>
|
||||
{{ t('components.library.AlbumDetail.description.libraries') }}
|
||||
</library-widget>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.playlist-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 2px;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
@ -182,7 +182,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
</option>
|
||||
</select>
|
||||
</Layout>
|
||||
<Layout grid
|
||||
<template
|
||||
v-if="result && result.results.length > 0"
|
||||
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
|
||||
>
|
||||
|
@ -191,7 +191,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
|
|||
v-if="result && result.results.length > 0"
|
||||
:playlists="result.results"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
<Layout
|
||||
stack
|
||||
v-else-if="result && result.results.length === 0"
|
||||
|
|
Loading…
Reference in New Issue