navidrome/ui/src/audioplayer/PlayerToolbar.jsx
Yuuta e06fcbbaeb
fix(ui): harden responsive lyrics layout and accessibility
Signed-off-by: ranokay <github@ranokay.com>
2026-07-29 13:58:30 +03:00

160 lines
4.3 KiB
JavaScript

import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { useGetOne } from 'react-admin'
import { GlobalHotKeys } from 'react-hotkeys'
import IconButton from '@material-ui/core/IconButton'
import { useMediaQuery } from '@material-ui/core'
import Tooltip from '@material-ui/core/Tooltip'
import { RiFileMusicLine } from 'react-icons/ri'
import { RiSaveLine } from 'react-icons/ri'
import { LoveButton, useToggleLove } from '../common'
import { openSaveQueueDialog } from '../actions'
import { keyMap } from '../hotkeys'
import { makeStyles } from '@material-ui/core/styles'
import { PLAYER_DESKTOP_MEDIA_QUERY } from './playerBreakpoints'
const useStyles = makeStyles((theme) => ({
toolbar: {
display: 'flex',
alignItems: 'center',
flexGrow: 1,
justifyContent: 'flex-end',
gap: '0.5rem',
listStyle: 'none',
padding: 0,
margin: 0,
},
mobileListItem: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
listStyle: 'none',
padding: theme.spacing(0.5),
margin: 0,
height: 24,
},
button: {
width: '2.5rem',
height: '2.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
},
mobileButton: {
width: 24,
height: 24,
padding: 0,
margin: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
},
mobileIcon: {
fontSize: '18px',
display: 'flex',
alignItems: 'center',
},
}))
const PlayerToolbar = ({
id,
isRadio,
onToggleLyrics,
lyricsActive = false,
lyricsDisabled = false,
lyricsLoading = false,
lyricsLabel = 'Toggle lyrics',
lyricsLoadingLabel = 'Loading lyrics',
lyricsToggleRef,
}) => {
const dispatch = useDispatch()
const { data, loading } = useGetOne('song', id, { enabled: !!id && !isRadio })
const [toggleLove, toggling] = useToggleLove('song', data)
const isDesktop = useMediaQuery(PLAYER_DESKTOP_MEDIA_QUERY)
const classes = useStyles()
const handlers = {
TOGGLE_LOVE: useCallback(() => toggleLove(), [toggleLove]),
}
const handleSaveQueue = useCallback(
(e) => {
dispatch(openSaveQueueDialog())
e.stopPropagation()
},
[dispatch],
)
const buttonClass = isDesktop ? classes.button : classes.mobileButton
const listItemClass = isDesktop ? classes.toolbar : classes.mobileListItem
const lyricsUnavailable = lyricsDisabled || (lyricsLoading && !lyricsActive)
const saveQueueButton = (
<IconButton
size={isDesktop ? 'small' : undefined}
onClick={handleSaveQueue}
disabled={isRadio}
data-testid="save-queue-button"
className={buttonClass}
>
<RiSaveLine className={!isDesktop ? classes.mobileIcon : undefined} />
</IconButton>
)
const loveButton = (
<LoveButton
record={data}
resource={'song'}
size={isDesktop ? undefined : 'inherit'}
disabled={loading || toggling || !id || isRadio}
className={buttonClass}
/>
)
const currentLyricsLabel = lyricsLoading ? lyricsLoadingLabel : lyricsLabel
const lyricsButton = (
<Tooltip title={currentLyricsLabel}>
<span>
<IconButton
ref={lyricsToggleRef}
size={isDesktop ? 'small' : undefined}
onClick={onToggleLyrics}
disabled={!onToggleLyrics || lyricsUnavailable}
data-testid="toggle-lyrics-button"
className={buttonClass}
color={lyricsActive ? 'primary' : 'default'}
aria-label={currentLyricsLabel}
aria-busy={lyricsLoading}
aria-pressed={lyricsActive}
>
<RiFileMusicLine
className={!isDesktop ? classes.mobileIcon : undefined}
/>
</IconButton>
</span>
</Tooltip>
)
return (
<>
<GlobalHotKeys keyMap={keyMap} handlers={handlers} allowChanges />
{isDesktop ? (
<li className={`${listItemClass} item`}>
{saveQueueButton}
{loveButton}
{lyricsButton}
</li>
) : (
<>
<li className={`${listItemClass} item`}>{saveQueueButton}</li>
<li className={`${listItemClass} item`}>{loveButton}</li>
<li className={`${listItemClass} item`}>{lyricsButton}</li>
</>
)}
</>
)
}
export default PlayerToolbar