mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-16 04:00:38 +03:00
Add "AddToPlaylist" to AlbumContextMenu
This commit is contained in:
parent
176bfe1506
commit
308163c2e0
5
ui/package-lock.json
generated
5
ui/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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}
|
||||
</MenuItem>
|
||||
))}
|
||||
<NestedMenuItem
|
||||
label={translate('resources.song.actions.addToPlaylist')}
|
||||
parentMenuOpen={open}
|
||||
>
|
||||
<AddToPlaylistMenu
|
||||
albumId={[record.id]}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
/>
|
||||
</NestedMenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
|
@ -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) => {
|
||||
<DatagridLoading
|
||||
classes={classes}
|
||||
className={className}
|
||||
expand={expand}
|
||||
expand={null}
|
||||
hasBulkActions={hasBulkActions}
|
||||
nbChildren={3}
|
||||
size={'small'}
|
||||
|
88
ui/src/common/AddToPlaylistMenu.js
Normal file
88
ui/src/common/AddToPlaylistMenu.js
Normal file
@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
useDataProvider,
|
||||
useGetList,
|
||||
useNotify,
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import { MenuItem } from '@material-ui/core'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export const addTracksToPlaylist = (dataProvider, selectedIds, playlistId) =>
|
||||
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 <MenuItem>Loading...</MenuItem>
|
||||
}
|
||||
|
||||
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) => (
|
||||
<MenuItem value={id} key={id} onClick={handleItemClick}>
|
||||
{data[id].name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AddToPlaylistMenu.propTypes = {
|
||||
selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired,
|
||||
albumId: PropTypes.string,
|
||||
}
|
||||
|
||||
AddToPlaylistMenu.defaultProps = {
|
||||
selectedIds: [],
|
||||
}
|
||||
|
||||
export default AddToPlaylistMenu
|
@ -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 <div />
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onClose(selectedValue)
|
||||
}
|
||||
|
||||
const handleListItemClick = (value) => {
|
||||
onClose(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onClose={handleClose}
|
||||
aria-labelledby="select-playlist-dialog-title"
|
||||
open={open}
|
||||
scroll={'paper'}
|
||||
>
|
||||
<DialogTitle id="select-playlist-dialog-title">
|
||||
{translate('resources.playlist.actions.selectPlaylist')}
|
||||
</DialogTitle>
|
||||
<List>
|
||||
{ids.map((id) => (
|
||||
<ListItem button onClick={() => handleListItemClick(id)} key={id}>
|
||||
<ListItemAvatar>
|
||||
<Avatar className={classes.avatar}>
|
||||
<PlaylistIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={data[id].name} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
SelectPlaylistDialog.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
selectedValue: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default SelectPlaylistDialog
|
@ -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,
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
aria-controls="simple-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
color="secondary"
|
||||
onClick={handleClickOpen}
|
||||
label={translate('resources.song.actions.addToPlaylist')}
|
||||
>
|
||||
<PlaylistAddIcon />
|
||||
</Button>
|
||||
<SelectPlaylistDialog
|
||||
selectedValue={selectedValue}
|
||||
open={open}
|
||||
<Menu
|
||||
id="simple-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
>
|
||||
<AddToPlaylistMenu
|
||||
selectedIds={selectedIds}
|
||||
menuOpen={Boolean(anchorEl)}
|
||||
/>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
<IconButton onClick={handleClick} size={'small'}>
|
||||
@ -46,7 +50,7 @@ export const SongContextMenu = ({ record }) => {
|
||||
<Menu
|
||||
id={'menu' + record.id}
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{Object.keys(options).map((key) => (
|
||||
@ -54,6 +58,15 @@ export const SongContextMenu = ({ record }) => {
|
||||
{options[key].label}
|
||||
</MenuItem>
|
||||
))}
|
||||
<NestedMenuItem
|
||||
label={translate('resources.song.actions.addToPlaylist')}
|
||||
parentMenuOpen={open}
|
||||
>
|
||||
<AddToPlaylistMenu
|
||||
selectedIds={[record.id]}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
/>
|
||||
</NestedMenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user