feat: support creating playlist directly from songs (#1617)

This commit is contained in:
Phan An 2022-12-06 11:28:48 +01:00 committed by GitHub
parent 0b486e699b
commit baa2e45a5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 225 additions and 204 deletions

View file

@ -40,8 +40,12 @@ const close = () => {
eventBus.on('MODAL_SHOW_ABOUT_KOEL', () => (activeModalName.value = 'about-koel')) eventBus.on('MODAL_SHOW_ABOUT_KOEL', () => (activeModalName.value = 'about-koel'))
.on('MODAL_SHOW_ADD_USER_FORM', () => (activeModalName.value = 'add-user-form')) .on('MODAL_SHOW_ADD_USER_FORM', () => (activeModalName.value = 'add-user-form'))
.on('MODAL_SHOW_CREATE_PLAYLIST_FORM', folder => { .on('MODAL_SHOW_CREATE_PLAYLIST_FORM', (folder, songs) => {
context.value = { folder } context.value = {
folder,
songs: songs ? arrayify(songs) : []
}
activeModalName.value = 'create-playlist-form' activeModalName.value = 'create-playlist-form'
}) })
.on('MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM', folder => { .on('MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM', folder => {

View file

@ -1,10 +1,10 @@
<template> <template>
<ContextMenuBase ref="base"> <ContextMenuBase ref="base">
<li data-testid="playlist-context-menu-create-simple" @click="onItemClicked('new-playlist')">New Playlist</li> <li data-testid="playlist-context-menu-create-simple" @click="onItemClicked('new-playlist')">New Playlist</li>
<li data-testid="playlist-context-menu-create-smart" @click="onItemClicked('new-smart-playlist')"> <li data-testid="playlist-context-menu-create-smart" @click="onItemClicked('new-smart-playlist')">
New Smart Playlist New Smart Playlist
</li> </li>
<li data-testid="playlist-context-menu-create-folder" @click="onItemClicked('new-folder')">New Folder</li> <li data-testid="playlist-context-menu-create-folder" @click="onItemClicked('new-folder')">New Folder</li>
</ContextMenuBase> </ContextMenuBase>
</template> </template>

View file

@ -9,9 +9,10 @@ import CreatePlaylistForm from './CreatePlaylistForm.vue'
new class extends UnitTestCase { new class extends UnitTestCase {
protected test () { protected test () {
it('submits', async () => { it('creates playlist with no songs', async () => {
const folder = factory<PlaylistFolder>('playlist-folder') const folder = factory<PlaylistFolder>('playlist-folder')
const storeMock = this.mock(playlistStore, 'store').mockResolvedValue(factory<Playlist>('playlist')) const storeMock = this.mock(playlistStore, 'store').mockResolvedValue(factory<Playlist>('playlist'))
this.render(CreatePlaylistForm, { this.render(CreatePlaylistForm, {
global: { global: {
provide: { provide: {
@ -20,12 +21,37 @@ new class extends UnitTestCase {
} }
}) })
expect(screen.queryByTestId('from-songs')).toBeNull()
await this.type(screen.getByPlaceholderText('Playlist name'), 'My playlist') await this.type(screen.getByPlaceholderText('Playlist name'), 'My playlist')
await this.user.click(screen.getByRole('button', { name: 'Save' })) await this.user.click(screen.getByRole('button', { name: 'Save' }))
expect(storeMock).toHaveBeenCalledWith('My playlist', { expect(storeMock).toHaveBeenCalledWith('My playlist', {
folder_id: folder.id folder_id: folder.id
}, [])
}) })
it('creates playlist with songs', async () => {
const songs = factory<Song>('song', 3)
const folder = factory<PlaylistFolder>('playlist-folder')
const storeMock = this.mock(playlistStore, 'store').mockResolvedValue(factory<Playlist>('playlist'))
this.render(CreatePlaylistForm, {
global: {
provide: {
[<symbol>ModalContextKey]: [ref({ folder, songs })]
}
}
})
screen.getByText('from 3 songs')
await this.type(screen.getByPlaceholderText('Playlist name'), 'My playlist')
await this.user.click(screen.getByRole('button', { name: 'Save' }))
expect(storeMock).toHaveBeenCalledWith('My playlist', {
folder_id: folder.id
}, songs)
}) })
} }
} }

View file

@ -1,7 +1,16 @@
<template> <template>
<form @submit.prevent="submit" @keydown.esc="maybeClose"> <form @submit.prevent="submit" @keydown.esc="maybeClose">
<header> <header>
<h1>New Playlist</h1> <h1>
New Playlist
<span
v-if="songs.length"
data-testid="from-songs"
class="text-secondary"
>
from {{ pluralize(songs, 'song') }}
</span>
</h1>
</header> </header>
<main> <main>
@ -37,7 +46,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, toRef } from 'vue' import { ref, toRef } from 'vue'
import { playlistFolderStore, playlistStore } from '@/stores' import { playlistFolderStore, playlistStore } from '@/stores'
import { logger } from '@/utils' import { logger, pluralize } from '@/utils'
import { useDialogBox, useMessageToaster, useModal, useOverlay, useRouter } from '@/composables' import { useDialogBox, useMessageToaster, useModal, useOverlay, useRouter } from '@/composables'
import Btn from '@/components/ui/Btn.vue' import Btn from '@/components/ui/Btn.vue'
@ -46,7 +55,10 @@ const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster() const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox() const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { go } = useRouter() const { go } = useRouter()
const targetFolder = useModal().getFromContext<PlaylistFolder | null>('folder') const { getFromContext } = useModal()
const targetFolder = getFromContext<PlaylistFolder | null>('folder') ?? null
const songs = getFromContext<Song[]>('songs') ?? []
const folderId = ref(targetFolder?.id) const folderId = ref(targetFolder?.id)
const name = ref('') const name = ref('')
@ -61,7 +73,7 @@ const submit = async () => {
try { try {
const playlist = await playlistStore.store(name.value, { const playlist = await playlistStore.store(name.value, {
folder_id: folderId.value folder_id: folderId.value
}) }, songs)
close() close()
toastSuccess(`Playlist "${playlist.name}" created.`) toastSuccess(`Playlist "${playlist.name}" created.`)

View file

@ -18,7 +18,7 @@ new class extends UnitTestCase {
await this.renderComponent(playlist) await this.renderComponent(playlist)
const emitMock = this.mock(eventBus, 'emit') const emitMock = this.mock(eventBus, 'emit')
await this.user.click(screen.getByText('Edit')) await this.user.click(screen.getByText('Edit'))
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist) expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist)
}) })
@ -28,7 +28,7 @@ new class extends UnitTestCase {
await this.renderComponent(playlist) await this.renderComponent(playlist)
const emitMock = this.mock(eventBus, 'emit') const emitMock = this.mock(eventBus, 'emit')
await this.user.click(screen.getByText('Edit')) await this.user.click(screen.getByText('Edit'))
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist) expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist)
}) })

View file

@ -1,6 +1,6 @@
<template> <template>
<ContextMenuBase ref="base"> <ContextMenuBase ref="base">
<li @click="editPlaylist">Edit</li> <li @click="editPlaylist">Edit</li>
<li @click="deletePlaylist">Delete</li> <li @click="deletePlaylist">Delete</li>
</ContextMenuBase> </ContextMenuBase>
</template> </template>

View file

@ -6,8 +6,8 @@
<li @click="shuffle">Shuffle All</li> <li @click="shuffle">Shuffle All</li>
<li class="separator" /> <li class="separator" />
</template> </template>
<li @click="createPlaylist">Create Playlist</li> <li @click="createPlaylist">New Playlist</li>
<li @click="createSmartPlaylist">Create Smart Playlist</li> <li @click="createSmartPlaylist">New Smart Playlist</li>
<li class="separator" /> <li class="separator" />
<li @click="rename">Rename</li> <li @click="rename">Rename</li>
<li @click="destroy">Delete</li> <li @click="destroy">Delete</li>

View file

@ -16,7 +16,6 @@
<template #controls> <template #controls>
<SongListControls <SongListControls
v-if="songs.length && (!isPhone || showingControls)" v-if="songs.length && (!isPhone || showingControls)"
:config="controlConfig"
@clear-queue="clearQueue" @clear-queue="clearQueue"
@play-all="playAll" @play-all="playAll"
@play-selected="playSelected" @play-selected="playSelected"
@ -63,8 +62,6 @@ import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
const { go } = useRouter() const { go } = useRouter()
const { showErrorDialog } = useDialogBox() const { showErrorDialog } = useDialogBox()
const controlConfig: Partial<SongListControlsConfig> = { clearQueue: true }
const { const {
SongList, SongList,
SongListControls, SongListControls,

View file

@ -22,11 +22,7 @@ exports[`renders 1`] = `
<li data-testid="queue" tabindex="0" data-v-42061e3e="">Queue</li> <li data-testid="queue" tabindex="0" data-v-42061e3e="">Queue</li>
<li class="favorites" data-testid="add-to-favorites" tabindex="0" data-v-42061e3e=""> Favorites </li> <li class="favorites" data-testid="add-to-favorites" tabindex="0" data-v-42061e3e=""> Favorites </li>
</ul> </ul>
</section> </section><button type="button" transparent="" data-v-e368fe26="" data-v-42061e3e="">New Playlist…</button>
<section class="new-playlist" data-testid="new-playlist" data-v-42061e3e="">
<p data-v-42061e3e="">or create a new playlist</p>
<form class="form-save form-simple form-new-playlist" data-v-42061e3e=""><input data-testid="new-playlist-name" placeholder="Playlist name" required="" type="text" data-v-42061e3e=""><button type="submit" title="Save" data-v-e368fe26="" data-v-42061e3e="">⏎</button></form>
</section>
</div> </div>
</div> </div>
</div> </div>

View file

@ -4,7 +4,7 @@ import { expect, it } from 'vitest'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { favoriteStore, playlistStore, queueStore } from '@/stores' import { favoriteStore, playlistStore, queueStore } from '@/stores'
import UnitTestCase from '@/__tests__/UnitTestCase' import UnitTestCase from '@/__tests__/UnitTestCase'
import { arrayify } from '@/utils' import { arrayify, eventBus } from '@/utils'
import Btn from '@/components/ui/Btn.vue' import Btn from '@/components/ui/Btn.vue'
import AddToMenu from './AddToMenu.vue' import AddToMenu from './AddToMenu.vue'
@ -12,9 +12,7 @@ let songs: Song[]
const config: AddToMenuConfig = { const config: AddToMenuConfig = {
queue: true, queue: true,
favorites: true, favorites: true
playlists: true,
newPlaylist: true
} }
new class extends UnitTestCase { new class extends UnitTestCase {
@ -49,8 +47,6 @@ new class extends UnitTestCase {
it.each<[keyof AddToMenuConfig, string | string[]]>([ it.each<[keyof AddToMenuConfig, string | string[]]>([
['queue', ['queue-after-current', 'queue-bottom', 'queue-top', 'queue']], ['queue', ['queue-after-current', 'queue-bottom', 'queue-top', 'queue']],
['favorites', 'add-to-favorites'], ['favorites', 'add-to-favorites'],
['playlists', 'add-to-playlist'],
['newPlaylist', 'new-playlist']
])('renders disabling %s config', (configKey: keyof AddToMenuConfig, testIds: string | string[]) => { ])('renders disabling %s config', (configKey: keyof AddToMenuConfig, testIds: string | string[]) => {
this.renderComponent({ [configKey]: false }) this.renderComponent({ [configKey]: false })
arrayify(testIds).forEach(id => expect(screen.queryByTestId(id)).toBeNull()) arrayify(testIds).forEach(id => expect(screen.queryByTestId(id)).toBeNull())
@ -90,5 +86,14 @@ new class extends UnitTestCase {
expect(mock).toHaveBeenCalledWith(playlistStore.state.playlists[1], songs) expect(mock).toHaveBeenCalledWith(playlistStore.state.playlists[1], songs)
}) })
it('creates playlist from selected songs', async () => {
const emitMock = this.mock(eventBus, 'emit')
this.renderComponent()
await this.user.click(screen.getByText('New Playlist…'))
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_CREATE_PLAYLIST_FORM', null, songs)
})
} }
} }

View file

@ -33,7 +33,6 @@
Favorites Favorites
</li> </li>
<template v-if="config.playlists">
<li <li
v-for="playlist in playlists" v-for="playlist in playlists"
:key="playlist.id" :key="playlist.id"
@ -44,43 +43,24 @@
> >
{{ playlist.name }} {{ playlist.name }}
</li> </li>
</template>
</ul> </ul>
</section> </section>
<section v-if="config.newPlaylist" class="new-playlist" data-testid="new-playlist"> <Btn transparent @click.prevent="addSongsToNewPlaylist">New Playlist</Btn>
<p>or create a new playlist</p>
<form class="form-save form-simple form-new-playlist" @submit.prevent="createNewPlaylistFromSongs">
<input
v-model="newPlaylistName"
data-testid="new-playlist-name"
placeholder="Playlist name"
required
type="text"
@keyup.esc.prevent="close"
>
<Btn title="Save" type="submit"></Btn>
</form>
</section>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, ref, toRef, toRefs, watch } from 'vue' import { computed, ref, toRef, toRefs, watch } from 'vue'
import { pluralize } from '@/utils' import { pluralize } from '@/utils'
import { playlistStore, queueStore } from '@/stores' import { playlistStore, queueStore } from '@/stores'
import { useMessageToaster, useRouter, useSongMenuMethods } from '@/composables' import { useSongMenuMethods } from '@/composables'
import Btn from '@/components/ui/Btn.vue' import Btn from '@/components/ui/Btn.vue'
const { toastSuccess } = useMessageToaster()
const { go } = useRouter()
const props = defineProps<{ songs: Song[], config: AddToMenuConfig }>() const props = defineProps<{ songs: Song[], config: AddToMenuConfig }>()
const { songs, config } = toRefs(props) const { songs, config } = toRefs(props)
const newPlaylistName = ref('')
const queue = toRef(queueStore.state, 'songs') const queue = toRef(queueStore.state, 'songs')
const currentSong = queueStore.current const currentSong = queueStore.current
@ -95,39 +75,18 @@ const {
queueSongsToBottom, queueSongsToBottom,
queueSongsToTop, queueSongsToTop,
addSongsToFavorite, addSongsToFavorite,
addSongsToExistingPlaylist addSongsToExistingPlaylist,
addSongsToNewPlaylist
} = useSongMenuMethods(songs, close) } = useSongMenuMethods(songs, close)
watch(songs, () => songs.value.length || close()) watch(songs, () => songs.value.length || close())
/**
* Save the selected songs as a playlist.
* As of current we don't have selective save.
*/
const createNewPlaylistFromSongs = async () => {
newPlaylistName.value = newPlaylistName.value.trim()
if (!newPlaylistName.value) {
return
}
const playlist = await playlistStore.store(newPlaylistName.value, {}, songs.value)
newPlaylistName.value = ''
toastSuccess(`Playlist "${playlist.name}" created.`)
// Activate the new playlist right away
await nextTick()
go(`playlist/${playlist.id}`)
close()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.add-to { .add-to {
width: 100%; width: 100%;
max-width: 225px; max-width: 256px;
min-width: 196px;
padding: .75rem; padding: .75rem;
> * + * { > * + * {
@ -170,29 +129,9 @@ const createNewPlaylistFromSongs = async () => {
} }
} }
form { button {
width: 100%; width: 100%;
display: flex; border: 1px solid rgba(255, 255, 255, .2);
justify-content: center;
align-items: center;
border-radius: 3px;
overflow: hidden;
input[type="text"] {
width: 100%;
height: 28px;
border-radius: 0;
}
button[type="submit"] {
margin-top: 0;
border-radius: 0;
height: 28px;
line-height: 28px;
padding-top: 0;
padding-bottom: 0;
margin-left: -2px !important;
}
} }
} }
</style> </style>

View file

@ -251,14 +251,14 @@ new class extends UnitTestCase {
// mock after render to ensure that the component is mounted properly // mock after render to ensure that the component is mounted properly
const emitMock = this.mock(eventBus, 'emit') const emitMock = this.mock(eventBus, 'emit')
await this.user.click(screen.getByText('Edit')) await this.user.click(screen.getByText('Edit'))
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_EDIT_SONG_FORM', songs) expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_EDIT_SONG_FORM', songs)
}) })
it('does not allow edit songs if current user is not admin', async () => { it('does not allow edit songs if current user is not admin', async () => {
await this.actingAs().renderComponent() await this.actingAs().renderComponent()
expect(screen.queryByText('Edit')).toBeNull() expect(screen.queryByText('Edit')).toBeNull()
}) })
it('has an option to copy shareable URL', async () => { it('has an option to copy shareable URL', async () => {
@ -288,5 +288,16 @@ new class extends UnitTestCase {
await this.actingAs().renderComponent() await this.actingAs().renderComponent()
expect(screen.queryByText('Delete from Filesystem')).toBeNull() expect(screen.queryByText('Delete from Filesystem')).toBeNull()
}) })
it('creates playlist from selected songs', async () => {
await this.actingAs().renderComponent()
// mock after render to ensure that the component is mounted properly
const emitMock = this.mock(eventBus, 'emit')
await this.user.click(screen.getByText('New Playlist…'))
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_CREATE_PLAYLIST_FORM', null, songs)
})
} }
} }

View file

@ -22,8 +22,12 @@
<li @click="addSongsToFavorite">Favorites</li> <li @click="addSongsToFavorite">Favorites</li>
</template> </template>
<li v-if="normalPlaylists.length" class="separator" /> <li v-if="normalPlaylists.length" class="separator" />
<ul v-if="normalPlaylists.length" class="normal-playlists">
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li> <li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
</ul> </ul>
<li class="separator" />
<li @click="addSongsToNewPlaylist">New Playlist</li>
</ul>
</li> </li>
<template v-if="isQueueScreen"> <template v-if="isQueueScreen">
@ -38,7 +42,7 @@
<li class="separator" /> <li class="separator" />
</template> </template>
<li v-if="isAdmin" @click="openEditForm">Edit</li> <li v-if="isAdmin" @click="openEditForm">Edit</li>
<li v-if="allowDownload" @click="download">Download</li> <li v-if="allowDownload" @click="download">Download</li>
<li v-if="onlyOneSongSelected" @click="copyUrl">Copy Shareable URL</li> <li v-if="onlyOneSongSelected" @click="copyUrl">Copy Shareable URL</li>
@ -83,12 +87,12 @@ const {
queueSongsToBottom, queueSongsToBottom,
queueSongsToTop, queueSongsToTop,
addSongsToFavorite, addSongsToFavorite,
addSongsToExistingPlaylist addSongsToExistingPlaylist,
addSongsToNewPlaylist
} = useSongMenuMethods(songs, close) } = useSongMenuMethods(songs, close)
const playlists = toRef(playlistStore.state, 'playlists') const playlists = toRef(playlistStore.state, 'playlists')
const allowDownload = toRef(commonStore.state, 'allow_download') const allowDownload = toRef(commonStore.state, 'allow_download')
const user = toRef(userStore.state, 'current')
const queue = toRef(queueStore.state, 'songs') const queue = toRef(queueStore.state, 'songs')
const currentSong = toRef(queueStore, 'current') const currentSong = toRef(queueStore, 'current')
@ -157,3 +161,10 @@ eventBus.on('SONG_CONTEXT_MENU_REQUESTED', async (e, _songs) => {
await open(e.pageY, e.pageX) await open(e.pageY, e.pageX)
}) })
</script> </script>
<style lang="scss" scoped>
ul.normal-playlists {
max-height: 256px;
overflow-y: auto;
}
</style>

View file

@ -8,13 +8,15 @@ import { screen } from '@testing-library/vue'
import SongListControls from './SongListControls.vue' import SongListControls from './SongListControls.vue'
new class extends UnitTestCase { new class extends UnitTestCase {
private renderComponent (selectedSongCount = 1, config: Partial<SongListControlsConfig> = {}) { private renderComponent (selectedSongCount = 1, screen: ScreenName = 'Songs') {
const songs = factory<Song>('song', 5) const songs = factory<Song>('song', 5)
this.router.activateRoute({
screen,
path: '_',
})
return this.render(SongListControls, { return this.render(SongListControls, {
props: {
config
},
global: { global: {
provide: { provide: {
[<symbol>SongsKey]: [ref(songs)], [<symbol>SongsKey]: [ref(songs)],
@ -62,7 +64,7 @@ new class extends UnitTestCase {
}) })
it('clears queue', async () => { it('clears queue', async () => {
const { emitted } = this.renderComponent(0, { clearQueue: true }) const { emitted } = this.renderComponent(0, 'Queue')
await this.user.click(screen.getByTitle('Clear current queue')) await this.user.click(screen.getByTitle('Clear current queue'))
@ -70,7 +72,7 @@ new class extends UnitTestCase {
}) })
it('deletes current playlist', async () => { it('deletes current playlist', async () => {
const { emitted } = this.renderComponent(0, { deletePlaylist: true }) const { emitted } = this.renderComponent(0, 'Playlist')
await this.user.click(screen.getByTitle('Delete this playlist')) await this.user.click(screen.getByTitle('Delete this playlist'))

View file

@ -2,7 +2,6 @@
<div ref="el" class="song-list-controls" data-testid="song-list-controls"> <div ref="el" class="song-list-controls" data-testid="song-list-controls">
<div class="wrapper"> <div class="wrapper">
<BtnGroup uppercased> <BtnGroup uppercased>
<template v-if="mergedConfig.play">
<template v-if="altPressed"> <template v-if="altPressed">
<Btn <Btn
v-if="selectedSongs.length < 2 && songs.length" v-if="selectedSongs.length < 2 && songs.length"
@ -52,13 +51,16 @@
Selected Selected
</Btn> </Btn>
</template> </template>
</template>
<Btn v-if="showAddToButton" ref="addToButton" green @click.prevent.stop="toggleAddToMenu"> <Btn
v-if="showAddToButton"
ref="addToButton"
green @click.prevent.stop="toggleAddToMenu"
>
{{ showingAddToMenu ? 'Cancel' : 'Add To…' }} {{ showingAddToMenu ? 'Cancel' : 'Add To…' }}
</Btn> </Btn>
<Btn v-if="showClearQueueButton" red title="Clear current queue" @click.prevent="clearQueue">Clear</Btn> <Btn v-if="config.clearQueue" red title="Clear current queue" @click.prevent="clearQueue">Clear</Btn>
</BtnGroup> </BtnGroup>
<BtnGroup> <BtnGroup>
@ -67,7 +69,7 @@
</Btn> </Btn>
<Btn <Btn
v-if="showDeletePlaylistButton" v-if="config.deletePlaylist"
v-koel-tooltip v-koel-tooltip
class="del btn-delete-playlist" class="del btn-delete-playlist"
red red
@ -80,7 +82,7 @@
</div> </div>
<div ref="addToMenu" v-koel-clickaway="closeAddToMenu" class="menu-wrapper"> <div ref="addToMenu" v-koel-clickaway="closeAddToMenu" class="menu-wrapper">
<AddToMenu :config="mergedConfig.addTo" :songs="selectedSongs" @closing="closeAddToMenu" /> <AddToMenu :config="config.addTo" :songs="selectedSongs" @closing="closeAddToMenu" />
</div> </div>
</div> </div>
</template> </template>
@ -90,14 +92,13 @@ import { faPlay, faRandom, faRotateRight, faTrashCan } from '@fortawesome/free-s
import { computed, nextTick, onBeforeUnmount, onMounted, Ref, ref, toRefs, watch } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, Ref, ref, toRefs, watch } from 'vue'
import { SelectedSongsKey, SongsKey } from '@/symbols' import { SelectedSongsKey, SongsKey } from '@/symbols'
import { requireInjection } from '@/utils' import { requireInjection } from '@/utils'
import { useFloatingUi } from '@/composables' import { useFloatingUi, useSongListControls } from '@/composables'
import AddToMenu from '@/components/song/AddToMenu.vue' import AddToMenu from '@/components/song/AddToMenu.vue'
import Btn from '@/components/ui/Btn.vue' import Btn from '@/components/ui/Btn.vue'
import BtnGroup from '@/components/ui/BtnGroup.vue' import BtnGroup from '@/components/ui/BtnGroup.vue'
const props = withDefaults(defineProps<{ config?: Partial<SongListControlsConfig> }>(), { config: () => ({}) }) const config = useSongListControls().getSongListControlsConfig()
const { config } = toRefs(props)
const [songs] = requireInjection<[Ref<Song[]>]>(SongsKey) const [songs] = requireInjection<[Ref<Song[]>]>(SongsKey)
const [selectedSongs] = requireInjection(SelectedSongsKey) const [selectedSongs] = requireInjection(SelectedSongsKey)
@ -108,23 +109,7 @@ const addToMenu = ref<HTMLDivElement>()
const showingAddToMenu = ref(false) const showingAddToMenu = ref(false)
const altPressed = ref(false) const altPressed = ref(false)
const mergedConfig = computed((): SongListControlsConfig => Object.assign({
play: true,
addTo: {
queue: true,
favorites: true,
playlists: true,
newPlaylist: true
},
clearQueue: false,
deletePlaylist: false,
refresh: false
}, config.value)
)
const showAddToButton = computed(() => Boolean(selectedSongs.value.length)) const showAddToButton = computed(() => Boolean(selectedSongs.value.length))
const showClearQueueButton = computed(() => mergedConfig.value.clearQueue)
const showDeletePlaylistButton = computed(() => mergedConfig.value.deletePlaylist)
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'playAll' | 'playSelected', shuffle: boolean): void, (e: 'playAll' | 'playSelected', shuffle: boolean): void,

View file

@ -11,10 +11,6 @@ exports[`renders 1`] = `
<li class="playlist" data-testid="add-to-playlist" tabindex="0" data-v-42061e3e="">Bar</li> <li class="playlist" data-testid="add-to-playlist" tabindex="0" data-v-42061e3e="">Bar</li>
<li class="playlist" data-testid="add-to-playlist" tabindex="0" data-v-42061e3e="">Baz</li> <li class="playlist" data-testid="add-to-playlist" tabindex="0" data-v-42061e3e="">Baz</li>
</ul> </ul>
</section> </section><button type="button" transparent="" data-v-e368fe26="" data-v-42061e3e="">New Playlist…</button>
<section class="new-playlist" data-testid="new-playlist" data-v-42061e3e="">
<p data-v-42061e3e="">or create a new playlist</p>
<form class="form-save form-simple form-new-playlist" data-v-42061e3e=""><input data-testid="new-playlist-name" placeholder="Playlist name" required="" type="text" data-v-42061e3e=""><button type="submit" title="Save" data-v-e368fe26="" data-v-42061e3e="">⏎</button></form>
</section>
</div> </div>
`; `;

View file

@ -13,6 +13,7 @@ export * from './usePlaylistManagement'
export * from './useRouter' export * from './useRouter'
export * from './useSmartPlaylistForm' export * from './useSmartPlaylistForm'
export * from './useSongList' export * from './useSongList'
export * from './useSongListControls'
export * from './useSongMenuMethods' export * from './useSongMenuMethods'
export * from './useThirdPartyServices' export * from './useThirdPartyServices'
export * from './useUpload' export * from './useUpload'

View file

@ -0,0 +1,31 @@
import { useRouter } from '@/composables'
export const useSongListControls = () => {
const { isCurrentScreen } = useRouter()
const getSongListControlsConfig = () => {
const config: SongListControlsConfig = {
play: true,
addTo: {
queue: true,
favorites: true,
},
clearQueue: true,
deletePlaylist: true,
refresh: true,
}
config.clearQueue = isCurrentScreen('Queue')
config.addTo.queue = !isCurrentScreen('Queue')
config.addTo.favorites = !isCurrentScreen('Favorites')
config.deletePlaylist = isCurrentScreen('Playlist')
config.refresh = isCurrentScreen('Playlist')
return config
}
return {
getSongListControlsConfig
}
}

View file

@ -1,6 +1,7 @@
import { Ref } from 'vue' import { Ref } from 'vue'
import { favoriteStore, queueStore } from '@/stores' import { favoriteStore, queueStore } from '@/stores'
import { usePlaylistManagement } from '@/composables' import { usePlaylistManagement } from '@/composables'
import { eventBus } from '@/utils'
export const useSongMenuMethods = (songs: Ref<Song[]>, close: Closure) => { export const useSongMenuMethods = (songs: Ref<Song[]>, close: Closure) => {
const { addSongsToPlaylist } = usePlaylistManagement() const { addSongsToPlaylist } = usePlaylistManagement()
@ -30,11 +31,17 @@ export const useSongMenuMethods = (songs: Ref<Song[]>, close: Closure) => {
await addSongsToPlaylist(playlist, songs.value) await addSongsToPlaylist(playlist, songs.value)
} }
const addSongsToNewPlaylist = () => {
close()
eventBus.emit('MODAL_SHOW_CREATE_PLAYLIST_FORM', null, songs.value)
}
return { return {
queueSongsAfterCurrent, queueSongsAfterCurrent,
queueSongsToBottom, queueSongsToBottom,
queueSongsToTop, queueSongsToTop,
addSongsToFavorite, addSongsToFavorite,
addSongsToExistingPlaylist addSongsToExistingPlaylist,
addSongsToNewPlaylist
} }
} }

View file

@ -18,9 +18,9 @@ export interface Events {
MODAL_SHOW_ADD_USER_FORM: () => void MODAL_SHOW_ADD_USER_FORM: () => void
MODAL_SHOW_EDIT_USER_FORM: (user: User) => void MODAL_SHOW_EDIT_USER_FORM: (user: User) => void
MODAL_SHOW_EDIT_SONG_FORM: (songs: Song | Song[], initialTab?: EditSongFormTabName) => void MODAL_SHOW_EDIT_SONG_FORM: (songs: Song | Song[], initialTab?: EditSongFormTabName) => void
MODAL_SHOW_CREATE_PLAYLIST_FORM: (folder: PlaylistFolder | null) => void MODAL_SHOW_CREATE_PLAYLIST_FORM: (folder?: PlaylistFolder | null, songs?: Song | Song[]) => void
MODAL_SHOW_EDIT_PLAYLIST_FORM: (playlist: Playlist) => void MODAL_SHOW_EDIT_PLAYLIST_FORM: (playlist: Playlist) => void
MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM: (folder: PlaylistFolder | null) => void MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM: (folder?: PlaylistFolder | null) => void
MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM: () => void MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM: () => void
MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM: (playlistFolder: PlaylistFolder) => void MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM: (playlistFolder: PlaylistFolder) => void
MODAL_SHOW_ABOUT_KOEL: () => void MODAL_SHOW_ABOUT_KOEL: () => void

View file

@ -117,7 +117,7 @@ export const playlistStore = {
}) })
playlist.is_smart && cache.remove(['playlist.songs', playlist.id]) playlist.is_smart && cache.remove(['playlist.songs', playlist.id])
Object.assign(this.byId(playlist.id), data) Object.assign(this.byId(playlist.id)!, data)
}, },
createEmptySmartPlaylistRule: (): SmartPlaylistRule => ({ createEmptySmartPlaylistRule: (): SmartPlaylistRule => ({

View file

@ -313,8 +313,6 @@ declare type ArtistAlbumCardLayout = 'full' | 'compact'
interface AddToMenuConfig { interface AddToMenuConfig {
queue: boolean queue: boolean
favorites: boolean favorites: boolean
playlists: boolean
newPlaylist: boolean
} }
interface SongListControlsConfig { interface SongListControlsConfig {