2022-04-15 14:24:30 +00:00
|
|
|
<template>
|
|
|
|
<li
|
2022-09-03 08:32:09 +00:00
|
|
|
ref="el"
|
2022-08-10 14:56:01 +00:00
|
|
|
:class="['playlist', type, playlist.is_smart ? 'smart' : '']"
|
2022-05-08 18:43:52 +00:00
|
|
|
data-testid="playlist-sidebar-item"
|
2022-08-10 14:56:01 +00:00
|
|
|
draggable="true"
|
2022-09-03 08:32:09 +00:00
|
|
|
@dragleave="onDragLeave"
|
|
|
|
@dragover="onDragOver"
|
|
|
|
@dragstart="onDragStart"
|
|
|
|
@drop="onDrop"
|
2022-05-08 18:43:52 +00:00
|
|
|
>
|
2022-09-03 08:32:09 +00:00
|
|
|
<a :class="{ active }" :href="url" @contextmenu.prevent="onContextMenu">
|
2022-07-15 07:23:55 +00:00
|
|
|
<icon v-if="type === 'recently-played'" :icon="faClockRotateLeft" class="text-green" fixed-width/>
|
2022-09-03 08:32:09 +00:00
|
|
|
<icon v-else-if="type === 'favorites'" :icon="faHeart" class="text-maroon" fixed-width/>
|
|
|
|
<icon
|
|
|
|
v-else-if="playlist.is_smart"
|
|
|
|
:icon="faBoltLightning"
|
|
|
|
:mask="faFile"
|
|
|
|
fixed-width
|
|
|
|
transform="shrink-7 down-2"
|
|
|
|
/>
|
|
|
|
<icon v-else :icon="faMusic" :mask="faFile" fixed-width transform="shrink-7 down-2"/>
|
2022-05-13 11:46:03 +00:00
|
|
|
{{ playlist.name }}
|
|
|
|
</a>
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-08-10 14:56:01 +00:00
|
|
|
<ContextMenu v-if="hasContextMenu" ref="contextMenu" :playlist="playlist"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
</li>
|
|
|
|
</template>
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
<script lang="ts" setup>
|
2022-07-15 07:23:55 +00:00
|
|
|
import { faBoltLightning, faClockRotateLeft, faFile, faHeart, faMusic } from '@fortawesome/free-solid-svg-icons'
|
2022-08-10 14:56:01 +00:00
|
|
|
import { computed, nextTick, ref, toRefs } from 'vue'
|
2022-09-03 08:32:09 +00:00
|
|
|
import { eventBus, pluralize, requireInjection } from '@/utils'
|
2022-06-10 10:47:46 +00:00
|
|
|
import { favoriteStore, playlistStore } from '@/stores'
|
2022-04-30 13:20:47 +00:00
|
|
|
import router from '@/router'
|
2022-07-26 09:51:19 +00:00
|
|
|
import { MessageToasterKey } from '@/symbols'
|
2022-09-03 08:32:09 +00:00
|
|
|
import { useDraggable, useDroppable } from '@/composables'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-08-10 14:56:01 +00:00
|
|
|
import ContextMenu from '@/components/playlist/PlaylistContextMenu.vue'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-09-03 08:32:09 +00:00
|
|
|
const { startDragging } = useDraggable('playlist')
|
|
|
|
const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist'])
|
|
|
|
|
2022-07-26 09:51:19 +00:00
|
|
|
const toaster = requireInjection(MessageToasterKey)
|
2022-04-20 10:35:36 +00:00
|
|
|
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
|
2022-09-03 08:32:09 +00:00
|
|
|
const el = ref<HTMLLIElement>()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-05-08 18:43:52 +00:00
|
|
|
const props = withDefaults(defineProps<{ playlist: Playlist, type?: PlaylistType }>(), { type: 'playlist' })
|
2022-04-15 17:00:08 +00:00
|
|
|
const { playlist, type } = toRefs(props)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
const active = ref(false)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
const url = computed(() => {
|
|
|
|
switch (type.value) {
|
|
|
|
case 'playlist':
|
|
|
|
return `#!/playlist/${playlist.value.id}`
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
case 'favorites':
|
|
|
|
return '#!/favorites'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
case 'recently-played':
|
|
|
|
return '#!/recently-played'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
default:
|
|
|
|
throw new Error('Invalid playlist type')
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const hasContextMenu = computed(() => type.value === 'playlist')
|
|
|
|
|
|
|
|
const contentEditable = computed(() => {
|
2022-09-03 08:32:09 +00:00
|
|
|
if (playlist.value.is_smart) return false
|
2022-04-15 17:00:08 +00:00
|
|
|
return type.value === 'playlist' || type.value === 'favorites'
|
|
|
|
})
|
|
|
|
|
2022-09-03 08:32:09 +00:00
|
|
|
const onContextMenu = async (event: MouseEvent) => {
|
|
|
|
if (hasContextMenu.value) {
|
|
|
|
await nextTick()
|
|
|
|
router.go(`/playlist/${playlist.value.id}`)
|
|
|
|
contextMenu.value?.open(event.pageY, event.pageX, { playlist })
|
2022-04-15 17:00:08 +00:00
|
|
|
}
|
2022-09-03 08:32:09 +00:00
|
|
|
}
|
2022-04-15 17:00:08 +00:00
|
|
|
|
2022-09-03 08:32:09 +00:00
|
|
|
const onDragStart = (event: DragEvent) => {
|
|
|
|
if (type.value === 'playlist') {
|
|
|
|
startDragging(event, playlist.value)
|
2022-04-15 17:00:08 +00:00
|
|
|
}
|
2022-09-03 08:32:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const onDragOver = (event: DragEvent) => {
|
|
|
|
if (!contentEditable.value) return false
|
|
|
|
if (!acceptsDrop(event)) return false
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
event.dataTransfer!.dropEffect = 'copy'
|
|
|
|
el.value!.classList.add('droppable')
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const onDragLeave = () => el.value!.classList.remove('droppable')
|
|
|
|
|
|
|
|
const onDrop = async (event: DragEvent) => {
|
|
|
|
el.value!.classList.remove('droppable')
|
|
|
|
|
|
|
|
if (!contentEditable.value) return false
|
|
|
|
if (!acceptsDrop(event)) return false
|
|
|
|
|
|
|
|
const songs = await resolveDroppedSongs(event)
|
|
|
|
|
|
|
|
if (!songs?.length) return false
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
|
|
if (type.value === 'favorites') {
|
2022-06-10 10:47:46 +00:00
|
|
|
await favoriteStore.like(songs)
|
2022-04-15 17:00:08 +00:00
|
|
|
} else if (type.value === 'playlist') {
|
2022-07-04 10:39:02 +00:00
|
|
|
await playlistStore.addSongs(playlist.value, songs)
|
2022-09-03 08:32:09 +00:00
|
|
|
toaster.value.success(`Added ${pluralize(songs, 'song')} into "${playlist.value.name}."`)
|
2022-04-15 17:00:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
eventBus.on('LOAD_MAIN_CONTENT', (view: MainViewName, _playlist: Playlist): void => {
|
|
|
|
switch (view) {
|
|
|
|
case 'Favorites':
|
|
|
|
active.value = type.value === 'favorites'
|
|
|
|
break
|
|
|
|
|
|
|
|
case 'RecentlyPlayed':
|
|
|
|
active.value = type.value === 'recently-played'
|
|
|
|
|
|
|
|
break
|
|
|
|
case 'Playlist':
|
|
|
|
active.value = playlist.value === _playlist
|
|
|
|
break
|
|
|
|
|
|
|
|
default:
|
|
|
|
active.value = false
|
|
|
|
break
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.playlist {
|
|
|
|
user-select: none;
|
2022-07-15 15:23:12 +00:00
|
|
|
overflow: hidden;
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-09-03 08:32:09 +00:00
|
|
|
&.droppable {
|
|
|
|
box-shadow: inset 0 0 0 1px var(--color-accent);
|
|
|
|
border-radius: 4px;
|
|
|
|
cursor: copy;
|
|
|
|
}
|
|
|
|
|
2022-08-10 14:56:01 +00:00
|
|
|
::v-deep(a) {
|
2022-04-15 14:24:30 +00:00
|
|
|
span {
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
input {
|
|
|
|
width: calc(100% - 32px);
|
|
|
|
margin: 5px 16px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|