2022-04-15 14:24:30 +00:00
|
|
|
|
<template>
|
2024-05-08 09:11:32 +00:00
|
|
|
|
<ContextMenu ref="base" data-testid="song-context-menu" extra-class="song-menu">
|
2022-04-15 17:00:08 +00:00
|
|
|
|
<template v-if="onlyOneSongSelected">
|
2022-09-15 09:07:25 +00:00
|
|
|
|
<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>
|
2024-05-19 05:49:42 +00:00
|
|
|
|
<template v-if="isSong(playables[0])">
|
|
|
|
|
<li @click="viewAlbum(playables[0])">Go to Album</li>
|
|
|
|
|
<li @click="viewArtist(playables[0])">Go to Artist</li>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>
|
|
|
|
|
<li @click="viewPodcast(playables[0] as Episode)">Go to Podcast</li>
|
|
|
|
|
<li @click="viewEpisode(playables[0] as Episode)">See Episode Description</li>
|
|
|
|
|
</template>
|
2022-04-15 14:24:30 +00:00
|
|
|
|
</template>
|
|
|
|
|
<li class="has-sub">
|
|
|
|
|
Add To
|
2024-04-04 22:20:42 +00:00
|
|
|
|
<ul class="submenu menu-add-to context-menu">
|
2022-04-30 20:57:04 +00:00
|
|
|
|
<template v-if="queue.length">
|
2024-05-19 05:49:42 +00:00
|
|
|
|
<li v-if="currentSong" @click="queueAfterCurrent">After Current</li>
|
|
|
|
|
<li @click="queueToBottom">Bottom of Queue</li>
|
|
|
|
|
<li @click="queueToTop">Top of Queue</li>
|
2022-04-30 20:57:04 +00:00
|
|
|
|
</template>
|
2024-05-19 05:49:42 +00:00
|
|
|
|
<li v-else @click="queueToBottom">Queue</li>
|
2022-10-24 15:27:17 +00:00
|
|
|
|
<template v-if="!isFavoritesScreen">
|
2022-12-02 16:17:37 +00:00
|
|
|
|
<li class="separator" />
|
2024-05-19 05:49:42 +00:00
|
|
|
|
<li @click="addToFavorites">Favorites</li>
|
2022-10-24 15:27:17 +00:00
|
|
|
|
</template>
|
2022-12-02 16:17:37 +00:00
|
|
|
|
<li v-if="normalPlaylists.length" class="separator" />
|
2024-01-11 23:54:21 +00:00
|
|
|
|
<template class="d-block">
|
2024-04-04 22:20:42 +00:00
|
|
|
|
<ul v-if="normalPlaylists.length" v-koel-overflow-fade class="relative max-h-48 overflow-y-auto">
|
2024-05-19 05:49:42 +00:00
|
|
|
|
<li v-for="p in normalPlaylists" :key="p.id" @click="addToExistingPlaylist(p)">{{ p.name }}</li>
|
2024-01-08 22:21:21 +00:00
|
|
|
|
</ul>
|
2024-01-11 23:54:21 +00:00
|
|
|
|
</template>
|
2022-12-06 10:28:48 +00:00
|
|
|
|
<li class="separator" />
|
2024-05-19 05:49:42 +00:00
|
|
|
|
<li @click="addToNewPlaylist">New Playlist…</li>
|
2022-04-15 14:24:30 +00:00
|
|
|
|
</ul>
|
|
|
|
|
</li>
|
2022-10-24 15:27:17 +00:00
|
|
|
|
|
|
|
|
|
<template v-if="isQueueScreen">
|
2022-12-02 16:17:37 +00:00
|
|
|
|
<li class="separator" />
|
2022-10-24 15:27:17 +00:00
|
|
|
|
<li @click="removeFromQueue">Remove from Queue</li>
|
2022-12-02 16:17:37 +00:00
|
|
|
|
<li class="separator" />
|
2022-10-24 15:27:17 +00:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-if="isFavoritesScreen">
|
2022-12-02 16:17:37 +00:00
|
|
|
|
<li class="separator" />
|
2022-10-24 15:27:17 +00:00
|
|
|
|
<li @click="removeFromFavorites">Remove from Favorites</li>
|
|
|
|
|
</template>
|
|
|
|
|
|
2024-01-08 22:21:21 +00:00
|
|
|
|
<template v-if="visibilityActions.length">
|
|
|
|
|
<li class="separator" />
|
|
|
|
|
<li v-for="action in visibilityActions" :key="action.label" @click="action.handler">{{ action.label }}</li>
|
|
|
|
|
</template>
|
|
|
|
|
|
2024-04-04 22:20:42 +00:00
|
|
|
|
<li v-if="canEditSongs" @click="openEditForm">Edit…</li>
|
2024-08-29 17:54:58 +00:00
|
|
|
|
<li v-if="downloadable" @click="download">Download</li>
|
2024-01-22 23:27:31 +00:00
|
|
|
|
<li v-if="onlyOneSongSelected && canBeShared" @click="copyUrl">Copy Shareable URL</li>
|
2022-10-24 15:27:17 +00:00
|
|
|
|
|
|
|
|
|
<template v-if="canBeRemovedFromPlaylist">
|
2022-12-02 16:17:37 +00:00
|
|
|
|
<li class="separator" />
|
2024-05-19 05:49:42 +00:00
|
|
|
|
<li @click="removePlayablesFromPlaylist">Remove from Playlist</li>
|
2022-10-24 15:27:17 +00:00
|
|
|
|
</template>
|
|
|
|
|
|
2024-04-04 22:20:42 +00:00
|
|
|
|
<template v-if="canEditSongs">
|
2022-12-02 16:17:37 +00:00
|
|
|
|
<li class="separator" />
|
2022-09-24 08:09:03 +00:00
|
|
|
|
<li @click="deleteFromFilesystem">Delete from Filesystem</li>
|
|
|
|
|
</template>
|
2024-05-08 09:11:32 +00:00
|
|
|
|
</ContextMenu>
|
2022-04-15 14:24:30 +00:00
|
|
|
|
</template>
|
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
|
<script lang="ts" setup>
|
2022-07-08 10:32:44 +00:00
|
|
|
|
import { computed, ref, toRef } from 'vue'
|
2024-06-03 07:16:29 +00:00
|
|
|
|
import { arrayify, copyText, eventBus, getPlayableCollectionContentType, isSong, 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'
|
2022-11-18 17:45:38 +00:00
|
|
|
|
import {
|
|
|
|
|
useContextMenu,
|
|
|
|
|
useDialogBox,
|
2024-01-08 16:59:05 +00:00
|
|
|
|
useKoelPlus,
|
2022-11-18 17:45:38 +00:00
|
|
|
|
useMessageToaster,
|
2024-05-19 05:49:42 +00:00
|
|
|
|
usePlayableMenuMethods,
|
2022-11-18 17:45:38 +00:00
|
|
|
|
usePlaylistManagement,
|
2024-04-23 21:01:27 +00:00
|
|
|
|
usePolicies,
|
2024-10-13 17:37:01 +00:00
|
|
|
|
useRouter,
|
2022-11-18 17:45:38 +00:00
|
|
|
|
} from '@/composables'
|
|
|
|
|
|
2024-01-25 16:21:26 +00:00
|
|
|
|
const { toastSuccess, toastError, toastWarning } = useMessageToaster()
|
2022-11-18 17:45:38 +00:00
|
|
|
|
const { showConfirmDialog } = useDialogBox()
|
2022-11-18 18:44:20 +00:00
|
|
|
|
const { go, getRouteParam, isCurrentScreen } = useRouter()
|
2024-05-08 09:11:32 +00:00
|
|
|
|
const { base, ContextMenu, open, close, trigger } = useContextMenu()
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const { removeFromPlaylist } = usePlaylistManagement()
|
2024-01-08 16:59:05 +00:00
|
|
|
|
const { isPlus } = useKoelPlus()
|
2022-10-24 15:27:17 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const playables = ref<Playable[]>([])
|
2022-11-18 19:30:43 +00:00
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
|
const {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
queueAfterCurrent,
|
|
|
|
|
queueToBottom,
|
|
|
|
|
queueToTop,
|
|
|
|
|
addToFavorites,
|
|
|
|
|
addToExistingPlaylist,
|
2024-10-13 17:37:01 +00:00
|
|
|
|
addToNewPlaylist,
|
2024-05-19 05:49:42 +00:00
|
|
|
|
} = usePlayableMenuMethods(playables, close)
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
2022-04-25 16:13:18 +00:00
|
|
|
|
const playlists = toRef(playlistStore.state, 'playlists')
|
2024-08-29 17:54:58 +00:00
|
|
|
|
|
|
|
|
|
const downloadable = computed(() => {
|
2024-10-13 17:37:01 +00:00
|
|
|
|
if (!commonStore.state.allows_download) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2024-08-29 17:54:58 +00:00
|
|
|
|
|
|
|
|
|
// If multiple playables are selected, make sure zip extension is available on the server
|
|
|
|
|
return playables.value.length === 1 || commonStore.state.supports_batch_downloading
|
|
|
|
|
})
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const queue = toRef(queueStore.state, 'playables')
|
2022-09-25 03:04:41 +00:00
|
|
|
|
const currentSong = toRef(queueStore, 'current')
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
2024-04-04 22:20:42 +00:00
|
|
|
|
const { currentUserCan } = usePolicies()
|
|
|
|
|
|
2024-10-13 17:37:01 +00:00
|
|
|
|
const contentType = computed(() => getPlayableCollectionContentType(playables.value))
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const canEditSongs = computed(() => contentType.value === 'songs' && currentUserCan.editSong(playables.value as Song[]))
|
|
|
|
|
const onlyOneSongSelected = computed(() => playables.value.length === 1)
|
|
|
|
|
const firstSongPlaying = computed(() => playables.value.length ? playables.value[0].playback_state === 'Playing' : false)
|
2024-01-24 22:39:47 +00:00
|
|
|
|
const normalPlaylists = computed(() => playlists.value.filter(({ is_smart }) => !is_smart))
|
2024-10-13 17:37:01 +00:00
|
|
|
|
const canBeShared = computed(() => !isPlus.value || (isSong(playables.value[0]) && playables.value[0].is_public))
|
2022-09-12 15:33:41 +00:00
|
|
|
|
|
2024-01-08 22:21:21 +00:00
|
|
|
|
const makePublic = () => trigger(async () => {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
if (contentType.value !== 'songs') {
|
2024-10-13 17:37:01 +00:00
|
|
|
|
throw new Error('Only songs can be marked as public or private')
|
2024-05-19 05:49:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await songStore.publicize(playables.value as Song[])
|
|
|
|
|
toastSuccess(`Unmarked ${pluralize(playables.value, 'song')} as private.`)
|
2024-01-08 22:21:21 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const makePrivate = () => trigger(async () => {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
if (contentType.value !== 'songs') {
|
2024-10-13 17:37:01 +00:00
|
|
|
|
throw new Error('Only songs can be marked as public or private')
|
2024-05-19 05:49:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const privatizedIds = await songStore.privatize(playables.value as Song[])
|
2024-01-25 16:21:26 +00:00
|
|
|
|
|
|
|
|
|
if (!privatizedIds.length) {
|
2024-10-13 17:37:01 +00:00
|
|
|
|
toastError('Songs cannot be marked as private if they’re part of a collaborative playlist.')
|
2024-01-25 16:21:26 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
if (privatizedIds.length < playables.value.length) {
|
2024-01-25 16:21:26 +00:00
|
|
|
|
toastWarning('Some songs cannot be marked as private as they’re part of a collaborative playlist.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
toastSuccess(`Marked ${pluralize(playables.value, 'song')} as private.`)
|
2024-01-08 22:21:21 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const visibilityActions = computed(() => {
|
2024-10-13 17:37:01 +00:00
|
|
|
|
if (contentType.value !== 'songs' || !canEditSongs.value) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
2024-01-08 22:21:21 +00:00
|
|
|
|
|
2024-10-13 17:37:01 +00:00
|
|
|
|
const visibilities = Array.from(new Set((playables.value as Song[]).map(song => song.is_public
|
|
|
|
|
? 'public'
|
|
|
|
|
: 'private',
|
|
|
|
|
)))
|
2024-01-08 22:21:21 +00:00
|
|
|
|
|
2024-06-02 17:15:31 +00:00
|
|
|
|
if (visibilities.length === 2) {
|
2024-01-08 22:21:21 +00:00
|
|
|
|
return [
|
|
|
|
|
{
|
2024-01-25 16:21:26 +00:00
|
|
|
|
label: 'Unmark as Private',
|
2024-10-13 17:37:01 +00:00
|
|
|
|
handler: makePublic,
|
2024-01-08 22:21:21 +00:00
|
|
|
|
},
|
|
|
|
|
{
|
2024-01-25 16:21:26 +00:00
|
|
|
|
label: 'Mark as Private',
|
2024-10-13 17:37:01 +00:00
|
|
|
|
handler: makePrivate,
|
|
|
|
|
},
|
2024-01-08 22:21:21 +00:00
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-22 21:04:03 +00:00
|
|
|
|
return visibilities[0] === 'public'
|
2024-01-25 16:21:26 +00:00
|
|
|
|
? [{ label: 'Mark as Private', handler: makePrivate }]
|
|
|
|
|
: [{ label: 'Unmark as Private', handler: makePublic }]
|
2024-01-08 22:21:21 +00:00
|
|
|
|
})
|
|
|
|
|
|
2022-10-24 15:27:17 +00:00
|
|
|
|
const canBeRemovedFromPlaylist = computed(() => {
|
2024-10-13 17:37:01 +00:00
|
|
|
|
if (!isCurrentScreen('Playlist')) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2024-01-18 11:13:05 +00:00
|
|
|
|
const playlist = playlistStore.byId(getRouteParam('id')!)
|
2022-10-24 15:27:17 +00:00
|
|
|
|
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
|
|
|
|
|
2024-06-07 12:53:24 +00:00
|
|
|
|
const doPlayback = () => trigger(async () => {
|
2024-10-13 17:37:01 +00:00
|
|
|
|
if (!playables.value.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
switch (playables.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':
|
2024-06-07 12:53:24 +00:00
|
|
|
|
await playbackService.resume()
|
2022-04-15 17:00:08 +00:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
default:
|
2024-06-07 12:53:24 +00:00
|
|
|
|
await playbackService.play(playables.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
|
|
|
|
|
2024-06-02 17:15:31 +00:00
|
|
|
|
const openEditForm = () => trigger(() =>
|
|
|
|
|
playables.value.length
|
|
|
|
|
&& contentType.value === 'songs'
|
2024-10-13 17:37:01 +00:00
|
|
|
|
&& eventBus.emit('MODAL_SHOW_EDIT_SONG_FORM', playables.value as Song[]),
|
2024-06-02 17:15:31 +00:00
|
|
|
|
)
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const viewAlbum = (song: Song) => trigger(() => go(`album/${song.album_id}`))
|
|
|
|
|
const viewArtist = (song: Song) => trigger(() => go(`artist/${song.artist_id}`))
|
|
|
|
|
const viewPodcast = (episode: Episode) => trigger(() => go(`podcasts/${episode.podcast_id}`))
|
|
|
|
|
const viewEpisode = (episode: Episode) => trigger(() => go(`episodes/${episode.id}`))
|
|
|
|
|
const download = () => trigger(() => downloadService.fromPlayables(playables.value))
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const removePlayablesFromPlaylist = () => trigger(async () => {
|
2024-01-18 11:13:05 +00:00
|
|
|
|
const playlist = playlistStore.byId(getRouteParam('id')!)
|
2024-10-13 17:37:01 +00:00
|
|
|
|
if (!playlist) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-10-24 15:27:17 +00:00
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
await removeFromPlaylist(playlist, playables.value)
|
2022-10-24 15:27:17 +00:00
|
|
|
|
})
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
const removeFromQueue = () => trigger(() => queueStore.unqueue(playables.value))
|
|
|
|
|
const removeFromFavorites = () => trigger(() => favoriteStore.unlike(playables.value))
|
2022-10-24 15:27:17 +00:00
|
|
|
|
|
2024-01-18 11:13:05 +00:00
|
|
|
|
const copyUrl = () => trigger(async () => {
|
2024-06-02 17:15:31 +00:00
|
|
|
|
await copyText(songStore.getShareableUrl(playables.value[0]))
|
2022-11-18 17:45:38 +00:00
|
|
|
|
toastSuccess('URL copied to clipboard.')
|
2022-06-10 10:47:46 +00:00
|
|
|
|
})
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
2022-09-15 09:07:25 +00:00
|
|
|
|
const deleteFromFilesystem = () => trigger(async () => {
|
2022-11-18 17:45:38 +00:00
|
|
|
|
if (await showConfirmDialog('Delete selected song(s) from the filesystem? This action is NOT reversible!')) {
|
2024-05-19 05:49:42 +00:00
|
|
|
|
await songStore.deleteFromFilesystem(playables.value as Song[])
|
|
|
|
|
toastSuccess(`Deleted ${pluralize(playables.value, 'song')} from the filesystem.`)
|
|
|
|
|
eventBus.emit('SONGS_DELETED', playables.value as Song[])
|
2022-09-15 09:07:25 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
|
eventBus.on('PLAYABLE_CONTEXT_MENU_REQUESTED', async ({ pageX, pageY }, _songs) => {
|
|
|
|
|
playables.value = arrayify(_songs)
|
2024-01-24 22:39:47 +00:00
|
|
|
|
await open(pageY, pageX)
|
2022-07-08 10:32:44 +00:00
|
|
|
|
})
|
2022-04-15 14:24:30 +00:00
|
|
|
|
</script>
|