navidrome/ui/src/common/ImageUploadOverlay.jsx
Deluan f33ca75378 refactor: rename EnableCoverArtUpload to EnableArtworkUpload
The config flag gates all image uploads (artists, radios, playlists),
not just cover art. Rename it to accurately reflect its scope across
the backend config, native API permission check, Subsonic CoverArtRole,
serve_index JSON key, and frontend config.
2026-03-27 19:33:46 -04:00

140 lines
3.5 KiB
JavaScript

import { IconButton, Tooltip } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import PhotoCameraIcon from '@material-ui/icons/PhotoCamera'
import DeleteIcon from '@material-ui/icons/Delete'
import { useTranslate, useNotify, useRefresh } from 'react-admin'
import { useCallback, useRef } from 'react'
import config from '../config'
import { REST_URL } from '../consts'
import { httpClient } from '../dataProvider'
const useStyles = makeStyles(() => ({
coverOverlay: {
position: 'absolute',
bottom: 0,
right: 0,
display: 'flex',
gap: '2px',
padding: '2px',
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: '4px 0 0 0',
opacity: 0,
transition: 'opacity 0.2s ease-in-out',
'*:hover > &': {
opacity: 1,
},
},
overlayButton: {
color: '#fff',
padding: '4px',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.2)',
},
},
overlayIcon: {
fontSize: '1.2rem',
},
}))
export const ImageUploadOverlay = ({
entityType,
entityId,
hasUploadedImage,
onImageChange,
}) => {
const translate = useTranslate()
const notify = useNotify()
const refresh = useRefresh()
const classes = useStyles()
const fileInputRef = useRef(null)
const canEdit =
config.enableArtworkUpload || localStorage.getItem('role') === 'admin'
const handleUploadClick = useCallback((e) => {
e.stopPropagation()
if (fileInputRef.current) {
fileInputRef.current.click()
}
}, [])
const handleFileChange = useCallback(
async (e) => {
const file = e.target.files[0]
if (!file || !entityId) return
const formData = new FormData()
formData.append('image', file)
try {
await httpClient(`${REST_URL}/${entityType}/${entityId}/image`, {
method: 'POST',
headers: new Headers({}),
body: formData,
})
notify(`message.coverUploaded`, 'success')
if (onImageChange) onImageChange()
refresh()
} catch (err) {
notify(`message.coverUploadError`, 'warning')
}
e.target.value = ''
},
[entityType, entityId, notify, refresh, onImageChange],
)
const handleRemoveCover = useCallback(
async (e) => {
e.stopPropagation()
if (!entityId) return
try {
await httpClient(`${REST_URL}/${entityType}/${entityId}/image`, {
method: 'DELETE',
})
notify(`message.coverRemoved`, 'success')
if (onImageChange) onImageChange()
refresh()
} catch (err) {
notify(`message.coverRemoveError`, 'warning')
}
},
[entityType, entityId, notify, refresh, onImageChange],
)
if (!canEdit) return null
return (
<div className={classes.coverOverlay}>
<Tooltip title={translate(`message.uploadCover`)}>
<IconButton
className={classes.overlayButton}
onClick={handleUploadClick}
size="small"
>
<PhotoCameraIcon className={classes.overlayIcon} />
</IconButton>
</Tooltip>
{hasUploadedImage && (
<Tooltip title={translate(`message.removeCover`)}>
<IconButton
className={classes.overlayButton}
onClick={handleRemoveCover}
size="small"
>
<DeleteIcon className={classes.overlayIcon} />
</IconButton>
</Tooltip>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
</div>
)
}