feat: allow playing/shuffling playlist from context menu (#1623)

This commit is contained in:
Phan An 2022-12-07 15:11:40 +01:00 committed by GitHub
parent 9f66ba5a56
commit c6c805c007
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 171 additions and 12 deletions

View file

@ -2,7 +2,10 @@ import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase' import UnitTestCase from '@/__tests__/UnitTestCase'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { screen } from '@testing-library/vue' import { screen, waitFor } from '@testing-library/vue'
import { songStore } from '@/stores'
import { playbackService } from '@/services'
import { MessageToasterStub } from '@/__tests__/stubs'
import PlaylistContextMenu from './PlaylistContextMenu.vue' import PlaylistContextMenu from './PlaylistContextMenu.vue'
new class extends UnitTestCase { new class extends UnitTestCase {
@ -42,5 +45,77 @@ new class extends UnitTestCase {
expect(emitMock).toHaveBeenCalledWith('PLAYLIST_DELETE', playlist) expect(emitMock).toHaveBeenCalledWith('PLAYLIST_DELETE', playlist)
}) })
it('plays', async () => {
const playlist = factory<Playlist>('playlist')
const songs = factory<Song>('song', 3)
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue(songs)
const queueMock = this.mock(playbackService, 'queueAndPlay')
const goMock = this.mock(this.router, 'go')
await this.renderComponent(playlist)
await this.user.click(screen.getByText('Play'))
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(playlist)
expect(queueMock).toHaveBeenCalledWith(songs)
expect(goMock).toHaveBeenCalledWith('queue')
})
})
it('warns if attempting to play an empty playlist', async () => {
const playlist = factory<Playlist>('playlist')
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue([])
const queueMock = this.mock(playbackService, 'queueAndPlay')
const goMock = this.mock(this.router, 'go')
const warnMock = this.mock(MessageToasterStub.value, 'warning')
await this.renderComponent(playlist)
await this.user.click(screen.getByText('Play'))
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(playlist)
expect(queueMock).not.toHaveBeenCalled()
expect(goMock).not.toHaveBeenCalled()
expect(warnMock).toHaveBeenCalledWith('The playlist is empty.')
})
})
it('shuffles', async () => {
const playlist = factory<Playlist>('playlist')
const songs = factory<Song>('song', 3)
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue(songs)
const queueMock = this.mock(playbackService, 'queueAndPlay')
const goMock = this.mock(this.router, 'go')
await this.renderComponent(playlist)
await this.user.click(screen.getByText('Shuffle'))
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(playlist)
expect(queueMock).toHaveBeenCalledWith(songs, true)
expect(goMock).toHaveBeenCalledWith('queue')
})
})
it('warns if attempting to shuffle an empty playlist', async () => {
const playlist = factory<Playlist>('playlist')
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue([])
const queueMock = this.mock(playbackService, 'queueAndPlay')
const goMock = this.mock(this.router, 'go')
const warnMock = this.mock(MessageToasterStub.value, 'warning')
await this.renderComponent(playlist)
await this.user.click(screen.getByText('Shuffle'))
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(playlist)
expect(queueMock).not.toHaveBeenCalled()
expect(goMock).not.toHaveBeenCalled()
expect(warnMock).toHaveBeenCalledWith('The playlist is empty.')
})
})
} }
} }

View file

