resources/assets/js/components/layout/main-wrapper/sidebar/PlaylistSidebarItem.vue

Summary

Maintainability
Test Coverage
<template>
  <SidebarItem
    :class="{ current, droppable }"
    :href="url"
    class="playlist select-none"
    draggable="true"
    @contextmenu="onContextMenu"
    @dragleave="onDragLeave"
    @dragover="onDragOver"
    @dragstart="onDragStart"
    @drop="onDrop"
  >
    <template #icon>
      <Icon v-if="isRecentlyPlayedList(list)" :icon="faClockRotateLeft" class="text-k-success" fixed-width />
      <Icon v-else-if="isFavoriteList(list)" :icon="faHeart" class="text-k-love" fixed-width />
      <Icon v-else-if="list.is_smart" :icon="faWandMagicSparkles" fixed-width />
      <Icon v-else-if="list.is_collaborative" :icon="faUsers" fixed-width />
      <ListMusicIcon v-else :size="16" />
    </template>
    {{ list.name }}
  </SidebarItem>
</template>

<script lang="ts" setup>
import { faClockRotateLeft, faHeart, faUsers, faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
import { ListMusicIcon } from 'lucide-vue-next'
import { computed, ref, toRefs } from 'vue'
import { eventBus } from '@/utils'
import { favoriteStore } from '@/stores'
import { useDraggable, useDroppable, usePlaylistManagement, useRouter } from '@/composables'

import SidebarItem from '@/components/layout/main-wrapper/sidebar/SidebarItem.vue'

const { onRouteChanged } = useRouter()
const { startDragging } = useDraggable('playlist')
const { acceptsDrop, resolveDroppedItems } = useDroppable(['playables', 'album', 'artist'])

const droppable = ref(false)

const { addToPlaylist } = usePlaylistManagement()

const props = defineProps<{ list: PlaylistLike }>()
const { list } = toRefs(props)

const isPlaylist = (list: PlaylistLike): list is Playlist => 'id' in list
const isFavoriteList = (list: PlaylistLike): list is FavoriteList => list.name === 'Favorites'
const isRecentlyPlayedList = (list: PlaylistLike): list is RecentlyPlayedList => list.name === 'Recently Played'

const current = ref(false)

const url = computed(() => {
  if (isPlaylist(list.value)) return `#/playlist/${list.value.id}`
  if (isFavoriteList(list.value)) return '#/favorites'
  if (isRecentlyPlayedList(list.value)) return '#/recently-played'

  throw new Error('Invalid playlist-like type.')
})

const contentEditable = computed(() => {
  if (isRecentlyPlayedList(list.value)) return false
  if (isFavoriteList(list.value)) return true

  return !list.value.is_smart
})

const onContextMenu = (event: MouseEvent) => {
  if (isPlaylist(list.value)) {
    event.preventDefault()
    eventBus.emit('PLAYLIST_CONTEXT_MENU_REQUESTED', event, list.value)
  }
}

const onDragStart = (event: DragEvent) => isPlaylist(list.value) && startDragging(event, list.value)

const onDragOver = (event: DragEvent) => {
  if (!contentEditable.value) return false
  if (!acceptsDrop(event)) return false

  event.preventDefault()
  droppable.value = true

  return false
}

const onDragLeave = () => (droppable.value = false)

const onDrop = async (event: DragEvent) => {
  droppable.value = false

  if (!contentEditable.value) return false
  if (!acceptsDrop(event)) return false

  const playables = await resolveDroppedItems(event)

  if (!playables?.length) return false

  if (isFavoriteList(list.value)) {
    await favoriteStore.like(playables)
  } else if (isPlaylist(list.value)) {
    await addToPlaylist(list.value, playables)
  }

  return false
}

onRouteChanged(route => {
  switch (route.screen) {
    case 'Favorites':
      current.value = isFavoriteList(list.value)
      break

    case 'RecentlyPlayed':
      current.value = isRecentlyPlayedList(list.value)
      break

    case 'Playlist':
      current.value = (list.value as Playlist).id === route.params!.id
      break

    default:
      current.value = false
      break
  }
})
</script>

<style lang="postcss" scoped>
.droppable {
  @apply ring-1 ring-offset-0 ring-k-accent rounded-md cursor-copy;
}
</style>