koel/resources/assets/js/components/song/SongContextMenu.vue

234 lines
7.8 KiB
Vue
Raw Normal View History

2022-04-15 14:24:30 +00:00
<template>
2022-07-19 08:19:57 +00:00
<ContextMenuBase ref="base" data-testid="song-context-menu" extra-class="song-menu">
2022-04-15 17:00:08 +00:00
<template v-if="onlyOneSongSelected">
<li @click.stop.prevent="doPlayback">
2022-04-15 14:24:30 +00:00
<span v-if="firstSongPlaying">Pause</span>
<span v-else>Play</span>
</li>
<li @click="viewAlbumDetails(songs[0].album_id)">Go to Album</li>
<li @click="viewArtistDetails(songs[0].artist_id)">Go to Artist</li>
2022-04-15 14:24:30 +00:00
</template>
<li class="has-sub">
Add To
<ul class="menu submenu menu-add-to">
<template v-if="queue.length">
<li v-if="currentSong" @click="queueSongsAfterCurrent">After Current Song</li>
<li @click="queueSongsToBottom">Bottom of Queue</li>
<li @click="queueSongsToTop">Top of Queue</li>
</template>
<li v-else @click="queueSongsToBottom">Queue</li>
<template v-if="!isFavoritesScreen">
2022-12-02 16:17:37 +00:00
<li class="separator" />
<li @click="addSongsToFavorite">Favorites</li>
</template>
2022-12-02 16:17:37 +00:00
<li v-if="normalPlaylists.length" class="separator" />
<template class="d-block">
<ul class="playlists" v-koel-overflow-fade v-if="normalPlaylists.length">
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
</ul>
</template>
<li class="separator" />
<li @click="addSongsToNewPlaylist">New Playlist</li>
2022-04-15 14:24:30 +00:00
</ul>
</li>
<template v-if="isQueueScreen">
2022-12-02 16:17:37 +00:00
<li class="separator" />
<li @click="removeFromQueue">Remove from Queue</li>
2022-12-02 16:17:37 +00:00
<li class="separator" />
</template>
<template v-if="isFavoritesScreen">
2022-12-02 16:17:37 +00:00
<li class="separator" />
<li @click="removeFromFavorites">Remove from Favorites</li>
</template>
<template v-if="visibilityActions.length">
<li class="separator" />
<li v-for="action in visibilityActions" :key="action.label" @click="action.handler">{{ action.label }}</li>
</template>
<li v-if="canModify" @click="openEditForm">Edit</li>
2024-01-04 11:35:36 +00:00
<li v-if="allowsDownload" @click="download">Download</li>
<li v-if="onlyOneSongSelected && canBeShared" @click="copyUrl">Copy Shareable URL</li>
<template v-if="canBeRemovedFromPlaylist">
2022-12-02 16:17:37 +00:00
<li class="separator" />
<li @click="removeFromPlaylist">Remove from Playlist</li>
</template>
<template v-if="canModify">
2022-12-02 16:17:37 +00:00
<li class="separator" />
<li @click="deleteFromFilesystem">Delete from Filesystem</li>
</template>
2022-04-24 08:29:14 +00:00
</ContextMenuBase>
2022-04-15 14:24:30 +00:00
</template>
2022-04-15 17:00:08 +00:00
<script lang="ts" setup>
import { computed, ref, toRef } from 'vue'
2022-11-18 18:44:20 +00:00
import { arrayify, copyText, eventBus, pluralize } from '@/utils'
2023-08-20 22:35:58 +00:00
import { commonStore, favoriteStore, playlistStore, queueStore, songStore } from '@/stores'
2022-04-24 08:50:45 +00:00
import { downloadService, playbackService } from '@/services'
import {
useAuthorization,
useContextMenu,
useDialogBox,
2024-01-08 16:59:05 +00:00
useKoelPlus,
useMessageToaster,
usePlaylistManagement,
2022-11-18 18:44:20 +00:00
useRouter,
useSongMenuMethods
} from '@/composables'
const { toastSuccess, toastError, toastWarning } = useMessageToaster()
const { showConfirmDialog } = useDialogBox()
2022-11-18 18:44:20 +00:00
const { go, getRouteParam, isCurrentScreen } = useRouter()
const { isAdmin, currentUser } = useAuthorization()
2022-12-02 16:17:37 +00:00
const { base, ContextMenuBase, open, close, trigger } = useContextMenu()
const { removeSongsFromPlaylist } = usePlaylistManagement()
2024-01-08 16:59:05 +00:00
const { isPlus } = useKoelPlus()
2022-11-18 19:30:43 +00:00
const songs = ref<Song[]>([])
2022-04-15 17:00:08 +00:00
const {
queueSongsAfterCurrent,
queueSongsToBottom,
queueSongsToTop,
addSongsToFavorite,
addSongsToExistingPlaylist,
addSongsToNewPlaylist
2022-04-20 10:20:09 +00:00
} = useSongMenuMethods(songs, close)
2022-04-15 17:00:08 +00:00
const playlists = toRef(playlistStore.state, 'playlists')
2024-01-04 11:35:36 +00:00
const allowsDownload = toRef(commonStore.state, 'allows_download')
const queue = toRef(queueStore.state, 'songs')
const currentSong = toRef(queueStore, 'current')
2022-04-15 17:00:08 +00:00
const canModify = computed(() => {
2024-01-24 22:39:47 +00:00
if (isPlus.value) return songs.value.every(({ owner_id }) => owner_id === currentUser.value?.id)
return isAdmin.value
})
2022-04-15 17:00:08 +00:00
const onlyOneSongSelected = computed(() => songs.value.length === 1)
2022-06-10 10:47:46 +00:00
const firstSongPlaying = computed(() => songs.value.length ? songs.value[0].playback_state === 'Playing' : false)
2024-01-24 22:39:47 +00:00
const normalPlaylists = computed(() => playlists.value.filter(({ is_smart }) => !is_smart))
const makePublic = () => trigger(async () => {
await songStore.publicize(songs.value)
toastSuccess(`Unmarked ${pluralize(songs.value, 'song')} as private.`)
})
const makePrivate = () => trigger(async () => {
const privatizedIds = await songStore.privatize(songs.value)
if (!privatizedIds.length) {
toastError('Songs cannot be marked as private if theypart of a collaborative playlist.')
return
}
if (privatizedIds.length < songs.value.length) {
toastWarning('Some songs cannot be marked as private as theyre part of a collaborative playlist.')
return
}
toastSuccess(`Marked ${pluralize(songs.value, 'song')} as private.`)
})
const canBeShared = computed(() => !isPlus.value || songs.value[0].is_public)
const visibilityActions = computed(() => {
if (!isPlus.value) return []
// If some songs don't belong to the current user, no actions are available.
2024-01-24 22:39:47 +00:00
if (songs.value.some(({ owner_id }) => owner_id !== currentUser.value?.id)) return []
const visibilities = Array.from(new Set(songs.value.map(song => song.is_public)))
if (visibilities.length === 2) {
return [
{
label: 'Unmark as Private',
handler: makePublic
},
{
label: 'Mark as Private',
handler: makePrivate
}
]
}
return visibilities[0]
? [{ label: 'Mark as Private', handler: makePrivate }]
: [{ label: 'Unmark as Private', handler: makePublic }]
})
const canBeRemovedFromPlaylist = computed(() => {
2022-11-18 18:44:20 +00:00
if (!isCurrentScreen('Playlist')) return false
2024-01-18 11:13:05 +00:00
const playlist = playlistStore.byId(getRouteParam('id')!)
return playlist && !playlist.is_smart
})
2022-11-18 18:44:20 +00:00
const isQueueScreen = computed(() => isCurrentScreen('Queue'))
const isFavoritesScreen = computed(() => isCurrentScreen('Favorites'))
2022-04-15 17:00:08 +00:00
2022-06-10 10:47:46 +00:00
const doPlayback = () => trigger(() => {
2022-04-15 17:00:08 +00:00
if (!songs.value.length) return
2022-06-10 10:47:46 +00:00
switch (songs.value[0].playback_state) {
2022-04-15 17:00:08 +00:00
case 'Playing':
2022-04-24 08:50:45 +00:00
playbackService.pause()
2022-04-15 17:00:08 +00:00
break
case 'Paused':
2022-04-24 08:50:45 +00:00
playbackService.resume()
2022-04-15 17:00:08 +00:00
break
default:
queueStore.queueIfNotQueued(songs.value[0])
2022-04-24 08:50:45 +00:00
playbackService.play(songs.value[0])
2022-04-15 17:00:08 +00:00
break
2022-04-15 14:24:30 +00:00
}
2022-06-10 10:47:46 +00:00
})
2022-04-15 17:00:08 +00:00
2022-06-10 10:47:46 +00:00
const openEditForm = () => trigger(() => songs.value.length && eventBus.emit('MODAL_SHOW_EDIT_SONG_FORM', songs.value))
2022-11-18 18:44:20 +00:00
const viewAlbumDetails = (albumId: number) => trigger(() => go(`album/${albumId}`))
const viewArtistDetails = (artistId: number) => trigger(() => go(`artist/${artistId}`))
2022-06-10 10:47:46 +00:00
const download = () => trigger(() => downloadService.fromSongs(songs.value))
2022-04-15 17:00:08 +00:00
const removeFromPlaylist = () => trigger(async () => {
2024-01-18 11:13:05 +00:00
const playlist = playlistStore.byId(getRouteParam('id')!)
if (!playlist) return
await removeSongsFromPlaylist(playlist, songs.value)
})
const removeFromQueue = () => trigger(() => queueStore.unqueue(songs.value))
const removeFromFavorites = () => trigger(() => favoriteStore.unlike(songs.value))
2024-01-18 11:13:05 +00:00
const copyUrl = () => trigger(async () => {
await copyText(songStore.getShareableUrl(songs.value[0]))
toastSuccess('URL copied to clipboard.')
2022-06-10 10:47:46 +00:00
})
2022-04-15 17:00:08 +00:00
const deleteFromFilesystem = () => trigger(async () => {
if (await showConfirmDialog('Delete selected song(s) from the filesystem? This action is NOT reversible!')) {
await songStore.deleteFromFilesystem(songs.value)
toastSuccess(`Deleted ${pluralize(songs.value, 'song')} from the filesystem.`)
eventBus.emit('SONGS_DELETED', songs.value)
}
})
2024-01-24 22:39:47 +00:00
eventBus.on('SONG_CONTEXT_MENU_REQUESTED', async ({ pageX, pageY }, _songs) => {
songs.value = arrayify(_songs)
2024-01-24 22:39:47 +00:00
await open(pageY, pageX)
})
2022-04-15 14:24:30 +00:00
</script>
<style lang="scss" scoped>
ul.playlists {
position: relative;
max-height: 192px;
overflow-y: auto;
}
</style>