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