navidrome/ui/src/common/RatingField.jsx
realrossmanngroup 8fbe8e2864 feat(ui): rate the current track from the player toolbar - #5569
Add an inline star rating control to the player toolbar, next to the Love button. It is shown only when EnableStarRating is enabled and is hidden for radio streams, and reflects/updates the current track's rating live.

Adds an `alwaysVisible` prop to RatingField so the empty stars are shown for unrated tracks (the list views only reveal them on row hover).

Signed-off-by: realrossmanngroup <youtube@rossmanngroup.com>
2026-06-05 19:46:27 -05:00

91 lines
2.2 KiB
JavaScript

import React, { useCallback } from 'react'
import PropTypes from 'prop-types'
import Rating from '@material-ui/lab/Rating'
import { makeStyles } from '@material-ui/core/styles'
import { isDateSet } from '../utils/validations'
import StarBorderIcon from '@material-ui/icons/StarBorder'
import clsx from 'clsx'
import { useRating } from './useRating'
import { useRecordContext } from 'react-admin'
const useStyles = makeStyles({
rating: {
color: (props) => props.color,
visibility: (props) => (props.visible === false ? 'hidden' : 'inherit'),
},
show: {
visibility: 'visible !important',
},
hide: {
visibility: 'hidden',
},
})
export const RatingField = ({
resource,
visible,
className,
size,
color,
alwaysVisible,
...rest
}) => {
const record = useRecordContext(rest) || {}
const [rate, rating] = useRating(resource, record)
const classes = useStyles({ color, visible })
const stopPropagation = (e) => {
e.stopPropagation()
}
const handleRating = useCallback(
(e, val) => {
const targetId = record.mediaFileId || record.id
rate(val ?? 0, targetId)
},
[rate, record.mediaFileId, record.id],
)
return (
<span
onClick={(e) => stopPropagation(e)}
title={
isDateSet(record.ratedAt)
? new Date(record.ratedAt).toLocaleString()
: undefined
}
>
<Rating
name={record.mediaFileId || record.id}
className={clsx(
className,
classes.rating,
rating > 0 || alwaysVisible ? classes.show : classes.hide,
)}
value={rating}
size={size}
disabled={record?.missing}
emptyIcon={<StarBorderIcon fontSize="inherit" />}
onChange={(e, newValue) => handleRating(e, newValue)}
/>
</span>
)
}
RatingField.propTypes = {
resource: PropTypes.string.isRequired,
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,
}