diff --git a/resources/assets/js/__tests__/UnitTestCase.ts b/resources/assets/js/__tests__/UnitTestCase.ts index 9f9f95f7..748e1470 100644 --- a/resources/assets/js/__tests__/UnitTestCase.ts +++ b/resources/assets/js/__tests__/UnitTestCase.ts @@ -2,7 +2,7 @@ import isMobile from 'ismobilejs' import { isObject, mergeWith } from 'lodash' import { cleanup, render, RenderOptions } from '@testing-library/vue' import { afterEach, beforeEach, vi } from 'vitest' -import { clickaway, droppable, focus } from '@/directives' +import { clickaway, focus } from '@/directives' import { defineComponent, nextTick } from 'vue' import { commonStore, userStore } from '@/stores' import factory from '@/__tests__/factory' @@ -81,8 +81,7 @@ export default abstract class UnitTestCase { global: { directives: { 'koel-clickaway': clickaway, - 'koel-focus': focus, - 'koel-droppable': droppable + 'koel-focus': focus }, components: { icon: this.stub('icon') diff --git a/resources/assets/js/app.ts b/resources/assets/js/app.ts index f127cc18..f4dd5dc5 100644 --- a/resources/assets/js/app.ts +++ b/resources/assets/js/app.ts @@ -1,6 +1,6 @@ import 'plyr/dist/plyr.js' import { createApp } from 'vue' -import { clickaway, droppable, focus } from '@/directives' +import { clickaway, focus } from '@/directives' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import App from './App.vue' @@ -8,7 +8,6 @@ createApp(App) .component('icon', FontAwesomeIcon) .directive('koel-focus', focus) .directive('koel-clickaway', clickaway) - .directive('koel-droppable', droppable) /** * For Ancelot, the ancient cross of war * for the holy town of Gods diff --git a/resources/assets/js/components/album/AlbumCard.vue b/resources/assets/js/components/album/AlbumCard.vue index cb9f5f9a..d988a45e 100644 --- a/resources/assets/js/components/album/AlbumCard.vue +++ b/resources/assets/js/components/album/AlbumCard.vue @@ -8,7 +8,7 @@ draggable="true" tabindex="0" @dblclick="shuffle" - @dragstart="dragStart" + @dragstart="onDragStart" @contextmenu.prevent="requestContextMenu" > @@ -56,12 +56,15 @@ diff --git a/resources/assets/js/components/artist/ArtistCard.vue b/resources/assets/js/components/artist/ArtistCard.vue index 6f7d35f3..48003f43 100644 --- a/resources/assets/js/components/artist/ArtistCard.vue +++ b/resources/assets/js/components/artist/ArtistCard.vue @@ -8,7 +8,7 @@ draggable="true" tabindex="0" @dblclick="shuffle" - @dragstart="dragStart" + @dragstart="onDragStart" @contextmenu.prevent="requestContextMenu" > @@ -56,11 +56,15 @@ diff --git a/resources/assets/js/components/layout/main-wrapper/Sidebar.vue b/resources/assets/js/components/layout/main-wrapper/Sidebar.vue index b03b741d..2607e16d 100644 --- a/resources/assets/js/components/layout/main-wrapper/Sidebar.vue +++ b/resources/assets/js/components/layout/main-wrapper/Sidebar.vue @@ -10,8 +10,8 @@ Home -
  • - +
  • + Current Queue @@ -86,19 +86,37 @@ import { } from '@fortawesome/free-solid-svg-icons' import { faYoutube } from '@fortawesome/free-brands-svg-icons' import { ref } from 'vue' -import { eventBus, resolveSongsFromDragEvent } from '@/utils' +import { eventBus } from '@/utils' import { queueStore } from '@/stores' -import { useAuthorization, useThirdPartyServices } from '@/composables' +import { useAuthorization, useDroppable, useThirdPartyServices } from '@/composables' import PlaylistList from '@/components/playlist/PlaylistSidebarList.vue' const showing = ref(!isMobile.phone) const currentView = ref('Home') +const queueMenuItemEl = ref() + +const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist', 'playlist']) const { useYouTube } = useThirdPartyServices() const { isAdmin } = useAuthorization() -const handleDrop = async (event: DragEvent) => { - const songs = await resolveSongsFromDragEvent(event) +const onQueueDragOver = (event: DragEvent) => { + if (!acceptsDrop(event)) return false + + event.preventDefault() + event.dataTransfer!.dropEffect = 'move' + queueMenuItemEl.value!.classList.add('droppable') +} + +const onQueueDragLeave = () => queueMenuItemEl.value!.classList.remove('droppable') + +const onQueueDrop = async (event: DragEvent) => { + queueMenuItemEl.value!.classList.remove('droppable') + + if (!acceptsDrop(event)) return false + + event.preventDefault() + const songs = await resolveDroppedSongs(event) || [] songs.length && queueStore.queue(songs) return false @@ -137,13 +155,10 @@ nav { -webkit-overflow-scrolling: touch; } - a.droppable { - transform: scale(1.2); - transition: .3s; - transform-origin: center left; - - color: var(--color-text-primary); - background-color: rgba(0, 0, 0, .3); + .droppable { + box-shadow: inset 0 0 0 1px var(--color-accent); + border-radius: 4px; + cursor: copy; } .queue > span { diff --git a/resources/assets/js/components/playlist/PlaylistFolderSidebarItem.vue b/resources/assets/js/components/playlist/PlaylistFolderSidebarItem.vue index 31160f0b..b0f9a98c 100644 --- a/resources/assets/js/components/playlist/PlaylistFolderSidebarItem.vue +++ b/resources/assets/js/components/playlist/PlaylistFolderSidebarItem.vue @@ -2,9 +2,9 @@
  • @@ -36,7 +36,8 @@ import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons' import { computed, defineAsyncComponent, ref, toRefs } from 'vue' import { playlistFolderStore, playlistStore } from '@/stores' -import { eventBus, logger } from '@/utils' +import { eventBus } from '@/utils' +import { useDroppable } from '@/composables' const PlaylistSidebarItem = defineAsyncComponent(() => import('@/components/playlist/PlaylistSidebarItem.vue')) @@ -49,41 +50,47 @@ const opened = ref(false) const playlistsInFolder = computed(() => playlistStore.byFolder(folder.value)) +const { acceptsDrop, resolveDroppedValue } = useDroppable(['playlist']) + const toggle = () => (opened.value = !opened.value) const onDragOver = (event: DragEvent) => { - if (!event.dataTransfer?.types.includes('playlist')) return false + if (!acceptsDrop(event)) return false event.preventDefault() - event.dataTransfer.dropEffect = 'move' - el.value!.classList.add('drag-over') - opened.value || (opened.value = true) + event.dataTransfer!.dropEffect = 'move' + el.value!.classList.add('droppable') + opened.value = true } -const onDragLeave = () => el.value!.classList.remove('drag-over') +const onDragLeave = () => el.value!.classList.remove('droppable') const onDrop = async (event: DragEvent) => { - el.value!.classList.remove('drag-over') - const playlist = getPlaylistFromDropEvent(event) + if (!acceptsDrop(event)) return false + + event.preventDefault() + + el.value!.classList.remove('droppable') + const playlist = await resolveDroppedValue(event) if (!playlist || playlist.folder_id === folder.value.id) return await playlistFolderStore.addPlaylistToFolder(folder.value, playlist) } -const onDragLeaveHatch = () => hatch.value!.classList.remove('drag-over') +const onDragLeaveHatch = () => hatch.value!.classList.remove('droppable') const onDragOverHatch = (event: DragEvent) => { - if (!event.dataTransfer?.types.includes('playlist')) return false + if (!acceptsDrop(event)) return false event.preventDefault() event.dataTransfer!.dropEffect = 'move' - hatch.value!.classList.add('drag-over') + hatch.value!.classList.add('droppable') } const onDropOnHatch = async (event: DragEvent) => { - hatch.value!.classList.remove('drag-over') - el.value!.classList.remove('drag-over') - const playlist = getPlaylistFromDropEvent(event)! + hatch.value!.classList.remove('droppable') + el.value!.classList.remove('droppable') + const playlist = (await resolveDroppedValue(event))! // if the playlist isn't in the folder, don't do anything. The folder will handle the drop. if (playlist.folder_id !== folder.value.id) return @@ -93,15 +100,6 @@ const onDropOnHatch = async (event: DragEvent) => { await playlistFolderStore.removePlaylistFromFolder(folder.value, playlist) } -const getPlaylistFromDropEvent = (event: DragEvent) => { - try { - const data = JSON.parse(event.dataTransfer?.getData('application/x-koel.text+plain')!) - return playlistStore.byId(data.value as number) - } catch (e) { - logger.error(e) - } -} - const onContextMenu = event => eventBus.emit('PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED', event, folder.value) @@ -109,7 +107,7 @@ const onContextMenu = event => eventBus.emit('PLAYLIST_FOLDER_CONTEXT_MENU_REQUE li.playlist-folder { position: relative; - &.drag-over { + &.droppable { box-shadow: inset 0 0 0 1px var(--color-accent); border-radius: 4px; cursor: copy; @@ -121,7 +119,7 @@ li.playlist-folder { width: 100%; height: .5rem; - &.drag-over { + &.droppable { border-bottom: 3px solid var(--color-highlight); } } diff --git a/resources/assets/js/components/playlist/PlaylistSidebarItem.vue b/resources/assets/js/components/playlist/PlaylistSidebarItem.vue index 6ae7e330..cc33feae 100644 --- a/resources/assets/js/components/playlist/PlaylistSidebarItem.vue +++ b/resources/assets/js/components/playlist/PlaylistSidebarItem.vue @@ -1,25 +1,25 @@