@ -1,20 +1,50 @@
<template> <template>
<ContextMenuBase ref="base"> <ContextMenuBase ref="base">
<li @click="editPlaylist">Edit</li> <li @click="play">Play</li>
<li @click="deletePlaylist">Delete</li> <li @click="shuffle">Shuffle</li>
<li class="separator" />
<li @click="edit">Edit</li>
<li @click="destroy">Delete</li>
</ContextMenuBase> </ContextMenuBase>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
import { useContextMenu } from '@/composables' import { useContextMenu, useMessageToaster, useRouter } from '@/composables'
import { playbackService } from '@/services'
import { songStore } from '@/stores'
const { base, ContextMenuBase, open, trigger } = useContextMenu() const { base, ContextMenuBase, open, trigger } = useContextMenu()
const { go } = useRouter()
const { toastWarning } = useMessageToaster()
const playlist = ref<Playlist>() const playlist = ref<Playlist>()
const editPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist.value!)) const edit = () => trigger(() => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist.value!))
const deletePlaylist = () => trigger(() => eventBus.emit('PLAYLIST_DELETE', playlist.value!)) const destroy = () => trigger(() => eventBus.emit('PLAYLIST_DELETE', playlist.value!))
const play = () => trigger(async () => {
const songs = await songStore.fetchForPlaylist(playlist.value!)
if (songs.length) {
playbackService.queueAndPlay(songs)
go('queue')
} else {
toastWarning('The playlist is empty.')
}
})
const shuffle = () => trigger(async () => {
const songs = await songStore.fetchForPlaylist(playlist.value!)
if (songs.length) {
playbackService.queueAndPlay(songs, true)
go('queue')
} else {
toastWarning('The playlist is empty.')
}
})
eventBus.on('PLAYLIST_CONTEXT_MENU_REQUESTED', async (event, _playlist) => { eventBus.on('PLAYLIST_CONTEXT_MENU_REQUESTED', async (event, _playlist) => {
playlist.value = _playlist playlist.value = _playlist

View file

@ -5,6 +5,7 @@ import factory from '@/__tests__/factory'
import { screen, waitFor } from '@testing-library/vue' import { screen, waitFor } from '@testing-library/vue'
import { playlistStore, songStore } from '@/stores' import { playlistStore, songStore } from '@/stores'
import { playbackService } from '@/services' import { playbackService } from '@/services'
import { MessageToasterStub } from '@/__tests__/stubs'
import PlaylistFolderContextMenu from './PlaylistFolderContextMenu.vue' import PlaylistFolderContextMenu from './PlaylistFolderContextMenu.vue'
new class extends UnitTestCase { new class extends UnitTestCase {
@ -52,6 +53,26 @@ new class extends UnitTestCase {
}) })
}) })
it('warns if attempting to play with no songs in folder', async () => {
const folder = this.createPlayableFolder()
const fetchMock = this.mock(songStore, 'fetchForPlaylistFolder').mockResolvedValue([])
const queueMock = this.mock(playbackService, 'queueAndPlay')
const goMock = this.mock(this.router, 'go')
const warnMock = this.mock(MessageToasterStub.value, 'warning')
await this.renderComponent(folder)
await this.user.click(screen.getByText('Play All'))
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(folder)
expect(queueMock).not.toHaveBeenCalled()
expect(goMock).not.toHaveBeenCalled()
expect(warnMock).toHaveBeenCalledWith('No songs available.')
})
})
it('shuffles', async () => { it('shuffles', async () => {
const folder = this.createPlayableFolder() const folder = this.createPlayableFolder()
const songs = factory<Song>('song', 3) const songs = factory<Song>('song', 3)
@ -76,6 +97,26 @@ new class extends UnitTestCase {
expect(screen.queryByText('Shuffle All')).toBeNull() expect(screen.queryByText('Shuffle All')).toBeNull()
expect(screen.queryByText('Play All')).toBeNull() expect(screen.queryByText('Play All')).toBeNull()
}) })
it('warns if attempting to shuffle with no songs in folder', async () => {
const folder = this.createPlayableFolder()
const fetchMock = this.mock(songStore, 'fetchForPlaylistFolder').mockResolvedValue([])
const queueMock = this.mock(playbackService, 'queueAndPlay')
const goMock = this.mock(this.router, 'go')
const warnMock = this.mock(MessageToasterStub.value, 'warning')
await this.renderComponent(folder)
await this.user.click(screen.getByText('Shuffle All'))
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith(folder)
expect(queueMock).not.toHaveBeenCalled()
expect(goMock).not.toHaveBeenCalled()
expect(warnMock).toHaveBeenCalledWith('No songs available.')
})
})
} }
private createPlayableFolder () { private createPlayableFolder () {

View file

@ -20,10 +20,11 @@ import { computed, ref } from 'vue'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
import { playlistStore, songStore } from '@/stores' import { playlistStore, songStore } from '@/stores'
import { playbackService } from '@/services' import { playbackService } from '@/services'
import { useContextMenu, useRouter } from '@/composables' import { useContextMenu, useMessageToaster, useRouter } from '@/composables'
const { go } = useRouter()
const { base, ContextMenuBase, open, trigger } = useContextMenu() const { base, ContextMenuBase, open, trigger } = useContextMenu()
const { go } = useRouter()
const { toastWarning } = useMessageToaster()
const folder = ref<PlaylistFolder>() const folder = ref<PlaylistFolder>()
@ -31,13 +32,25 @@ const playlistsInFolder = computed(() => folder.value ? playlistStore.byFolder(f
const playable = computed(() => playlistsInFolder.value.length > 0) const playable = computed(() => playlistsInFolder.value.length > 0)
const play = () => trigger(async () => { const play = () => trigger(async () => {
playbackService.queueAndPlay(await songStore.fetchForPlaylistFolder(folder.value!)) const songs = await songStore.fetchForPlaylistFolder(folder.value!)
go('queue')
if (songs.length) {
playbackService.queueAndPlay(songs)
go('queue')
} else {
toastWarning('No songs available.')
}
}) })
const shuffle = () => trigger(async () => { const shuffle = () => trigger(async () => {
playbackService.queueAndPlay(await songStore.fetchForPlaylistFolder(folder.value!), true) const songs = await songStore.fetchForPlaylistFolder(folder.value!)
go('queue')
if (songs.length) {
playbackService.queueAndPlay(songs, true)
go('queue')
} else {
toastWarning('No songs available.')
}
}) })
const createPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_CREATE_PLAYLIST_FORM', folder.value!)) const createPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_CREATE_PLAYLIST_FORM', folder.value!))