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