diff --git a/conf/configuration.go b/conf/configuration.go
index f6a8a5bcb..9293c6f4a 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -41,6 +41,7 @@ type configOptions struct {
UIWelcomeMessage string
EnableGravatar bool
EnableFavourites bool
+ EnableStarRating bool
GATrackingID string
AuthRequestLimit int
AuthWindowLength time.Duration
@@ -145,6 +146,7 @@ func init() {
viper.SetDefault("uiwelcomemessage", "")
viper.SetDefault("enablegravatar", false)
viper.SetDefault("enablefavourites", true)
+ viper.SetDefault("enablestarrating", true)
viper.SetDefault("gatrackingid", "")
viper.SetDefault("authrequestlimit", 5)
viper.SetDefault("authwindowlength", 20*time.Second)
diff --git a/server/app/serve_index.go b/server/app/serve_index.go
index b1a5ac47d..4b36ae87e 100644
--- a/server/app/serve_index.go
+++ b/server/app/serve_index.go
@@ -44,6 +44,7 @@ func serveIndex(ds model.DataStore, fs fs.FS) http.HandlerFunc {
"enableDownloads": conf.Server.EnableDownloads,
"enableFavourites": conf.Server.EnableFavourites,
"losslessFormats": strings.ToUpper(strings.Join(consts.LosslessFormats, ",")),
+ "enableStarRating": conf.Server.EnableStarRating,
"devActivityPanel": conf.Server.DevActivityPanel,
"devFastAccessCoverArt": conf.Server.DevFastAccessCoverArt,
}
diff --git a/server/app/serve_index_test.go b/server/app/serve_index_test.go
index 90f47dab6..eae43cfd3 100644
--- a/server/app/serve_index_test.go
+++ b/server/app/serve_index_test.go
@@ -136,6 +136,17 @@ var _ = Describe("serveIndex", func() {
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() {
conf.Server.GATrackingID = "UA-12345"
r := httptest.NewRequest("GET", "/index.html", nil)
diff --git a/ui/src/album/AlbumDetails.js b/ui/src/album/AlbumDetails.js
index 9cb8f6e16..f04e4b380 100644
--- a/ui/src/album/AlbumDetails.js
+++ b/ui/src/album/AlbumDetails.js
@@ -19,6 +19,7 @@ import {
formatRange,
SizeField,
LoveButton,
+ RatingField,
} from '../common'
import config from '../config'
@@ -167,7 +168,10 @@ const AlbumDetails = ({ record }) => {
-
+
{record.name}
{config.enableFavourites && (
{
{' ยท '}
+ {config.enableStarRating && (
+
+ )}
{isDesktop && record['comment'] && }
diff --git a/ui/src/album/AlbumListView.js b/ui/src/album/AlbumListView.js
index cecaac5cd..99ef1e5b4 100644
--- a/ui/src/album/AlbumListView.js
+++ b/ui/src/album/AlbumListView.js
@@ -25,6 +25,7 @@ import {
SimpleList,
MultiLineTextField,
AlbumContextMenu,
+ RatingField,
} from '../common'
import config from '../config'
@@ -39,6 +40,9 @@ const useStyles = makeStyles({
'& $contextMenu': {
visibility: 'visible',
},
+ '& $ratingField': {
+ visibility: 'visible',
+ },
},
},
tableCell: {
@@ -47,6 +51,9 @@ const useStyles = makeStyles({
contextMenu: {
visibility: 'hidden',
},
+ ratingField: {
+ visibility: 'hidden',
+ },
})
const AlbumDetails = (props) => {
@@ -105,7 +112,23 @@ const AlbumListView = ({
return isXsmall ? (
r.name}
- secondaryText={(r) => r.albumArtist}
+ secondaryText={(r) => (
+ <>
+ {r.albumArtist}
+ {config.enableStarRating && (
+ <>
+
+
+ >
+ )}
+ >
+ )}
tertiaryText={(r) => (
<>
@@ -129,6 +152,14 @@ const AlbumListView = ({
{isDesktop && }
{isDesktop && }
+ {config.enableStarRating && (
+
+ )}
(props.isDesktop ? 'hidden' : 'visible'),
},
+ ratingField: {
+ visibility: 'hidden',
+ },
}),
{ name: 'RaList' }
)
@@ -121,6 +128,14 @@ const AlbumSongs = (props) => {
{isDesktop && }
+ {isDesktop && config.enableStarRating && (
+
+ )}
(
@@ -68,6 +75,14 @@ const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
+ {config.enableStarRating && (
+
+ )}
{data[id].name}
+ <>
+ {data[id].name}
+ {config.enableStarRating && (
+
+ )}
+ >
}
/>
diff --git a/ui/src/common/RatingField.js b/ui/src/common/RatingField.js
new file mode 100644
index 000000000..b22c535b8
--- /dev/null
+++ b/ui/src/common/RatingField.js
@@ -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 (
+ stopPropagation(e)}>
+ 0 ? classes.show : classes.hide
+ )}
+ value={rating}
+ size={size}
+ emptyIcon={}
+ onChange={(e, newValue) => handleRating(e, newValue)}
+ />
+
+ )
+}
+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',
+}
diff --git a/ui/src/common/SongSimpleList.js b/ui/src/common/SongSimpleList.js
index ef3918a41..e6644dae1 100644
--- a/ui/src/common/SongSimpleList.js
+++ b/ui/src/common/SongSimpleList.js
@@ -7,9 +7,10 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import { makeStyles } from '@material-ui/core/styles'
import { sanitizeListRestProps } from 'ra-core'
-import { DurationField, SongContextMenu } from './index'
+import { DurationField, SongContextMenu, RatingField } from './index'
import { setTrack } from '../actions'
import { useDispatch } from 'react-redux'
+import config from '../config'
const useStyles = makeStyles(
{
@@ -77,17 +78,27 @@ export const SongSimpleList = ({
{data[id].title}
}
secondary={
-
-
- {data[id].artist}
+ <>
+
+
+ {data[id].artist}
+
+
+
+
-
-
-
-
+ )}
+ >
}
/>
diff --git a/ui/src/common/index.js b/ui/src/common/index.js
index fa952af99..a7a8a50ee 100644
--- a/ui/src/common/index.js
+++ b/ui/src/common/index.js
@@ -28,3 +28,5 @@ export * from './useTraceUpdate'
export * from './Writable'
export * from './SongSimpleList'
export * from './ArtistSimpleList'
+export * from './RatingField'
+export * from './useRating'
diff --git a/ui/src/common/useRating.js b/ui/src/common/useRating.js
new file mode 100644
index 000000000..43fc40579
--- /dev/null
+++ b/ui/src/common/useRating.js
@@ -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]
+}
diff --git a/ui/src/config.js b/ui/src/config.js
index c2f9449c3..fcc491e8e 100644
--- a/ui/src/config.js
+++ b/ui/src/config.js
@@ -15,6 +15,7 @@ const defaultConfig = {
gaTrackingId: '',
devActivityPanel: true,
devFastAccessCoverArt: false,
+ enableStarRating: true,
}
let config
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index eef3fbed8..44300c1fa 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -20,6 +20,7 @@
"bitRate": "Bit rate",
"discSubtitle": "Disc Subtitle",
"starred": "Favourite",
+ "rating": "Rating",
"comment": "Comment",
"quality": "Quality"
},
@@ -45,7 +46,8 @@
"compilation": "Compilation",
"year": "Year",
"updatedAt": "Updated at",
- "comment": "Comment"
+ "comment": "Comment",
+ "rating": "Rating"
},
"actions": {
"playAll": "Play",
@@ -61,7 +63,8 @@
"recentlyAdded": "Recently Added",
"recentlyPlayed": "Recently Played",
"mostPlayed": "Most Played",
- "starred": "Favourites"
+ "starred": "Favourites",
+ "topRated": "Top Rated"
}
},
"artist": {
@@ -70,7 +73,8 @@
"name": "Name",
"albumCount": "Album Count",
"songCount": "Song Count",
- "playCount": "Plays"
+ "playCount": "Plays",
+ "rating": "Rating"
}
},
"user": {
diff --git a/ui/src/song/SongList.js b/ui/src/song/SongList.js
index a5efcf7bc..58e089b5e 100644
--- a/ui/src/song/SongList.js
+++ b/ui/src/song/SongList.js
@@ -17,6 +17,7 @@ import {
QuickFilter,
SongTitleField,
SongSimpleList,
+ RatingField,
} from '../common'
import { useDispatch } from 'react-redux'
import { setTrack } from '../actions'
@@ -40,11 +41,17 @@ const useStyles = makeStyles({
'& $contextMenu': {
visibility: 'visible',
},
+ '& $ratingField': {
+ visibility: 'visible',
+ },
},
},
contextMenu: {
visibility: 'hidden',
},
+ ratingField: {
+ visibility: 'hidden',
+ },
})
const SongFilter = (props) => (
@@ -114,6 +121,14 @@ const SongList = (props) => {
)}
+ {config.enableStarRating && (
+
+ )}
{
return url('getCoverArt', record.coverArtId || 'not_found', options)
}
+const setRating = (id, rating) =>
+ fetchUtils.fetchJson(url('setRating', id, { rating }))
+
export default {
url,
getCoverArtUrl,
@@ -47,4 +50,5 @@ export default {
download,
star,
unstar,
+ setRating,
}