mirror of
https://github.com/koel/koel
synced 2024-11-24 13:13:05 +00:00
feat: support creating playlist directly from songs (#1617)
This commit is contained in:
parent
0b486e699b
commit
baa2e45a5d
22 changed files with 225 additions and 204 deletions
|
@ -40,8 +40,12 @@ const close = () => {
|
|||
|
||||
eventBus.on('MODAL_SHOW_ABOUT_KOEL', () => (activeModalName.value = 'about-koel'))
|
||||
.on('MODAL_SHOW_ADD_USER_FORM', () => (activeModalName.value = 'add-user-form'))
|
||||
.on('MODAL_SHOW_CREATE_PLAYLIST_FORM', folder => {
|
||||
context.value = { folder }
|
||||
.on('MODAL_SHOW_CREATE_PLAYLIST_FORM', (folder, songs) => {
|
||||
context.value = {
|
||||
folder,
|
||||
songs: songs ? arrayify(songs) : []
|
||||
}
|
||||
|
||||
activeModalName.value = 'create-playlist-form'
|
||||
})
|
||||
.on('MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM', folder => {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<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')">
|
||||
New Smart Playlist
|
||||
New Smart Playlist…
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -9,9 +9,10 @@ import CreatePlaylistForm from './CreatePlaylistForm.vue'
|
|||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it('submits', async () => {
|
||||
it('creates playlist with no songs', async () => {
|
||||
const folder = factory<PlaylistFolder>('playlist-folder')
|
||||
const storeMock = this.mock(playlistStore, 'store').mockResolvedValue(factory<Playlist>('playlist'))
|
||||
|
||||
this.render(CreatePlaylistForm, {
|
||||
global: {
|
||||
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.user.click(screen.getByRole('button', { name: 'Save' }))
|
||||
|
||||
expect(storeMock).toHaveBeenCalledWith('My playlist', {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
<template>
|
||||
<form @submit.prevent="submit" @keydown.esc="maybeClose">
|
||||
<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>
|
||||
|
||||
<main>
|
||||
|
@ -37,7 +46,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, toRef } from 'vue'
|
||||
import { playlistFolderStore, playlistStore } from '@/stores'
|
||||
import { logger } from '@/utils'
|
||||
import { logger, pluralize } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster, useModal, useOverlay, useRouter } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
@ -46,7 +55,10 @@ const { showOverlay, hideOverlay } = useOverlay()
|
|||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
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 name = ref('')
|
||||
|
@ -61,7 +73,7 @@ const submit = async () => {
|
|||
try {
|
||||
const playlist = await playlistStore.store(name.value, {
|
||||
folder_id: folderId.value
|
||||
})
|
||||
}, songs)
|
||||
|
||||
close()
|
||||
toastSuccess(`Playlist "${playlist.name}" created.`)
|
||||
|
|
|
@ -18,7 +18,7 @@ new class extends UnitTestCase {
|
|||
await this.renderComponent(playlist)
|
||||
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)
|
||||
})
|
||||
|
@ -28,7 +28,7 @@ new class extends UnitTestCase {
|
|||
await this.renderComponent(playlist)
|
||||
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)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<ContextMenuBase ref="base">
|
||||
<li @click="editPlaylist">Edit</li>
|
||||
<li @click="editPlaylist">Edit…</li>
|
||||
<li @click="deletePlaylist">Delete</li>
|
||||
</ContextMenuBase>
|
||||
</template>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<li @click="shuffle">Shuffle All</li>
|
||||
<li class="separator" />
|
||||
</template>
|
||||
<li @click="createPlaylist">Create Playlist</li>
|
||||
<li @click="createSmartPlaylist">Create Smart Playlist</li>
|
||||
<li @click="createPlaylist">New Playlist…</li>
|
||||
<li @click="createSmartPlaylist">New Smart Playlist…</li>
|
||||
<li class="separator" />
|
||||
<li @click="rename">Rename</li>
|
||||
<li @click="destroy">Delete</li>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
:config="controlConfig"
|
||||
@clear-queue="clearQueue"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
|
@ -63,8 +62,6 @@ import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
|||
const { go } = useRouter()
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
|
||||
const controlConfig: Partial<SongListControlsConfig> = { clearQueue: true }
|
||||
|
||||
const {
|
||||
SongList,
|
||||
SongListControls,
|
||||
|
|
|
@ -22,11 +22,7 @@ exports[`renders 1`] = `
|
|||
<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>
|
||||
</ul>
|
||||
</section>
|
||||
<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>
|
||||
</section><button type="button" transparent="" data-v-e368fe26="" data-v-42061e3e="">New Playlist…</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { expect, it } from 'vitest'
|
|||
import factory from '@/__tests__/factory'
|
||||
import { favoriteStore, playlistStore, queueStore } from '@/stores'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { arrayify } from '@/utils'
|
||||
import { arrayify, eventBus } from '@/utils'
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
import AddToMenu from './AddToMenu.vue'
|
||||
|
||||
|
@ -12,9 +12,7 @@ let songs: Song[]
|
|||
|
||||
const config: AddToMenuConfig = {
|
||||
queue: true,
|
||||
favorites: true,
|
||||
playlists: true,
|
||||
newPlaylist: true
|
||||
favorites: true
|
||||
}
|
||||
|
||||
new class extends UnitTestCase {
|
||||
|
@ -49,8 +47,6 @@ new class extends UnitTestCase {
|
|||
it.each<[keyof AddToMenuConfig, string | string[]]>([
|
||||
['queue', ['queue-after-current', 'queue-bottom', 'queue-top', 'queue']],
|
||||
['favorites', 'add-to-favorites'],
|
||||
['playlists', 'add-to-playlist'],
|
||||
['newPlaylist', 'new-playlist']
|
||||
])('renders disabling %s config', (configKey: keyof AddToMenuConfig, testIds: string | string[]) => {
|
||||
this.renderComponent({ [configKey]: false })
|
||||
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)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,54 +33,34 @@
|
|||
Favorites
|
||||
</li>
|
||||
|
||||
<template v-if="config.playlists">
|
||||
<li
|
||||
v-for="playlist in playlists"
|
||||
:key="playlist.id"
|
||||
class="playlist"
|
||||
data-testid="add-to-playlist"
|
||||
tabindex="0"
|
||||
@click="addSongsToExistingPlaylist(playlist)"
|
||||
>
|
||||
{{ playlist.name }}
|
||||
</li>
|
||||
</template>
|
||||
<li
|
||||
v-for="playlist in playlists"
|
||||
:key="playlist.id"
|
||||
class="playlist"
|
||||
data-testid="add-to-playlist"
|
||||
tabindex="0"
|
||||
@click="addSongsToExistingPlaylist(playlist)"
|
||||
>
|
||||
{{ playlist.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section v-if="config.newPlaylist" class="new-playlist" data-testid="new-playlist">
|
||||
<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>
|
||||
<Btn transparent @click.prevent="addSongsToNewPlaylist">New Playlist…</Btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { playlistStore, queueStore } from '@/stores'
|
||||
import { useMessageToaster, useRouter, useSongMenuMethods } from '@/composables'
|
||||
import { useSongMenuMethods } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { go } = useRouter()
|
||||
|
||||
const props = defineProps<{ songs: Song[], config: AddToMenuConfig }>()
|
||||
const { songs, config } = toRefs(props)
|
||||
|
||||
const newPlaylistName = ref('')
|
||||
const queue = toRef(queueStore.state, 'songs')
|
||||
const currentSong = queueStore.current
|
||||
|
||||
|
@ -95,39 +75,18 @@ const {
|
|||
queueSongsToBottom,
|
||||
queueSongsToTop,
|
||||
addSongsToFavorite,
|
||||
addSongsToExistingPlaylist
|
||||
addSongsToExistingPlaylist,
|
||||
addSongsToNewPlaylist
|
||||
} = useSongMenuMethods(songs, 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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-to {
|
||||
width: 100%;
|
||||
max-width: 225px;
|
||||
max-width: 256px;
|
||||
min-width: 196px;
|
||||
padding: .75rem;
|
||||
|
||||
> * + * {
|
||||
|
@ -170,29 +129,9 @@ const createNewPlaylistFromSongs = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
form {
|
||||
button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
border: 1px solid rgba(255, 255, 255, .2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -251,14 +251,14 @@ new class extends UnitTestCase {
|
|||
|
||||
// mock after render to ensure that the component is mounted properly
|
||||
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)
|
||||
})
|
||||
|
||||
it('does not allow edit songs if current user is not admin', async () => {
|
||||
await this.actingAs().renderComponent()
|
||||
expect(screen.queryByText('Edit')).toBeNull()
|
||||
expect(screen.queryByText('Edit…')).toBeNull()
|
||||
})
|
||||
|
||||
it('has an option to copy shareable URL', async () => {
|
||||
|
@ -288,5 +288,16 @@ new class extends UnitTestCase {
|
|||
await this.actingAs().renderComponent()
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,11 @@
|
|||
<li @click="addSongsToFavorite">Favorites</li>
|
||||
</template>
|
||||
<li v-if="normalPlaylists.length" class="separator" />
|
||||
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
|
||||
<ul v-if="normalPlaylists.length" class="normal-playlists">
|
||||
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
|
||||
</ul>
|
||||
<li class="separator" />
|
||||
<li @click="addSongsToNewPlaylist">New Playlist…</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
@ -38,7 +42,7 @@
|
|||
<li class="separator" />
|
||||
</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="onlyOneSongSelected" @click="copyUrl">Copy Shareable URL</li>
|
||||
|
||||
|
@ -83,12 +87,12 @@ const {
|
|||
queueSongsToBottom,
|
||||
queueSongsToTop,
|
||||
addSongsToFavorite,
|
||||
addSongsToExistingPlaylist
|
||||
addSongsToExistingPlaylist,
|
||||
addSongsToNewPlaylist
|
||||
} = useSongMenuMethods(songs, close)
|
||||
|
||||
const playlists = toRef(playlistStore.state, 'playlists')
|
||||
const allowDownload = toRef(commonStore.state, 'allow_download')
|
||||
const user = toRef(userStore.state, 'current')
|
||||
const queue = toRef(queueStore.state, 'songs')
|
||||
const currentSong = toRef(queueStore, 'current')
|
||||
|
||||
|
@ -157,3 +161,10 @@ eventBus.on('SONG_CONTEXT_MENU_REQUESTED', async (e, _songs) => {
|
|||
await open(e.pageY, e.pageX)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
ul.normal-playlists {
|
||||
max-height: 256px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,13 +8,15 @@ import { screen } from '@testing-library/vue'
|
|||
import SongListControls from './SongListControls.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent (selectedSongCount = 1, config: Partial<SongListControlsConfig> = {}) {
|
||||
private renderComponent (selectedSongCount = 1, screen: ScreenName = 'Songs') {
|
||||
const songs = factory<Song>('song', 5)
|
||||
|
||||
this.router.activateRoute({
|
||||
screen,
|
||||
path: '_',
|
||||
})
|
||||
|
||||
return this.render(SongListControls, {
|
||||
props: {
|
||||
config
|
||||
},
|
||||
global: {
|
||||
provide: {
|
||||
[<symbol>SongsKey]: [ref(songs)],
|
||||
|
@ -62,7 +64,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
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'))
|
||||
|
||||
|
@ -70,7 +72,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
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'))
|
||||
|
||||
|
|
|
@ -2,63 +2,65 @@
|
|||
<div ref="el" class="song-list-controls" data-testid="song-list-controls">
|
||||
<div class="wrapper">
|
||||
<BtnGroup uppercased>
|
||||
<template v-if="mergedConfig.play">
|
||||
<template v-if="altPressed">
|
||||
<Btn
|
||||
v-if="selectedSongs.length < 2 && songs.length"
|
||||
class="btn-play-all"
|
||||
orange
|
||||
title="Play all songs"
|
||||
@click.prevent="playAll"
|
||||
>
|
||||
<icon :icon="faPlay" fixed-width />
|
||||
All
|
||||
</Btn>
|
||||
<template v-if="altPressed">
|
||||
<Btn
|
||||
v-if="selectedSongs.length < 2 && songs.length"
|
||||
class="btn-play-all"
|
||||
orange
|
||||
title="Play all songs"
|
||||
@click.prevent="playAll"
|
||||
>
|
||||
<icon :icon="faPlay" fixed-width />
|
||||
All
|
||||
</Btn>
|
||||
|
||||
<Btn
|
||||
v-if="selectedSongs.length > 1"
|
||||
class="btn-play-selected"
|
||||
orange
|
||||
title="Play selected songs"
|
||||
@click.prevent="playSelected"
|
||||
>
|
||||
<icon :icon="faPlay" fixed-width />
|
||||
Selected
|
||||
</Btn>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Btn
|
||||
v-if="selectedSongs.length < 2 && songs.length"
|
||||
class="btn-shuffle-all"
|
||||
data-testid="btn-shuffle-all"
|
||||
orange
|
||||
title="Shuffle all songs"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
<icon :icon="faRandom" fixed-width />
|
||||
All
|
||||
</Btn>
|
||||
|
||||
<Btn
|
||||
v-if="selectedSongs.length > 1"
|
||||
class="btn-shuffle-selected"
|
||||
data-testid="btn-shuffle-selected"
|
||||
orange
|
||||
title="Shuffle selected songs"
|
||||
@click.prevent="shuffleSelected"
|
||||
>
|
||||
<icon :icon="faRandom" fixed-width />
|
||||
Selected
|
||||
</Btn>
|
||||
</template>
|
||||
<Btn
|
||||
v-if="selectedSongs.length > 1"
|
||||
class="btn-play-selected"
|
||||
orange
|
||||
title="Play selected songs"
|
||||
@click.prevent="playSelected"
|
||||
>
|
||||
<icon :icon="faPlay" fixed-width />
|
||||
Selected
|
||||
</Btn>
|
||||
</template>
|
||||
|
||||
<Btn v-if="showAddToButton" ref="addToButton" green @click.prevent.stop="toggleAddToMenu">
|
||||
<template v-else>
|
||||
<Btn
|
||||
v-if="selectedSongs.length < 2 && songs.length"
|
||||
class="btn-shuffle-all"
|
||||
data-testid="btn-shuffle-all"
|
||||
orange
|
||||
title="Shuffle all songs"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
<icon :icon="faRandom" fixed-width />
|
||||
All
|
||||
</Btn>
|
||||
|
||||
<Btn
|
||||
v-if="selectedSongs.length > 1"
|
||||
class="btn-shuffle-selected"
|
||||
data-testid="btn-shuffle-selected"
|
||||
orange
|
||||
title="Shuffle selected songs"
|
||||
@click.prevent="shuffleSelected"
|
||||
>
|
||||
<icon :icon="faRandom" fixed-width />
|
||||
Selected
|
||||
</Btn>
|
||||
</template>
|
||||
|
||||
<Btn
|
||||
v-if="showAddToButton"
|
||||
ref="addToButton"
|
||||
green @click.prevent.stop="toggleAddToMenu"
|
||||
>
|
||||
{{ showingAddToMenu ? 'Cancel' : 'Add To…' }}
|
||||
</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>
|
||||
|
@ -67,7 +69,7 @@
|
|||
</Btn>
|
||||
|
||||
<Btn
|
||||
v-if="showDeletePlaylistButton"
|
||||
v-if="config.deletePlaylist"
|
||||
v-koel-tooltip
|
||||
class="del btn-delete-playlist"
|
||||
red
|
||||
|
@ -80,7 +82,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
</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 { SelectedSongsKey, SongsKey } from '@/symbols'
|
||||
import { requireInjection } from '@/utils'
|
||||
import { useFloatingUi } from '@/composables'
|
||||
import { useFloatingUi, useSongListControls } from '@/composables'
|
||||
|
||||
import AddToMenu from '@/components/song/AddToMenu.vue'
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
import BtnGroup from '@/components/ui/BtnGroup.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{ config?: Partial<SongListControlsConfig> }>(), { config: () => ({}) })
|
||||
const { config } = toRefs(props)
|
||||
const config = useSongListControls().getSongListControlsConfig()
|
||||
|
||||
const [songs] = requireInjection<[Ref<Song[]>]>(SongsKey)
|
||||
const [selectedSongs] = requireInjection(SelectedSongsKey)
|
||||
|
@ -108,23 +109,7 @@ const addToMenu = ref<HTMLDivElement>()
|
|||
const showingAddToMenu = 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 showClearQueueButton = computed(() => mergedConfig.value.clearQueue)
|
||||
const showDeletePlaylistButton = computed(() => mergedConfig.value.deletePlaylist)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'playAll' | 'playSelected', shuffle: boolean): void,
|
||||
|
|
|
@ -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="">Baz</li>
|
||||
</ul>
|
||||
</section>
|
||||
<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>
|
||||
</section><button type="button" transparent="" data-v-e368fe26="" data-v-42061e3e="">New Playlist…</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -13,6 +13,7 @@ export * from './usePlaylistManagement'
|
|||
export * from './useRouter'
|
||||
export * from './useSmartPlaylistForm'
|
||||
export * from './useSongList'
|
||||
export * from './useSongListControls'
|
||||
export * from './useSongMenuMethods'
|
||||
export * from './useThirdPartyServices'
|
||||
export * from './useUpload'
|
||||
|
|
31
resources/assets/js/composables/useSongListControls.ts
Normal file
31
resources/assets/js/composables/useSongListControls.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { Ref } from 'vue'
|
||||
import { favoriteStore, queueStore } from '@/stores'
|
||||
import { usePlaylistManagement } from '@/composables'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
export const useSongMenuMethods = (songs: Ref<Song[]>, close: Closure) => {
|
||||
const { addSongsToPlaylist } = usePlaylistManagement()
|
||||
|
@ -30,11 +31,17 @@ export const useSongMenuMethods = (songs: Ref<Song[]>, close: Closure) => {
|
|||
await addSongsToPlaylist(playlist, songs.value)
|
||||
}
|
||||
|
||||
const addSongsToNewPlaylist = () => {
|
||||
close()
|
||||
eventBus.emit('MODAL_SHOW_CREATE_PLAYLIST_FORM', null, songs.value)
|
||||
}
|
||||
|
||||
return {
|
||||
queueSongsAfterCurrent,
|
||||
queueSongsToBottom,
|
||||
queueSongsToTop,
|
||||
addSongsToFavorite,
|
||||
addSongsToExistingPlaylist
|
||||
addSongsToExistingPlaylist,
|
||||
addSongsToNewPlaylist
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ export interface Events {
|
|||
MODAL_SHOW_ADD_USER_FORM: () => void
|
||||
MODAL_SHOW_EDIT_USER_FORM: (user: User) => 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_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_EDIT_PLAYLIST_FOLDER_FORM: (playlistFolder: PlaylistFolder) => void
|
||||
MODAL_SHOW_ABOUT_KOEL: () => void
|
||||
|
|
|
@ -117,7 +117,7 @@ export const playlistStore = {
|
|||
})
|
||||
|
||||
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 => ({
|
||||
|
|
2
resources/assets/js/types.d.ts
vendored
2
resources/assets/js/types.d.ts
vendored
|
@ -313,8 +313,6 @@ declare type ArtistAlbumCardLayout = 'full' | 'compact'
|
|||
interface AddToMenuConfig {
|
||||
queue: boolean
|
||||
favorites: boolean
|
||||
playlists: boolean
|
||||
newPlaylist: boolean
|
||||
}
|
||||
|
||||
interface SongListControlsConfig {
|
||||
|
|
Loading…
Reference in a new issue