Merge 8fbe8e2864e4e160bf0aedd743fb354cc8fc9456 into 600ea5482c36d3705fbca1d9ab749d9bdafd6f80

This commit is contained in:
realrossmanngroup 2026-07-31 00:48:19 +03:00 committed by GitHub
commit e1423dec2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 9 deletions

View File

@ -5,10 +5,11 @@ import { GlobalHotKeys } from 'react-hotkeys'
import IconButton from '@material-ui/core/IconButton'
import { useMediaQuery } from '@material-ui/core'
import { RiSaveLine } from 'react-icons/ri'
import { LoveButton, useToggleLove } from '../common'
import { LoveButton, RatingField, useToggleLove } from '../common'
import { openSaveQueueDialog } from '../actions'
import { keyMap } from '../hotkeys'
import { makeStyles } from '@material-ui/core/styles'
import config from '../config'
const useStyles = makeStyles((theme) => ({
toolbar: {
@ -53,6 +54,10 @@ const useStyles = makeStyles((theme) => ({
display: 'flex',
alignItems: 'center',
},
rating: {
display: 'flex',
alignItems: 'center',
},
}))
const PlayerToolbar = ({ id, isRadio }) => {
@ -99,16 +104,34 @@ const PlayerToolbar = ({ id, isRadio }) => {
/>
)
// Inline star rating for the currently-playing track. Only shown when star
// rating is enabled and we have a real (non-radio) song loaded. `alwaysVisible`
// keeps the empty stars on screen so an unrated track can be rated in place.
const ratingControl =
config.enableStarRating && !isRadio && !!id && data ? (
<RatingField
record={data}
resource={'song'}
size={'small'}
alwaysVisible
className={classes.rating}
/>
) : null
return (
<>
<GlobalHotKeys keyMap={keyMap} handlers={handlers} allowChanges />
{isDesktop ? (
<li className={`${listItemClass} item`}>
{ratingControl}
{saveQueueButton}
{loveButton}
</li>
) : (
<>
{ratingControl && (
<li className={`${listItemClass} item`}>{ratingControl}</li>
)}
<li className={`${listItemClass} item`}>{saveQueueButton}</li>
<li className={`${listItemClass} item`}>{loveButton}</li>
</>

View File

@ -30,6 +30,11 @@ vi.mock('../common', () => ({
Love
</button>
),
RatingField: ({ className }) => (
<span data-testid="rating-field" className={className}>
Rating
</span>
),
useToggleLove: vi.fn(),
}))
@ -61,14 +66,15 @@ describe('<PlayerToolbar />', () => {
useMediaQuery.mockReturnValue(true) // isDesktop = true
})
it('renders desktop toolbar with both buttons', () => {
it('renders desktop toolbar with rating and buttons', () => {
render(<PlayerToolbar id="song-1" />)
// Both buttons should be in a single list item
// Rating + both buttons should be in a single list item
const listItems = screen.getAllByRole('listitem')
expect(listItems).toHaveLength(1)
// Verify both buttons are rendered
// Verify rating and both buttons are rendered
expect(screen.getByTestId('rating-field')).toBeInTheDocument()
expect(screen.getByTestId('save-queue-button')).toBeInTheDocument()
expect(screen.getByTestId('love-button')).toBeInTheDocument()
@ -76,6 +82,13 @@ describe('<PlayerToolbar />', () => {
expect(listItems[0].className).toContain('toolbar')
})
it('hides the rating for radio streams', () => {
render(<PlayerToolbar id="song-1" isRadio={true} />)
expect(screen.queryByTestId('rating-field')).not.toBeInTheDocument()
expect(screen.getByTestId('love-button')).toBeInTheDocument()
})
it('disables save queue button when isRadio is true', () => {
render(<PlayerToolbar id="song-1" isRadio={true} />)
@ -109,20 +122,22 @@ describe('<PlayerToolbar />', () => {
useMediaQuery.mockReturnValue(false) // isDesktop = false
})
it('renders mobile toolbar with buttons in separate list items', () => {
it('renders mobile toolbar with rating and buttons in separate list items', () => {
render(<PlayerToolbar id="song-1" />)
// Each button should be in its own list item
// Rating + each button should be in its own list item
const listItems = screen.getAllByRole('listitem')
expect(listItems).toHaveLength(2)
expect(listItems).toHaveLength(3)
// Verify both buttons are rendered
// Verify rating and both buttons are rendered
expect(screen.getByTestId('rating-field')).toBeInTheDocument()
expect(screen.getByTestId('save-queue-button')).toBeInTheDocument()
expect(screen.getByTestId('love-button')).toBeInTheDocument()
// Verify mobile classes are applied
expect(listItems[0].className).toContain('mobileListItem')
expect(listItems[1].className).toContain('mobileListItem')
expect(listItems[2].className).toContain('mobileListItem')
})
it('disables save queue button when isRadio is true', () => {

View File

@ -27,6 +27,7 @@ export const RatingField = ({
className,
size,
color,
alwaysVisible,
...rest
}) => {
const record = useRecordContext(rest) || {}
@ -59,7 +60,7 @@ export const RatingField = ({
className={clsx(
className,
classes.rating,
rating > 0 ? classes.show : classes.hide,
rating > 0 || alwaysVisible ? classes.show : classes.hide,
)}
value={rating}
size={size}
@ -75,10 +76,15 @@ RatingField.propTypes = {
record: PropTypes.object,
visible: PropTypes.bool,
size: PropTypes.string,
// When true, the empty stars are always shown (not just on hover / when
// already rated). Used by the player toolbar so the current track can be
// rated directly.
alwaysVisible: PropTypes.bool,
}
RatingField.defaultProps = {
visible: true,
size: 'small',
color: 'inherit',
alwaysVisible: false,
}