mirror of
https://github.com/koel/koel
synced 2024-09-20 14:21:55 +00:00
feat: implement drop zone for uploading
This commit is contained in:
parent
91e51f8b54
commit
aea0fabe73
36 changed files with 296 additions and 192 deletions
|
@ -3,7 +3,7 @@
|
|||
<DialogBox ref="dialog"/>
|
||||
<MessageToaster ref="toaster"/>
|
||||
|
||||
<div id="main" v-if="authenticated">
|
||||
<div id="main" v-if="authenticated" @dragover="onDragOver" @drop="onDrop" @dragend="onDragEnd">
|
||||
<Hotkeys/>
|
||||
<GlobalEventListeners/>
|
||||
<AppHeader/>
|
||||
|
@ -16,6 +16,7 @@
|
|||
<PlaylistContextMenu/>
|
||||
<PlaylistFolderContextMenu/>
|
||||
<CreateNewPlaylistContextMenu/>
|
||||
<DropZone v-show="showDropZone"/>
|
||||
</div>
|
||||
|
||||
<div class="login-wrapper" v-else>
|
||||
|
@ -28,7 +29,7 @@ import { defineAsyncComponent, nextTick, onMounted, provide, ref } from 'vue'
|
|||
import { eventBus, hideOverlay, showOverlay } from '@/utils'
|
||||
import { commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { authService, playbackService, socketListener, socketService, uploadService } from '@/services'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import { ActiveScreenKey, DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
|
||||
import DialogBox from '@/components/ui/DialogBox.vue'
|
||||
import MessageToaster from '@/components/ui/MessageToaster.vue'
|
||||
|
@ -50,10 +51,13 @@ const PlaylistFolderContextMenu = defineAsyncComponent(() => import('@/component
|
|||
const SongContextMenu = defineAsyncComponent(() => import('@/components/song/SongContextMenu.vue'))
|
||||
const CreateNewPlaylistContextMenu = defineAsyncComponent(() => import('@/components/playlist/CreateNewPlaylistContextMenu.vue'))
|
||||
const SupportKoel = defineAsyncComponent(() => import('@/components/meta/SupportKoel.vue'))
|
||||
const DropZone = defineAsyncComponent(() => import('@/components/ui/upload/DropZone.vue'))
|
||||
|
||||
const dialog = ref<InstanceType<typeof DialogBox>>()
|
||||
const toaster = ref<InstanceType<typeof MessageToaster>>()
|
||||
const authenticated = ref(false)
|
||||
const showDropZone = ref(false)
|
||||
const activeScreen = ref<ScreenName>()
|
||||
|
||||
/**
|
||||
* Request for notification permission if it's not provided and the user is OK with notifications.
|
||||
|
@ -110,6 +114,18 @@ const init = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
showDropZone.value = Boolean(e.dataTransfer?.types.includes('Files')) && activeScreen.value !== 'Upload'
|
||||
}
|
||||
|
||||
const onDragEnd = () => (showDropZone.value = false)
|
||||
const onDrop = () => (showDropZone.value = false)
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('ACTIVATE_SCREEN', (screen: ScreenName) => (activeScreen.value = screen))
|
||||
})
|
||||
|
||||
provide(ActiveScreenKey, activeScreen)
|
||||
provide(DialogBoxKey, dialog)
|
||||
provide(MessageToasterKey, toaster)
|
||||
</script>
|
||||
|
|
|
@ -62,29 +62,29 @@ provideReadonly(SongsKey, songsToEdit, false)
|
|||
provideReadonly(EditSongFormInitialTabKey, editSongFormInitialTab)
|
||||
|
||||
eventBus.on({
|
||||
'MODAL_SHOW_ABOUT_KOEL': () => (showingModalName.value = 'about-koel'),
|
||||
'MODAL_SHOW_ADD_USER_FORM': () => (showingModalName.value = 'add-user-form'),
|
||||
'MODAL_SHOW_CREATE_PLAYLIST_FORM': () => (showingModalName.value = 'create-playlist-form'),
|
||||
'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM': () => (showingModalName.value = 'create-smart-playlist-form'),
|
||||
'MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM': () => (showingModalName.value = 'create-playlist-folder-form'),
|
||||
MODAL_SHOW_ABOUT_KOEL: () => (showingModalName.value = 'about-koel'),
|
||||
MODAL_SHOW_ADD_USER_FORM: () => (showingModalName.value = 'add-user-form'),
|
||||
MODAL_SHOW_CREATE_PLAYLIST_FORM: () => (showingModalName.value = 'create-playlist-form'),
|
||||
MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM: () => (showingModalName.value = 'create-smart-playlist-form'),
|
||||
MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM: () => (showingModalName.value = 'create-playlist-folder-form'),
|
||||
|
||||
'MODAL_SHOW_EDIT_PLAYLIST_FORM': (playlist: Playlist) => {
|
||||
MODAL_SHOW_EDIT_PLAYLIST_FORM: (playlist: Playlist) => {
|
||||
playlistToEdit.value = playlist
|
||||
showingModalName.value = playlist.is_smart ? 'edit-smart-playlist-form' : 'edit-playlist-form'
|
||||
},
|
||||
|
||||
'MODAL_SHOW_EDIT_USER_FORM': (user: User) => {
|
||||
MODAL_SHOW_EDIT_USER_FORM: (user: User) => {
|
||||
userToEdit.value = user
|
||||
showingModalName.value = 'edit-user-form'
|
||||
},
|
||||
|
||||
'MODAL_SHOW_EDIT_SONG_FORM': (songs: Song | Song[], initialTab: EditSongFormTabName = 'details') => {
|
||||
MODAL_SHOW_EDIT_SONG_FORM: (songs: Song | Song[], initialTab: EditSongFormTabName = 'details') => {
|
||||
songsToEdit.value = arrayify(songs)
|
||||
editSongFormInitialTab.value = initialTab
|
||||
showingModalName.value = 'edit-song-form'
|
||||
},
|
||||
|
||||
'MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM': (folder: PlaylistFolder) => {
|
||||
MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM: (folder: PlaylistFolder) => {
|
||||
playlistFolderToEdit.value = folder
|
||||
showingModalName.value = 'edit-playlist-folder-form'
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ const toggleEqualizer = () => (showEqualizer.value = !showEqualizer.value)
|
|||
const closeEqualizer = () => (showEqualizer.value = false)
|
||||
const toggleVisualizer = () => isMobile.any || eventBus.emit('TOGGLE_VISUALIZER')
|
||||
|
||||
eventBus.on('LOAD_MAIN_CONTENT', (view: MainViewName) => (viewingQueue.value = view === 'Queue'))
|
||||
eventBus.on('ACTIVATE_SCREEN', (screen: ScreenName) => (viewingQueue.value = screen === 'Queue'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -25,16 +25,8 @@ const requestContextMenu = (event: MouseEvent) => {
|
|||
}
|
||||
|
||||
eventBus.on({
|
||||
/**
|
||||
* Listen to song:played event to set the current playing song.
|
||||
*/
|
||||
'SONG_STARTED': (newSong: Song) => (song.value = newSong),
|
||||
|
||||
/**
|
||||
* Listen to main-content-view:load event and highlight the Queue icon if
|
||||
* the Queue screen is being loaded.
|
||||
*/
|
||||
'LOAD_MAIN_CONTENT': (view: MainViewName) => (viewingQueue.value = view === 'Queue')
|
||||
SONG_STARTED: (newSong: Song) => (song.value = newSong),
|
||||
ACTIVATE_SCREEN: (screen: ScreenName) => (viewingQueue.value = screen === 'Queue')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -139,8 +139,8 @@ const fetchSongInfo = async (_song: Song) => {
|
|||
}
|
||||
|
||||
eventBus.on({
|
||||
'SONG_STARTED': async (song: Song) => await fetchSongInfo(song),
|
||||
'LOAD_MAIN_CONTENT': (): void => {
|
||||
SONG_STARTED: async (song: Song) => await fetchSongInfo(song),
|
||||
KOEL_READY: () => {
|
||||
// On ready, add 'with-extra-panel' class.
|
||||
isMobile.any || document.documentElement.classList.add('with-extra-panel')
|
||||
|
||||
|
|
|
@ -8,24 +8,24 @@
|
|||
<Visualizer v-if="showingVisualizer"/>
|
||||
<AlbumArtOverlay v-if="showAlbumArtOverlay && currentSong" :album="currentSong?.album_id"/>
|
||||
|
||||
<HomeScreen v-show="view === 'Home'"/>
|
||||
<QueueScreen v-show="view === 'Queue'"/>
|
||||
<AllSongsScreen v-show="view === 'Songs'"/>
|
||||
<AlbumListScreen v-show="view === 'Albums'"/>
|
||||
<ArtistListScreen v-show="view === 'Artists'"/>
|
||||
<PlaylistScreen v-show="view === 'Playlist'"/>
|
||||
<FavoritesScreen v-show="view === 'Favorites'"/>
|
||||
<RecentlyPlayedScreen v-show="view === 'RecentlyPlayed'"/>
|
||||
<UploadScreen v-show="view === 'Upload'"/>
|
||||
<SearchExcerptsScreen v-show="view === 'Search.Excerpt'"/>
|
||||
<HomeScreen v-show="screen === 'Home'"/>
|
||||
<QueueScreen v-show="screen === 'Queue'"/>
|
||||
<AllSongsScreen v-show="screen === 'Songs'"/>
|
||||
<AlbumListScreen v-show="screen === 'Albums'"/>
|
||||
<ArtistListScreen v-show="screen === 'Artists'"/>
|
||||
<PlaylistScreen v-show="screen === 'Playlist'"/>
|
||||
<FavoritesScreen v-show="screen === 'Favorites'"/>
|
||||
<RecentlyPlayedScreen v-show="screen === 'RecentlyPlayed'"/>
|
||||
<UploadScreen v-show="screen === 'Upload'"/>
|
||||
<SearchExcerptsScreen v-show="screen === 'Search.Excerpt'"/>
|
||||
|
||||
<SearchSongResultsScreen v-if="view === 'Search.Songs'" :q="screenProps"/>
|
||||
<AlbumScreen v-if="view === 'Album'" :album="screenProps"/>
|
||||
<ArtistScreen v-if="view === 'Artist'" :artist="screenProps"/>
|
||||
<SettingsScreen v-if="view === 'Settings'"/>
|
||||
<ProfileScreen v-if="view === 'Profile'"/>
|
||||
<UserListScreen v-if="view === 'Users'"/>
|
||||
<YoutubeScreen v-if="useYouTube" v-show="view === 'YouTube'"/>
|
||||
<SearchSongResultsScreen v-if="screen === 'Search.Songs'" :q="screenProps"/>
|
||||
<AlbumScreen v-if="screen === 'Album'" :album="screenProps"/>
|
||||
<ArtistScreen v-if="screen === 'Artist'" :artist="screenProps"/>
|
||||
<SettingsScreen v-if="screen === 'Settings'"/>
|
||||
<ProfileScreen v-if="screen === 'Profile'"/>
|
||||
<UserListScreen v-if="screen === 'Users'"/>
|
||||
<YoutubeScreen v-if="useYouTube" v-show="screen === 'YouTube'"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
@ -61,17 +61,17 @@ const { useYouTube } = useThirdPartyServices()
|
|||
const showAlbumArtOverlay = toRef(preferenceStore.state, 'showAlbumArtOverlay')
|
||||
const showingVisualizer = ref(false)
|
||||
const screenProps = ref<any>(null)
|
||||
const view = ref<MainViewName>('Home')
|
||||
const screen = ref<ScreenName>('Home')
|
||||
const currentSong = ref<Song | null>(null)
|
||||
|
||||
eventBus.on({
|
||||
LOAD_MAIN_CONTENT (_view: MainViewName, data: any) {
|
||||
ACTIVATE_SCREEN (screenName: ScreenName, data: any) {
|
||||
screenProps.value = data
|
||||
view.value = _view
|
||||
screen.value = screenName
|
||||
},
|
||||
|
||||
'TOGGLE_VISUALIZER': () => (showingVisualizer.value = !showingVisualizer.value),
|
||||
'SONG_STARTED': (song: Song) => (currentSong.value = song)
|
||||
TOGGLE_VISUALIZER: () => (showingVisualizer.value = !showingVisualizer.value),
|
||||
SONG_STARTED: (song: Song) => (currentSong.value = song)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -5,37 +5,42 @@
|
|||
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['home', currentView === 'Home' ? 'active' : '']" href="#!/home">
|
||||
<a :class="['home', activeScreen === 'Home' ? 'active' : '']" href="#!/home">
|
||||
<icon :icon="faHome" fixed-width/>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li ref="queueMenuItemEl" @dragleave="onQueueDragLeave" @dragover="onQueueDragOver" @drop="onQueueDrop">
|
||||
<a :class="['queue', currentView === 'Queue' ? 'active' : '']" href="#!/queue">
|
||||
<li
|
||||
:class="droppableToQueue && 'droppable'"
|
||||
@dragleave="onQueueDragLeave"
|
||||
@dragover="onQueueDragOver"
|
||||
@drop="onQueueDrop"
|
||||
>
|
||||
<a :class="['queue', activeScreen === 'Queue' ? 'active' : '']" href="#!/queue">
|
||||
<icon :icon="faListOl" fixed-width/>
|
||||
Current Queue
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['songs', currentView === 'Songs' ? 'active' : '']" href="#!/songs">
|
||||
<a :class="['songs', activeScreen === 'Songs' ? 'active' : '']" href="#!/songs">
|
||||
<icon :icon="faMusic" fixed-width/>
|
||||
All Songs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['albums', currentView === 'Albums' ? 'active' : '']" href="#!/albums">
|
||||
<a :class="['albums', activeScreen === 'Albums' ? 'active' : '']" href="#!/albums">
|
||||
<icon :icon="faCompactDisc" fixed-width/>
|
||||
Albums
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['artists', currentView === 'Artists' ? 'active' : '']" href="#!/artists">
|
||||
<a :class="['artists', activeScreen === 'Artists' ? 'active' : '']" href="#!/artists">
|
||||
<icon :icon="faMicrophone" fixed-width/>
|
||||
Artists
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="useYouTube">
|
||||
<a :class="['youtube', currentView === 'YouTube' ? 'active' : '']" href="#!/youtube">
|
||||
<a :class="['youtube', activeScreen === 'YouTube' ? 'active' : '']" href="#!/youtube">
|
||||
<icon :icon="faYoutube" fixed-width/>
|
||||
YouTube Video
|
||||
</a>
|
||||
|
@ -43,26 +48,26 @@
|
|||
</ul>
|
||||
</section>
|
||||
|
||||
<PlaylistList :current-view="currentView"/>
|
||||
<PlaylistList/>
|
||||
|
||||
<section v-if="isAdmin" class="manage">
|
||||
<h1>Manage</h1>
|
||||
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['settings', currentView === 'Settings' ? 'active' : '']" href="#!/settings">
|
||||
<a :class="['settings', activeScreen === 'Settings' ? 'active' : '']" href="#!/settings">
|
||||
<icon :icon="faTools" fixed-width/>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['upload', currentView === 'Upload' ? 'active' : '']" href="#!/upload">
|
||||
<a :class="['upload', activeScreen === 'Upload' ? 'active' : '']" href="#!/upload">
|
||||
<icon :icon="faUpload" fixed-width/>
|
||||
Upload
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['users', currentView === 'Users' ? 'active' : '']" href="#!/users">
|
||||
<a :class="['users', activeScreen === 'Users' ? 'active' : '']" href="#!/users">
|
||||
<icon :icon="faUsers" fixed-width/>
|
||||
Users
|
||||
</a>
|
||||
|
@ -86,15 +91,16 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faYoutube } from '@fortawesome/free-brands-svg-icons'
|
||||
import { ref } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { queueStore } from '@/stores'
|
||||
import { useAuthorization, useDroppable, useThirdPartyServices } from '@/composables'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
|
||||
import PlaylistList from '@/components/playlist/PlaylistSidebarList.vue'
|
||||
|
||||
const showing = ref(!isMobile.phone)
|
||||
const currentView = ref<MainViewName>('Home')
|
||||
const queueMenuItemEl = ref<HTMLLIElement>()
|
||||
const activeScreen = requireInjection(ActiveScreenKey, ref('Home'))
|
||||
const droppableToQueue = ref(false)
|
||||
|
||||
const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist', 'playlist'])
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
|
@ -105,13 +111,13 @@ const onQueueDragOver = (event: DragEvent) => {
|
|||
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'move'
|
||||
queueMenuItemEl.value?.classList.add('droppable')
|
||||
droppableToQueue.value = true
|
||||
}
|
||||
|
||||
const onQueueDragLeave = () => queueMenuItemEl.value?.classList.remove('droppable')
|
||||
const onQueueDragLeave = () => (droppableToQueue.value = false)
|
||||
|
||||
const onQueueDrop = async (event: DragEvent) => {
|
||||
queueMenuItemEl.value?.classList.remove('droppable')
|
||||
droppableToQueue.value = false
|
||||
|
||||
if (!acceptsDrop(event)) return false
|
||||
|
||||
|
@ -123,16 +129,16 @@ const onQueueDrop = async (event: DragEvent) => {
|
|||
}
|
||||
|
||||
eventBus.on({
|
||||
LOAD_MAIN_CONTENT (view: MainViewName) {
|
||||
currentView.value = view
|
||||
// Hide the sidebar if on mobile
|
||||
isMobile.phone && (showing.value = false)
|
||||
},
|
||||
/**
|
||||
* Listen to sidebar:toggle event to show or hide the sidebar.
|
||||
* On mobile, hide the sidebar whenever a screen is activated.
|
||||
*/
|
||||
ACTIVATE_SCREEN: () => isMobile.phone && (showing.value = false),
|
||||
|
||||
/**
|
||||
* Listen to toggle sidebar event to show or hide the sidebar.
|
||||
* This should only be triggered on a mobile device.
|
||||
*/
|
||||
['TOGGLE_SIDEBAR']: () => (showing.value = !showing.value)
|
||||
TOGGLE_SIDEBAR: () => (showing.value = !showing.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<li
|
||||
ref="el"
|
||||
class="playlist-folder"
|
||||
:class="{ droppable }"
|
||||
@dragleave="onDragLeave"
|
||||
@dragover="onDragOver"
|
||||
@drop="onDrop"
|
||||
|
@ -18,10 +18,10 @@
|
|||
|
||||
<div
|
||||
v-if="opened"
|
||||
ref="hatch"
|
||||
:class="droppableOnHatch && 'droppable'"
|
||||
class="hatch"
|
||||
@dragleave.prevent="onDragLeaveHatch"
|
||||
@dragover="onDragOverHatch"
|
||||
@dragleave.prevent="onDragLeaveHatch"
|
||||
@drop.prevent="onDropOnHatch"
|
||||
/>
|
||||
</li>
|
||||
|
@ -39,9 +39,9 @@ const PlaylistSidebarItem = defineAsyncComponent(() => import('@/components/play
|
|||
const props = defineProps<{ folder: PlaylistFolder }>()
|
||||
const { folder } = toRefs(props)
|
||||
|
||||
const el = ref<HTMLLIElement>()
|
||||
const hatch = ref<HTMLLIElement>()
|
||||
const opened = ref(false)
|
||||
const droppable = ref(false)
|
||||
const droppableOnHatch = ref(false)
|
||||
|
||||
const playlistsInFolder = computed(() => playlistStore.byFolder(folder.value))
|
||||
|
||||
|
@ -54,37 +54,39 @@ const onDragOver = (event: DragEvent) => {
|
|||
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'move'
|
||||
el.value?.classList.add('droppable')
|
||||
droppable.value = true
|
||||
opened.value = true
|
||||
}
|
||||
|
||||
const onDragLeave = () => el.value?.classList.remove('droppable')
|
||||
const onDragLeave = () => (droppable.value = false)
|
||||
|
||||
const onDrop = async (event: DragEvent) => {
|
||||
droppable.value = false
|
||||
|
||||
if (!acceptsDrop(event)) return false
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
el.value?.classList.remove('droppable')
|
||||
const playlist = await resolveDroppedValue<Playlist>(event)
|
||||
if (!playlist || playlist.folder_id === folder.value.id) return
|
||||
|
||||
await playlistFolderStore.addPlaylistToFolder(folder.value, playlist)
|
||||
}
|
||||
|
||||
const onDragLeaveHatch = () => hatch.value?.classList.remove('droppable')
|
||||
const onDragLeaveHatch = () => (droppableOnHatch.value = false)
|
||||
|
||||
const onDragOverHatch = (event: DragEvent) => {
|
||||
if (!acceptsDrop(event)) return false
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'move'
|
||||
hatch.value?.classList.add('droppable')
|
||||
droppableOnHatch.value = true
|
||||
}
|
||||
|
||||
const onDropOnHatch = async (event: DragEvent) => {
|
||||
hatch.value?.classList.remove('droppable')
|
||||
el.value?.classList.remove('droppable')
|
||||
droppableOnHatch.value = false
|
||||
droppable.value = false
|
||||
|
||||
const playlist = (await resolveDroppedValue<Playlist>(event))!
|
||||
|
||||
// if the playlist isn't in the folder, don't do anything. The folder will handle the drop.
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<li
|
||||
ref="el"
|
||||
class="playlist"
|
||||
:class="{ droppable }"
|
||||
data-testid="playlist-sidebar-item"
|
||||
draggable="true"
|
||||
@contextmenu="onContextMenu"
|
||||
|
@ -38,7 +39,7 @@ const { startDragging } = useDraggable('playlist')
|
|||
const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist'])
|
||||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const el = ref<HTMLLIElement>()
|
||||
const droppable = ref(false)
|
||||
|
||||
const props = defineProps<{ list: PlaylistLike }>()
|
||||
const { list } = toRefs(props)
|
||||
|
@ -79,15 +80,15 @@ const onDragOver = (event: DragEvent) => {
|
|||
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'copy'
|
||||
el.value?.classList.add('droppable')
|
||||
droppable.value = true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const onDragLeave = () => el.value?.classList.remove('droppable')
|
||||
const onDragLeave = () => (droppable.value = false)
|
||||
|
||||
const onDrop = async (event: DragEvent) => {
|
||||
el.value?.classList.remove('droppable')
|
||||
droppable.value = false
|
||||
|
||||
if (!contentEditable.value) return false
|
||||
if (!acceptsDrop(event)) return false
|
||||
|
@ -106,8 +107,8 @@ const onDrop = async (event: DragEvent) => {
|
|||
return false
|
||||
}
|
||||
|
||||
eventBus.on('LOAD_MAIN_CONTENT', (view: MainViewName, _list: PlaylistLike): void => {
|
||||
switch (view) {
|
||||
eventBus.on('ACTIVATE_SCREEN', (screen: ScreenName, _list: PlaylistLike): void => {
|
||||
switch (screen) {
|
||||
case 'Favorites':
|
||||
active.value = isFavoriteList(list.value)
|
||||
break
|
||||
|
|
|
@ -25,7 +25,7 @@ new class extends UnitTestCase {
|
|||
preferenceStore.albumsViewMode = mode
|
||||
|
||||
const { getByTestId } = this.renderComponent()
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'Albums')
|
||||
eventBus.emit('ACTIVATE_SCREEN', 'Albums')
|
||||
|
||||
await waitFor(() => expect(getByTestId('album-list').classList.contains(`as-${mode}`)).toBe(true))
|
||||
})
|
||||
|
|
|
@ -64,8 +64,8 @@ const fetchAlbums = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
|
||||
eventBus.on('LOAD_MAIN_CONTENT', async (view: MainViewName) => {
|
||||
if (view === 'Albums' && !initialized) {
|
||||
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
|
||||
if (screen === 'Albums' && !initialized) {
|
||||
viewMode.value = preferences.albumsViewMode || 'thumbnails'
|
||||
await makeScrollable()
|
||||
initialized = true
|
||||
|
|
|
@ -12,7 +12,7 @@ new class extends UnitTestCase {
|
|||
private async renderComponent () {
|
||||
commonStore.state.song_count = 420
|
||||
commonStore.state.song_length = 123_456
|
||||
songStore.state.songs = factory<Song[]>('song', 20)
|
||||
songStore.state.songs = factory<Song>('song', 20)
|
||||
const fetchMock = this.mock(songStore, 'paginate').mockResolvedValue(2)
|
||||
|
||||
const rendered = this.render(AllSongsScreen, {
|
||||
|
@ -23,7 +23,7 @@ new class extends UnitTestCase {
|
|||
}
|
||||
})
|
||||
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'Songs')
|
||||
eventBus.emit('ACTIVATE_SCREEN', 'Songs')
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith('title', 'asc', 1))
|
||||
return rendered
|
||||
|
|
|
@ -102,8 +102,8 @@ const playAll = async (shuffle: boolean) => {
|
|||
await router.go('queue')
|
||||
}
|
||||
|
||||
eventBus.on('LOAD_MAIN_CONTENT', async (view: MainViewName) => {
|
||||
if (view === 'Songs' && !initialized) {
|
||||
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
|
||||
if (screen === 'Songs' && !initialized) {
|
||||
await fetchSongs()
|
||||
initialized = true
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ new class extends UnitTestCase {
|
|||
preferenceStore.artistsViewMode = mode
|
||||
|
||||
const { getByTestId } = this.renderComponent()
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'Artists')
|
||||
eventBus.emit('ACTIVATE_SCREEN', 'Artists')
|
||||
|
||||
await waitFor(() => expect(getByTestId('artist-list').classList.contains(`as-${mode}`)).toBe(true))
|
||||
})
|
||||
|
|
|
@ -64,8 +64,8 @@ const fetchArtists = async () => {
|
|||
loading.value = false
|
||||
}
|
||||
|
||||
eventBus.on('LOAD_MAIN_CONTENT', async (view: MainViewName) => {
|
||||
if (view === 'Artists' && !initialized) {
|
||||
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
|
||||
if (screen === 'Artists' && !initialized) {
|
||||
viewMode.value = preferences.artistsViewMode || 'thumbnails'
|
||||
await makeScrollable()
|
||||
initialized = true
|
||||
|
|
|
@ -11,7 +11,7 @@ new class extends UnitTestCase {
|
|||
const fetchMock = this.mock(favoriteStore, 'fetch')
|
||||
const rendered = this.render(FavoritesScreen)
|
||||
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'Favorites')
|
||||
eventBus.emit('ACTIVATE_SCREEN', 'Favorites')
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
|
||||
|
||||
return rendered
|
||||
|
@ -19,7 +19,7 @@ new class extends UnitTestCase {
|
|||
|
||||
protected test () {
|
||||
it('renders a list of favorites', async () => {
|
||||
favoriteStore.state.songs = factory<Song[]>('song', 13)
|
||||
favoriteStore.state.songs = factory<Song>('song', 13)
|
||||
const { queryByTestId } = await this.renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
@ -106,8 +106,8 @@ const fetchSongs = async () => {
|
|||
sort()
|
||||
}
|
||||
|
||||
eventBus.on('LOAD_MAIN_CONTENT', async (view: MainViewName) => {
|
||||
if (view === 'Favorites' && !initialized) {
|
||||
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
|
||||
if (screen === 'Favorites' && !initialized) {
|
||||
await fetchSongs()
|
||||
initialized = true
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ const libraryEmpty = computed(() => commonStore.state.song_length === 0)
|
|||
const loading = ref(false)
|
||||
let initialized = false
|
||||
|
||||
eventBus.on('LOAD_MAIN_CONTENT', async (view: MainViewName) => {
|
||||
eventBus.on('ACTIVATE_SCREEN', async (view: ScreenName) => {
|
||||
if (view === 'Home' && !initialized) {
|
||||
loading.value = true
|
||||
await overviewStore.init()
|
||||
|
|
|
@ -15,7 +15,7 @@ new class extends UnitTestCase {
|
|||
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue(songs)
|
||||
|
||||
const rendered = this.render(PlaylistScreen)
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'Playlist', playlist)
|
||||
eventBus.emit('ACTIVATE_SCREEN', 'Playlist', playlist)
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(playlist))
|
||||
|
||||
|
@ -54,7 +54,7 @@ new class extends UnitTestCase {
|
|||
it('deletes the playlist', async () => {
|
||||
const { getByTitle } = (await this.renderComponent([])).rendered
|
||||
|
||||
// mock *after* rendering to not tamper with "LOAD_MAIN_CONTENT" emission
|
||||
// mock *after* rendering to not tamper with "ACTIVATE_SCREEN" emission
|
||||
const emitMock = this.mock(eventBus, 'emit')
|
||||
|
||||
await fireEvent.click(getByTitle('Delete this playlist'))
|
||||
|
|
|
@ -124,14 +124,14 @@ const fetchSongs = async () => {
|
|||
}
|
||||
|
||||
eventBus.on({
|
||||
'LOAD_MAIN_CONTENT': async (view: MainViewName, p: any) => {
|
||||
if (view === 'Playlist') {
|
||||
ACTIVATE_SCREEN: async (screen: ScreenName, p: any) => {
|
||||
if (screen === 'Playlist') {
|
||||
playlist.value = p as Playlist
|
||||
await fetchSongs()
|
||||
}
|
||||
},
|
||||
|
||||
'SMART_PLAYLIST_UPDATED': async (updated: Playlist) => updated === playlist.value && await fetchSongs()
|
||||
SMART_PLAYLIST_UPDATED: async (updated: Playlist) => updated === playlist.value && await fetchSongs()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
|
|
@ -19,7 +19,7 @@ new class extends UnitTestCase {
|
|||
}
|
||||
})
|
||||
|
||||
eventBus.emit('LOAD_MAIN_CONTENT', 'RecentlyPlayed')
|
||||
eventBus.emit('ACTIVATE_SCREEN', 'RecentlyPlayed')
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
|
||||
|
||||
|
|
|
@ -70,8 +70,8 @@ let initialized = false
|
|||
let loading = ref(false)
|
||||
|
||||
eventBus.on({
|
||||
'LOAD_MAIN_CONTENT': async (view: MainViewName) => {
|
||||
if (view === 'RecentlyPlayed' && !initialized) {
|
||||
ACTIVATE_SCREEN: async (screen: ScreenName) => {
|
||||
if (screen === 'RecentlyPlayed' && !initialized) {
|
||||
loading.value = true
|
||||
await recentlyPlayedStore.fetch()
|
||||
loading.value = false
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<div class="main-scroll-wrap">
|
||||
<div
|
||||
v-if="mediaPath"
|
||||
v-if="mediaPathSetUp"
|
||||
:class="{ droppable }"
|
||||
class="upload-panel"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
|
@ -58,15 +58,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ismobile from 'ismobilejs'
|
||||
import { faRotateBack, faTimes, faUpload, faWarning } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, defineAsyncComponent, ref, toRef } from 'vue'
|
||||
|
||||
import { settingStore } from '@/stores'
|
||||
import { eventBus, getAllFileEntries, isDirectoryReadingSupported as canDropFolders } from '@/utils'
|
||||
import { acceptedMediaTypes, UploadFile } from '@/config'
|
||||
import { isDirectoryReadingSupported as canDropFolders } from '@/utils'
|
||||
import { acceptedMediaTypes } from '@/config'
|
||||
import { uploadService } from '@/services'
|
||||
import { useAuthorization } from '@/composables'
|
||||
import { useUpload } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
@ -77,63 +75,31 @@ const UploadItem = defineAsyncComponent(() => import('@/components/ui/upload/Upl
|
|||
|
||||
const acceptAttribute = acceptedMediaTypes.join(',')
|
||||
|
||||
const mediaPath = toRef(settingStore.state, 'media_path')
|
||||
const { allowsUpload, mediaPathSetUp, queueFilesForUpload, handleDropEvent } = useUpload()
|
||||
|
||||
const files = toRef(uploadService.state, 'files')
|
||||
const droppable = ref(false)
|
||||
const hasUploadFailures = ref(false)
|
||||
|
||||
const { isAdmin } = useAuthorization()
|
||||
const allowsUpload = computed(() => isAdmin.value && !ismobile.any)
|
||||
const hasUploadFailures = computed(() => files.value.filter((file) => file.status === 'Errored').length > 0)
|
||||
|
||||
const onDragEnter = () => (droppable.value = allowsUpload.value)
|
||||
const onDragLeave = () => (droppable.value = false)
|
||||
|
||||
const handleFiles = (files: Array<File>) => {
|
||||
const uploadCandidates = files
|
||||
.filter(file => acceptedMediaTypes.includes(file.type))
|
||||
.map((file): UploadFile => ({
|
||||
file,
|
||||
id: `${file.name}-${file.size}`, // for simplicity, a file's identity is determined by its name and size
|
||||
status: 'Ready',
|
||||
name: file.name,
|
||||
progress: 0
|
||||
}))
|
||||
|
||||
uploadService.queue(uploadCandidates)
|
||||
}
|
||||
|
||||
const fileEntryToFile = async (entry: FileSystemEntry) => new Promise<File>(resolve => entry.file(resolve))
|
||||
|
||||
const onFileInputChange = (event: InputEvent) => {
|
||||
const selectedFileList = (event.target as HTMLInputElement).files
|
||||
selectedFileList?.length && handleFiles(Array.from(selectedFileList))
|
||||
|
||||
if (selectedFileList?.length) {
|
||||
queueFilesForUpload(Array.from(selectedFileList))
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = async (event: DragEvent) => {
|
||||
droppable.value = false
|
||||
|
||||
if (!event.dataTransfer) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileEntries = await getAllFileEntries(event.dataTransfer.items)
|
||||
const files = await Promise.all(fileEntries.map(async entry => await fileEntryToFile(entry)))
|
||||
handleFiles(files)
|
||||
await handleDropEvent(event)
|
||||
}
|
||||
|
||||
const retryAll = () => {
|
||||
uploadService.retryAll()
|
||||
hasUploadFailures.value = false
|
||||
}
|
||||
|
||||
const removeFailedEntries = () => {
|
||||
uploadService.removeFailed()
|
||||
hasUploadFailures.value = false
|
||||
}
|
||||
|
||||
eventBus.on('UPLOAD_QUEUE_FINISHED', () => {
|
||||
hasUploadFailures.value = uploadService.getFilesByStatus('Errored').length !== 0
|
||||
})
|
||||
const retryAll = () => uploadService.retryAll()
|
||||
const removeFailedEntries = () => uploadService.removeFailed()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -55,7 +55,7 @@ eventBus.on({
|
|||
/**
|
||||
* Stop video playback when a song is played/resumed.
|
||||
*/
|
||||
'SONG_STARTED': () => player && player.pauseVideo()
|
||||
SONG_STARTED: () => player && player.pauseVideo()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ new class extends UnitTestCase {
|
|||
},
|
||||
global: {
|
||||
provide: {
|
||||
[SongsKey]: [ref(songs)],
|
||||
[SelectedSongsKey]: [ref(take(songs, selectedSongCount))]
|
||||
[<symbol>SongsKey]: [ref(songs)],
|
||||
[<symbol>SelectedSongsKey]: [ref(take(songs, selectedSongCount))]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -36,8 +36,8 @@ const show = (options: Partial<OverlayState>) => {
|
|||
const hide = () => (state.showing = false)
|
||||
|
||||
eventBus.on({
|
||||
'SHOW_OVERLAY': show,
|
||||
'HIDE_OVERLAY': hide
|
||||
SHOW_OVERLAY: show,
|
||||
HIDE_OVERLAY: hide
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ if (process.env.NODE_ENV !== 'test') {
|
|||
const goToSearchScreen = () => router.go('search')
|
||||
|
||||
eventBus.on({
|
||||
FOCUS_SEARCH_FIELD () {
|
||||
FOCUS_SEARCH_FIELD: () => {
|
||||
input.value?.focus()
|
||||
input.value?.select()
|
||||
}
|
||||
|
|
74
resources/assets/js/components/ui/upload/DropZone.vue
Normal file
74
resources/assets/js/components/ui/upload/DropZone.vue
Normal file
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="allowsUpload && mediaPathSetUp"
|
||||
:class="{ droppable }"
|
||||
class="drop-zone"
|
||||
@dragleave="onDropLeave"
|
||||
@dragover="onDragOver"
|
||||
@drop="onDrop"
|
||||
>
|
||||
<icon :icon="faUpload" size="6x"/>
|
||||
<h3>Drop to upload</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { faUpload } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useUpload } from '@/composables'
|
||||
|
||||
const { allowsUpload, mediaPathSetUp, handleDropEvent } = useUpload()
|
||||
|
||||
const droppable = ref(false)
|
||||
|
||||
const onDropLeave = () => (droppable.value = false)
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
if (!event.dataTransfer?.types.includes('Files')) return false
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'copy'
|
||||
droppable.value = true
|
||||
}
|
||||
|
||||
const onDrop = async (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
droppable.value = false
|
||||
await handleDropEvent(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drop-zone {
|
||||
height: 256px;
|
||||
max-height: 66vh;
|
||||
aspect-ratio: 1/1;
|
||||
outline: 3px dashed #ccc;
|
||||
position: fixed;
|
||||
z-index: 9;
|
||||
transform: translate(calc(50vw - 50%), calc(50vh - 50%));
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 2rem;
|
||||
background: rgba(0, 0, 0, .4);
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
transition: 0.2s;
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: var(--font-weight-thin);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&.droppable {
|
||||
height: 384px;
|
||||
outline: 3px dashed #fff;
|
||||
background: rgba(0, 0, 0, .9);
|
||||
box-shadow: 0 0 0 999rem rgba(0, 0, 0, .7);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -17,7 +17,7 @@ const toaster = requireInjection(MessageToasterKey)
|
|||
const dialog = requireInjection(DialogBoxKey)
|
||||
|
||||
eventBus.on({
|
||||
'PLAYLIST_DELETE': async (playlist: Playlist) => {
|
||||
PLAYLIST_DELETE: async (playlist: Playlist) => {
|
||||
if (await dialog.value.confirm(`Delete the playlist "${playlist.name}"?`)) {
|
||||
await playlistStore.delete(playlist)
|
||||
toaster.value.success(`Playlist "${playlist.name}" deleted.`)
|
||||
|
@ -25,7 +25,7 @@ eventBus.on({
|
|||
}
|
||||
},
|
||||
|
||||
'PLAYLIST_FOLDER_DELETE': async (folder: PlaylistFolder) => {
|
||||
PLAYLIST_FOLDER_DELETE: async (folder: PlaylistFolder) => {
|
||||
if (await dialog.value.confirm(`Delete the playlist folder "${folder.name}"?`)) {
|
||||
await playlistFolderStore.delete(folder)
|
||||
toaster.value.success(`Playlist folder "${folder.name}" deleted.`)
|
||||
|
@ -36,17 +36,17 @@ eventBus.on({
|
|||
/**
|
||||
* Log the current user out and reset the application state.
|
||||
*/
|
||||
'LOG_OUT': async () => {
|
||||
LOG_OUT: async () => {
|
||||
await userStore.logout()
|
||||
authService.destroy()
|
||||
forceReloadWindow()
|
||||
},
|
||||
|
||||
'KOEL_READY': () => router.resolveRoute(),
|
||||
KOEL_READY: () => router.resolveRoute(),
|
||||
|
||||
/**
|
||||
* Hide the panel away if a main view is triggered on mobile.
|
||||
*/
|
||||
'LOAD_MAIN_CONTENT': () => isMobile.phone && (preferenceStore.showExtraPanel = false)
|
||||
ACTIVATE_SCREEN: () => isMobile.phone && (preferenceStore.showExtraPanel = false)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -6,3 +6,4 @@ export * from './useAuthorization'
|
|||
export * from './useNewVersionNotification'
|
||||
export * from './useThirdPartyServices'
|
||||
export * from './useDragAndDrop'
|
||||
export * from './useUpload'
|
||||
|
|
62
resources/assets/js/composables/useUpload.ts
Normal file
62
resources/assets/js/composables/useUpload.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { useAuthorization } from '@/composables/useAuthorization'
|
||||
import { settingStore } from '@/stores'
|
||||
import { acceptedMediaTypes, UploadFile } from '@/config'
|
||||
import { uploadService } from '@/services'
|
||||
import { getAllFileEntries, pluralize, requireInjection } from '@/utils'
|
||||
import { ActiveScreenKey, MessageToasterKey } from '@/symbols'
|
||||
import router from '@/router'
|
||||
|
||||
export const useUpload = () => {
|
||||
const { isAdmin } = useAuthorization()
|
||||
|
||||
const activeScreen = requireInjection(ActiveScreenKey)
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const mediaPath = toRef(settingStore.state, 'media_path')
|
||||
|
||||
const mediaPathSetUp = computed(() => Boolean(mediaPath.value))
|
||||
const allowsUpload = computed(() => isAdmin.value && !isMobile.any)
|
||||
|
||||
const fileEntryToFile = async (entry: FileSystemEntry) => new Promise<File>(resolve => entry.file(resolve))
|
||||
|
||||
const queueFilesForUpload = (files: Array<File>) => {
|
||||
const uploadCandidates = files
|
||||
.filter(file => acceptedMediaTypes.includes(file.type))
|
||||
.map((file): UploadFile => ({
|
||||
file,
|
||||
id: `${file.name}-${file.size}`, // for simplicity, a file's identity is determined by its name and size
|
||||
status: 'Ready',
|
||||
name: file.name,
|
||||
progress: 0
|
||||
}))
|
||||
|
||||
uploadService.queue(uploadCandidates)
|
||||
|
||||
return uploadCandidates
|
||||
}
|
||||
|
||||
const handleDropEvent = async (event: DragEvent) => {
|
||||
if (!event.dataTransfer) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileEntries = await getAllFileEntries(event.dataTransfer.items)
|
||||
const files = await Promise.all(fileEntries.map(async entry => await fileEntryToFile(entry)))
|
||||
const queuedFiles = queueFilesForUpload(files)
|
||||
|
||||
if (queuedFiles.length) {
|
||||
toaster.value.success(`Queued ${pluralize(queuedFiles, 'file')} for upload`)
|
||||
activeScreen.value === 'Upload' || router.go('upload')
|
||||
} else {
|
||||
toaster.value.warning('No files applicable for upload')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mediaPathSetUp,
|
||||
allowsUpload,
|
||||
handleDropEvent,
|
||||
queueFilesForUpload
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
export type EventName =
|
||||
'KOEL_READY'
|
||||
| 'LOAD_MAIN_CONTENT'
|
||||
| 'ACTIVATE_SCREEN'
|
||||
| 'LOG_OUT'
|
||||
| 'TOGGLE_SIDEBAR'
|
||||
| 'SHOW_OVERLAY'
|
||||
|
@ -36,10 +36,6 @@ export type EventName =
|
|||
| 'SONGS_UPDATED'
|
||||
| 'SONG_QUEUED_FROM_ROUTE'
|
||||
|
||||
// upload-related
|
||||
| 'SONG_UPLOADED'
|
||||
| 'UPLOAD_QUEUE_FINISHED'
|
||||
|
||||
// socket events
|
||||
| 'SOCKET_TOGGLE_PLAYBACK'
|
||||
| 'SOCKET_TOGGLE_FAVORITE'
|
||||
|
|
|
@ -3,7 +3,7 @@ import { reactive } from 'vue'
|
|||
import { UploadFile, UploadStatus } from '@/config'
|
||||
import { httpService } from '@/services'
|
||||
import { albumStore, overviewStore, songStore } from '@/stores'
|
||||
import { eventBus, logger } from '@/utils'
|
||||
import { logger } from '@/utils'
|
||||
|
||||
interface UploadResult {
|
||||
song: Song
|
||||
|
@ -81,8 +81,6 @@ export const uploadService = {
|
|||
file.message = `Upload failed: ${error.response?.data?.message || 'Unknown error'}`
|
||||
file.status = 'Errored'
|
||||
this.proceed() // upload the next file
|
||||
} finally {
|
||||
this.checkUploadQueueStatus()
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -102,22 +100,10 @@ export const uploadService = {
|
|||
file.progress = 0
|
||||
},
|
||||
|
||||
clear () {
|
||||
this.state.files = []
|
||||
},
|
||||
|
||||
removeFailed () {
|
||||
this.state.files = this.state.files.filter(file => file.status !== 'Errored')
|
||||
},
|
||||
|
||||
checkUploadQueueStatus () {
|
||||
const uploadingFiles = this.state.files.filter(file => file.status === 'Uploading')
|
||||
|
||||
if (uploadingFiles.length === 0) {
|
||||
eventBus.emit('UPLOAD_QUEUE_FINISHED')
|
||||
}
|
||||
},
|
||||
|
||||
getFilesByStatus (status: UploadStatus) {
|
||||
return this.state.files.filter(file => file.status === status)
|
||||
}
|
||||
|
|
|
@ -20,3 +20,5 @@ export const EditSongFormInitialTabKey: ReadonlyInjectionKey<Ref<EditSongFormTab
|
|||
export const PlaylistKey: ReadonlyInjectionKey<Ref<Playlist>> = Symbol('Playlist')
|
||||
export const PlaylistFolderKey: ReadonlyInjectionKey<Ref<PlaylistFolder>> = Symbol('PlaylistFolder')
|
||||
export const UserKey: ReadonlyInjectionKey<Ref<User>> = Symbol('User')
|
||||
|
||||
export const ActiveScreenKey: InjectionKey<Ref<ScreenName>> = Symbol('ActiveScreen')
|
||||
|
|
2
resources/assets/js/types.d.ts
vendored
2
resources/assets/js/types.d.ts
vendored
|
@ -293,7 +293,7 @@ interface EqualizerPreset {
|
|||
}
|
||||
|
||||
declare type PlaybackState = 'Stopped' | 'Playing' | 'Paused'
|
||||
declare type MainViewName =
|
||||
declare type ScreenName =
|
||||
| 'Home'
|
||||
| 'Default'
|
||||
| 'Queue'
|
||||
|
|
|
@ -10,7 +10,7 @@ export { defaultCover }
|
|||
* @param view
|
||||
* @param {...*} args Extra data to attach to the view.
|
||||
*/
|
||||
export const loadMainView = (view: MainViewName, ...args: any[]) => eventBus.emit('LOAD_MAIN_CONTENT', view, ...args)
|
||||
export const loadMainView = (view: ScreenName, ...args: any[]) => eventBus.emit('ACTIVATE_SCREEN', view, ...args)
|
||||
|
||||
/**
|
||||
* Force reloading window regardless of "Confirm before reload" setting.
|
||||
|
|
Loading…
Reference in a new issue