feat(ui): add loading state to artist action buttons for improved user experience

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan
2025-11-21 22:23:38 -05:00
parent 67c4e24957
commit f6b2ab5726
+33 -13
View File
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { useMediaQuery } from '@material-ui/core' import { useMediaQuery, CircularProgress } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { import {
Button, Button,
@@ -45,6 +45,12 @@ const useStyles = makeStyles((theme) => ({
}, },
})) }))
const LoadingButton = ({ loading, icon, ...rest }) => (
<Button {...rest}>
{loading ? <CircularProgress size={20} color="inherit" /> : icon}
</Button>
)
const ArtistActions = ({ className, record, ...rest }) => { const ArtistActions = ({ className, record, ...rest }) => {
const dispatch = useDispatch() const dispatch = useDispatch()
const translate = useTranslate() const translate = useTranslate()
@@ -52,34 +58,45 @@ const ArtistActions = ({ className, record, ...rest }) => {
const notify = useNotify() const notify = useNotify()
const classes = useStyles() const classes = useStyles()
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('xs')) const isMobile = useMediaQuery((theme) => theme.breakpoints.down('xs'))
const [loadingAction, setLoadingAction] = React.useState(null)
const isLoading = !!loadingAction
const handlePlay = React.useCallback(async () => { const handlePlay = React.useCallback(async () => {
setLoadingAction('play')
try { try {
await playTopSongs(dispatch, notify, record.name) await playTopSongs(dispatch, notify, record.name)
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Error fetching top songs for artist:', e) console.error('Error fetching top songs for artist:', e)
notify('ra.page.error', 'warning') notify('ra.page.error', 'warning')
} finally {
setLoadingAction(null)
} }
}, [dispatch, notify, record]) }, [dispatch, notify, record])
const handleShuffle = React.useCallback(async () => { const handleShuffle = React.useCallback(async () => {
setLoadingAction('shuffle')
try { try {
await playShuffle(dataProvider, dispatch, record.id) await playShuffle(dataProvider, dispatch, record.id)
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Error fetching songs for shuffle:', e) console.error('Error fetching songs for shuffle:', e)
notify('ra.page.error', 'warning') notify('ra.page.error', 'warning')
} finally {
setLoadingAction(null)
} }
}, [dataProvider, dispatch, record, notify]) }, [dataProvider, dispatch, record, notify])
const handleRadio = React.useCallback(async () => { const handleRadio = React.useCallback(async () => {
setLoadingAction('radio')
try { try {
await playSimilar(dispatch, notify, record.id) await playSimilar(dispatch, notify, record.id)
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Error starting radio for artist:', e) console.error('Error starting radio for artist:', e)
notify('ra.page.error', 'warning') notify('ra.page.error', 'warning')
} finally {
setLoadingAction(null)
} }
}, [dispatch, notify, record]) }, [dispatch, notify, record])
@@ -88,30 +105,33 @@ const ArtistActions = ({ className, record, ...rest }) => {
className={`${className} ${classes.toolbar}`} className={`${className} ${classes.toolbar}`}
{...sanitizeListRestProps(rest)} {...sanitizeListRestProps(rest)}
> >
<Button <LoadingButton
onClick={handlePlay} onClick={handlePlay}
label={translate('resources.artist.actions.topSongs')} label={translate('resources.artist.actions.topSongs')}
className={classes.button} className={classes.button}
size={isMobile ? 'small' : 'medium'} size={isMobile ? 'small' : 'medium'}
> disabled={isLoading}
<PlayArrowIcon /> loading={loadingAction === 'play'}
</Button> icon={<PlayArrowIcon />}
<Button />
<LoadingButton
onClick={handleShuffle} onClick={handleShuffle}
label={translate('resources.artist.actions.shuffle')} label={translate('resources.artist.actions.shuffle')}
className={classes.button} className={classes.button}
size={isMobile ? 'small' : 'medium'} size={isMobile ? 'small' : 'medium'}
> disabled={isLoading}
<ShuffleIcon /> loading={loadingAction === 'shuffle'}
</Button> icon={<ShuffleIcon />}
<Button />
<LoadingButton
onClick={handleRadio} onClick={handleRadio}
label={translate('resources.artist.actions.radio')} label={translate('resources.artist.actions.radio')}
className={classes.button} className={classes.button}
size={isMobile ? 'small' : 'medium'} size={isMobile ? 'small' : 'medium'}
> disabled={isLoading}
<IoIosRadio className={classes.radioIcon} /> loading={loadingAction === 'radio'}
</Button> icon={<IoIosRadio className={classes.radioIcon} />}
/>
</TopToolbar> </TopToolbar>
) )
} }