diff --git a/ui/package-lock.json b/ui/package-lock.json
index d465feee..ba02b2c4 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -10530,6 +10530,11 @@
"object-visit": "^1.0.0"
}
},
+ "material-ui-nested-menu-item": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/material-ui-nested-menu-item/-/material-ui-nested-menu-item-1.0.2.tgz",
+ "integrity": "sha512-LZb8xI0FrAI/A3P2vT3CB9bmSoOFWOK0dikTc1t9VvEpp1a8hZkbVUz7VhETnoLUYu3NXCkgulmXcl3zitqI9A=="
+ },
"md5-hex": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz",
diff --git a/ui/package.json b/ui/package.json
index 2f4b08b8..382f45da 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -6,6 +6,7 @@
"deepmerge": "^4.2.2",
"jwt-decode": "^2.2.0",
"lodash.throttle": "^4.1.1",
+ "material-ui-nested-menu-item": "^1.0.2",
"md5-hex": "^3.0.1",
"prop-types": "^15.7.2",
"ra-data-json-server": "^3.4.1",
diff --git a/ui/src/album/AlbumContextMenu.js b/ui/src/album/AlbumContextMenu.js
index 93a6262a..cc5e36d0 100644
--- a/ui/src/album/AlbumContextMenu.js
+++ b/ui/src/album/AlbumContextMenu.js
@@ -7,6 +7,8 @@ import { makeStyles } from '@material-ui/core/styles'
import { useDataProvider, useNotify, useTranslate } from 'react-admin'
import { useDispatch } from 'react-redux'
import { addTracks, playTracks, shuffleTracks } from '../audioplayer'
+import NestedMenuItem from 'material-ui-nested-menu-item'
+import { AddToPlaylistMenu } from '../common'
const useStyles = makeStyles({
icon: {
@@ -96,6 +98,15 @@ const AlbumContextMenu = ({ record, color }) => {
{options[key].label}
))}
+
+ setAnchorEl(null)}
+ />
+
)
diff --git a/ui/src/album/AlbumSongs.js b/ui/src/album/AlbumSongs.js
index cd5ebc9d..cf4ce535 100644
--- a/ui/src/album/AlbumSongs.js
+++ b/ui/src/album/AlbumSongs.js
@@ -67,7 +67,7 @@ const AlbumSongs = (props) => {
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
const controllerProps = useListController(props)
- const { bulkActionButtons, albumId, expand, className } = props
+ const { bulkActionButtons, albumId, className } = props
const { data, ids, version, loaded } = controllerProps
let multiDisc = false
@@ -116,7 +116,7 @@ const AlbumSongs = (props) => {
+ dataProvider
+ .create('playlistTrack', {
+ data: { ids: selectedIds },
+ filter: { playlist_id: playlistId },
+ })
+ .then(() => selectedIds.length)
+
+export const addAlbumToPlaylist = (dataProvider, albumId, playlistId) =>
+ dataProvider
+ .getList('albumSong', {
+ pagination: { page: 1, perPage: -1 },
+ sort: { field: 'discNumber asc, trackNumber asc', order: 'ASC' },
+ filter: { album_id: albumId },
+ })
+ .then((response) => response.data.map((song) => song.id))
+ .then((ids) => addTracksToPlaylist(dataProvider, ids, playlistId))
+
+const AddToPlaylistMenu = ({ selectedIds, albumId, onClose }) => {
+ const translate = useTranslate()
+ const notify = useNotify()
+ const dataProvider = useDataProvider()
+ const { ids, data, loaded } = useGetList(
+ 'playlist',
+ { page: 1, perPage: -1 },
+ { field: 'name', order: 'ASC' },
+ {}
+ )
+
+ if (!loaded) {
+ return
+ }
+
+ const handleItemClick = (e) => {
+ e.preventDefault()
+ const value = e.target.getAttribute('value')
+ if (value !== '') {
+ const add = albumId
+ ? addAlbumToPlaylist(dataProvider, albumId, value)
+ : addTracksToPlaylist(dataProvider, selectedIds, value)
+
+ add
+ .then((len) => {
+ notify(
+ translate('message.songsAddedToPlaylist', {
+ smart_count: len,
+ })
+ )
+ })
+ .catch(() => {
+ notify('ra.page.error', 'warning')
+ })
+ }
+ e.stopPropagation()
+ onClose && onClose()
+ }
+
+ return (
+ <>
+ {ids.map((id) => (
+
+ ))}
+ >
+ )
+}
+
+AddToPlaylistMenu.propTypes = {
+ selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired,
+ albumId: PropTypes.string,
+}
+
+AddToPlaylistMenu.defaultProps = {
+ selectedIds: [],
+}
+
+export default AddToPlaylistMenu
diff --git a/ui/src/common/SelectPlaylistDialog.js b/ui/src/common/SelectPlaylistDialog.js
deleted file mode 100644
index 225d3353..00000000
--- a/ui/src/common/SelectPlaylistDialog.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { useGetList, useTranslate } from 'react-admin'
-import { makeStyles } from '@material-ui/core/styles'
-import Avatar from '@material-ui/core/Avatar'
-import List from '@material-ui/core/List'
-import ListItem from '@material-ui/core/ListItem'
-import ListItemAvatar from '@material-ui/core/ListItemAvatar'
-import ListItemText from '@material-ui/core/ListItemText'
-import DialogTitle from '@material-ui/core/DialogTitle'
-import Dialog from '@material-ui/core/Dialog'
-import { blue } from '@material-ui/core/colors'
-import PlaylistIcon from '../icons/Playlist'
-
-const useStyles = makeStyles({
- avatar: {
- backgroundColor: blue[100],
- color: blue[600],
- },
-})
-
-function SelectPlaylistDialog(props) {
- const classes = useStyles()
- const translate = useTranslate()
- const { onClose, selectedValue, open } = props
- const { ids, data, loaded } = useGetList(
- 'playlist',
- { page: 1, perPage: -1 },
- { field: '', order: '' },
- {}
- )
-
- if (!loaded) {
- return
- }
-
- const handleClose = () => {
- onClose(selectedValue)
- }
-
- const handleListItemClick = (value) => {
- onClose(value)
- }
-
- return (
-
- )
-}
-
-SelectPlaylistDialog.propTypes = {
- onClose: PropTypes.func.isRequired,
- open: PropTypes.bool.isRequired,
- selectedValue: PropTypes.string.isRequired,
-}
-
-export default SelectPlaylistDialog
diff --git a/ui/src/common/index.js b/ui/src/common/index.js
index 4b08c6cb..14f3399c 100644
--- a/ui/src/common/index.js
+++ b/ui/src/common/index.js
@@ -11,7 +11,7 @@ import SizeField from './SizeField'
import DocLink from './DocLink'
import List from './List'
import SongDatagridRow from './SongDatagridRow'
-import SelectPlaylistDialog from './SelectPlaylistDialog'
+import AddToPlaylistMenu from './AddToPlaylistMenu'
export {
Title,
@@ -29,5 +29,5 @@ export {
formatRange,
ArtistLinkField,
artistLink,
- SelectPlaylistDialog,
+ AddToPlaylistMenu,
}
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index c10cd895..4802d871 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -237,7 +237,8 @@
"transcodingDisabled": "Changing the transcoding configuration through the web interface is disabled for security reasons. If you would like to change (edit or add) transcoding options, restart the server with the %{config} configuration option.",
"transcodingEnabled": "Navidrome is currently running with %{config}, making it possible to run system commands from the transcoding settings using the web interface. We recommend to disable it for security reasons and only enable it when configuring Transcoding options.",
"discSubtitle": "%{subtitle} (disc %{number})",
- "discWithoutSubtitle": "Disc %{number}"
+ "discWithoutSubtitle": "Disc %{number}",
+ "songsAddedToPlaylist": "Added 1 song to playlist |||| Added %{smart_count} songs to playlist"
},
"menu": {
"library": "Library",
diff --git a/ui/src/song/AddToPlaylistButton.js b/ui/src/song/AddToPlaylistButton.js
index a3e6b349..2d7147cf 100644
--- a/ui/src/song/AddToPlaylistButton.js
+++ b/ui/src/song/AddToPlaylistButton.js
@@ -1,59 +1,46 @@
-import React, { useState } from 'react'
-import {
- Button,
- useTranslate,
- useUnselectAll,
- useDataProvider,
- useNotify,
-} from 'react-admin'
-import SelectPlaylistDialog from '../common/SelectPlaylistDialog'
+import React from 'react'
+import { Button, useTranslate, useUnselectAll } from 'react-admin'
+import { Menu } from '@material-ui/core'
import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd'
+import { AddToPlaylistMenu } from '../common'
const AddToPlaylistButton = ({ resource, selectedIds }) => {
- const [open, setOpen] = useState(false)
- const [selectedValue, setSelectedValue] = useState('')
+ const [anchorEl, setAnchorEl] = React.useState(null)
const translate = useTranslate()
const unselectAll = useUnselectAll()
- const notify = useNotify()
- const dataProvider = useDataProvider()
- const handleClickOpen = () => {
- setOpen(true)
+ const handleClick = (event) => {
+ setAnchorEl(event.currentTarget)
}
- const handleClose = (value) => {
- if (value !== '') {
- dataProvider
- .create('playlistTrack', {
- data: { ids: selectedIds },
- filter: { playlist_id: value },
- })
- .then(() => {
- notify(`Added ${selectedIds.length} songs to playlist`)
- })
- .catch(() => {
- notify('ra.page.error', 'warning')
- })
- }
- setOpen(false)
- setSelectedValue(value)
+ const handleClose = () => {
+ setAnchorEl(null)
unselectAll(resource)
}
return (
<>
-
+ >
+
+
>
)
}
diff --git a/ui/src/song/SongContextMenu.js b/ui/src/song/SongContextMenu.js
index f0f8d0bb..92822733 100644
--- a/ui/src/song/SongContextMenu.js
+++ b/ui/src/song/SongContextMenu.js
@@ -4,6 +4,8 @@ import { useTranslate } from 'react-admin'
import { IconButton, Menu, MenuItem } from '@material-ui/core'
import MoreVertIcon from '@material-ui/icons/MoreVert'
import { addTracks, setTrack } from '../audioplayer'
+import { AddToPlaylistMenu } from '../common'
+import NestedMenuItem from 'material-ui-nested-menu-item'
export const SongContextMenu = ({ record }) => {
const dispatch = useDispatch()
@@ -38,6 +40,8 @@ export const SongContextMenu = ({ record }) => {
e.stopPropagation()
}
+ const open = Boolean(anchorEl)
+
return (
<>
@@ -46,7 +50,7 @@ export const SongContextMenu = ({ record }) => {
>
)