mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-04 13:07:36 +03:00
Add 5-star rating system(#986)
* Added Star Rating functionality for Songs * Added Star Rating functionality for Artists * Added Star Rating functionality for AlbumListView * Added Star Rating functionality for AlbumDetails and improved typography for title * Added functionality to turn on/off Star Rating functionality for Songs * Added functionality to turn on/off Star Rating functionality for Artists * Added functionality to turn on/off Star Rating functionality for Albums * Added enableStarRating to server config * Resolved the bugs and improved the ratings functionality. * synced repo and removed duplicate key * changed the default rating size to small, and changed the color to match the theme. * Added translations for ratings, and Top Rated tab in side menu. * Changed rating translation to topRated in albumLists, and added has_rating filter to topRated. * Added empty stars icon to RatingField. * Added sortable=false in AlbumSongs and added sortByOrder=DESC in all List components. * Added translations for rating, for artists and albums, and removed the sortByOrder=DESC from SimpleLists.
This commit is contained in:
parent
840521ffe2
commit
48ae4f7479
17 changed files with 277 additions and 16 deletions
|
@ -41,6 +41,7 @@ type configOptions struct {
|
||||||
UIWelcomeMessage string
|
UIWelcomeMessage string
|
||||||
EnableGravatar bool
|
EnableGravatar bool
|
||||||
EnableFavourites bool
|
EnableFavourites bool
|
||||||
|
EnableStarRating bool
|
||||||
GATrackingID string
|
GATrackingID string
|
||||||
AuthRequestLimit int
|
AuthRequestLimit int
|
||||||
AuthWindowLength time.Duration
|
AuthWindowLength time.Duration
|
||||||
|
@ -145,6 +146,7 @@ func init() {
|
||||||
viper.SetDefault("uiwelcomemessage", "")
|
viper.SetDefault("uiwelcomemessage", "")
|
||||||
viper.SetDefault("enablegravatar", false)
|
viper.SetDefault("enablegravatar", false)
|
||||||
viper.SetDefault("enablefavourites", true)
|
viper.SetDefault("enablefavourites", true)
|
||||||
|
viper.SetDefault("enablestarrating", true)
|
||||||
viper.SetDefault("gatrackingid", "")
|
viper.SetDefault("gatrackingid", "")
|
||||||
viper.SetDefault("authrequestlimit", 5)
|
viper.SetDefault("authrequestlimit", 5)
|
||||||
viper.SetDefault("authwindowlength", 20*time.Second)
|
viper.SetDefault("authwindowlength", 20*time.Second)
|
||||||
|
|
|
@ -44,6 +44,7 @@ func serveIndex(ds model.DataStore, fs fs.FS) http.HandlerFunc {
|
||||||
"enableDownloads": conf.Server.EnableDownloads,
|
"enableDownloads": conf.Server.EnableDownloads,
|
||||||
"enableFavourites": conf.Server.EnableFavourites,
|
"enableFavourites": conf.Server.EnableFavourites,
|
||||||
"losslessFormats": strings.ToUpper(strings.Join(consts.LosslessFormats, ",")),
|
"losslessFormats": strings.ToUpper(strings.Join(consts.LosslessFormats, ",")),
|
||||||
|
"enableStarRating": conf.Server.EnableStarRating,
|
||||||
"devActivityPanel": conf.Server.DevActivityPanel,
|
"devActivityPanel": conf.Server.DevActivityPanel,
|
||||||
"devFastAccessCoverArt": conf.Server.DevFastAccessCoverArt,
|
"devFastAccessCoverArt": conf.Server.DevFastAccessCoverArt,
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,17 @@ var _ = Describe("serveIndex", func() {
|
||||||
Expect(config).To(HaveKeyWithValue("enableFavourites", true))
|
Expect(config).To(HaveKeyWithValue("enableFavourites", true))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("sets the enableStarRating", func() {
|
||||||
|
conf.Server.EnableStarRating = true
|
||||||
|
r := httptest.NewRequest("GET", "/index.html", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
serveIndex(ds, fs)(w, r)
|
||||||
|
|
||||||
|
config := extractAppConfig(w.Body.String())
|
||||||
|
Expect(config).To(HaveKeyWithValue("enableStarRating", true))
|
||||||
|
})
|
||||||
|
|
||||||
It("sets the gaTrackingId", func() {
|
It("sets the gaTrackingId", func() {
|
||||||
conf.Server.GATrackingID = "UA-12345"
|
conf.Server.GATrackingID = "UA-12345"
|
||||||
r := httptest.NewRequest("GET", "/index.html", nil)
|
r := httptest.NewRequest("GET", "/index.html", nil)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
formatRange,
|
formatRange,
|
||||||
SizeField,
|
SizeField,
|
||||||
LoveButton,
|
LoveButton,
|
||||||
|
RatingField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
|
||||||
|
@ -167,7 +168,10 @@ const AlbumDetails = ({ record }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.details}>
|
<div className={classes.details}>
|
||||||
<CardContent className={classes.content}>
|
<CardContent className={classes.content}>
|
||||||
<Typography variant="h5" className={classes.recordName}>
|
<Typography
|
||||||
|
variant={isDesktop ? 'h5' : 'h6'}
|
||||||
|
className={classes.recordName}
|
||||||
|
>
|
||||||
{record.name}
|
{record.name}
|
||||||
{config.enableFavourites && (
|
{config.enableFavourites && (
|
||||||
<LoveButton
|
<LoveButton
|
||||||
|
@ -195,6 +199,13 @@ const AlbumDetails = ({ record }) => {
|
||||||
{' · '}
|
{' · '}
|
||||||
<SizeField record={record} source="size" />
|
<SizeField record={record} source="size" />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
record={record}
|
||||||
|
resource={'album'}
|
||||||
|
size={isDesktop ? 'medium' : 'small'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isDesktop && record['comment'] && <AlbumComment record={record} />}
|
{isDesktop && record['comment'] && <AlbumComment record={record} />}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
SimpleList,
|
SimpleList,
|
||||||
MultiLineTextField,
|
MultiLineTextField,
|
||||||
AlbumContextMenu,
|
AlbumContextMenu,
|
||||||
|
RatingField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
|
||||||
|
@ -39,6 +40,9 @@ const useStyles = makeStyles({
|
||||||
'& $contextMenu': {
|
'& $contextMenu': {
|
||||||
visibility: 'visible',
|
visibility: 'visible',
|
||||||
},
|
},
|
||||||
|
'& $ratingField': {
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tableCell: {
|
tableCell: {
|
||||||
|
@ -47,6 +51,9 @@ const useStyles = makeStyles({
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
},
|
},
|
||||||
|
ratingField: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const AlbumDetails = (props) => {
|
const AlbumDetails = (props) => {
|
||||||
|
@ -105,7 +112,23 @@ const AlbumListView = ({
|
||||||
return isXsmall ? (
|
return isXsmall ? (
|
||||||
<SimpleList
|
<SimpleList
|
||||||
primaryText={(r) => r.name}
|
primaryText={(r) => r.name}
|
||||||
secondaryText={(r) => r.albumArtist}
|
secondaryText={(r) => (
|
||||||
|
<>
|
||||||
|
{r.albumArtist}
|
||||||
|
{config.enableStarRating && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<RatingField
|
||||||
|
record={r}
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
source={'rating'}
|
||||||
|
resource={'album'}
|
||||||
|
size={'small'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
tertiaryText={(r) => (
|
tertiaryText={(r) => (
|
||||||
<>
|
<>
|
||||||
<RangeField record={r} source={'year'} sortBy={'maxYear'} />
|
<RangeField record={r} source={'year'} sortBy={'maxYear'} />
|
||||||
|
@ -129,6 +152,14 @@ const AlbumListView = ({
|
||||||
{isDesktop && <NumberField source="playCount" sortByOrder={'DESC'} />}
|
{isDesktop && <NumberField source="playCount" sortByOrder={'DESC'} />}
|
||||||
<RangeField source={'year'} sortBy={'maxYear'} sortByOrder={'DESC'} />
|
<RangeField source={'year'} sortBy={'maxYear'} sortByOrder={'DESC'} />
|
||||||
{isDesktop && <DurationField source="duration" />}
|
{isDesktop && <DurationField source="duration" />}
|
||||||
|
{config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source={'rating'}
|
||||||
|
resource={'album'}
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<AlbumContextMenu
|
<AlbumContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortBy={'starred ASC, starredAt ASC'}
|
sortBy={'starred ASC, starredAt ASC'}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
SongDatagrid,
|
SongDatagrid,
|
||||||
SongDetails,
|
SongDetails,
|
||||||
SongTitleField,
|
SongTitleField,
|
||||||
|
RatingField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import { AddToPlaylistDialog } from '../dialogs'
|
import { AddToPlaylistDialog } from '../dialogs'
|
||||||
import { QualityInfo } from '../common/QualityInfo'
|
import { QualityInfo } from '../common/QualityInfo'
|
||||||
|
@ -63,11 +64,17 @@ const useStyles = makeStyles(
|
||||||
'& $contextMenu': {
|
'& $contextMenu': {
|
||||||
visibility: 'visible',
|
visibility: 'visible',
|
||||||
},
|
},
|
||||||
|
'& $ratingField': {
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
visibility: (props) => (props.isDesktop ? 'hidden' : 'visible'),
|
visibility: (props) => (props.isDesktop ? 'hidden' : 'visible'),
|
||||||
},
|
},
|
||||||
|
ratingField: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{ name: 'RaList' }
|
{ name: 'RaList' }
|
||||||
)
|
)
|
||||||
|
@ -121,6 +128,14 @@ const AlbumSongs = (props) => {
|
||||||
{isDesktop && <TextField source="artist" sortable={false} />}
|
{isDesktop && <TextField source="artist" sortable={false} />}
|
||||||
<DurationField source="duration" sortable={false} />
|
<DurationField source="duration" sortable={false} />
|
||||||
<QualityInfo source="quality" sortable={false} />
|
<QualityInfo source="quality" sortable={false} />
|
||||||
|
{isDesktop && config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source="rating"
|
||||||
|
resource={'albumSong'}
|
||||||
|
sortable={false}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SongContextMenu
|
<SongContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortable={false}
|
sortable={false}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import VideoLibraryIcon from '@material-ui/icons/VideoLibrary'
|
||||||
import RepeatIcon from '@material-ui/icons/Repeat'
|
import RepeatIcon from '@material-ui/icons/Repeat'
|
||||||
import AlbumIcon from '@material-ui/icons/Album'
|
import AlbumIcon from '@material-ui/icons/Album'
|
||||||
import FavoriteIcon from '@material-ui/icons/Favorite'
|
import FavoriteIcon from '@material-ui/icons/Favorite'
|
||||||
|
import StarIcon from '@material-ui/icons/Star'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -18,6 +19,12 @@ export default {
|
||||||
params: 'sort=starred_at&order=DESC&filter={"starred":true}',
|
params: 'sort=starred_at&order=DESC&filter={"starred":true}',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
...(config.enableStarRating && {
|
||||||
|
topRated: {
|
||||||
|
icon: StarIcon,
|
||||||
|
params: 'sort=rating&order=DESC&filter={"has_rating":true}',
|
||||||
|
},
|
||||||
|
}),
|
||||||
recentlyAdded: {
|
recentlyAdded: {
|
||||||
icon: LibraryAddIcon,
|
icon: LibraryAddIcon,
|
||||||
params: 'sort=recently_added&order=DESC',
|
params: 'sort=recently_added&order=DESC',
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
QuickFilter,
|
QuickFilter,
|
||||||
useGetHandleArtistClick,
|
useGetHandleArtistClick,
|
||||||
ArtistSimpleList,
|
ArtistSimpleList,
|
||||||
|
RatingField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
|
@ -32,11 +33,17 @@ const useStyles = makeStyles({
|
||||||
'& $contextMenu': {
|
'& $contextMenu': {
|
||||||
visibility: 'visible',
|
visibility: 'visible',
|
||||||
},
|
},
|
||||||
|
'& $ratingField': {
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
},
|
},
|
||||||
|
ratingField: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const ArtistFilter = (props) => (
|
const ArtistFilter = (props) => (
|
||||||
|
@ -68,6 +75,14 @@ const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
|
||||||
<NumberField source="albumCount" sortByOrder={'DESC'} />
|
<NumberField source="albumCount" sortByOrder={'DESC'} />
|
||||||
<NumberField source="songCount" sortByOrder={'DESC'} />
|
<NumberField source="songCount" sortByOrder={'DESC'} />
|
||||||
<NumberField source="playCount" sortByOrder={'DESC'} />
|
<NumberField source="playCount" sortByOrder={'DESC'} />
|
||||||
|
{config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source="rating"
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
resource={'artist'}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ArtistContextMenu
|
<ArtistContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortBy={'starred ASC, starredAt ASC'}
|
sortBy={'starred ASC, starredAt ASC'}
|
||||||
|
|
|
@ -7,7 +7,8 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||||
import ListItemText from '@material-ui/core/ListItemText'
|
import ListItemText from '@material-ui/core/ListItemText'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { sanitizeListRestProps } from 'ra-core'
|
import { sanitizeListRestProps } from 'ra-core'
|
||||||
import { ArtistContextMenu } from './index'
|
import { ArtistContextMenu, RatingField } from './index'
|
||||||
|
import config from '../config'
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
{
|
{
|
||||||
|
@ -48,7 +49,17 @@ export const ArtistSimpleList = ({
|
||||||
<ListItem className={classes.listItem} button={true}>
|
<ListItem className={classes.listItem} button={true}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<div className={classes.title}>{data[id].name}</div>
|
<>
|
||||||
|
<div className={classes.title}>{data[id].name}</div>
|
||||||
|
{config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
record={data[id]}
|
||||||
|
source={'rating'}
|
||||||
|
resource={'artist'}
|
||||||
|
size={'small'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction className={classes.rightIcon}>
|
<ListItemSecondaryAction className={classes.rightIcon}>
|
||||||
|
|
73
ui/src/common/RatingField.js
Normal file
73
ui/src/common/RatingField.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
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 StarBorderIcon from '@material-ui/icons/StarBorder'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { useRating } from './useRating'
|
||||||
|
|
||||||
|
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,
|
||||||
|
record,
|
||||||
|
visible,
|
||||||
|
className,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
}) => {
|
||||||
|
const [rate, rating] = useRating(resource, record)
|
||||||
|
const classes = useStyles({ visible, rating: record.rating, color })
|
||||||
|
|
||||||
|
const stopPropagation = (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRating = useCallback(
|
||||||
|
(e, val) => {
|
||||||
|
rate(val, e.target.name)
|
||||||
|
},
|
||||||
|
[rate]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span onClick={(e) => stopPropagation(e)}>
|
||||||
|
<Rating
|
||||||
|
name={record.id}
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
classes.rating,
|
||||||
|
rating > 0 ? classes.show : classes.hide
|
||||||
|
)}
|
||||||
|
value={rating}
|
||||||
|
size={size}
|
||||||
|
emptyIcon={<StarBorderIcon fontSize="inherit" />}
|
||||||
|
onChange={(e, newValue) => handleRating(e, newValue)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RatingField.propTypes = {
|
||||||
|
resource: PropTypes.string.isRequired,
|
||||||
|
record: PropTypes.object.isRequired,
|
||||||
|
visible: PropTypes.bool,
|
||||||
|
size: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
RatingField.defaultProps = {
|
||||||
|
record: {},
|
||||||
|
visible: true,
|
||||||
|
size: 'small',
|
||||||
|
color: 'inherit',
|
||||||
|
}
|
|
@ -7,9 +7,10 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||||
import ListItemText from '@material-ui/core/ListItemText'
|
import ListItemText from '@material-ui/core/ListItemText'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { sanitizeListRestProps } from 'ra-core'
|
import { sanitizeListRestProps } from 'ra-core'
|
||||||
import { DurationField, SongContextMenu } from './index'
|
import { DurationField, SongContextMenu, RatingField } from './index'
|
||||||
import { setTrack } from '../actions'
|
import { setTrack } from '../actions'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
import config from '../config'
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
{
|
{
|
||||||
|
@ -77,17 +78,27 @@ export const SongSimpleList = ({
|
||||||
<div className={classes.title}>{data[id].title}</div>
|
<div className={classes.title}>{data[id].title}</div>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
<span className={classes.secondary}>
|
<>
|
||||||
<span className={classes.artist}>
|
<span className={classes.secondary}>
|
||||||
{data[id].artist}
|
<span className={classes.artist}>
|
||||||
|
{data[id].artist}
|
||||||
|
</span>
|
||||||
|
<span className={classes.timeStamp}>
|
||||||
|
<DurationField
|
||||||
|
record={data[id]}
|
||||||
|
source={'duration'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className={classes.timeStamp}>
|
{config.enableStarRating && (
|
||||||
<DurationField
|
<RatingField
|
||||||
record={data[id]}
|
record={data[id]}
|
||||||
source={'duration'}
|
source={'rating'}
|
||||||
|
resource={'song'}
|
||||||
|
size={'small'}
|
||||||
/>
|
/>
|
||||||
</span>
|
)}
|
||||||
</span>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction className={classes.rightIcon}>
|
<ListItemSecondaryAction className={classes.rightIcon}>
|
||||||
|
|
|
@ -28,3 +28,5 @@ export * from './useTraceUpdate'
|
||||||
export * from './Writable'
|
export * from './Writable'
|
||||||
export * from './SongSimpleList'
|
export * from './SongSimpleList'
|
||||||
export * from './ArtistSimpleList'
|
export * from './ArtistSimpleList'
|
||||||
|
export * from './RatingField'
|
||||||
|
export * from './useRating'
|
||||||
|
|
47
ui/src/common/useRating.js
Normal file
47
ui/src/common/useRating.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
|
import { useDataProvider, useNotify } from 'react-admin'
|
||||||
|
import subsonic from '../subsonic'
|
||||||
|
|
||||||
|
export const useRating = (resource, record) => {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const notify = useNotify()
|
||||||
|
const dataProvider = useDataProvider()
|
||||||
|
const mountedRef = useRef(false)
|
||||||
|
const rating = record.rating
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mountedRef.current = true
|
||||||
|
return () => {
|
||||||
|
mountedRef.current = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const refreshRating = useCallback(() => {
|
||||||
|
dataProvider
|
||||||
|
.getOne(resource, { id: record.id })
|
||||||
|
.then(() => {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log('Error encountered: ' + e)
|
||||||
|
})
|
||||||
|
}, [dataProvider, record, resource])
|
||||||
|
|
||||||
|
const rate = (val, id) => {
|
||||||
|
setLoading(true)
|
||||||
|
subsonic
|
||||||
|
.setRating(id, val)
|
||||||
|
.then(refreshRating)
|
||||||
|
.catch((e) => {
|
||||||
|
console.log('Error setting star rating: ', e)
|
||||||
|
notify('ra.page.error', 'warning')
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return [rate, rating, loading]
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ const defaultConfig = {
|
||||||
gaTrackingId: '',
|
gaTrackingId: '',
|
||||||
devActivityPanel: true,
|
devActivityPanel: true,
|
||||||
devFastAccessCoverArt: false,
|
devFastAccessCoverArt: false,
|
||||||
|
enableStarRating: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
let config
|
let config
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"bitRate": "Bit rate",
|
"bitRate": "Bit rate",
|
||||||
"discSubtitle": "Disc Subtitle",
|
"discSubtitle": "Disc Subtitle",
|
||||||
"starred": "Favourite",
|
"starred": "Favourite",
|
||||||
|
"rating": "Rating",
|
||||||
"comment": "Comment",
|
"comment": "Comment",
|
||||||
"quality": "Quality"
|
"quality": "Quality"
|
||||||
},
|
},
|
||||||
|
@ -45,7 +46,8 @@
|
||||||
"compilation": "Compilation",
|
"compilation": "Compilation",
|
||||||
"year": "Year",
|
"year": "Year",
|
||||||
"updatedAt": "Updated at",
|
"updatedAt": "Updated at",
|
||||||
"comment": "Comment"
|
"comment": "Comment",
|
||||||
|
"rating": "Rating"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"playAll": "Play",
|
"playAll": "Play",
|
||||||
|
@ -61,7 +63,8 @@
|
||||||
"recentlyAdded": "Recently Added",
|
"recentlyAdded": "Recently Added",
|
||||||
"recentlyPlayed": "Recently Played",
|
"recentlyPlayed": "Recently Played",
|
||||||
"mostPlayed": "Most Played",
|
"mostPlayed": "Most Played",
|
||||||
"starred": "Favourites"
|
"starred": "Favourites",
|
||||||
|
"topRated": "Top Rated"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"artist": {
|
"artist": {
|
||||||
|
@ -70,7 +73,8 @@
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"albumCount": "Album Count",
|
"albumCount": "Album Count",
|
||||||
"songCount": "Song Count",
|
"songCount": "Song Count",
|
||||||
"playCount": "Plays"
|
"playCount": "Plays",
|
||||||
|
"rating": "Rating"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
QuickFilter,
|
QuickFilter,
|
||||||
SongTitleField,
|
SongTitleField,
|
||||||
SongSimpleList,
|
SongSimpleList,
|
||||||
|
RatingField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { setTrack } from '../actions'
|
import { setTrack } from '../actions'
|
||||||
|
@ -40,11 +41,17 @@ const useStyles = makeStyles({
|
||||||
'& $contextMenu': {
|
'& $contextMenu': {
|
||||||
visibility: 'visible',
|
visibility: 'visible',
|
||||||
},
|
},
|
||||||
|
'& $ratingField': {
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
},
|
},
|
||||||
|
ratingField: {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const SongFilter = (props) => (
|
const SongFilter = (props) => (
|
||||||
|
@ -114,6 +121,14 @@ const SongList = (props) => {
|
||||||
)}
|
)}
|
||||||
<QualityInfo source="quality" sortable={false} />
|
<QualityInfo source="quality" sortable={false} />
|
||||||
<DurationField source="duration" />
|
<DurationField source="duration" />
|
||||||
|
{config.enableStarRating && (
|
||||||
|
<RatingField
|
||||||
|
source="rating"
|
||||||
|
sortByOrder={'DESC'}
|
||||||
|
resource={'song'}
|
||||||
|
className={classes.ratingField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SongContextMenu
|
<SongContextMenu
|
||||||
source={'starred'}
|
source={'starred'}
|
||||||
sortBy={'starred ASC, starredAt ASC'}
|
sortBy={'starred ASC, starredAt ASC'}
|
||||||
|
|
|
@ -40,6 +40,9 @@ const getCoverArtUrl = (record, size) => {
|
||||||
return url('getCoverArt', record.coverArtId || 'not_found', options)
|
return url('getCoverArt', record.coverArtId || 'not_found', options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setRating = (id, rating) =>
|
||||||
|
fetchUtils.fetchJson(url('setRating', id, { rating }))
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
url,
|
url,
|
||||||
getCoverArtUrl,
|
getCoverArtUrl,
|
||||||
|
@ -47,4 +50,5 @@ export default {
|
||||||
download,
|
download,
|
||||||
star,
|
star,
|
||||||
unstar,
|
unstar,
|
||||||
|
setRating,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue