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>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
a.username {
 | 
					a.username {
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  height: 25px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,79 +1,78 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<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 PlayButton from '~/components/audio/PlayButton.vue'
 | 
				
			||||||
import { computed } from 'vue'
 | 
					import Layout from '~/components/ui/Layout.vue'
 | 
				
			||||||
import { useI18n } from 'vue-i18n'
 | 
					import Card from '~/components/ui/Card.vue'
 | 
				
			||||||
import { useStore } from '~/store'
 | 
					import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
 | 
				
			||||||
import { useRouter } from 'vue-router'
 | 
					import Spacer from '~/components/ui/Spacer.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { t } = useI18n()
 | 
					import { type Album, type ArtistCredit } from '~/types'
 | 
				
			||||||
const store = useStore()
 | 
					 | 
				
			||||||
const router = useRouter()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  serie: Album
 | 
					  serie: Album
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { t } = useI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<Props>()
 | 
					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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="channel-serie-card">
 | 
					  <Card
 | 
				
			||||||
    <div class="two-images">
 | 
					    :title="serie?.title"
 | 
				
			||||||
      <img
 | 
					    :image="imageUrl"
 | 
				
			||||||
        v-if="cover && cover.urls.original"
 | 
					    :tags="serie?.tags"
 | 
				
			||||||
        v-lazy="store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)"
 | 
					    :to="{name: 'library.albums.detail', params: {id: serie?.id}}"
 | 
				
			||||||
        alt=""
 | 
					    small
 | 
				
			||||||
        class="channel-image"
 | 
					  >
 | 
				
			||||||
        @click="router.push({name: 'library.albums.detail', params: {id: serie.id}})"
 | 
					    <template #topright>
 | 
				
			||||||
      >
 | 
					      <PlayButton
 | 
				
			||||||
      <img
 | 
					        icon-only
 | 
				
			||||||
        v-else
 | 
					        :is-playable="serie?.is_playable"
 | 
				
			||||||
        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']"
 | 
					 | 
				
			||||||
        :album="serie"
 | 
					        :album="serie"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </template>
 | 
				
			||||||
  </div>
 | 
					
 | 
				
			||||||
 | 
					    <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>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
 | 
					.play-button {
 | 
				
			||||||
 | 
					  top: 16px;
 | 
				
			||||||
 | 
					  right: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,7 @@ interface Props extends PlayOptionsProps {
 | 
				
			||||||
  iconOnly?: boolean
 | 
					  iconOnly?: boolean
 | 
				
			||||||
  playing?: boolean
 | 
					  playing?: boolean
 | 
				
			||||||
  paused?: boolean
 | 
					  paused?: boolean
 | 
				
			||||||
 | 
					  lowHeight?: boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
 | 
					  // TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
 | 
				
			||||||
  isPlayable?: boolean
 | 
					  isPlayable?: boolean
 | 
				
			||||||
| 
						 | 
					@ -56,7 +57,8 @@ const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
  iconOnly: () => false,
 | 
					  iconOnly: () => false,
 | 
				
			||||||
  isPlayable: () => false,
 | 
					  isPlayable: () => false,
 | 
				
			||||||
  playing: () => false,
 | 
					  playing: () => false,
 | 
				
			||||||
  paused: () => false
 | 
					  paused: () => false,
 | 
				
			||||||
 | 
					  lowHeight: () => false
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// (1) Create a PlayButton
 | 
					// (1) Create a PlayButton
 | 
				
			||||||
| 
						 | 
					@ -125,6 +127,8 @@ const isOpen = ref(false)
 | 
				
			||||||
      :class="[...buttonClasses, 'play-button']"
 | 
					      :class="[...buttonClasses, 'play-button']"
 | 
				
			||||||
      :isloading="isLoading"
 | 
					      :isloading="isLoading"
 | 
				
			||||||
      :dropdown-only="dropdownOnly"
 | 
					      :dropdown-only="dropdownOnly"
 | 
				
			||||||
 | 
					      :low-height="lowHeight || undefined"
 | 
				
			||||||
 | 
					      style="align-self: start;"
 | 
				
			||||||
      @click.stop.prevent="replacePlay()"
 | 
					      @click.stop.prevent="replacePlay()"
 | 
				
			||||||
      @split-click="isOpen = !isOpen"
 | 
					      @split-click="isOpen = !isOpen"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
| 
						 | 
					@ -238,6 +242,7 @@ const isOpen = ref(false)
 | 
				
			||||||
    :round="iconOnly"
 | 
					    :round="iconOnly"
 | 
				
			||||||
    :primary="iconOnly && !discrete"
 | 
					    :primary="iconOnly && !discrete"
 | 
				
			||||||
    :ghost="discrete"
 | 
					    :ghost="discrete"
 | 
				
			||||||
 | 
					    :low-height="lowHeight || undefined"
 | 
				
			||||||
    @click.stop.prevent="replacePlay()"
 | 
					    @click.stop.prevent="replacePlay()"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <template v-if="!discrete && !iconOnly">
 | 
					    <template v-if="!discrete && !iconOnly">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -110,8 +110,8 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
 | 
				
			||||||
        {{ generateTrackCreditString(track) }}
 | 
					        {{ generateTrackCreditString(track) }}
 | 
				
			||||||
        <span class="middle middledot symbol" />
 | 
					        <span class="middle middledot symbol" />
 | 
				
			||||||
        <human-duration
 | 
					        <human-duration
 | 
				
			||||||
          v-if="track.uploads[0] && track.uploads[0].duration"
 | 
					          v-if="track.uploads?.[0]?.duration"
 | 
				
			||||||
          :duration="track.uploads[0].duration"
 | 
					          :duration="track.uploads[0]?.duration"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
import clip from 'text-clipper'
 | 
					import clip from 'text-clipper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Layout from '~/components/ui/Layout.vue'
 | 
				
			||||||
import Button from '~/components/ui/Button.vue'
 | 
					import Button from '~/components/ui/Button.vue'
 | 
				
			||||||
import Alert from '~/components/ui/Alert.vue'
 | 
					import Alert from '~/components/ui/Alert.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +24,7 @@ interface Props {
 | 
				
			||||||
  fetchHtml?: boolean
 | 
					  fetchHtml?: boolean
 | 
				
			||||||
  permissive?: boolean
 | 
					  permissive?: boolean
 | 
				
			||||||
  truncateLength?: number
 | 
					  truncateLength?: number
 | 
				
			||||||
 | 
					  moreLink?: boolean
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { t } = useI18n()
 | 
					const { t } = useI18n()
 | 
				
			||||||
| 
						 | 
					@ -35,7 +37,8 @@ const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
  canUpdate: true,
 | 
					  canUpdate: true,
 | 
				
			||||||
  fetchHtml: false,
 | 
					  fetchHtml: false,
 | 
				
			||||||
  permissive: false,
 | 
					  permissive: false,
 | 
				
			||||||
  truncateLength: 200
 | 
					  truncateLength: 200,
 | 
				
			||||||
 | 
					  moreLink: true
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const preview = ref('')
 | 
					const preview = ref('')
 | 
				
			||||||
| 
						 | 
					@ -89,34 +92,40 @@ const submit = async () => {
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <template v-if="content && !isUpdating">
 | 
					  <Layout
 | 
				
			||||||
 | 
					    v-if="content && !isUpdating"
 | 
				
			||||||
 | 
					    flex
 | 
				
			||||||
 | 
					    gap-4
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
    <!-- Render the truncated or full description -->
 | 
					    <!-- Render the truncated or full description -->
 | 
				
			||||||
 | 
					    <sanitized-html
 | 
				
			||||||
    <sanitized-html :html="html" />
 | 
					      :html="html"
 | 
				
			||||||
 | 
					      :class="['description', isTruncated ? 'truncated' : '']"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- Display the `show more` / `show less` button -->
 | 
					    <!-- Display the `show more` / `show less` button -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <template v-if="isTruncated">
 | 
					    <template v-if="isTruncated">
 | 
				
			||||||
      <a
 | 
					      <a
 | 
				
			||||||
        v-if="showMore === false"
 | 
					        v-if="showMore === false && props.moreLink !== false"
 | 
				
			||||||
        class="more"
 | 
					        class="more"
 | 
				
			||||||
        style="align-self: end; color: var(--fw-primary);"
 | 
					        style="align-self: flex-end; color: var(--fw-primary);"
 | 
				
			||||||
        href=""
 | 
					        href=""
 | 
				
			||||||
        @click.stop.prevent="showMore = true"
 | 
					        @click.stop.prevent="showMore = true"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {{ t('components.common.RenderedDescription.button.more') }}
 | 
					        {{ t('components.common.RenderedDescription.button.more') }}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
      <a
 | 
					      <a
 | 
				
			||||||
        v-else
 | 
					        v-else-if="props.moreLink !== false"
 | 
				
			||||||
        class="more"
 | 
					        class="more"
 | 
				
			||||||
        style="align-self: end; color: var(--fw-primary);"
 | 
					        style="align-self: center; color: var(--fw-primary);"
 | 
				
			||||||
        href=""
 | 
					        href=""
 | 
				
			||||||
        @click.stop.prevent="showMore = false"
 | 
					        @click.stop.prevent="showMore = false"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {{ t('components.common.RenderedDescription.button.less') }}
 | 
					        {{ t('components.common.RenderedDescription.button.less') }}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
  </template>
 | 
					  </Layout>
 | 
				
			||||||
  <span v-else-if="!isUpdating">
 | 
					  <span v-else-if="!isUpdating">
 | 
				
			||||||
    {{ t('components.common.RenderedDescription.empty.noDescription') }}
 | 
					    {{ t('components.common.RenderedDescription.empty.noDescription') }}
 | 
				
			||||||
  </span>
 | 
					  </span>
 | 
				
			||||||
| 
						 | 
					@ -166,3 +175,19 @@ const submit = async () => {
 | 
				
			||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
</template>
 | 
					</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 PlayButton from '~/components/audio/PlayButton.vue'
 | 
				
			||||||
import AlbumDropdown from './AlbumDropdown.vue'
 | 
					import AlbumDropdown from './AlbumDropdown.vue'
 | 
				
			||||||
import Layout from '~/components/ui/Layout.vue'
 | 
					import Layout from '~/components/ui/Layout.vue'
 | 
				
			||||||
 | 
					import Header from '~/components/ui/Header.vue'
 | 
				
			||||||
import Spacer from '~/components/ui/Spacer.vue'
 | 
					import Spacer from '~/components/ui/Spacer.vue'
 | 
				
			||||||
import Loader from '~/components/ui/Loader.vue'
 | 
					import Loader from '~/components/ui/Loader.vue'
 | 
				
			||||||
import Button from '~/components/ui/Button.vue'
 | 
					import Button from '~/components/ui/Button.vue'
 | 
				
			||||||
| 
						 | 
					@ -147,147 +148,154 @@ const remove = async () => {
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <Layout
 | 
					  <Loader
 | 
				
			||||||
    stack
 | 
					    v-if="isLoading"
 | 
				
			||||||
    main
 | 
					    v-title="labels.title"
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					  <Header
 | 
				
			||||||
 | 
					    v-if="object"
 | 
				
			||||||
 | 
					    :h1="object.title"
 | 
				
			||||||
 | 
					    page-heading
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <Loader
 | 
					    <template #image>
 | 
				
			||||||
      v-if="isLoading"
 | 
					      <img
 | 
				
			||||||
      v-title="labels.title"
 | 
					        v-if="object.cover && object.cover.urls.original"
 | 
				
			||||||
    />
 | 
					        v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)"
 | 
				
			||||||
    <template v-if="object">
 | 
					        :alt="object.title"
 | 
				
			||||||
      <Layout flex>
 | 
					        class="channel-image"
 | 
				
			||||||
        <img
 | 
					      >
 | 
				
			||||||
          v-if="object.cover && object.cover.urls.original"
 | 
					      <img
 | 
				
			||||||
          v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)"
 | 
					        v-else
 | 
				
			||||||
          :alt="object.title"
 | 
					        alt=""
 | 
				
			||||||
          class="channel-image"
 | 
					        class="channel-image"
 | 
				
			||||||
        >
 | 
					        src="../../assets/audio/default-cover.png"
 | 
				
			||||||
        <img
 | 
					      >
 | 
				
			||||||
          v-else
 | 
					 | 
				
			||||||
          alt=""
 | 
					 | 
				
			||||||
          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>
 | 
					 | 
				
			||||||
          <artist-credit-label
 | 
					 | 
				
			||||||
            v-if="artistCredit"
 | 
					 | 
				
			||||||
            :artist-credit="artistCredit"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <!-- Metadata: -->
 | 
					 | 
				
			||||||
          <div class="meta">
 | 
					 | 
				
			||||||
            <template v-if="object.release_date">
 | 
					 | 
				
			||||||
              {{ momentFormat(new Date(object.release_date ?? '1970-01-01'), 'Y') }}
 | 
					 | 
				
			||||||
              <i class="bi bi-dot" />
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
            <template v-if="totalTracks > 0">
 | 
					 | 
				
			||||||
              <span v-if="isSerie">
 | 
					 | 
				
			||||||
                {{ t('components.library.AlbumBase.meta.episodes', totalTracks) }}
 | 
					 | 
				
			||||||
              </span>
 | 
					 | 
				
			||||||
              <span v-else>
 | 
					 | 
				
			||||||
                {{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
 | 
					 | 
				
			||||||
              </span>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
            <i
 | 
					 | 
				
			||||||
              v-if="totalDuration > 0"
 | 
					 | 
				
			||||||
              class="bi bi-dot"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <human-duration
 | 
					 | 
				
			||||||
              v-if="totalDuration > 0"
 | 
					 | 
				
			||||||
              :duration="totalDuration"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <!--TODO: License -->
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <Layout flex>
 | 
					 | 
				
			||||||
            <rendered-description
 | 
					 | 
				
			||||||
              v-if="object.description"
 | 
					 | 
				
			||||||
              :content="object.description"
 | 
					 | 
				
			||||||
              :can-update="true"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </Layout>
 | 
					 | 
				
			||||||
          <Layout flex>
 | 
					 | 
				
			||||||
            <PlayButton
 | 
					 | 
				
			||||||
              v-if="object.tracks"
 | 
					 | 
				
			||||||
              split
 | 
					 | 
				
			||||||
              :tracks="object.tracks"
 | 
					 | 
				
			||||||
              :is-playable="object.is_playable"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <Button
 | 
					 | 
				
			||||||
              v-if="object?.tracks?.length && object?.tracks?.length > 2"
 | 
					 | 
				
			||||||
              primary
 | 
					 | 
				
			||||||
              icon="bi-shuffle"
 | 
					 | 
				
			||||||
              :aria-label="labels.shuffle"
 | 
					 | 
				
			||||||
              @click.prevent.stop="shuffle()"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {{ labels.shuffle }}
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
            <DangerousButton
 | 
					 | 
				
			||||||
              v-if="artistCredit[0] &&
 | 
					 | 
				
			||||||
                store.state.auth.authenticated &&
 | 
					 | 
				
			||||||
                artistCredit[0].artist.channel
 | 
					 | 
				
			||||||
                /* TODO: Re-implement once attributed_to is not only a number
 | 
					 | 
				
			||||||
                   && artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername
 | 
					 | 
				
			||||||
                */"
 | 
					 | 
				
			||||||
              :is-loading="isLoading"
 | 
					 | 
				
			||||||
              icon="bi-trash"
 | 
					 | 
				
			||||||
              @confirm="remove()"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {{ t('components.library.AlbumDropdown.button.delete') }}
 | 
					 | 
				
			||||||
            </DangerousButton>
 | 
					 | 
				
			||||||
            <Spacer
 | 
					 | 
				
			||||||
              h
 | 
					 | 
				
			||||||
              grow
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <TrackFavoriteIcon
 | 
					 | 
				
			||||||
              v-if="store.state.auth.authenticated"
 | 
					 | 
				
			||||||
              :album="object"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <TrackPlaylistIcon
 | 
					 | 
				
			||||||
              v-if="store.state.auth.authenticated"
 | 
					 | 
				
			||||||
              :album="object"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <!-- TODO: Share Button -->
 | 
					 | 
				
			||||||
            <album-dropdown
 | 
					 | 
				
			||||||
              :object="object"
 | 
					 | 
				
			||||||
              :public-libraries="publicLibraries"
 | 
					 | 
				
			||||||
              :is-loading="isLoading"
 | 
					 | 
				
			||||||
              :is-album="isAlbum"
 | 
					 | 
				
			||||||
              :is-serie="isSerie"
 | 
					 | 
				
			||||||
              :is-channel="isChannel"
 | 
					 | 
				
			||||||
              :artist-credit="artistCredit"
 | 
					 | 
				
			||||||
              @remove="remove"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </Layout>
 | 
					 | 
				
			||||||
        </Layout>
 | 
					 | 
				
			||||||
      </Layout>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div style="flex 1;">
 | 
					 | 
				
			||||||
        <router-view
 | 
					 | 
				
			||||||
          v-if="object"
 | 
					 | 
				
			||||||
          :key="route.fullPath"
 | 
					 | 
				
			||||||
          :paginate-by="paginateBy"
 | 
					 | 
				
			||||||
          :total-tracks="totalTracks"
 | 
					 | 
				
			||||||
          :is-serie="isSerie"
 | 
					 | 
				
			||||||
          :artist-credit="artistCredit"
 | 
					 | 
				
			||||||
          :object="object"
 | 
					 | 
				
			||||||
          :is-loading-tracks="isLoadingTracks"
 | 
					 | 
				
			||||||
          object-type="album"
 | 
					 | 
				
			||||||
          @libraries-loaded="libraries = $event"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
  </Layout>
 | 
					    <artist-credit-label
 | 
				
			||||||
 | 
					      v-if="artistCredit"
 | 
				
			||||||
 | 
					      :artist-credit="artistCredit"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <!-- Metadata: -->
 | 
				
			||||||
 | 
					    <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" />
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					        <template v-if="totalTracks > 0">
 | 
				
			||||||
 | 
					          <span v-if="isSerie">
 | 
				
			||||||
 | 
					            {{ t('components.library.AlbumBase.meta.episodes', totalTracks) }}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <span v-else>
 | 
				
			||||||
 | 
					            {{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					        <i
 | 
				
			||||||
 | 
					          v-if="totalDuration > 0"
 | 
				
			||||||
 | 
					          class="bi bi-dot"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <human-duration
 | 
				
			||||||
 | 
					          v-if="totalDuration > 0"
 | 
				
			||||||
 | 
					          :duration="totalDuration"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <!--TODO: License -->
 | 
				
			||||||
 | 
					      </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()"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {{ labels.shuffle }}
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					      <DangerousButton
 | 
				
			||||||
 | 
					        v-if="artistCredit[0] &&
 | 
				
			||||||
 | 
					          store.state.auth.authenticated &&
 | 
				
			||||||
 | 
					          artistCredit[0].artist.channel
 | 
				
			||||||
 | 
					          /* TODO: Re-implement once attributed_to is not only a number
 | 
				
			||||||
 | 
					              && artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername
 | 
				
			||||||
 | 
					          */"
 | 
				
			||||||
 | 
					        :is-loading="isLoading"
 | 
				
			||||||
 | 
					        low-height
 | 
				
			||||||
 | 
					        icon="bi-trash"
 | 
				
			||||||
 | 
					        @confirm="remove()"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {{ t('components.library.AlbumDropdown.button.delete') }}
 | 
				
			||||||
 | 
					      </DangerousButton>
 | 
				
			||||||
 | 
					      <Spacer
 | 
				
			||||||
 | 
					        h
 | 
				
			||||||
 | 
					        grow
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <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 -->
 | 
				
			||||||
 | 
					      <album-dropdown
 | 
				
			||||||
 | 
					        :object="object"
 | 
				
			||||||
 | 
					        :public-libraries="publicLibraries"
 | 
				
			||||||
 | 
					        :is-loading="isLoading"
 | 
				
			||||||
 | 
					        :is-album="isAlbum"
 | 
				
			||||||
 | 
					        :is-serie="isSerie"
 | 
				
			||||||
 | 
					        :is-channel="isChannel"
 | 
				
			||||||
 | 
					        :artist-credit="artistCredit"
 | 
				
			||||||
 | 
					        @remove="remove"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Layout>
 | 
				
			||||||
 | 
					  </Header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div style="flex 1;">
 | 
				
			||||||
 | 
					    <router-view
 | 
				
			||||||
 | 
					      v-if="object"
 | 
				
			||||||
 | 
					      :key="route.fullPath"
 | 
				
			||||||
 | 
					      :paginate-by="paginateBy"
 | 
				
			||||||
 | 
					      :total-tracks="totalTracks"
 | 
				
			||||||
 | 
					      :is-serie="isSerie"
 | 
				
			||||||
 | 
					      :artist-credit="artistCredit"
 | 
				
			||||||
 | 
					      :object="object"
 | 
				
			||||||
 | 
					      :is-loading-tracks="isLoadingTracks"
 | 
				
			||||||
 | 
					      object-type="album"
 | 
				
			||||||
 | 
					      @libraries-loaded="libraries = $event"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scopen>
 | 
					<style scoped lang="scss">
 | 
				
			||||||
  .meta {
 | 
					  .meta {
 | 
				
			||||||
    line-height: 48px;
 | 
					    font-size: 15px;
 | 
				
			||||||
 | 
					    @include light-theme {
 | 
				
			||||||
 | 
					      color: var(--fw-gray-700);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @include dark-theme {
 | 
				
			||||||
 | 
					      color: var(--fw-gray-500);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,10 +16,6 @@ import Popover from '~/components/ui/Popover.vue'
 | 
				
			||||||
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
 | 
					import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
 | 
				
			||||||
import OptionsButton from '~/components/ui/button/Options.vue'
 | 
					import OptionsButton from '~/components/ui/button/Options.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Events {
 | 
					 | 
				
			||||||
  (e: 'remove'): void
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  isLoading: boolean
 | 
					  isLoading: boolean
 | 
				
			||||||
  artistCredit: ArtistCredit[]
 | 
					  artistCredit: ArtistCredit[]
 | 
				
			||||||
| 
						 | 
					@ -28,10 +24,10 @@ interface Props {
 | 
				
			||||||
  isAlbum: boolean
 | 
					  isAlbum: boolean
 | 
				
			||||||
  isChannel: boolean
 | 
					  isChannel: boolean
 | 
				
			||||||
  isSerie: boolean
 | 
					  isSerie: boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const store = useStore()
 | 
					const store = useStore()
 | 
				
			||||||
const emit = defineEmits<Events>()
 | 
					 | 
				
			||||||
const props = defineProps<Props>()
 | 
					const props = defineProps<Props>()
 | 
				
			||||||
const { report, getReportableObjects } = useReport()
 | 
					const { report, getReportableObjects } = useReport()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,7 +71,7 @@ const open = ref(false)
 | 
				
			||||||
      <template #default="{ toggleOpen }">
 | 
					      <template #default="{ toggleOpen }">
 | 
				
			||||||
        <OptionsButton
 | 
					        <OptionsButton
 | 
				
			||||||
          :title="labels.more"
 | 
					          :title="labels.more"
 | 
				
			||||||
          is-square
 | 
					          is-square-small
 | 
				
			||||||
          @click="toggleOpen()"
 | 
					          @click="toggleOpen()"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,11 +11,14 @@ import { useStore } from '~/store'
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
import useReport from '~/composables/moderation/useReport'
 | 
					import useReport from '~/composables/moderation/useReport'
 | 
				
			||||||
import useLogger from '~/composables/useLogger'
 | 
					import useLogger from '~/composables/useLogger'
 | 
				
			||||||
 | 
					import { useModal } from '~/ui/composables/useModal.ts'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import HumanDuration from '~/components/common/HumanDuration.vue'
 | 
					import HumanDuration from '~/components/common/HumanDuration.vue'
 | 
				
			||||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
 | 
					import EmbedWizard from '~/components/audio/EmbedWizard.vue'
 | 
				
			||||||
import Loader from '~/components/ui/Loader.vue'
 | 
					import Loader from '~/components/ui/Loader.vue'
 | 
				
			||||||
 | 
					import Header from '~/components/ui/Header.vue'
 | 
				
			||||||
import Button from '~/components/ui/Button.vue'
 | 
					import Button from '~/components/ui/Button.vue'
 | 
				
			||||||
 | 
					import Link from '~/components/ui/Link.vue'
 | 
				
			||||||
import OptionsButton from '~/components/ui/button/Options.vue'
 | 
					import OptionsButton from '~/components/ui/button/Options.vue'
 | 
				
			||||||
import PlayButton from '~/components/audio/PlayButton.vue'
 | 
					import PlayButton from '~/components/audio/PlayButton.vue'
 | 
				
			||||||
import RadioButton from '~/components/radios/Button.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 Layout from '~/components/ui/Layout.vue'
 | 
				
			||||||
import Modal from '~/components/ui/Modal.vue'
 | 
					import Modal from '~/components/ui/Modal.vue'
 | 
				
			||||||
import Spacer from '~/components/ui/Spacer.vue'
 | 
					import Spacer from '~/components/ui/Spacer.vue'
 | 
				
			||||||
 | 
					import RenderedDescription from '../common/RenderedDescription.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  id: number | string
 | 
					  id: number | string
 | 
				
			||||||
| 
						 | 
					@ -125,192 +129,246 @@ const fetchData = async () => {
 | 
				
			||||||
const totalDuration = computed(() => sum((tracks.value ?? []).map(track => track.uploads[0]?.duration ?? 0)))
 | 
					const totalDuration = computed(() => sum((tracks.value ?? []).map(track => track.uploads[0]?.duration ?? 0)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch(() => props.id, fetchData, { immediate: true })
 | 
					watch(() => props.id, fetchData, { immediate: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isOpen = useModal('artist-description').isOpen
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <Layout
 | 
					  <Loader v-if="isLoading" />
 | 
				
			||||||
 | 
					  <Header
 | 
				
			||||||
 | 
					    v-if="object && !isLoading"
 | 
				
			||||||
    v-title="labels.title"
 | 
					    v-title="labels.title"
 | 
				
			||||||
    stack
 | 
					    :h1="object.name"
 | 
				
			||||||
    main
 | 
					    page-heading
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <Loader v-if="isLoading" />
 | 
					    <template #image>
 | 
				
			||||||
    <template v-if="object && !isLoading">
 | 
					      <img
 | 
				
			||||||
      <Layout flex>
 | 
					        v-lazy="cover.urls.large_square_crop"
 | 
				
			||||||
        <img
 | 
					        :alt="object.name"
 | 
				
			||||||
          v-lazy="cover.urls.large_square_crop"
 | 
					        class="channel-image"
 | 
				
			||||||
          :alt="object.name"
 | 
					 | 
				
			||||||
          class="channel-image"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
        <Layout
 | 
					 | 
				
			||||||
          stack
 | 
					 | 
				
			||||||
          style="flex: 1; gap: 8px;"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <h1 style="margin-top: 64px; margin-bottom: 8px;">
 | 
					 | 
				
			||||||
            {{ object.name }}
 | 
					 | 
				
			||||||
          </h1>
 | 
					 | 
				
			||||||
          <Layout
 | 
					 | 
				
			||||||
            flex
 | 
					 | 
				
			||||||
            class="meta"
 | 
					 | 
				
			||||||
            style="gap: 0;"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <div
 | 
					 | 
				
			||||||
              v-if="albums"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {{ t('components.library.ArtistBase.meta.tracks', totalTracks) }}
 | 
					 | 
				
			||||||
              {{ t('components.library.ArtistBase.meta.albums', totalAlbums) }}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div v-if="totalDuration > 0">
 | 
					 | 
				
			||||||
              <i class="bi bi-dot" />
 | 
					 | 
				
			||||||
              <human-duration
 | 
					 | 
				
			||||||
                v-if="totalDuration > 0"
 | 
					 | 
				
			||||||
                :duration="totalDuration"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </Layout>
 | 
					 | 
				
			||||||
          <Spacer />
 | 
					 | 
				
			||||||
          <Layout flex>
 | 
					 | 
				
			||||||
            <PlayButton
 | 
					 | 
				
			||||||
              :is-playable="isPlayable"
 | 
					 | 
				
			||||||
              split
 | 
					 | 
				
			||||||
              :artist="object"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {{ t('components.library.ArtistBase.button.play') }}
 | 
					 | 
				
			||||||
            </PlayButton>
 | 
					 | 
				
			||||||
            <radio-button
 | 
					 | 
				
			||||||
              type="artist"
 | 
					 | 
				
			||||||
              :object-id="object.id"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <Spacer grow />
 | 
					 | 
				
			||||||
            <Popover>
 | 
					 | 
				
			||||||
              <template #default="{ toggleOpen }">
 | 
					 | 
				
			||||||
                <OptionsButton
 | 
					 | 
				
			||||||
                  @click="toggleOpen"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <template #items>
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  v-if="object.fid && domain != store.getters['instance/domain']"
 | 
					 | 
				
			||||||
                  :to="object.fid"
 | 
					 | 
				
			||||||
                  target="_blank"
 | 
					 | 
				
			||||||
                  icon="bi-box-arrow-up-right"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.link.domain', {domain: domain}) }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  v-if="publicLibraries.length > 0"
 | 
					 | 
				
			||||||
                  icon="bi-code-square"
 | 
					 | 
				
			||||||
                  @click="showEmbedModal = true"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.button.embed') }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  :to="wikipediaUrl"
 | 
					 | 
				
			||||||
                  target="_blank"
 | 
					 | 
				
			||||||
                  rel="noreferrer noopener"
 | 
					 | 
				
			||||||
                  icon="bi-wikipedia"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.link.wikipedia') }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  v-if="musicbrainzUrl"
 | 
					 | 
				
			||||||
                  :to="musicbrainzUrl"
 | 
					 | 
				
			||||||
                  target="_blank"
 | 
					 | 
				
			||||||
                  rel="noreferrer noopener"
 | 
					 | 
				
			||||||
                  icon="bi-box-arrow-up-right"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.link.musicbrainz') }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  :to="discogsUrl"
 | 
					 | 
				
			||||||
                  target="_blank"
 | 
					 | 
				
			||||||
                  rel="noreferrer noopener"
 | 
					 | 
				
			||||||
                  icon="bi-box-arrow-up-right"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.link.discogs') }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  v-if="object.is_local"
 | 
					 | 
				
			||||||
                  :to="{name: 'library.artists.edit', params: {id: object.id }}"
 | 
					 | 
				
			||||||
                  icon="bi-pencil-fill"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.button.edit') }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <hr v-if="getReportableObjects({artist: object}).length>0">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  v-for="obj in getReportableObjects({artist: object})"
 | 
					 | 
				
			||||||
                  :key="obj.target.type + obj.target.id"
 | 
					 | 
				
			||||||
                  icon="bi-share-fill"
 | 
					 | 
				
			||||||
                  @click="report(obj)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ obj.label }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <hr v-if="getReportableObjects({artist: object}).length>0">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  v-if="store.state.auth.availablePermissions['library']"
 | 
					 | 
				
			||||||
                  :to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
 | 
					 | 
				
			||||||
                  icon="bi-wrench"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.link.moderation') }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <PopoverItem
 | 
					 | 
				
			||||||
                  v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
 | 
					 | 
				
			||||||
                  :to="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
 | 
					 | 
				
			||||||
                  target="_blank"
 | 
					 | 
				
			||||||
                  rel="noopener noreferrer"
 | 
					 | 
				
			||||||
                  icon="bi-wrench"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  {{ t('components.library.ArtistBase.link.django') }}
 | 
					 | 
				
			||||||
                </PopoverItem>
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
            </Popover>
 | 
					 | 
				
			||||||
          </Layout>
 | 
					 | 
				
			||||||
        </Layout>
 | 
					 | 
				
			||||||
      </Layout>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <Modal
 | 
					 | 
				
			||||||
        v-if="publicLibraries.length > 0"
 | 
					 | 
				
			||||||
        v-model="showEmbedModal"
 | 
					 | 
				
			||||||
        :title="t('components.library.ArtistBase.modal.embed.header')"
 | 
					 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <embed-wizard
 | 
					 | 
				
			||||||
          :id="object.id"
 | 
					 | 
				
			||||||
          type="artist"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <template #actions>
 | 
					 | 
				
			||||||
          <Button secondary>
 | 
					 | 
				
			||||||
            {{ t('components.library.ArtistBase.button.cancel') }}
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </Modal>
 | 
					 | 
				
			||||||
      <hr>
 | 
					 | 
				
			||||||
      <router-view
 | 
					 | 
				
			||||||
        :key="route.fullPath"
 | 
					 | 
				
			||||||
        :tracks="tracks"
 | 
					 | 
				
			||||||
        :next-tracks-url="nextTracksUrl"
 | 
					 | 
				
			||||||
        :next-albums-url="nextAlbumsUrl"
 | 
					 | 
				
			||||||
        :albums="albums"
 | 
					 | 
				
			||||||
        :is-loading-albums="isLoading"
 | 
					 | 
				
			||||||
        :object="object"
 | 
					 | 
				
			||||||
        object-type="artist"
 | 
					 | 
				
			||||||
        @libraries-loaded="libraries = $event"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
  </Layout>
 | 
					    <Layout
 | 
				
			||||||
 | 
					      flex
 | 
				
			||||||
 | 
					      class="meta"
 | 
				
			||||||
 | 
					      no-gap
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        v-if="albums"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {{ t('components.library.ArtistBase.meta.tracks', totalTracks) }}
 | 
				
			||||||
 | 
					        {{ t('components.library.ArtistBase.meta.albums', totalAlbums) }}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div v-if="totalDuration > 0">
 | 
				
			||||||
 | 
					        <i class="bi bi-dot" />
 | 
				
			||||||
 | 
					        <human-duration
 | 
				
			||||||
 | 
					          v-if="totalDuration > 0"
 | 
				
			||||||
 | 
					          :duration="totalDuration"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Layout>
 | 
				
			||||||
 | 
					    <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <template #items>
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            v-if="object.fid && domain != store.getters['instance/domain']"
 | 
				
			||||||
 | 
					            :to="object.fid"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            icon="bi-box-arrow-up-right"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.link.domain', {domain: domain}) }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            v-if="publicLibraries.length > 0"
 | 
				
			||||||
 | 
					            icon="bi-code-square"
 | 
				
			||||||
 | 
					            @click="showEmbedModal = true"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.button.embed') }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            :to="wikipediaUrl"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noreferrer noopener"
 | 
				
			||||||
 | 
					            icon="bi-wikipedia"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.link.wikipedia') }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            v-if="musicbrainzUrl"
 | 
				
			||||||
 | 
					            :to="musicbrainzUrl"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noreferrer noopener"
 | 
				
			||||||
 | 
					            icon="bi-box-arrow-up-right"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.link.musicbrainz') }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            :to="discogsUrl"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noreferrer noopener"
 | 
				
			||||||
 | 
					            icon="bi-box-arrow-up-right"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.link.discogs') }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            v-if="object.is_local"
 | 
				
			||||||
 | 
					            :to="{name: 'library.artists.edit', params: {id: object.id }}"
 | 
				
			||||||
 | 
					            icon="bi-pencil-fill"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.button.edit') }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <hr v-if="getReportableObjects({artist: object}).length>0">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            v-for="obj in getReportableObjects({artist: object})"
 | 
				
			||||||
 | 
					            :key="obj.target.type + obj.target.id"
 | 
				
			||||||
 | 
					            icon="bi-share-fill"
 | 
				
			||||||
 | 
					            @click="report(obj)"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ obj.label }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <hr v-if="getReportableObjects({artist: object}).length>0">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            v-if="store.state.auth.availablePermissions['library']"
 | 
				
			||||||
 | 
					            :to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
 | 
				
			||||||
 | 
					            icon="bi-wrench"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.link.moderation') }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <PopoverItem
 | 
				
			||||||
 | 
					            v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
 | 
				
			||||||
 | 
					            :to="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            rel="noopener noreferrer"
 | 
				
			||||||
 | 
					            icon="bi-wrench"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {{ t('components.library.ArtistBase.link.django') }}
 | 
				
			||||||
 | 
					          </PopoverItem>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </Popover>
 | 
				
			||||||
 | 
					    </Layout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      v-if="publicLibraries.length > 0"
 | 
				
			||||||
 | 
					      v-model="showEmbedModal"
 | 
				
			||||||
 | 
					      :title="t('components.library.ArtistBase.modal.embed.header')"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <embed-wizard
 | 
				
			||||||
 | 
					        :id="object.id"
 | 
				
			||||||
 | 
					        type="artist"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <template #actions>
 | 
				
			||||||
 | 
					        <Button secondary>
 | 
				
			||||||
 | 
					          {{ t('components.library.ArtistBase.button.cancel') }}
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </template>
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  </Header>
 | 
				
			||||||
 | 
					  <hr>
 | 
				
			||||||
 | 
					  <router-view
 | 
				
			||||||
 | 
					    :key="route.fullPath"
 | 
				
			||||||
 | 
					    :tracks="tracks"
 | 
				
			||||||
 | 
					    :next-tracks-url="nextTracksUrl"
 | 
				
			||||||
 | 
					    :next-albums-url="nextAlbumsUrl"
 | 
				
			||||||
 | 
					    :albums="albums"
 | 
				
			||||||
 | 
					    :is-loading-albums="isLoading"
 | 
				
			||||||
 | 
					    :object="object"
 | 
				
			||||||
 | 
					    object-type="artist"
 | 
				
			||||||
 | 
					    @libraries-loaded="libraries = $event"
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped lang="scss">
 | 
				
			||||||
  .channel-image {
 | 
					  .channel-image {
 | 
				
			||||||
    border-radius: 50%;
 | 
					    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>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -242,8 +242,8 @@ const trackDetails: {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss">
 | 
					<style lang="scss">
 | 
				
			||||||
.channel-image {
 | 
					.channel-image {
 | 
				
			||||||
  width: 300px;
 | 
					  width: 200px;
 | 
				
			||||||
  height: 300px;
 | 
					  height: 200px;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import axios from 'axios'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import useErrorHandler from '~/composables/useErrorHandler'
 | 
					import useErrorHandler from '~/composables/useErrorHandler'
 | 
				
			||||||
import OptionsButton from '~/components/ui/button/Options.vue'
 | 
					import OptionsButton from '~/components/ui/button/Options.vue'
 | 
				
			||||||
 | 
					import EmbedWizard from '~/components/audio/EmbedWizard.vue'
 | 
				
			||||||
import Modal from '~/components/ui/Modal.vue'
 | 
					import Modal from '~/components/ui/Modal.vue'
 | 
				
			||||||
import Alert from '~/components/ui/Alert.vue'
 | 
					import Alert from '~/components/ui/Alert.vue'
 | 
				
			||||||
import Button from '~/components/ui/Button.vue'
 | 
					import Button from '~/components/ui/Button.vue'
 | 
				
			||||||
| 
						 | 
					@ -104,7 +105,10 @@ const showDeleteModal = ref(false)
 | 
				
			||||||
  <span>
 | 
					  <span>
 | 
				
			||||||
    <Popover v-model="open">
 | 
					    <Popover v-model="open">
 | 
				
			||||||
      <template #default="{ toggleOpen }">
 | 
					      <template #default="{ toggleOpen }">
 | 
				
			||||||
        <OptionsButton @click="toggleOpen" />
 | 
					        <OptionsButton
 | 
				
			||||||
 | 
					          is-square-small
 | 
				
			||||||
 | 
					          @click="toggleOpen"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
      <template #items>
 | 
					      <template #items>
 | 
				
			||||||
        <PopoverItem
 | 
					        <PopoverItem
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@ const props = defineProps<{
 | 
				
			||||||
  alignLeft?: boolean
 | 
					  alignLeft?: boolean
 | 
				
			||||||
  action?: { text: string } & (ComponentProps<typeof Link> | ComponentProps<typeof Button>)
 | 
					  action?: { text: string } & (ComponentProps<typeof Link> | ComponentProps<typeof Button>)
 | 
				
			||||||
  icon?: string
 | 
					  icon?: string
 | 
				
			||||||
 | 
					  noGap?: false
 | 
				
			||||||
} & {
 | 
					} & {
 | 
				
			||||||
  [H in `h${ '1' | '2' | '3' | '4' | '5' | '6' }`]? : string
 | 
					  [H in `h${ '1' | '2' | '3' | '4' | '5' | '6' }`]? : string
 | 
				
			||||||
} & {
 | 
					} & {
 | 
				
			||||||
| 
						 | 
					@ -35,7 +36,8 @@ const props = defineProps<{
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <Layout
 | 
					    <Layout
 | 
				
			||||||
      stack
 | 
					      stack
 | 
				
			||||||
      gap-8
 | 
					      :gap-8="!(props.noGap as boolean)"
 | 
				
			||||||
 | 
					      :no-gap="props.noGap"
 | 
				
			||||||
      style="flex-grow: 1;"
 | 
					      style="flex-grow: 1;"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <Layout
 | 
					      <Layout
 | 
				
			||||||
| 
						 | 
					@ -46,7 +48,7 @@ const props = defineProps<{
 | 
				
			||||||
        <!-- Set distance between baseline and previous row -->
 | 
					        <!-- Set distance between baseline and previous row -->
 | 
				
			||||||
        <Spacer
 | 
					        <Spacer
 | 
				
			||||||
          v
 | 
					          v
 | 
				
			||||||
          :size="68"
 | 
					          :size="53"
 | 
				
			||||||
          style="align-self: baseline;"
 | 
					          style="align-self: baseline;"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
| 
						 | 
					@ -66,7 +68,7 @@ const props = defineProps<{
 | 
				
			||||||
          v-bind="props"
 | 
					          v-bind="props"
 | 
				
			||||||
          style="
 | 
					          style="
 | 
				
			||||||
            align-self: baseline;
 | 
					            align-self: baseline;
 | 
				
			||||||
            padding: 0 0 24px 0;
 | 
					            padding: 0 0 0 0;
 | 
				
			||||||
            margin: 0;
 | 
					            margin: 0;
 | 
				
			||||||
          "
 | 
					          "
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
| 
						 | 
					@ -93,6 +95,7 @@ const props = defineProps<{
 | 
				
			||||||
      <slot />
 | 
					      <slot />
 | 
				
			||||||
    </Layout>
 | 
					    </Layout>
 | 
				
			||||||
  </Layout>
 | 
					  </Layout>
 | 
				
			||||||
 | 
					  <Spacer />
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style module lang="scss">
 | 
					<style module lang="scss">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import Button from '../Button.vue'
 | 
				
			||||||
defineProps<{
 | 
					defineProps<{
 | 
				
			||||||
  isSquare?: boolean
 | 
					  isSquare?: boolean
 | 
				
			||||||
  isGhost?: boolean
 | 
					  isGhost?: boolean
 | 
				
			||||||
 | 
					  isSquareSmall?: boolean
 | 
				
			||||||
}>()
 | 
					}>()
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +15,8 @@ defineProps<{
 | 
				
			||||||
    :class="['options-button', {'is-ghost': isGhost}]"
 | 
					    :class="['options-button', {'is-ghost': isGhost}]"
 | 
				
			||||||
    :secondary="!isGhost"
 | 
					    :secondary="!isGhost"
 | 
				
			||||||
    :ghost="isGhost"
 | 
					    :ghost="isGhost"
 | 
				
			||||||
    :round="!isSquare"
 | 
					    :round="!isSquare && !isSquareSmall"
 | 
				
			||||||
 | 
					    :square-small="isSquareSmall"
 | 
				
			||||||
  />
 | 
					  />
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2824,6 +2824,40 @@
 | 
				
			||||||
          "tracks": "No tracks | {n} track | {n} tracks"
 | 
					          "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": {
 | 
					      "Editor": {
 | 
				
			||||||
        "button": {
 | 
					        "button": {
 | 
				
			||||||
          "addDuplicate": "Add anyway",
 | 
					          "addDuplicate": "Add anyway",
 | 
				
			||||||
| 
						 | 
					@ -2877,6 +2911,50 @@
 | 
				
			||||||
          "name": "My awesome playlist"
 | 
					          "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": {
 | 
					      "PlaylistModal": {
 | 
				
			||||||
        "button": {
 | 
					        "button": {
 | 
				
			||||||
          "addDuplicate": "Add anyway",
 | 
					          "addDuplicate": "Add anyway",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2925,19 +2925,6 @@
 | 
				
			||||||
        "placeholder": {
 | 
					        "placeholder": {
 | 
				
			||||||
          "noPlaylists": "No playlists have been created yet"
 | 
					          "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": {
 | 
					    "radios": {
 | 
				
			||||||
| 
						 | 
					@ -4581,6 +4568,12 @@
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "playlists": {
 | 
					    "playlists": {
 | 
				
			||||||
 | 
					      "Card": {
 | 
				
			||||||
 | 
					        "title": "Updated on {date}",
 | 
				
			||||||
 | 
					        "meta": {
 | 
				
			||||||
 | 
					          "tracks": "No tracks | {n} track | {n} tracks"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "Detail": {
 | 
					      "Detail": {
 | 
				
			||||||
        "button": {
 | 
					        "button": {
 | 
				
			||||||
          "cancel": "Cancel",
 | 
					          "cancel": "Cancel",
 | 
				
			||||||
| 
						 | 
					@ -4615,6 +4608,59 @@
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "title": "Playlist"
 | 
					        "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": {
 | 
					      "List": {
 | 
				
			||||||
        "button": {
 | 
					        "button": {
 | 
				
			||||||
          "create": "Create a playlist",
 | 
					          "create": "Create a playlist",
 | 
				
			||||||
| 
						 | 
					@ -4645,6 +4691,72 @@
 | 
				
			||||||
        "placeholder": {
 | 
					        "placeholder": {
 | 
				
			||||||
          "search": "Enter playlist name…"
 | 
					          "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": {
 | 
					    "radios": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -113,8 +113,8 @@
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.channel-image {
 | 
					.channel-image {
 | 
				
			||||||
  width: 300px;
 | 
					  width: 200px;
 | 
				
			||||||
  height: 300px;
 | 
					  height: 200px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.large {
 | 
					  &.large {
 | 
				
			||||||
    width: 8em !important;
 | 
					    width: 8em !important;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,16 +9,21 @@ import { useStore } from '~/store'
 | 
				
			||||||
import { hashCode, intToRGB } from '~/utils/color'
 | 
					import { hashCode, intToRGB } from '~/utils/color'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import UserFollowButton from '~/components/federation/UserFollowButton.vue'
 | 
					import UserFollowButton from '~/components/federation/UserFollowButton.vue'
 | 
				
			||||||
 | 
					import { useModal } from '~/ui/composables/useModal.ts'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import useErrorHandler from '~/composables/useErrorHandler'
 | 
					import useErrorHandler from '~/composables/useErrorHandler'
 | 
				
			||||||
import RenderedDescription from '~/components/common/RenderedDescription.vue'
 | 
					import RenderedDescription from '~/components/common/RenderedDescription.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Layout from '~/components/ui/Layout.vue'
 | 
					import Layout from '~/components/ui/Layout.vue'
 | 
				
			||||||
 | 
					import Spacer from '~/components/ui/Spacer.vue'
 | 
				
			||||||
import Header from '~/components/ui/Header.vue'
 | 
					import Header from '~/components/ui/Header.vue'
 | 
				
			||||||
import Button from '~/components/ui/Button.vue'
 | 
					import Button from '~/components/ui/Button.vue'
 | 
				
			||||||
 | 
					import Link from '~/components/ui/Link.vue'
 | 
				
			||||||
import Nav from '~/components/ui/Nav.vue'
 | 
					import Nav from '~/components/ui/Nav.vue'
 | 
				
			||||||
import Alert from '~/components/ui/Alert.vue'
 | 
					import Alert from '~/components/ui/Alert.vue'
 | 
				
			||||||
 | 
					import Modal from '~/components/ui/Modal.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Events {
 | 
					interface Events {
 | 
				
			||||||
  (e: 'updated', value: components['schemas']['FullActor']): void
 | 
					  (e: 'updated', value: components['schemas']['FullActor']): void
 | 
				
			||||||
| 
						 | 
					@ -96,6 +101,8 @@ const tabs = ref([{
 | 
				
			||||||
      }]
 | 
					      }]
 | 
				
			||||||
    : []
 | 
					    : []
 | 
				
			||||||
)])
 | 
					)])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isOpen = useModal('artist-description').isOpen
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					@ -103,6 +110,7 @@ const tabs = ref([{
 | 
				
			||||||
    v-title="labels.usernameProfile"
 | 
					    v-title="labels.usernameProfile"
 | 
				
			||||||
    stack
 | 
					    stack
 | 
				
			||||||
    main
 | 
					    main
 | 
				
			||||||
 | 
					    no-gap
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <!-- TODO: Translate Edit Link -->
 | 
					    <!-- 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! -->
 | 
					    <!-- 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
 | 
					        // @ts-ignore
 | 
				
			||||||
        solid: true,
 | 
					        solid: true,
 | 
				
			||||||
        // @ts-ignore
 | 
					        // @ts-ignore
 | 
				
			||||||
        icon: 'bi-pencil-fill'
 | 
					        icon: 'bi-pencil-fill',
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
 | 
					        lowHeight: true
 | 
				
			||||||
      }"
 | 
					      }"
 | 
				
			||||||
      style="margin-top: 58px;"
 | 
					      no-gap
 | 
				
			||||||
      page-heading
 | 
					      page-heading
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <template #image>
 | 
					      <template #image>
 | 
				
			||||||
| 
						 | 
					@ -148,6 +158,7 @@ const tabs = ref([{
 | 
				
			||||||
            :title="t('components.common.CopyInput.button.copy')"
 | 
					            :title="t('components.common.CopyInput.button.copy')"
 | 
				
			||||||
            ghost
 | 
					            ghost
 | 
				
			||||||
            secondary
 | 
					            secondary
 | 
				
			||||||
 | 
					            low-height
 | 
				
			||||||
            @click="copy(fullUsername)"
 | 
					            @click="copy(fullUsername)"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
| 
						 | 
					@ -174,19 +185,43 @@ const tabs = ref([{
 | 
				
			||||||
        flex
 | 
					        flex
 | 
				
			||||||
        no-gap
 | 
					        no-gap
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <!-- TODO: Fix error with `$event` not being the right type -->
 | 
					 | 
				
			||||||
        <!-- @vue-ignore -->
 | 
					 | 
				
			||||||
        <RenderedDescription
 | 
					        <RenderedDescription
 | 
				
			||||||
          :content="{ html: object?.summary?.html || '' }"
 | 
					          v-if="object?.summary"
 | 
				
			||||||
          :field-name="'summary'"
 | 
					          class="description"
 | 
				
			||||||
          :update-url="`users/${store.state.auth.username}/`"
 | 
					          :content="{ html: object?.summary.html || '' }"
 | 
				
			||||||
          :can-update="store.state.auth.authenticated && object?.full_username === store.state.auth.fullUsername"
 | 
					 | 
				
			||||||
          :truncate-length="100"
 | 
					          :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>
 | 
					      </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
 | 
					      <UserFollowButton
 | 
				
			||||||
        v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername"
 | 
					        v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername"
 | 
				
			||||||
 | 
					        low-height
 | 
				
			||||||
        :actor="object"
 | 
					        :actor="object"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </Header>
 | 
					    </Header>
 | 
				
			||||||
| 
						 | 
					@ -201,8 +236,8 @@ const tabs = ref([{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped lang="scss">
 | 
					<style scoped lang="scss">
 | 
				
			||||||
  img.avatar {
 | 
					  img.avatar {
 | 
				
			||||||
    width: 300px;
 | 
					    width: 200px;
 | 
				
			||||||
    height: 300px;
 | 
					    height: 200px;
 | 
				
			||||||
    border-radius: 50%;
 | 
					    border-radius: 50%;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -215,8 +250,8 @@ const tabs = ref([{
 | 
				
			||||||
    align-content: center;
 | 
					    align-content: center;
 | 
				
			||||||
    background-color: var(--fw-gray-500);
 | 
					    background-color: var(--fw-gray-500);
 | 
				
			||||||
    border-radius: 50%;
 | 
					    border-radius: 50%;
 | 
				
			||||||
    width: 300px;
 | 
					    width: 200px;
 | 
				
			||||||
    height: 300px;
 | 
					    height: 200px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  h1 {
 | 
					  h1 {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,13 +22,14 @@ import TagsList from '~/components/tags/List.vue'
 | 
				
			||||||
import RadioButton from '~/components/radios/Button.vue'
 | 
					import RadioButton from '~/components/radios/Button.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Loader from '~/components/ui/Loader.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 Button from '~/components/ui/Button.vue'
 | 
				
			||||||
import Link from '~/components/ui/Link.vue'
 | 
					import Link from '~/components/ui/Link.vue'
 | 
				
			||||||
import Nav from '~/components/ui/Nav.vue'
 | 
					import Nav from '~/components/ui/Nav.vue'
 | 
				
			||||||
import OptionsButton from '~/components/ui/button/Options.vue'
 | 
					import OptionsButton from '~/components/ui/button/Options.vue'
 | 
				
			||||||
import Popover from '~/components/ui/Popover.vue'
 | 
					import Popover from '~/components/ui/Popover.vue'
 | 
				
			||||||
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
 | 
					import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
 | 
				
			||||||
import Layout from '~/components/ui/Layout.vue'
 | 
					 | 
				
			||||||
import Spacer from '~/components/ui/Spacer.vue'
 | 
					import Spacer from '~/components/ui/Spacer.vue'
 | 
				
			||||||
import Modal from '~/components/ui/Modal.vue'
 | 
					import Modal from '~/components/ui/Modal.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,345 +167,351 @@ const tabs = ref([
 | 
				
			||||||
  <Layout
 | 
					  <Layout
 | 
				
			||||||
    v-title="labels.title"
 | 
					    v-title="labels.title"
 | 
				
			||||||
    stack
 | 
					    stack
 | 
				
			||||||
 | 
					    no-gap
 | 
				
			||||||
    main
 | 
					    main
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <Loader v-if="isLoading" />
 | 
					    <Loader v-if="isLoading" />
 | 
				
			||||||
    <template v-if="object && !isLoading">
 | 
					    <Header
 | 
				
			||||||
      <section
 | 
					      v-if="object && !isLoading"
 | 
				
			||||||
        v-title="object.artist?.name"
 | 
					      v-title="object.artist?.name"
 | 
				
			||||||
 | 
					      :h1="object.artist?.name"
 | 
				
			||||||
 | 
					      page-heading
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <template #image>
 | 
				
			||||||
 | 
					        <img
 | 
				
			||||||
 | 
					          v-if="object.artist?.cover"
 | 
				
			||||||
 | 
					          alt=""
 | 
				
			||||||
 | 
					          :class="['huge', object.artist?.content_category === 'podcast' ? 'podcast-image' : 'channel-image']"
 | 
				
			||||||
 | 
					          :src="store.getters['instance/absoluteUrl'](object.artist.cover.urls.large_square_crop)"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					        <i
 | 
				
			||||||
 | 
					          v-else
 | 
				
			||||||
 | 
					          class="bi bi-person-circle"
 | 
				
			||||||
 | 
					          style="font-size: 300px; margin-top: -32px;"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </template>
 | 
				
			||||||
 | 
					      <Layout
 | 
				
			||||||
 | 
					        stack
 | 
				
			||||||
 | 
					        class="meta"
 | 
				
			||||||
 | 
					        style="gap: 8px;"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <Layout flex>
 | 
					        <Layout
 | 
				
			||||||
          <img
 | 
					          flex
 | 
				
			||||||
            v-if="object.artist?.cover"
 | 
					          no-gap
 | 
				
			||||||
            alt=""
 | 
					        >
 | 
				
			||||||
            :class="['huge', object.artist?.content_category === 'podcast' ? 'podcast-image' : 'channel-image']"
 | 
					          <template v-if="totalTracks > 0">
 | 
				
			||||||
            :src="store.getters['instance/absoluteUrl'](object.artist.cover.urls.large_square_crop)"
 | 
					            <span
 | 
				
			||||||
          >
 | 
					              v-if="object.artist?.content_category === 'podcast'"
 | 
				
			||||||
          <i
 | 
					 | 
				
			||||||
            v-else
 | 
					 | 
				
			||||||
            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>
 | 
					 | 
				
			||||||
            <Layout
 | 
					 | 
				
			||||||
              stack
 | 
					 | 
				
			||||||
              class="meta"
 | 
					 | 
				
			||||||
              style="gap: 8px;"
 | 
					 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <Layout
 | 
					              {{ t('views.channels.DetailBase.meta.episodes', totalTracks) }}
 | 
				
			||||||
                flex
 | 
					            </span>
 | 
				
			||||||
                no-gap
 | 
					            <span
 | 
				
			||||||
              >
 | 
					              v-else
 | 
				
			||||||
                <template v-if="totalTracks > 0">
 | 
					            >
 | 
				
			||||||
                  <span
 | 
					              {{ t('views.channels.DetailBase.meta.tracks', totalTracks) }}
 | 
				
			||||||
                    v-if="object.artist?.content_category === 'podcast'"
 | 
					            </span>
 | 
				
			||||||
                  >
 | 
					            <i class="bi bi-dot" />
 | 
				
			||||||
                    {{ t('views.channels.DetailBase.meta.episodes', totalTracks) }}
 | 
					          </template>
 | 
				
			||||||
                  </span>
 | 
					          {{ t('views.channels.DetailBase.meta.subscribers', object?.subscriptions_count ?? 0) }}
 | 
				
			||||||
                  <span
 | 
					          <i class="bi bi-dot" />
 | 
				
			||||||
                    v-else
 | 
					          {{ t('views.channels.DetailBase.meta.listenings', object?.downloads_count ?? 0) }}
 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {{ t('views.channels.DetailBase.meta.tracks', totalTracks) }}
 | 
					 | 
				
			||||||
                  </span>
 | 
					 | 
				
			||||||
                  <i class="bi bi-dot" />
 | 
					 | 
				
			||||||
                </template>
 | 
					 | 
				
			||||||
                {{ t('views.channels.DetailBase.meta.subscribers', object?.subscriptions_count ?? 0) }}
 | 
					 | 
				
			||||||
                <i class="bi bi-dot" />
 | 
					 | 
				
			||||||
                {{ t('views.channels.DetailBase.meta.listenings', object?.downloads_count ?? 0) }}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div v-if="totalTracks > 0">
 | 
					          <div v-if="totalTracks > 0">
 | 
				
			||||||
                  <i class="bi bi-dot" />
 | 
					            <i class="bi bi-dot" />
 | 
				
			||||||
                  <human-duration
 | 
					            <human-duration
 | 
				
			||||||
                    v-if="totalTracks > 0"
 | 
					              v-if="totalTracks > 0"
 | 
				
			||||||
                    :duration="totalTracks"
 | 
					              :duration="totalTracks"
 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </Layout>
 | 
					 | 
				
			||||||
              <Layout
 | 
					 | 
				
			||||||
                flex
 | 
					 | 
				
			||||||
                no-gap
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <template v-if="object.artist?.content_category === 'podcast'">
 | 
					 | 
				
			||||||
                  <span>
 | 
					 | 
				
			||||||
                    {{ t('views.channels.DetailBase.header.podcastChannel') }}
 | 
					 | 
				
			||||||
                  </span>
 | 
					 | 
				
			||||||
                  <span
 | 
					 | 
				
			||||||
                    v-if="!object.actor"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <i class="bi bi-dot" />
 | 
					 | 
				
			||||||
                    <a
 | 
					 | 
				
			||||||
                      :href="object.url || object.rss_url"
 | 
					 | 
				
			||||||
                      rel="noopener noreferrer"
 | 
					 | 
				
			||||||
                      target="_blank"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      <i class="bi bi-box-arrow-up-right" />
 | 
					 | 
				
			||||||
                      {{ t('views.channels.DetailBase.link.mirrored', {domain: externalDomain}) }}
 | 
					 | 
				
			||||||
                    </a>
 | 
					 | 
				
			||||||
                  </span>
 | 
					 | 
				
			||||||
                </template>
 | 
					 | 
				
			||||||
                <template v-else>
 | 
					 | 
				
			||||||
                  <span>
 | 
					 | 
				
			||||||
                    {{ t('views.channels.DetailBase.header.artistChannel') }}
 | 
					 | 
				
			||||||
                  </span>
 | 
					 | 
				
			||||||
                </template>
 | 
					 | 
				
			||||||
                <span v-if="object.actor">
 | 
					 | 
				
			||||||
                  <i class="bi bi-dot" />
 | 
					 | 
				
			||||||
                  {{ t('views.library.LibraryBase.link.owner') }}
 | 
					 | 
				
			||||||
                </span>
 | 
					 | 
				
			||||||
                <Spacer
 | 
					 | 
				
			||||||
                  h
 | 
					 | 
				
			||||||
                  :size="4"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <ActorLink
 | 
					 | 
				
			||||||
                  v-if="object.actor"
 | 
					 | 
				
			||||||
                  discrete
 | 
					 | 
				
			||||||
                  :avatar="true"
 | 
					 | 
				
			||||||
                  :actor="object.attributed_to"
 | 
					 | 
				
			||||||
                  :display-name="true"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </Layout>
 | 
					 | 
				
			||||||
            </Layout>
 | 
					 | 
				
			||||||
            <rendered-description
 | 
					 | 
				
			||||||
              :content="object.artist?.description"
 | 
					 | 
				
			||||||
              :update-url="`channels/${object.uuid}/`"
 | 
					 | 
				
			||||||
              :can-update="false"
 | 
					 | 
				
			||||||
              @updated="object = $event"
 | 
					 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <Layout
 | 
					          </div>
 | 
				
			||||||
              flex
 | 
					 | 
				
			||||||
              class="header-buttons"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <Link
 | 
					 | 
				
			||||||
                v-if="isOwner"
 | 
					 | 
				
			||||||
                solid
 | 
					 | 
				
			||||||
                primary
 | 
					 | 
				
			||||||
                icon="bi-upload"
 | 
					 | 
				
			||||||
                :to="useModal('upload').to"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                {{ t('views.channels.DetailBase.button.upload') }}
 | 
					 | 
				
			||||||
              </Link>
 | 
					 | 
				
			||||||
              <PlayButton
 | 
					 | 
				
			||||||
                :is-playable="isPlayable"
 | 
					 | 
				
			||||||
                split
 | 
					 | 
				
			||||||
                class="vibrant"
 | 
					 | 
				
			||||||
                :artist="object.artist"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                {{ t('views.channels.DetailBase.button.play') }}
 | 
					 | 
				
			||||||
              </PlayButton>
 | 
					 | 
				
			||||||
              <RadioButton
 | 
					 | 
				
			||||||
                type="artist"
 | 
					 | 
				
			||||||
                :object-id="object.artist.id"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <Popover>
 | 
					 | 
				
			||||||
                <template #default="{ toggleOpen }">
 | 
					 | 
				
			||||||
                  <OptionsButton
 | 
					 | 
				
			||||||
                    @click="toggleOpen"
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                </template>
 | 
					 | 
				
			||||||
                <template #items>
 | 
					 | 
				
			||||||
                  <PopoverItem
 | 
					 | 
				
			||||||
                    v-if="totalTracks > 0"
 | 
					 | 
				
			||||||
                    icon="bi-code-slash"
 | 
					 | 
				
			||||||
                    @click.prevent="showEmbedModal = !showEmbedModal"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {{ t('views.channels.DetailBase.button.embed') }}
 | 
					 | 
				
			||||||
                  </PopoverItem>
 | 
					 | 
				
			||||||
                  <PopoverItem
 | 
					 | 
				
			||||||
                    v-if="object.actor && object.actor.domain != store.getters['instance/domain']"
 | 
					 | 
				
			||||||
                    :href="object.url"
 | 
					 | 
				
			||||||
                    target="_blank"
 | 
					 | 
				
			||||||
                    icon="bi-box-arrow-up-right"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {{ t('views.channels.DetailBase.link.domainView', {domain: object.actor.domain}) }}
 | 
					 | 
				
			||||||
                  </PopoverItem>
 | 
					 | 
				
			||||||
                  <hr>
 | 
					 | 
				
			||||||
                  <PopoverItem
 | 
					 | 
				
			||||||
                    v-for="obj in getReportableObjects({account: object.attributed_to, channel: object})"
 | 
					 | 
				
			||||||
                    :key="obj.target.type + obj.target.id"
 | 
					 | 
				
			||||||
                    icon="bi-share"
 | 
					 | 
				
			||||||
                    @click.stop.prevent="report(obj)"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {{ obj.label }}
 | 
					 | 
				
			||||||
                  </PopoverItem>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  <template v-if="isOwner">
 | 
					 | 
				
			||||||
                    <hr>
 | 
					 | 
				
			||||||
                    <PopoverItem
 | 
					 | 
				
			||||||
                      icon="bi-pencil"
 | 
					 | 
				
			||||||
                      @click.stop.prevent="showEditModal = true"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      {{ t('views.channels.DetailBase.button.edit') }}
 | 
					 | 
				
			||||||
                    </PopoverItem>
 | 
					 | 
				
			||||||
                    <dangerous-button
 | 
					 | 
				
			||||||
                      v-if="object"
 | 
					 | 
				
			||||||
                      popover-item
 | 
					 | 
				
			||||||
                      :title="t('views.channels.DetailBase.button.confirm')"
 | 
					 | 
				
			||||||
                      :is-loading="isLoading"
 | 
					 | 
				
			||||||
                      icon="bi-trash"
 | 
					 | 
				
			||||||
                      @confirm="remove()"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      {{ t('views.channels.DetailBase.button.confirm') }}
 | 
					 | 
				
			||||||
                      <template #modal-content>
 | 
					 | 
				
			||||||
                        {{ t('views.channels.DetailBase.modal.delete.content.warning') }}
 | 
					 | 
				
			||||||
                      </template>
 | 
					 | 
				
			||||||
                      <template #modal-confirm>
 | 
					 | 
				
			||||||
                        <p>
 | 
					 | 
				
			||||||
                          {{ t('views.channels.DetailBase.button.confirm') }}
 | 
					 | 
				
			||||||
                        </p>
 | 
					 | 
				
			||||||
                      </template>
 | 
					 | 
				
			||||||
                    </dangerous-button>
 | 
					 | 
				
			||||||
                  </template>
 | 
					 | 
				
			||||||
                  <template v-if="store.state.auth.availablePermissions['library']">
 | 
					 | 
				
			||||||
                    <hr>
 | 
					 | 
				
			||||||
                    <PopoverItem
 | 
					 | 
				
			||||||
                      :to="{ name: 'manage.channels.detail', params: { id: object.uuid } }"
 | 
					 | 
				
			||||||
                      icon="bi-wrench"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      {{ t('views.channels.DetailBase.link.moderation') }}
 | 
					 | 
				
			||||||
                    </PopoverItem>
 | 
					 | 
				
			||||||
                  </template>
 | 
					 | 
				
			||||||
                </template>
 | 
					 | 
				
			||||||
              </Popover>
 | 
					 | 
				
			||||||
              <Spacer
 | 
					 | 
				
			||||||
                h
 | 
					 | 
				
			||||||
                grow
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <subscribe-button
 | 
					 | 
				
			||||||
                v-if="store.state.auth.authenticated && object?.attributed_to.full_username !== store.state.auth.fullUsername"
 | 
					 | 
				
			||||||
                :channel="object"
 | 
					 | 
				
			||||||
                @subscribed="updateSubscriptionCount(1)"
 | 
					 | 
				
			||||||
                @unsubscribed="updateSubscriptionCount(-1)"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <Modal
 | 
					 | 
				
			||||||
                v-if="totalTracks > 0"
 | 
					 | 
				
			||||||
                v-model="showEmbedModal"
 | 
					 | 
				
			||||||
                :title="t('views.channels.DetailBase.modal.embed.header')"
 | 
					 | 
				
			||||||
                :cancel="t('views.channels.DetailBase.button.cancel')"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <div class="scrolling content">
 | 
					 | 
				
			||||||
                  <div class="description">
 | 
					 | 
				
			||||||
                    <embed-wizard
 | 
					 | 
				
			||||||
                      :id="object.artist!.id"
 | 
					 | 
				
			||||||
                      type="artist"
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <template #actions>
 | 
					 | 
				
			||||||
                  <button class="ui basic deny button">
 | 
					 | 
				
			||||||
                    {{ t('views.channels.DetailBase.button.cancel') }}
 | 
					 | 
				
			||||||
                  </button>
 | 
					 | 
				
			||||||
                </template>
 | 
					 | 
				
			||||||
              </Modal>
 | 
					 | 
				
			||||||
              <Modal
 | 
					 | 
				
			||||||
                v-if="isOwner"
 | 
					 | 
				
			||||||
                v-model="showEditModal"
 | 
					 | 
				
			||||||
                :title="
 | 
					 | 
				
			||||||
                  object.artist?.content_category === 'podcast'
 | 
					 | 
				
			||||||
                    ? t('views.channels.DetailBase.header.podcastChannel')
 | 
					 | 
				
			||||||
                    : t('views.channels.DetailBase.header.artistChannel')
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <div class="scrolling content">
 | 
					 | 
				
			||||||
                  <channel-form
 | 
					 | 
				
			||||||
                    ref="editForm"
 | 
					 | 
				
			||||||
                    :object="object"
 | 
					 | 
				
			||||||
                    @loading="edit.loading = $event"
 | 
					 | 
				
			||||||
                    @submittable="edit.submittable = $event"
 | 
					 | 
				
			||||||
                    @updated="fetchData"
 | 
					 | 
				
			||||||
                  />
 | 
					 | 
				
			||||||
                  <div class="ui hidden divider" />
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <template #actions>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    primary
 | 
					 | 
				
			||||||
                    autofocus
 | 
					 | 
				
			||||||
                    :is-loading="edit.loading"
 | 
					 | 
				
			||||||
                    :disabled="!edit.submittable"
 | 
					 | 
				
			||||||
                    @click.stop="editForm?.submit"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {{ t('views.channels.DetailBase.button.updateChannel') }}
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </template>
 | 
					 | 
				
			||||||
              </Modal>
 | 
					 | 
				
			||||||
              <Button
 | 
					 | 
				
			||||||
                secondary
 | 
					 | 
				
			||||||
                icon="bi-rss"
 | 
					 | 
				
			||||||
                @click.stop.prevent="showSubscribeModal = true"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <Modal
 | 
					 | 
				
			||||||
                v-model="showSubscribeModal"
 | 
					 | 
				
			||||||
                :title="t('views.channels.DetailBase.modal.subscribe.header')"
 | 
					 | 
				
			||||||
                class="tiny"
 | 
					 | 
				
			||||||
                :cancel="t('views.channels.DetailBase.button.cancel')"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <div class="scrollable content">
 | 
					 | 
				
			||||||
                  <div class="description">
 | 
					 | 
				
			||||||
                    <template v-if="object.rss_url">
 | 
					 | 
				
			||||||
                      <h3>
 | 
					 | 
				
			||||||
                        <i class="feed icon" />
 | 
					 | 
				
			||||||
                        {{ t('views.channels.DetailBase.modal.subscribe.rss.header') }}
 | 
					 | 
				
			||||||
                      </h3>
 | 
					 | 
				
			||||||
                      <p>
 | 
					 | 
				
			||||||
                        {{ t('views.channels.DetailBase.modal.subscribe.rss.content.help') }}
 | 
					 | 
				
			||||||
                      </p>
 | 
					 | 
				
			||||||
                      <copy-input :value="object.rss_url" />
 | 
					 | 
				
			||||||
                    </template>
 | 
					 | 
				
			||||||
                    <template v-if="object.actor">
 | 
					 | 
				
			||||||
                      <h3>
 | 
					 | 
				
			||||||
                        <i class="bell icon" />
 | 
					 | 
				
			||||||
                        {{ t('views.channels.DetailBase.modal.subscribe.fediverse.header') }}
 | 
					 | 
				
			||||||
                      </h3>
 | 
					 | 
				
			||||||
                      <p>
 | 
					 | 
				
			||||||
                        {{ t('views.channels.DetailBase.modal.subscribe.fediverse.content.help') }}
 | 
					 | 
				
			||||||
                      </p>
 | 
					 | 
				
			||||||
                      <copy-input
 | 
					 | 
				
			||||||
                        id="copy-tag"
 | 
					 | 
				
			||||||
                        :value="`@${object.actor.full_username}`"
 | 
					 | 
				
			||||||
                      />
 | 
					 | 
				
			||||||
                    </template>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </Modal>
 | 
					 | 
				
			||||||
            </Layout>
 | 
					 | 
				
			||||||
          </Layout>
 | 
					 | 
				
			||||||
        </Layout>
 | 
					        </Layout>
 | 
				
			||||||
        <hr>
 | 
					        <Layout
 | 
				
			||||||
        <TagsList
 | 
					          flex
 | 
				
			||||||
          v-if="object.artist?.tags && object.artist?.tags.length > 0"
 | 
					          no-gap
 | 
				
			||||||
          :tags="object.artist.tags"
 | 
					        >
 | 
				
			||||||
          :limit="5"
 | 
					          <template v-if="object.artist?.content_category === 'podcast'">
 | 
				
			||||||
          :show-more="true"
 | 
					            <span>
 | 
				
			||||||
 | 
					              {{ t('views.channels.DetailBase.header.podcastChannel') }}
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					            <span
 | 
				
			||||||
 | 
					              v-if="!object.actor"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <i class="bi bi-dot" />
 | 
				
			||||||
 | 
					              <a
 | 
				
			||||||
 | 
					                :href="object.url || object.rss_url"
 | 
				
			||||||
 | 
					                rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                target="_blank"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <i class="bi bi-box-arrow-up-right" />
 | 
				
			||||||
 | 
					                {{ t('views.channels.DetailBase.link.mirrored', {domain: externalDomain}) }}
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					          <template v-else>
 | 
				
			||||||
 | 
					            <span>
 | 
				
			||||||
 | 
					              {{ t('views.channels.DetailBase.header.artistChannel') }}
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					          <span v-if="object.actor">
 | 
				
			||||||
 | 
					            <i class="bi bi-dot" />
 | 
				
			||||||
 | 
					            {{ t('views.library.LibraryBase.link.owner') }}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <Spacer
 | 
				
			||||||
 | 
					            h
 | 
				
			||||||
 | 
					            :size="8"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <ActorLink
 | 
				
			||||||
 | 
					            v-if="object.actor"
 | 
				
			||||||
 | 
					            discrete
 | 
				
			||||||
 | 
					            :avatar="true"
 | 
				
			||||||
 | 
					            :actor="object.attributed_to"
 | 
				
			||||||
 | 
					            :display-name="true"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Layout>
 | 
				
			||||||
 | 
					      </Layout>
 | 
				
			||||||
 | 
					      <rendered-description
 | 
				
			||||||
 | 
					        :content="object.artist?.description"
 | 
				
			||||||
 | 
					        :update-url="`channels/${object.uuid}/`"
 | 
				
			||||||
 | 
					        :can-update="false"
 | 
				
			||||||
 | 
					        @updated="object = $event"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <Layout
 | 
				
			||||||
 | 
					        flex
 | 
				
			||||||
 | 
					        class="header-buttons"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Link
 | 
				
			||||||
 | 
					          v-if="isOwner"
 | 
				
			||||||
 | 
					          solid
 | 
				
			||||||
 | 
					          primary
 | 
				
			||||||
 | 
					          low-height
 | 
				
			||||||
 | 
					          icon="bi-upload"
 | 
				
			||||||
 | 
					          :to="useModal('upload').to"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {{ t('views.channels.DetailBase.button.upload') }}
 | 
				
			||||||
 | 
					        </Link>
 | 
				
			||||||
 | 
					        <PlayButton
 | 
				
			||||||
 | 
					          :is-playable="isPlayable"
 | 
				
			||||||
 | 
					          split
 | 
				
			||||||
 | 
					          low-height
 | 
				
			||||||
 | 
					          class="vibrant"
 | 
				
			||||||
 | 
					          :artist="object.artist"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {{ t('views.channels.DetailBase.button.play') }}
 | 
				
			||||||
 | 
					        </PlayButton>
 | 
				
			||||||
 | 
					        <RadioButton
 | 
				
			||||||
 | 
					          type="artist"
 | 
				
			||||||
 | 
					          :object-id="object.artist.id"
 | 
				
			||||||
 | 
					          low-height
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <Nav v-model="tabs" />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <router-view
 | 
					        <Popover>
 | 
				
			||||||
          v-if="object"
 | 
					          <template #default="{ toggleOpen }">
 | 
				
			||||||
          :object="object"
 | 
					            <OptionsButton
 | 
				
			||||||
          @tracks-loaded="totalTracks = $event"
 | 
					              is-square-small
 | 
				
			||||||
 | 
					              @click="toggleOpen"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					          <template #items>
 | 
				
			||||||
 | 
					            <PopoverItem
 | 
				
			||||||
 | 
					              v-if="totalTracks > 0"
 | 
				
			||||||
 | 
					              icon="bi-code-slash"
 | 
				
			||||||
 | 
					              @click.prevent="showEmbedModal = !showEmbedModal"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {{ t('views.channels.DetailBase.button.embed') }}
 | 
				
			||||||
 | 
					            </PopoverItem>
 | 
				
			||||||
 | 
					            <PopoverItem
 | 
				
			||||||
 | 
					              v-if="object.actor && object.actor.domain != store.getters['instance/domain']"
 | 
				
			||||||
 | 
					              :href="object.url"
 | 
				
			||||||
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					              icon="bi-box-arrow-up-right"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {{ t('views.channels.DetailBase.link.domainView', {domain: object.actor.domain}) }}
 | 
				
			||||||
 | 
					            </PopoverItem>
 | 
				
			||||||
 | 
					            <hr>
 | 
				
			||||||
 | 
					            <PopoverItem
 | 
				
			||||||
 | 
					              v-for="obj in getReportableObjects({account: object.attributed_to, channel: object})"
 | 
				
			||||||
 | 
					              :key="obj.target.type + obj.target.id"
 | 
				
			||||||
 | 
					              icon="bi-share"
 | 
				
			||||||
 | 
					              @click.stop.prevent="report(obj)"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {{ obj.label }}
 | 
				
			||||||
 | 
					            </PopoverItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template v-if="isOwner">
 | 
				
			||||||
 | 
					              <hr>
 | 
				
			||||||
 | 
					              <PopoverItem
 | 
				
			||||||
 | 
					                icon="bi-pencil"
 | 
				
			||||||
 | 
					                @click.stop.prevent="showEditModal = true"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {{ t('views.channels.DetailBase.button.edit') }}
 | 
				
			||||||
 | 
					              </PopoverItem>
 | 
				
			||||||
 | 
					              <dangerous-button
 | 
				
			||||||
 | 
					                v-if="object"
 | 
				
			||||||
 | 
					                popover-item
 | 
				
			||||||
 | 
					                :title="t('views.channels.DetailBase.button.confirm')"
 | 
				
			||||||
 | 
					                :is-loading="isLoading"
 | 
				
			||||||
 | 
					                icon="bi-trash"
 | 
				
			||||||
 | 
					                @confirm="remove()"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {{ t('views.channels.DetailBase.button.confirm') }}
 | 
				
			||||||
 | 
					                <template #modal-content>
 | 
				
			||||||
 | 
					                  {{ t('views.channels.DetailBase.modal.delete.content.warning') }}
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					                <template #modal-confirm>
 | 
				
			||||||
 | 
					                  <p>
 | 
				
			||||||
 | 
					                    {{ t('views.channels.DetailBase.button.confirm') }}
 | 
				
			||||||
 | 
					                  </p>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					              </dangerous-button>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <template v-if="store.state.auth.availablePermissions['library']">
 | 
				
			||||||
 | 
					              <hr>
 | 
				
			||||||
 | 
					              <PopoverItem
 | 
				
			||||||
 | 
					                :to="{ name: 'manage.channels.detail', params: { id: object.uuid } }"
 | 
				
			||||||
 | 
					                icon="bi-wrench"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {{ t('views.channels.DetailBase.link.moderation') }}
 | 
				
			||||||
 | 
					              </PopoverItem>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </Popover>
 | 
				
			||||||
 | 
					        <Spacer
 | 
				
			||||||
 | 
					          h
 | 
				
			||||||
 | 
					          grow
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </section>
 | 
					        <subscribe-button
 | 
				
			||||||
    </template>
 | 
					          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)"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Modal
 | 
				
			||||||
 | 
					          v-if="totalTracks > 0"
 | 
				
			||||||
 | 
					          v-model="showEmbedModal"
 | 
				
			||||||
 | 
					          :title="t('views.channels.DetailBase.modal.embed.header')"
 | 
				
			||||||
 | 
					          :cancel="t('views.channels.DetailBase.button.cancel')"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div class="scrolling content">
 | 
				
			||||||
 | 
					            <div class="description">
 | 
				
			||||||
 | 
					              <embed-wizard
 | 
				
			||||||
 | 
					                :id="object.artist!.id"
 | 
				
			||||||
 | 
					                type="artist"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <template #actions>
 | 
				
			||||||
 | 
					            <button class="ui basic deny button">
 | 
				
			||||||
 | 
					              {{ t('views.channels.DetailBase.button.cancel') }}
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </Modal>
 | 
				
			||||||
 | 
					        <Modal
 | 
				
			||||||
 | 
					          v-if="isOwner"
 | 
				
			||||||
 | 
					          v-model="showEditModal"
 | 
				
			||||||
 | 
					          :title="
 | 
				
			||||||
 | 
					            object.artist?.content_category === 'podcast'
 | 
				
			||||||
 | 
					              ? t('views.channels.DetailBase.header.podcastChannel')
 | 
				
			||||||
 | 
					              : t('views.channels.DetailBase.header.artistChannel')
 | 
				
			||||||
 | 
					          "
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div class="scrolling content">
 | 
				
			||||||
 | 
					            <channel-form
 | 
				
			||||||
 | 
					              ref="editForm"
 | 
				
			||||||
 | 
					              :object="object"
 | 
				
			||||||
 | 
					              @loading="edit.loading = $event"
 | 
				
			||||||
 | 
					              @submittable="edit.submittable = $event"
 | 
				
			||||||
 | 
					              @updated="fetchData"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <div class="ui hidden divider" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <template #actions>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              primary
 | 
				
			||||||
 | 
					              autofocus
 | 
				
			||||||
 | 
					              low-height
 | 
				
			||||||
 | 
					              :is-loading="edit.loading"
 | 
				
			||||||
 | 
					              :disabled="!edit.submittable"
 | 
				
			||||||
 | 
					              @click.stop="editForm?.submit"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {{ t('views.channels.DetailBase.button.updateChannel') }}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </Modal>
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          secondary
 | 
				
			||||||
 | 
					          icon="bi-rss"
 | 
				
			||||||
 | 
					          square-small
 | 
				
			||||||
 | 
					          @click.stop.prevent="showSubscribeModal = true"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <Modal
 | 
				
			||||||
 | 
					          v-model="showSubscribeModal"
 | 
				
			||||||
 | 
					          :title="t('views.channels.DetailBase.modal.subscribe.header')"
 | 
				
			||||||
 | 
					          class="tiny"
 | 
				
			||||||
 | 
					          :cancel="t('views.channels.DetailBase.button.cancel')"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div class="scrollable content">
 | 
				
			||||||
 | 
					            <div class="description">
 | 
				
			||||||
 | 
					              <template v-if="object.rss_url">
 | 
				
			||||||
 | 
					                <h3>
 | 
				
			||||||
 | 
					                  <i class="feed icon" />
 | 
				
			||||||
 | 
					                  {{ t('views.channels.DetailBase.modal.subscribe.rss.header') }}
 | 
				
			||||||
 | 
					                </h3>
 | 
				
			||||||
 | 
					                <p>
 | 
				
			||||||
 | 
					                  {{ t('views.channels.DetailBase.modal.subscribe.rss.content.help') }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <copy-input :value="object.rss_url" />
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					              <template v-if="object.actor">
 | 
				
			||||||
 | 
					                <h3>
 | 
				
			||||||
 | 
					                  <i class="bell icon" />
 | 
				
			||||||
 | 
					                  {{ t('views.channels.DetailBase.modal.subscribe.fediverse.header') }}
 | 
				
			||||||
 | 
					                </h3>
 | 
				
			||||||
 | 
					                <p>
 | 
				
			||||||
 | 
					                  {{ t('views.channels.DetailBase.modal.subscribe.fediverse.content.help') }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <copy-input
 | 
				
			||||||
 | 
					                  id="copy-tag"
 | 
				
			||||||
 | 
					                  :value="`@${object.actor.full_username}`"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </Modal>
 | 
				
			||||||
 | 
					      </Layout>
 | 
				
			||||||
 | 
					    </Header>
 | 
				
			||||||
 | 
					    <hr>
 | 
				
			||||||
 | 
					    <TagsList
 | 
				
			||||||
 | 
					      v-if="object?.artist?.tags && object?.artist?.tags.length > 0"
 | 
				
			||||||
 | 
					      :tags="object?.artist.tags"
 | 
				
			||||||
 | 
					      :limit="5"
 | 
				
			||||||
 | 
					      :show-more="true"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <Nav v-model="tabs" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <router-view
 | 
				
			||||||
 | 
					      v-if="object"
 | 
				
			||||||
 | 
					      :object="object"
 | 
				
			||||||
 | 
					      @tracks-loaded="totalTracks = $event"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
  </Layout>
 | 
					  </Layout>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped lang="scss">
 | 
				
			||||||
  .channel-image {
 | 
					  .channel-image {
 | 
				
			||||||
    border-radius: 50%;
 | 
					    border-radius: 50%;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .huge {
 | 
					  .huge {
 | 
				
			||||||
    width: 300px;
 | 
					    width: 200px;
 | 
				
			||||||
    height: 300px;
 | 
					    height: 200px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  .meta {
 | 
					  .meta {
 | 
				
			||||||
    line-height: 24px;
 | 
					 | 
				
			||||||
    font-size: 15px;
 | 
					    font-size: 15px;
 | 
				
			||||||
 | 
					    @include light-theme {
 | 
				
			||||||
 | 
					      color: var(--fw-gray-700);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @include dark-theme {
 | 
				
			||||||
 | 
					      color: var(--fw-gray-500);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,6 @@ import { useStore } from '~/store'
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import defaultCover from '~/assets/audio/default-cover.png'
 | 
					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 HumanDate from '~/components/common/HumanDate.vue'
 | 
					import HumanDate from '~/components/common/HumanDate.vue'
 | 
				
			||||||
| 
						 | 
					@ -134,160 +133,161 @@ const shuffle = () => {}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <Layout
 | 
					  <Loader
 | 
				
			||||||
    v-title="playlist?.name"
 | 
					    v-if="isLoading"
 | 
				
			||||||
    stack
 | 
					    v-title="labels.playlist"
 | 
				
			||||||
    main
 | 
					  />
 | 
				
			||||||
 | 
					  <Header
 | 
				
			||||||
 | 
					    v-if="!isLoading && playlist"
 | 
				
			||||||
 | 
					    :h1="playlist.name"
 | 
				
			||||||
 | 
					    page-heading
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <Loader
 | 
					    <template #image>
 | 
				
			||||||
      v-if="isLoading"
 | 
					      <div class="playlist-grid">
 | 
				
			||||||
      v-title="labels.playlist"
 | 
					        <img
 | 
				
			||||||
    />
 | 
					          v-for="(url, idx) in images"
 | 
				
			||||||
    <Header
 | 
					          :key="idx"
 | 
				
			||||||
      v-if="!isLoading && playlist"
 | 
					          v-lazy="url"
 | 
				
			||||||
      :h1="playlist.name"
 | 
					          :alt="playlist.name"
 | 
				
			||||||
      page-heading
 | 
					          :style="{ backgroundColor: randomizedColors[idx % randomizedColors.length] }"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					    <Layout
 | 
				
			||||||
 | 
					      gap-4
 | 
				
			||||||
 | 
					      class="meta"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <template #image>
 | 
					      <Layout
 | 
				
			||||||
        <div class="playlist-grid">
 | 
					        flex
 | 
				
			||||||
          <img
 | 
					        gap-4
 | 
				
			||||||
            v-for="(url, idx) in images"
 | 
					      >
 | 
				
			||||||
            :key="idx"
 | 
					 | 
				
			||||||
            v-lazy="url"
 | 
					 | 
				
			||||||
            :alt="playlist.name"
 | 
					 | 
				
			||||||
            :style="{ backgroundColor: randomizedColors[idx % randomizedColors.length] }"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </template>
 | 
					 | 
				
			||||||
      <div class="meta">
 | 
					 | 
				
			||||||
        {{ playlist.tracks_count }}
 | 
					        {{ playlist.tracks_count }}
 | 
				
			||||||
        {{ t('views.playlists.Detail.header.tracks') }}
 | 
					        {{ t('views.playlists.Detail.header.tracks') }}
 | 
				
			||||||
        <i class="bi bi-dot" />
 | 
					        <i class="bi bi-dot" />
 | 
				
			||||||
        <Duration :seconds="playlist.duration" />
 | 
					        <Duration :seconds="playlist.duration" />
 | 
				
			||||||
      </div>
 | 
					      </Layout>
 | 
				
			||||||
      <Layout
 | 
					      <Layout
 | 
				
			||||||
        flex
 | 
					        flex
 | 
				
			||||||
        gap-8
 | 
					        gap-4
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {{ t('views.playlists.Detail.meta.attribution') }}
 | 
					        {{ t('views.playlists.Detail.meta.attribution') }}
 | 
				
			||||||
        <ActorLink
 | 
					        {{ playlist.actor.full_username }}
 | 
				
			||||||
          :actor="playlist.actor"
 | 
					 | 
				
			||||||
          :avatar="false"
 | 
					 | 
				
			||||||
          :discrete="true"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <i class="bi bi-dot" />
 | 
					        <i class="bi bi-dot" />
 | 
				
			||||||
        {{ t('views.playlists.Detail.meta.updated') }}
 | 
					        {{ t('views.playlists.Detail.meta.updated') }}
 | 
				
			||||||
        <HumanDate
 | 
					        <HumanDate
 | 
				
			||||||
          :date="playlist.modification_date"
 | 
					          :date="playlist.modification_date"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </Layout>
 | 
					      </Layout>
 | 
				
			||||||
      <RenderedDescription
 | 
					    </Layout>
 | 
				
			||||||
        :content="{ html: playlist.description }"
 | 
					    <RenderedDescription
 | 
				
			||||||
        :truncate-length="200"
 | 
					      :content="{ html: playlist.description }"
 | 
				
			||||||
        :show-more="true"
 | 
					      :truncate-length="100"
 | 
				
			||||||
 | 
					      :show-more="true"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <Layout
 | 
				
			||||||
 | 
					      flex
 | 
				
			||||||
 | 
					      class="header-buttons"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <PlayButton
 | 
				
			||||||
 | 
					        split
 | 
				
			||||||
 | 
					        low-height
 | 
				
			||||||
 | 
					        :is-playable="true"
 | 
				
			||||||
 | 
					        :tracks="tracks"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {{ t('views.playlists.Detail.button.playAll') }}
 | 
				
			||||||
 | 
					      </PlayButton>
 | 
				
			||||||
 | 
					      <Button
 | 
				
			||||||
 | 
					        v-if="playlist.tracks_count > 1"
 | 
				
			||||||
 | 
					        primary
 | 
				
			||||||
 | 
					        icon="bi-shuffle"
 | 
				
			||||||
 | 
					        low-height
 | 
				
			||||||
 | 
					        :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
 | 
				
			||||||
 | 
					        low-height
 | 
				
			||||||
 | 
					        icon="bi-pencil"
 | 
				
			||||||
 | 
					        @click="edit = !edit"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <template v-if="edit">
 | 
				
			||||||
 | 
					          {{ t('views.playlists.Detail.button.stopEdit') }}
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					        <template v-else>
 | 
				
			||||||
 | 
					          {{ t('views.playlists.Detail.button.edit') }}
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					      <Spacer
 | 
				
			||||||
 | 
					        h
 | 
				
			||||||
 | 
					        grow
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					      <playlist-dropdown
 | 
				
			||||||
 | 
					        :playlist="playlist"
 | 
				
			||||||
 | 
					        @import="fetchData"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Layout>
 | 
				
			||||||
 | 
					  </Header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <Layout stack>
 | 
				
			||||||
 | 
					    <template v-if="edit">
 | 
				
			||||||
 | 
					      <playlist-editor
 | 
				
			||||||
 | 
					        v-model:playlist="playlist"
 | 
				
			||||||
 | 
					        v-model:playlist-tracks="playlistTracks"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					    <template v-else-if="tracks.length > 0">
 | 
				
			||||||
 | 
					      <track-table
 | 
				
			||||||
 | 
					        :show-position="true"
 | 
				
			||||||
 | 
					        :tracks="tracks"
 | 
				
			||||||
 | 
					        :unique="false"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					    <Alert
 | 
				
			||||||
 | 
					      v-else-if="!isLoading"
 | 
				
			||||||
 | 
					      blue
 | 
				
			||||||
 | 
					      align-items="center"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <Layout
 | 
					      <Layout
 | 
				
			||||||
        flex
 | 
					        flex
 | 
				
			||||||
        class="header-buttons"
 | 
					        :gap="8"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <PlayButton
 | 
					        <i class="bi bi-music-note-list" />
 | 
				
			||||||
          split
 | 
					        {{ t('views.playlists.Detail.empty.noTracks') }}
 | 
				
			||||||
          :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
 | 
					 | 
				
			||||||
          icon="bi-pencil"
 | 
					 | 
				
			||||||
          @click="edit = !edit"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template v-if="edit">
 | 
					 | 
				
			||||||
            {{ t('views.playlists.Detail.button.stopEdit') }}
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
          <template v-else>
 | 
					 | 
				
			||||||
            {{ t('views.playlists.Detail.button.edit') }}
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </Button>
 | 
					 | 
				
			||||||
        <Spacer
 | 
					 | 
				
			||||||
          h
 | 
					 | 
				
			||||||
          grow
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <playlist-dropdown
 | 
					 | 
				
			||||||
          :playlist="playlist"
 | 
					 | 
				
			||||||
          @import="fetchData"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </Layout>
 | 
					      </Layout>
 | 
				
			||||||
    </Header>
 | 
					      <Spacer size-16 />
 | 
				
			||||||
 | 
					      <Button
 | 
				
			||||||
    <Layout stack>
 | 
					        primary
 | 
				
			||||||
      <template v-if="edit">
 | 
					        icon="bi-pencil"
 | 
				
			||||||
        <playlist-editor
 | 
					        align-self="center"
 | 
				
			||||||
          v-model:playlist="playlist"
 | 
					        @click="edit = !edit"
 | 
				
			||||||
          v-model:playlist-tracks="playlistTracks"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </template>
 | 
					 | 
				
			||||||
      <template v-else-if="tracks.length > 0">
 | 
					 | 
				
			||||||
        <track-table
 | 
					 | 
				
			||||||
          :show-position="true"
 | 
					 | 
				
			||||||
          :tracks="tracks"
 | 
					 | 
				
			||||||
          :unique="false"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </template>
 | 
					 | 
				
			||||||
      <Alert
 | 
					 | 
				
			||||||
        v-else-if="!isLoading"
 | 
					 | 
				
			||||||
        blue
 | 
					 | 
				
			||||||
        align-items="center"
 | 
					 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <Layout
 | 
					        {{ t('views.playlists.Detail.button.edit') }}
 | 
				
			||||||
          flex
 | 
					      </Button>
 | 
				
			||||||
          :gap="8"
 | 
					    </Alert>
 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <i class="bi bi-music-note-list" />
 | 
					 | 
				
			||||||
          {{ t('views.playlists.Detail.empty.noTracks') }}
 | 
					 | 
				
			||||||
        </Layout>
 | 
					 | 
				
			||||||
        <Spacer size-16 />
 | 
					 | 
				
			||||||
        <Button
 | 
					 | 
				
			||||||
          primary
 | 
					 | 
				
			||||||
          icon="bi-pencil"
 | 
					 | 
				
			||||||
          align-self="center"
 | 
					 | 
				
			||||||
          @click="edit = !edit"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {{ t('views.playlists.Detail.button.edit') }}
 | 
					 | 
				
			||||||
        </Button>
 | 
					 | 
				
			||||||
      </Alert>
 | 
					 | 
				
			||||||
    </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>
 | 
					 | 
				
			||||||
      <template #actions>
 | 
					 | 
				
			||||||
        <Button variant="outline">
 | 
					 | 
				
			||||||
          {{ t('views.playlists.Detail.button.cancel') }}
 | 
					 | 
				
			||||||
        </Button>
 | 
					 | 
				
			||||||
      </template>
 | 
					 | 
				
			||||||
    </Modal>
 | 
					 | 
				
			||||||
  </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>
 | 
				
			||||||
 | 
					    <template #actions>
 | 
				
			||||||
 | 
					      <Button variant="outline">
 | 
				
			||||||
 | 
					        {{ t('views.playlists.Detail.button.cancel') }}
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					  </Modal>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					@ -297,8 +297,8 @@ const shuffle = () => {}
 | 
				
			||||||
  grid-template-columns: repeat(2, 1fr);
 | 
					  grid-template-columns: repeat(2, 1fr);
 | 
				
			||||||
  grid-template-rows: repeat(2, 1fr);
 | 
					  grid-template-rows: repeat(2, 1fr);
 | 
				
			||||||
  gap: 2px;
 | 
					  gap: 2px;
 | 
				
			||||||
  width: 300px;
 | 
					  width: 200px;
 | 
				
			||||||
  height: 300px;
 | 
					  height: 200px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.playlist-grid img {
 | 
					.playlist-grid img {
 | 
				
			||||||
| 
						 | 
					@ -307,9 +307,14 @@ const shuffle = () => {}
 | 
				
			||||||
  object-fit: cover;
 | 
					  object-fit: cover;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.playlist-meta {
 | 
					.meta {
 | 
				
			||||||
  display: flex;
 | 
					  font-size: 15px;
 | 
				
			||||||
  align-items: center;
 | 
					  @include light-theme {
 | 
				
			||||||
 | 
					    color: var(--fw-gray-700);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  @include dark-theme {
 | 
				
			||||||
 | 
					    color: var(--fw-gray-500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.playlist-action {
 | 
					.playlist-action {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue