fix(front): consistent pixel perfect header with description modal on all detail pages
This commit is contained in:
parent
a05e509d36
commit
dcb664162c
|
@ -66,5 +66,6 @@ const getRoute = (ac: ArtistCredit) => {
|
|||
<style lang="scss" scoped>
|
||||
a.username {
|
||||
text-decoration: none;
|
||||
height: 25px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,79 +1,78 @@
|
|||
<script setup lang="ts">
|
||||
import type { Album } from '~/types'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { momentFormat } from '~/utils/filters'
|
||||
import defaultCover from '~/assets/audio/default-cover.png'
|
||||
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useStore } from '~/store'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Card from '~/components/ui/Card.vue'
|
||||
import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
import { type Album, type ArtistCredit } from '~/types'
|
||||
|
||||
interface Props {
|
||||
serie: Album
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cover = computed(() => props.serie?.cover ?? null)
|
||||
const { serie } = props
|
||||
|
||||
const artistCredit = serie.artist_credit || []
|
||||
|
||||
const store = useStore()
|
||||
const imageUrl = computed(() => serie?.cover?.urls.original
|
||||
? store.getters['instance/absoluteUrl'](serie.cover?.urls.medium_square_crop)
|
||||
: defaultCover
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="channel-serie-card">
|
||||
<div class="two-images">
|
||||
<img
|
||||
v-if="cover && cover.urls.original"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
@click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
<Card
|
||||
:title="serie?.title"
|
||||
:image="imageUrl"
|
||||
:tags="serie?.tags"
|
||||
:to="{name: 'library.albums.detail', params: {id: serie?.id}}"
|
||||
small
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
@click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
>
|
||||
<img
|
||||
v-if="cover && cover.urls.original"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="channel-image"
|
||||
@click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
alt=""
|
||||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
@click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
|
||||
>
|
||||
</div>
|
||||
<div class="content ellipsis">
|
||||
<strong>
|
||||
<router-link
|
||||
class="discrete link"
|
||||
:to="{name: 'library.albums.detail', params: {id: serie.id}}"
|
||||
>
|
||||
{{ serie.title }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<div class="description">
|
||||
<span>
|
||||
{{ t('components.audio.ChannelSerieCard.meta.episodes', serie.tracks_count) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<play-button
|
||||
:icon-only="true"
|
||||
:is-playable="true"
|
||||
:button-classes="['ui', 'circular', 'vibrant', 'icon', 'button']"
|
||||
<template #topright>
|
||||
<PlayButton
|
||||
icon-only
|
||||
:is-playable="serie?.is_playable"
|
||||
:album="serie"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<span v-if="serie?.release_date">
|
||||
{{ momentFormat(new Date(serie?.release_date), 'Y') }}
|
||||
</span>
|
||||
<i class="bi bi-dot" />
|
||||
<span>
|
||||
{{ t('components.audio.album.Card.meta.tracks', serie?.tracks_count) }}
|
||||
</span>
|
||||
<Spacer
|
||||
h
|
||||
grow
|
||||
/>
|
||||
<PlayButton
|
||||
:dropdown-only="true"
|
||||
discrete
|
||||
:is-playable="serie?.is_playable"
|
||||
:album="serie"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.play-button {
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -25,6 +25,7 @@ interface Props extends PlayOptionsProps {
|
|||
iconOnly?: boolean
|
||||
playing?: boolean
|
||||
paused?: boolean
|
||||
lowHeight?: boolean
|
||||
|
||||
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
||||
isPlayable?: boolean
|
||||
|
@ -56,7 +57,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
iconOnly: () => false,
|
||||
isPlayable: () => false,
|
||||
playing: () => false,
|
||||
paused: () => false
|
||||
paused: () => false,
|
||||
lowHeight: () => false
|
||||
})
|
||||
|
||||
// (1) Create a PlayButton
|
||||
|
@ -125,6 +127,8 @@ const isOpen = ref(false)
|
|||
:class="[...buttonClasses, 'play-button']"
|
||||
:isloading="isLoading"
|
||||
:dropdown-only="dropdownOnly"
|
||||
:low-height="lowHeight || undefined"
|
||||
style="align-self: start;"
|
||||
@click.stop.prevent="replacePlay()"
|
||||
@split-click="isOpen = !isOpen"
|
||||
>
|
||||
|
@ -238,6 +242,7 @@ const isOpen = ref(false)
|
|||
:round="iconOnly"
|
||||
:primary="iconOnly && !discrete"
|
||||
:ghost="discrete"
|
||||
:low-height="lowHeight || undefined"
|
||||
@click.stop.prevent="replacePlay()"
|
||||
>
|
||||
<template v-if="!discrete && !iconOnly">
|
||||
|
|
|
@ -110,8 +110,8 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
|
|||
{{ generateTrackCreditString(track) }}
|
||||
<span class="middle middledot symbol" />
|
||||
<human-duration
|
||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||
:duration="track.uploads[0].duration"
|
||||
v-if="track.uploads?.[0]?.duration"
|
||||
:duration="track.uploads[0]?.duration"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'
|
|||
import axios from 'axios'
|
||||
import clip from 'text-clipper'
|
||||
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
|
||||
|
@ -23,6 +24,7 @@ interface Props {
|
|||
fetchHtml?: boolean
|
||||
permissive?: boolean
|
||||
truncateLength?: number
|
||||
moreLink?: boolean
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
@ -35,7 +37,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
canUpdate: true,
|
||||
fetchHtml: false,
|
||||
permissive: false,
|
||||
truncateLength: 200
|
||||
truncateLength: 200,
|
||||
moreLink: true
|
||||
})
|
||||
|
||||
const preview = ref('')
|
||||
|
@ -89,34 +92,40 @@ const submit = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="content && !isUpdating">
|
||||
<Layout
|
||||
v-if="content && !isUpdating"
|
||||
flex
|
||||
gap-4
|
||||
>
|
||||
<!-- Render the truncated or full description -->
|
||||
|
||||
<sanitized-html :html="html" />
|
||||
<sanitized-html
|
||||
:html="html"
|
||||
:class="['description', isTruncated ? 'truncated' : '']"
|
||||
/>
|
||||
|
||||
<!-- Display the `show more` / `show less` button -->
|
||||
|
||||
<template v-if="isTruncated">
|
||||
<a
|
||||
v-if="showMore === false"
|
||||
v-if="showMore === false && props.moreLink !== false"
|
||||
class="more"
|
||||
style="align-self: end; color: var(--fw-primary);"
|
||||
style="align-self: flex-end; color: var(--fw-primary);"
|
||||
href=""
|
||||
@click.stop.prevent="showMore = true"
|
||||
>
|
||||
{{ t('components.common.RenderedDescription.button.more') }}
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
v-else-if="props.moreLink !== false"
|
||||
class="more"
|
||||
style="align-self: end; color: var(--fw-primary);"
|
||||
style="align-self: center; color: var(--fw-primary);"
|
||||
href=""
|
||||
@click.stop.prevent="showMore = false"
|
||||
>
|
||||
{{ t('components.common.RenderedDescription.button.less') }}
|
||||
</a>
|
||||
</template>
|
||||
</template>
|
||||
</Layout>
|
||||
<span v-else-if="!isUpdating">
|
||||
{{ t('components.common.RenderedDescription.empty.noDescription') }}
|
||||
</span>
|
||||
|
@ -166,3 +175,19 @@ const submit = async () => {
|
|||
</Button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
&.truncated {
|
||||
-webkit-line-clamp: 1; /* Number of lines to show */
|
||||
line-clamp: 1;
|
||||
max-height: 72px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,6 +17,7 @@ import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
|
|||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import AlbumDropdown from './AlbumDropdown.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Header from '~/components/ui/Header.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
|
@ -147,16 +148,16 @@ const remove = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
/>
|
||||
<template v-if="object">
|
||||
<Layout flex>
|
||||
<Header
|
||||
v-if="object"
|
||||
:h1="object.title"
|
||||
page-heading
|
||||
>
|
||||
<template #image>
|
||||
<img
|
||||
v-if="object.cover && object.cover.urls.original"
|
||||
v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)"
|
||||
|
@ -169,20 +170,20 @@ const remove = async () => {
|
|||
class="channel-image"
|
||||
src="../../assets/audio/default-cover.png"
|
||||
>
|
||||
<Layout
|
||||
stack
|
||||
no-gap
|
||||
style="flex: 1;"
|
||||
>
|
||||
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
||||
{{ object.title }}
|
||||
</h1>
|
||||
</template>
|
||||
<artist-credit-label
|
||||
v-if="artistCredit"
|
||||
:artist-credit="artistCredit"
|
||||
/>
|
||||
<!-- Metadata: -->
|
||||
<div class="meta">
|
||||
<Layout
|
||||
gap-4
|
||||
class="meta"
|
||||
>
|
||||
<Layout
|
||||
flex
|
||||
gap-4
|
||||
>
|
||||
<template v-if="object.release_date">
|
||||
{{ momentFormat(new Date(object.release_date ?? '1970-01-01'), 'Y') }}
|
||||
<i class="bi bi-dot" />
|
||||
|
@ -204,25 +205,26 @@ const remove = async () => {
|
|||
:duration="totalDuration"
|
||||
/>
|
||||
<!--TODO: License -->
|
||||
</div>
|
||||
<Layout flex>
|
||||
<rendered-description
|
||||
v-if="object.description"
|
||||
:content="object.description"
|
||||
:can-update="true"
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<RenderedDescription
|
||||
v-if="object.description"
|
||||
:content="{ html: object.description.html }"
|
||||
:truncate-length="50"
|
||||
/>
|
||||
<Layout flex>
|
||||
<PlayButton
|
||||
v-if="object.tracks"
|
||||
split
|
||||
:tracks="object.tracks"
|
||||
low-height
|
||||
:is-playable="object.is_playable"
|
||||
/>
|
||||
<Button
|
||||
v-if="object?.tracks?.length && object?.tracks?.length > 2"
|
||||
primary
|
||||
icon="bi-shuffle"
|
||||
low-height
|
||||
:aria-label="labels.shuffle"
|
||||
@click.prevent.stop="shuffle()"
|
||||
>
|
||||
|
@ -236,6 +238,7 @@ const remove = async () => {
|
|||
&& artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername
|
||||
*/"
|
||||
:is-loading="isLoading"
|
||||
low-height
|
||||
icon="bi-trash"
|
||||
@confirm="remove()"
|
||||
>
|
||||
|
@ -247,10 +250,12 @@ const remove = async () => {
|
|||
/>
|
||||
<TrackFavoriteIcon
|
||||
v-if="store.state.auth.authenticated"
|
||||
square-small
|
||||
:album="object"
|
||||
/>
|
||||
<TrackPlaylistIcon
|
||||
v-if="store.state.auth.authenticated"
|
||||
square-small
|
||||
:album="object"
|
||||
/>
|
||||
<!-- TODO: Share Button -->
|
||||
|
@ -265,8 +270,7 @@ const remove = async () => {
|
|||
@remove="remove"
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Header>
|
||||
|
||||
<div style="flex 1;">
|
||||
<router-view
|
||||
|
@ -283,11 +287,15 @@ const remove = async () => {
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style scopen>
|
||||
<style scoped lang="scss">
|
||||
.meta {
|
||||
line-height: 48px;
|
||||
font-size: 15px;
|
||||
@include light-theme {
|
||||
color: var(--fw-gray-700);
|
||||
}
|
||||
@include dark-theme {
|
||||
color: var(--fw-gray-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,10 +16,6 @@ import Popover from '~/components/ui/Popover.vue'
|
|||
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
|
||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'remove'): void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean
|
||||
artistCredit: ArtistCredit[]
|
||||
|
@ -28,10 +24,10 @@ interface Props {
|
|||
isAlbum: boolean
|
||||
isChannel: boolean
|
||||
isSerie: boolean
|
||||
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const emit = defineEmits<Events>()
|
||||
const props = defineProps<Props>()
|
||||
const { report, getReportableObjects } = useReport()
|
||||
|
||||
|
@ -75,7 +71,7 @@ const open = ref(false)
|
|||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton
|
||||
:title="labels.more"
|
||||
is-square
|
||||
is-square-small
|
||||
@click="toggleOpen()"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -11,11 +11,14 @@ import { useStore } from '~/store'
|
|||
import axios from 'axios'
|
||||
import useReport from '~/composables/moderation/useReport'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import { useModal } from '~/ui/composables/useModal.ts'
|
||||
|
||||
import HumanDuration from '~/components/common/HumanDuration.vue'
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Header from '~/components/ui/Header.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import RadioButton from '~/components/radios/Button.vue'
|
||||
|
@ -24,6 +27,7 @@ import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
|
|||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import RenderedDescription from '../common/RenderedDescription.vue'
|
||||
|
||||
interface Props {
|
||||
id: number | string
|
||||
|
@ -125,33 +129,29 @@ const fetchData = async () => {
|
|||
const totalDuration = computed(() => sum((tracks.value ?? []).map(track => track.uploads[0]?.duration ?? 0)))
|
||||
|
||||
watch(() => props.id, fetchData, { immediate: true })
|
||||
|
||||
const isOpen = useModal('artist-description').isOpen
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout
|
||||
v-title="labels.title"
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Loader v-if="isLoading" />
|
||||
<template v-if="object && !isLoading">
|
||||
<Layout flex>
|
||||
<Header
|
||||
v-if="object && !isLoading"
|
||||
v-title="labels.title"
|
||||
:h1="object.name"
|
||||
page-heading
|
||||
>
|
||||
<template #image>
|
||||
<img
|
||||
v-lazy="cover.urls.large_square_crop"
|
||||
:alt="object.name"
|
||||
class="channel-image"
|
||||
>
|
||||
<Layout
|
||||
stack
|
||||
style="flex: 1; gap: 8px;"
|
||||
>
|
||||
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
||||
{{ object.name }}
|
||||
</h1>
|
||||
</template>
|
||||
<Layout
|
||||
flex
|
||||
class="meta"
|
||||
style="gap: 0;"
|
||||
no-gap
|
||||
>
|
||||
<div
|
||||
v-if="albums"
|
||||
|
@ -167,23 +167,64 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
<Spacer />
|
||||
<Layout
|
||||
flex
|
||||
gap-4
|
||||
>
|
||||
<RenderedDescription
|
||||
v-if="object.description"
|
||||
class="description"
|
||||
:content="{ ...object.description, text: object.description.text ?? undefined }"
|
||||
:truncate-length="100"
|
||||
:more-link="false"
|
||||
/>
|
||||
<Spacer grow />
|
||||
<Link
|
||||
v-if="object.description"
|
||||
:to="useModal('artist-description').to"
|
||||
style="color: var(--fw-primary); text-decoration: underline;"
|
||||
thin-font
|
||||
force-underline
|
||||
>
|
||||
{{ t('components.common.RenderedDescription.button.more') }}
|
||||
</Link>
|
||||
</Layout>
|
||||
<Modal
|
||||
v-if="object.description"
|
||||
v-model="isOpen"
|
||||
:title="object.name"
|
||||
>
|
||||
<img
|
||||
v-if="object.cover"
|
||||
v-lazy="object.cover.urls.original"
|
||||
:alt="object.name"
|
||||
style="object-fit: cover; width: 100%; height: 100%;"
|
||||
>
|
||||
<sanitized-html
|
||||
v-if="object.description"
|
||||
:html="object.description.html"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Layout flex>
|
||||
<PlayButton
|
||||
:is-playable="isPlayable"
|
||||
split
|
||||
:artist="object"
|
||||
low-height
|
||||
>
|
||||
{{ t('components.library.ArtistBase.button.play') }}
|
||||
</PlayButton>
|
||||
<radio-button
|
||||
type="artist"
|
||||
:object-id="object.id"
|
||||
low-height
|
||||
/>
|
||||
<Spacer grow />
|
||||
<Popover>
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton
|
||||
is-square-small
|
||||
@click="toggleOpen"
|
||||
/>
|
||||
</template>
|
||||
|
@ -275,8 +316,6 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
</template>
|
||||
</Popover>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Modal
|
||||
v-if="publicLibraries.length > 0"
|
||||
|
@ -293,6 +332,7 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</Header>
|
||||
<hr>
|
||||
<router-view
|
||||
:key="route.fullPath"
|
||||
|
@ -306,11 +346,29 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
@libraries-loaded="libraries = $event"
|
||||
/>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.channel-image {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 15px;
|
||||
@include light-theme {
|
||||
color: var(--fw-gray-700);
|
||||
}
|
||||
@include dark-theme {
|
||||
color: var(--fw-gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
-webkit-line-clamp: 1; /* Number of lines to show */
|
||||
line-clamp: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -242,8 +242,8 @@ const trackDetails: {
|
|||
|
||||
<style lang="scss">
|
||||
.channel-image {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import axios from 'axios'
|
|||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
|
@ -104,7 +105,10 @@ const showDeleteModal = ref(false)
|
|||
<span>
|
||||
<Popover v-model="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton @click="toggleOpen" />
|
||||
<OptionsButton
|
||||
is-square-small
|
||||
@click="toggleOpen"
|
||||
/>
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
|
|
|
@ -15,6 +15,7 @@ const props = defineProps<{
|
|||
alignLeft?: boolean
|
||||
action?: { text: string } & (ComponentProps<typeof Link> | ComponentProps<typeof Button>)
|
||||
icon?: string
|
||||
noGap?: false
|
||||
} & {
|
||||
[H in `h${ '1' | '2' | '3' | '4' | '5' | '6' }`]? : string
|
||||
} & {
|
||||
|
@ -35,7 +36,8 @@ const props = defineProps<{
|
|||
</div>
|
||||
<Layout
|
||||
stack
|
||||
gap-8
|
||||
:gap-8="!(props.noGap as boolean)"
|
||||
:no-gap="props.noGap"
|
||||
style="flex-grow: 1;"
|
||||
>
|
||||
<Layout
|
||||
|
@ -46,7 +48,7 @@ const props = defineProps<{
|
|||
<!-- Set distance between baseline and previous row -->
|
||||
<Spacer
|
||||
v
|
||||
:size="68"
|
||||
:size="53"
|
||||
style="align-self: baseline;"
|
||||
/>
|
||||
<div
|
||||
|
@ -66,7 +68,7 @@ const props = defineProps<{
|
|||
v-bind="props"
|
||||
style="
|
||||
align-self: baseline;
|
||||
padding: 0 0 24px 0;
|
||||
padding: 0 0 0 0;
|
||||
margin: 0;
|
||||
"
|
||||
/>
|
||||
|
@ -93,6 +95,7 @@ const props = defineProps<{
|
|||
<slot />
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Spacer />
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
|
|
|
@ -4,6 +4,7 @@ import Button from '../Button.vue'
|
|||
defineProps<{
|
||||
isSquare?: boolean
|
||||
isGhost?: boolean
|
||||
isSquareSmall?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
@ -14,7 +15,8 @@ defineProps<{
|
|||
:class="['options-button', {'is-ghost': isGhost}]"
|
||||
:secondary="!isGhost"
|
||||
:ghost="isGhost"
|
||||
:round="!isSquare"
|
||||
:round="!isSquare && !isSquareSmall"
|
||||
:square-small="isSquareSmall"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2824,6 +2824,40 @@
|
|||
"tracks": "No tracks | {n} track | {n} tracks"
|
||||
}
|
||||
},
|
||||
"Detail": {
|
||||
"button": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Delete playlist",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"embed": "Embed",
|
||||
"playAll": "Play all",
|
||||
"stopEdit": "Stop Editing"
|
||||
},
|
||||
"empty": {
|
||||
"noTracks": "There are no tracks in this playlist yet"
|
||||
},
|
||||
"header": {
|
||||
"tracks": "Tracks"
|
||||
},
|
||||
"meta": {
|
||||
"attribution": "by",
|
||||
"tracks": "Playlist containing {n} track, by {username} | Playlist containing {n} tracks, by {username}",
|
||||
"updated": "updated"
|
||||
},
|
||||
"modal": {
|
||||
"delete": {
|
||||
"content": {
|
||||
"warning": "This will completely delete this playlist and cannot be undone."
|
||||
},
|
||||
"header": "Do you want to delete the playlist {playlist}?"
|
||||
},
|
||||
"embed": {
|
||||
"header": "Embed this playlist on your website"
|
||||
}
|
||||
},
|
||||
"title": "Playlist"
|
||||
},
|
||||
"Editor": {
|
||||
"button": {
|
||||
"addDuplicate": "Add anyway",
|
||||
|
@ -2877,6 +2911,50 @@
|
|||
"name": "My awesome playlist"
|
||||
}
|
||||
},
|
||||
"List": {
|
||||
"button": {
|
||||
"create": "Create a playlist",
|
||||
"manage": "Manage your playlists",
|
||||
"search": "Search"
|
||||
},
|
||||
"empty": {
|
||||
"noResults": "No results matching your query"
|
||||
},
|
||||
"header": {
|
||||
"browse": "Browsing playlists",
|
||||
"playlists": "Playlists"
|
||||
},
|
||||
"label": {
|
||||
"search": "Search"
|
||||
},
|
||||
"ordering": {
|
||||
"direction": {
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending",
|
||||
"label": "Order"
|
||||
},
|
||||
"label": "Ordering"
|
||||
},
|
||||
"pagination": {
|
||||
"results": "Results per page"
|
||||
},
|
||||
"placeholder": {
|
||||
"search": "Enter playlist name…"
|
||||
}
|
||||
},
|
||||
"PlaylistDropdown": {
|
||||
"button": {
|
||||
"import": {
|
||||
"header": "Rebuild playlist",
|
||||
"description": "This will update the playlist with the content of the xspf file. Existing playlist tracks will be deleted"
|
||||
},
|
||||
"export": {
|
||||
"header": "Download playlist",
|
||||
"description": "This will provide an xspf file with the playlist data"
|
||||
}
|
||||
},
|
||||
"more": "More"
|
||||
},
|
||||
"PlaylistModal": {
|
||||
"button": {
|
||||
"addDuplicate": "Add anyway",
|
||||
|
|
|
@ -2925,19 +2925,6 @@
|
|||
"placeholder": {
|
||||
"noPlaylists": "No playlists have been created yet"
|
||||
}
|
||||
},
|
||||
"PlaylistDropdown": {
|
||||
"button": {
|
||||
"import": {
|
||||
"header": "Rebuild playlist",
|
||||
"description": "This will update the playlist with the content of the xspf file. Existing playlist tracks will be deleted"
|
||||
},
|
||||
"export": {
|
||||
"header": "Download playlist",
|
||||
"description": "This will provide an xspf file with the playlist data"
|
||||
}
|
||||
},
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"radios": {
|
||||
|
@ -4581,6 +4568,12 @@
|
|||
}
|
||||
},
|
||||
"playlists": {
|
||||
"Card": {
|
||||
"title": "Updated on {date}",
|
||||
"meta": {
|
||||
"tracks": "No tracks | {n} track | {n} tracks"
|
||||
}
|
||||
},
|
||||
"Detail": {
|
||||
"button": {
|
||||
"cancel": "Cancel",
|
||||
|
@ -4615,6 +4608,59 @@
|
|||
},
|
||||
"title": "Playlist"
|
||||
},
|
||||
"Editor": {
|
||||
"button": {
|
||||
"addDuplicate": "Add anyway",
|
||||
"clear": "Clear playlist",
|
||||
"copy": "Copy the current queue to this playlist",
|
||||
"insertFromQueue": "Insert from queue ({n} track) | Insert from queue ({n} tracks"
|
||||
},
|
||||
"error": {
|
||||
"sync": "An error occurred while saving your changes"
|
||||
},
|
||||
"header": {
|
||||
"editor": "Playlist editor"
|
||||
},
|
||||
"help": {
|
||||
"reorder": "Drag and drop rows to reorder tracks in the playlist"
|
||||
},
|
||||
"loading": {
|
||||
"sync": "Syncing changes to server…"
|
||||
},
|
||||
"message": {
|
||||
"sync": "Changes synced with server"
|
||||
},
|
||||
"modal": {
|
||||
"clearPlaylist": {
|
||||
"content": {
|
||||
"warning": "This will remove all tracks from this playlist and cannot be undone."
|
||||
},
|
||||
"header": "Do you want to clear the playlist \"{playlist}\"?"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"duplicate": "Some tracks in your queue are already in this playlist:"
|
||||
}
|
||||
},
|
||||
"Form": {
|
||||
"button": {
|
||||
"create": "Create playlist",
|
||||
"update": "Update playlist"
|
||||
},
|
||||
"header": {
|
||||
"createFailure": "The playlist could not be created",
|
||||
"createPlaylist": "Create a new playlist",
|
||||
"createSuccess": "Playlist created",
|
||||
"updateSuccess": "Playlist updated"
|
||||
},
|
||||
"label": {
|
||||
"name": "Playlist name",
|
||||
"visibility": "Playlist visibility"
|
||||
},
|
||||
"placeholder": {
|
||||
"name": "My awesome playlist"
|
||||
}
|
||||
},
|
||||
"List": {
|
||||
"button": {
|
||||
"create": "Create a playlist",
|
||||
|
@ -4645,6 +4691,72 @@
|
|||
"placeholder": {
|
||||
"search": "Enter playlist name…"
|
||||
}
|
||||
},
|
||||
"PlaylistModal": {
|
||||
"button": {
|
||||
"addDuplicate": "Add anyway",
|
||||
"addToPlaylist": "Add to this playlist",
|
||||
"addTrack": "Add track",
|
||||
"cancel": "Cancel",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"empty": {
|
||||
"noPlaylists": "No playlists have been created yet"
|
||||
},
|
||||
"header": {
|
||||
"addFailure": "The track can't be added to a playlist",
|
||||
"addToPlaylist": "Add to playlist",
|
||||
"available": "Available playlists",
|
||||
"manage": "Manage playlists",
|
||||
"noResults": "No results matching your filter",
|
||||
"track": "{title}, by {artist}"
|
||||
},
|
||||
"label": {
|
||||
"filter": "Filter"
|
||||
},
|
||||
"placeholder": {
|
||||
"filterPlaylist": "Enter playlist name"
|
||||
},
|
||||
"table": {
|
||||
"edit": {
|
||||
"header": {
|
||||
"edit": "Edit",
|
||||
"lastModification": "Last modification",
|
||||
"name": "Name",
|
||||
"tracks": "Tracks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"duplicate": "{ 0 } is already in { 1 }."
|
||||
}
|
||||
},
|
||||
"TrackPlaylistIcon": {
|
||||
"button": {
|
||||
"add": "Add to playlist…"
|
||||
}
|
||||
},
|
||||
"Widget": {
|
||||
"button": {
|
||||
"create": "Create playlist",
|
||||
"more": "Show more"
|
||||
},
|
||||
"placeholder": {
|
||||
"noPlaylists": "No playlists have been created yet"
|
||||
}
|
||||
},
|
||||
"PlaylistDropdown": {
|
||||
"button": {
|
||||
"import": {
|
||||
"header": "Rebuild playlist",
|
||||
"description": "This will update the playlist with the content of the xspf file. Existing playlist tracks will be deleted"
|
||||
},
|
||||
"export": {
|
||||
"header": "Download playlist",
|
||||
"description": "This will provide an xspf file with the playlist data"
|
||||
}
|
||||
},
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"radios": {
|
||||
|
|
|
@ -113,8 +113,8 @@
|
|||
}
|
||||
}
|
||||
.channel-image {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
|
||||
&.large {
|
||||
width: 8em !important;
|
||||
|
|
|
@ -9,16 +9,21 @@ import { useStore } from '~/store'
|
|||
import { hashCode, intToRGB } from '~/utils/color'
|
||||
|
||||
import UserFollowButton from '~/components/federation/UserFollowButton.vue'
|
||||
import { useModal } from '~/ui/composables/useModal.ts'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import RenderedDescription from '~/components/common/RenderedDescription.vue'
|
||||
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Header from '~/components/ui/Header.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
import Nav from '~/components/ui/Nav.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'updated', value: components['schemas']['FullActor']): void
|
||||
|
@ -96,6 +101,8 @@ const tabs = ref([{
|
|||
}]
|
||||
: []
|
||||
)])
|
||||
|
||||
const isOpen = useModal('artist-description').isOpen
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -103,6 +110,7 @@ const tabs = ref([{
|
|||
v-title="labels.usernameProfile"
|
||||
stack
|
||||
main
|
||||
no-gap
|
||||
>
|
||||
<!-- TODO: Translate Edit Link -->
|
||||
<!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! -->
|
||||
|
@ -118,9 +126,11 @@ const tabs = ref([{
|
|||
// @ts-ignore
|
||||
solid: true,
|
||||
// @ts-ignore
|
||||
icon: 'bi-pencil-fill'
|
||||
icon: 'bi-pencil-fill',
|
||||
// @ts-ignore
|
||||
lowHeight: true
|
||||
}"
|
||||
style="margin-top: 58px;"
|
||||
no-gap
|
||||
page-heading
|
||||
>
|
||||
<template #image>
|
||||
|
@ -148,6 +158,7 @@ const tabs = ref([{
|
|||
:title="t('components.common.CopyInput.button.copy')"
|
||||
ghost
|
||||
secondary
|
||||
low-height
|
||||
@click="copy(fullUsername)"
|
||||
/>
|
||||
</span>
|
||||
|
@ -174,19 +185,43 @@ const tabs = ref([{
|
|||
flex
|
||||
no-gap
|
||||
>
|
||||
<!-- TODO: Fix error with `$event` not being the right type -->
|
||||
<!-- @vue-ignore -->
|
||||
<RenderedDescription
|
||||
:content="{ html: object?.summary?.html || '' }"
|
||||
:field-name="'summary'"
|
||||
:update-url="`users/${store.state.auth.username}/`"
|
||||
:can-update="store.state.auth.authenticated && object?.full_username === store.state.auth.fullUsername"
|
||||
v-if="object?.summary"
|
||||
class="description"
|
||||
:content="{ html: object?.summary.html || '' }"
|
||||
:truncate-length="100"
|
||||
@updated="emit('updated', $event)"
|
||||
:more-link="false"
|
||||
/>
|
||||
<Spacer grow />
|
||||
<Link
|
||||
v-if="object?.summary"
|
||||
:to="useModal('artist-description').to"
|
||||
style="color: var(--fw-primary); text-decoration: underline;"
|
||||
thin-font
|
||||
force-underline
|
||||
>
|
||||
{{ t('components.common.RenderedDescription.button.more') }}
|
||||
</Link>
|
||||
</Layout>
|
||||
<Modal
|
||||
v-if="object?.summary"
|
||||
v-model="isOpen"
|
||||
:title="object?.name"
|
||||
>
|
||||
<img
|
||||
v-if="object?.user.avatar"
|
||||
v-lazy="object?.user.avatar.urls.original"
|
||||
:alt="object?.name"
|
||||
style="object-fit: cover; width: 100%; height: 100%;"
|
||||
>
|
||||
<sanitized-html
|
||||
v-if="object?.summary"
|
||||
:html="object?.summary.html"
|
||||
/>
|
||||
</Modal>
|
||||
<UserFollowButton
|
||||
v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername"
|
||||
low-height
|
||||
:actor="object"
|
||||
/>
|
||||
</Header>
|
||||
|
@ -201,8 +236,8 @@ const tabs = ref([{
|
|||
|
||||
<style scoped lang="scss">
|
||||
img.avatar {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
@ -215,8 +250,8 @@ const tabs = ref([{
|
|||
align-content: center;
|
||||
background-color: var(--fw-gray-500);
|
||||
border-radius: 50%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
|
|
@ -22,13 +22,14 @@ import TagsList from '~/components/tags/List.vue'
|
|||
import RadioButton from '~/components/radios/Button.vue'
|
||||
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Header from '~/components/ui/Header.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
import Nav from '~/components/ui/Nav.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 Layout from '~/components/ui/Layout.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
|
||||
|
@ -166,14 +167,17 @@ const tabs = ref([
|
|||
<Layout
|
||||
v-title="labels.title"
|
||||
stack
|
||||
no-gap
|
||||
main
|
||||
>
|
||||
<Loader v-if="isLoading" />
|
||||
<template v-if="object && !isLoading">
|
||||
<section
|
||||
<Header
|
||||
v-if="object && !isLoading"
|
||||
v-title="object.artist?.name"
|
||||
:h1="object.artist?.name"
|
||||
page-heading
|
||||
>
|
||||
<Layout flex>
|
||||
<template #image>
|
||||
<img
|
||||
v-if="object.artist?.cover"
|
||||
alt=""
|
||||
|
@ -185,13 +189,7 @@ const tabs = ref([
|
|||
class="bi bi-person-circle"
|
||||
style="font-size: 300px; margin-top: -32px;"
|
||||
/>
|
||||
<Layout
|
||||
stack
|
||||
style="flex: 1; gap: 16px;"
|
||||
>
|
||||
<h1 style="margin-top: 64px; margin-bottom: 8px;">
|
||||
{{ object.artist?.name }}
|
||||
</h1>
|
||||
</template>
|
||||
<Layout
|
||||
stack
|
||||
class="meta"
|
||||
|
@ -259,7 +257,7 @@ const tabs = ref([
|
|||
</span>
|
||||
<Spacer
|
||||
h
|
||||
:size="4"
|
||||
:size="8"
|
||||
/>
|
||||
<ActorLink
|
||||
v-if="object.actor"
|
||||
|
@ -284,6 +282,7 @@ const tabs = ref([
|
|||
v-if="isOwner"
|
||||
solid
|
||||
primary
|
||||
low-height
|
||||
icon="bi-upload"
|
||||
:to="useModal('upload').to"
|
||||
>
|
||||
|
@ -292,6 +291,7 @@ const tabs = ref([
|
|||
<PlayButton
|
||||
:is-playable="isPlayable"
|
||||
split
|
||||
low-height
|
||||
class="vibrant"
|
||||
:artist="object.artist"
|
||||
>
|
||||
|
@ -300,11 +300,13 @@ const tabs = ref([
|
|||
<RadioButton
|
||||
type="artist"
|
||||
:object-id="object.artist.id"
|
||||
low-height
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton
|
||||
is-square-small
|
||||
@click="toggleOpen"
|
||||
/>
|
||||
</template>
|
||||
|
@ -378,6 +380,7 @@ const tabs = ref([
|
|||
/>
|
||||
<subscribe-button
|
||||
v-if="store.state.auth.authenticated && object?.attributed_to.full_username !== store.state.auth.fullUsername"
|
||||
low-height
|
||||
:channel="object"
|
||||
@subscribed="updateSubscriptionCount(1)"
|
||||
@unsubscribed="updateSubscriptionCount(-1)"
|
||||
|
@ -426,6 +429,7 @@ const tabs = ref([
|
|||
<Button
|
||||
primary
|
||||
autofocus
|
||||
low-height
|
||||
:is-loading="edit.loading"
|
||||
:disabled="!edit.submittable"
|
||||
@click.stop="editForm?.submit"
|
||||
|
@ -437,6 +441,7 @@ const tabs = ref([
|
|||
<Button
|
||||
secondary
|
||||
icon="bi-rss"
|
||||
square-small
|
||||
@click.stop.prevent="showSubscribeModal = true"
|
||||
/>
|
||||
<Modal
|
||||
|
@ -474,12 +479,11 @@ const tabs = ref([
|
|||
</div>
|
||||
</Modal>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Header>
|
||||
<hr>
|
||||
<TagsList
|
||||
v-if="object.artist?.tags && object.artist?.tags.length > 0"
|
||||
:tags="object.artist.tags"
|
||||
v-if="object?.artist?.tags && object?.artist?.tags.length > 0"
|
||||
:tags="object?.artist.tags"
|
||||
:limit="5"
|
||||
:show-more="true"
|
||||
/>
|
||||
|
@ -490,21 +494,24 @@ const tabs = ref([
|
|||
:object="object"
|
||||
@tracks-loaded="totalTracks = $event"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.channel-image {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.huge {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
.meta {
|
||||
line-height: 24px;
|
||||
font-size: 15px;
|
||||
@include light-theme {
|
||||
color: var(--fw-gray-700);
|
||||
}
|
||||
@include dark-theme {
|
||||
color: var(--fw-gray-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,6 @@ import { useStore } from '~/store'
|
|||
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 HumanDate from '~/components/common/HumanDate.vue'
|
||||
|
@ -134,11 +133,6 @@ const shuffle = () => {}
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Layout
|
||||
v-title="playlist?.name"
|
||||
stack
|
||||
main
|
||||
>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.playlist"
|
||||
|
@ -159,31 +153,35 @@ const shuffle = () => {}
|
|||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="meta">
|
||||
<Layout
|
||||
gap-4
|
||||
class="meta"
|
||||
>
|
||||
<Layout
|
||||
flex
|
||||
gap-4
|
||||
>
|
||||
{{ playlist.tracks_count }}
|
||||
{{ t('views.playlists.Detail.header.tracks') }}
|
||||
<i class="bi bi-dot" />
|
||||
<Duration :seconds="playlist.duration" />
|
||||
</div>
|
||||
</Layout>
|
||||
<Layout
|
||||
flex
|
||||
gap-8
|
||||
gap-4
|
||||
>
|
||||
{{ t('views.playlists.Detail.meta.attribution') }}
|
||||
<ActorLink
|
||||
:actor="playlist.actor"
|
||||
:avatar="false"
|
||||
:discrete="true"
|
||||
/>
|
||||
{{ playlist.actor.full_username }}
|
||||
<i class="bi bi-dot" />
|
||||
{{ t('views.playlists.Detail.meta.updated') }}
|
||||
<HumanDate
|
||||
:date="playlist.modification_date"
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<RenderedDescription
|
||||
:content="{ html: playlist.description }"
|
||||
:truncate-length="200"
|
||||
:truncate-length="100"
|
||||
:show-more="true"
|
||||
/>
|
||||
<Layout
|
||||
|
@ -192,7 +190,8 @@ const shuffle = () => {}
|
|||
>
|
||||
<PlayButton
|
||||
split
|
||||
:is-playable="playlist.is_playable"
|
||||
low-height
|
||||
:is-playable="true"
|
||||
:tracks="tracks"
|
||||
>
|
||||
{{ t('views.playlists.Detail.button.playAll') }}
|
||||
|
@ -201,6 +200,7 @@ const shuffle = () => {}
|
|||
v-if="playlist.tracks_count > 1"
|
||||
primary
|
||||
icon="bi-shuffle"
|
||||
low-height
|
||||
:aria-label="t('components.audio.Player.label.shuffleQueue')"
|
||||
@click.prevent.stop="shuffle()"
|
||||
>
|
||||
|
@ -209,6 +209,7 @@ const shuffle = () => {}
|
|||
<Button
|
||||
v-if="store.state.auth.profile && playlist.actor.full_username === store.state.auth.fullUsername"
|
||||
secondary
|
||||
low-height
|
||||
icon="bi-pencil"
|
||||
@click="edit = !edit"
|
||||
>
|
||||
|
@ -287,7 +288,6 @@ const shuffle = () => {}
|
|||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -297,8 +297,8 @@ const shuffle = () => {}
|
|||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 2px;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.playlist-grid img {
|
||||
|
@ -307,9 +307,14 @@ const shuffle = () => {}
|
|||
object-fit: cover;
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.meta {
|
||||
font-size: 15px;
|
||||
@include light-theme {
|
||||
color: var(--fw-gray-700);
|
||||
}
|
||||
@include dark-theme {
|
||||
color: var(--fw-gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-action {
|
||||
|
|
Loading…
Reference in New Issue