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'))
.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 => {

View file

@ -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>

View file

@ -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)
})
}
}

View file

@ -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.`)

View file

@ -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)
})

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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>

View file

@ -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)
})
}
}

View file

@ -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>

View file

@ -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)
})
}
}

View file

@ -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>

View file

@ -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'))

View file

@ -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,

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="">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>
`;

View file

@ -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'

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 { 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
}
}

View file

@ -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

View file

@ -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 => ({

View file

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