Add ToggleStar to SongContextMenu (WIP)

This commit is contained in:
Deluan 2020-05-22 15:23:42 -04:00
parent e21262675e
commit 8a68cecdb9
14 changed files with 132 additions and 42 deletions

View File

@ -37,6 +37,7 @@ type nd struct {
DevLogSourceLine bool `default:"false"` DevLogSourceLine bool `default:"false"`
DevAutoCreateAdminPassword string `default:""` DevAutoCreateAdminPassword string `default:""`
DevEnableUIPlaylists bool `default:"true"` DevEnableUIPlaylists bool `default:"true"`
DevEnableUIStarred bool `default:"false"`
} }
var Server = &nd{} var Server = &nd{}

View File

@ -3,6 +3,8 @@ package model
import "time" import "time"
type Album struct { type Album struct {
Annotations
ID string `json:"id" orm:"column(id)"` ID string `json:"id" orm:"column(id)"`
Name string `json:"name"` Name string `json:"name"`
CoverArtPath string `json:"coverArtPath"` CoverArtPath string `json:"coverArtPath"`
@ -25,13 +27,6 @@ type Album struct {
OrderAlbumArtistName string `json:"orderAlbumArtistName"` OrderAlbumArtistName string `json:"orderAlbumArtistName"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
// Annotations
PlayCount int64 `json:"playCount" orm:"-"`
PlayDate time.Time `json:"playDate" orm:"-"`
Rating int `json:"rating" orm:"-"`
Starred bool `json:"starred" orm:"-"`
StarredAt time.Time `json:"starredAt" orm:"-"`
} }
type Albums []Album type Albums []Album
@ -48,3 +43,7 @@ type AlbumRepository interface {
Refresh(ids ...string) error Refresh(ids ...string) error
AnnotatedRepository AnnotatedRepository
} }
func (a Album) GetAnnotations() Annotations {
return a.Annotations
}

View File

@ -2,6 +2,18 @@ package model
import "time" import "time"
type Annotations struct {
PlayCount int64 `json:"playCount"`
PlayDate time.Time `json:"playDate"`
Rating int `json:"rating"`
Starred bool `json:"starred"`
StarredAt time.Time `json:"starredAt"`
}
type AnnotatedModel interface {
GetAnnotations() Annotations
}
type AnnotatedRepository interface { type AnnotatedRepository interface {
IncPlayCount(itemID string, ts time.Time) error IncPlayCount(itemID string, ts time.Time) error
SetStar(starred bool, itemIDs ...string) error SetStar(starred bool, itemIDs ...string) error

View File

@ -1,8 +1,8 @@
package model package model
import "time"
type Artist struct { type Artist struct {
Annotations
ID string `json:"id" orm:"column(id)"` ID string `json:"id" orm:"column(id)"`
Name string `json:"name"` Name string `json:"name"`
AlbumCount int `json:"albumCount"` AlbumCount int `json:"albumCount"`
@ -10,13 +10,6 @@ type Artist struct {
FullText string `json:"fullText"` FullText string `json:"fullText"`
SortArtistName string `json:"sortArtistName"` SortArtistName string `json:"sortArtistName"`
OrderArtistName string `json:"orderArtistName"` OrderArtistName string `json:"orderArtistName"`
// Annotations
PlayCount int64 `json:"playCount" orm:"-"`
PlayDate time.Time `json:"playDate" orm:"-"`
Rating int `json:"rating" orm:"-"`
Starred bool `json:"starred" orm:"-"`
StarredAt time.Time `json:"starredAt" orm:"-"`
} }
type Artists []Artist type Artists []Artist
@ -38,3 +31,7 @@ type ArtistRepository interface {
GetIndex() (ArtistIndexes, error) GetIndex() (ArtistIndexes, error)
AnnotatedRepository AnnotatedRepository
} }
func (a Artist) GetAnnotations() Annotations {
return a.Annotations
}

View File

@ -6,6 +6,8 @@ import (
) )
type MediaFile struct { type MediaFile struct {
Annotations
ID string `json:"id" orm:"pk;column(id)"` ID string `json:"id" orm:"pk;column(id)"`
Path string `json:"path"` Path string `json:"path"`
Title string `json:"title"` Title string `json:"title"`
@ -36,13 +38,6 @@ type MediaFile struct {
Compilation bool `json:"compilation"` Compilation bool `json:"compilation"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
// Annotations
PlayCount int64 `json:"playCount" orm:"-"`
PlayDate time.Time `json:"playDate" orm:"-"`
Rating int `json:"rating" orm:"-"`
Starred bool `json:"starred" orm:"-"`
StarredAt time.Time `json:"starredAt" orm:"-"`
} }
func (mf *MediaFile) ContentType() string { func (mf *MediaFile) ContentType() string {
@ -67,3 +62,7 @@ type MediaFileRepository interface {
AnnotatedRepository AnnotatedRepository
} }
func (mf MediaFile) GetAnnotations() Annotations {
return mf.Annotations
}

View File

@ -74,12 +74,14 @@ func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuild
func (r *albumRepository) Get(id string) (*model.Album, error) { func (r *albumRepository) Get(id string) (*model.Album, error) {
sq := r.selectAlbum().Where(Eq{"id": id}) sq := r.selectAlbum().Where(Eq{"id": id})
var res model.Album var res model.Albums
err := r.queryOne(sq, &res) if err := r.queryAll(sq, &res); err != nil {
if err != nil {
return nil, err return nil, err
} }
return &res, nil if len(res) == 0 {
return nil, model.ErrNotFound
}
return &res[0], nil
} }
func (r *albumRepository) FindByArtist(artistId string) (model.Albums, error) { func (r *albumRepository) FindByArtist(artistId string) (model.Albums, error) {

View File

@ -55,9 +55,14 @@ func (r *artistRepository) Put(a *model.Artist) error {
func (r *artistRepository) Get(id string) (*model.Artist, error) { func (r *artistRepository) Get(id string) (*model.Artist, error) {
sel := r.selectArtist().Where(Eq{"id": id}) sel := r.selectArtist().Where(Eq{"id": id})
var res model.Artist var res model.Artists
err := r.queryOne(sel, &res) if err := r.queryAll(sel, &res); err != nil {
return &res, err return nil, err
}
if len(res) == 0 {
return nil, model.ErrNotFound
}
return &res[0], nil
} }
func (r *artistRepository) GetAll(options ...model.QueryOptions) (model.Artists, error) { func (r *artistRepository) GetAll(options ...model.QueryOptions) (model.Artists, error) {

View File

@ -54,9 +54,14 @@ func (r mediaFileRepository) selectMediaFile(options ...model.QueryOptions) Sele
func (r mediaFileRepository) Get(id string) (*model.MediaFile, error) { func (r mediaFileRepository) Get(id string) (*model.MediaFile, error) {
sel := r.selectMediaFile().Where(Eq{"id": id}) sel := r.selectMediaFile().Where(Eq{"id": id})
var res model.MediaFile var res model.MediaFiles
err := r.queryOne(sel, &res) if err := r.queryAll(sel, &res); err != nil {
return &res, err return nil, err
}
if len(res) == 0 {
return nil, model.ErrNotFound
}
return &res[0], nil
} }
func (r mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) { func (r mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
@ -155,8 +160,20 @@ func (r mediaFileRepository) EntityName() string {
} }
func (r mediaFileRepository) NewInstance() interface{} { func (r mediaFileRepository) NewInstance() interface{} {
return model.MediaFile{} return &model.MediaFile{}
}
func (r mediaFileRepository) Save(entity interface{}) (string, error) {
mf := entity.(*model.MediaFile)
err := r.Put(mf)
return mf.ID, err
}
func (r mediaFileRepository) Update(entity interface{}, cols ...string) error {
mf := entity.(*model.MediaFile)
return r.Put(mf)
} }
var _ model.MediaFileRepository = (*mediaFileRepository)(nil) var _ model.MediaFileRepository = (*mediaFileRepository)(nil)
var _ model.ResourceRepository = (*mediaFileRepository)(nil) var _ model.ResourceRepository = (*mediaFileRepository)(nil)
var _ rest.Persistable = (*mediaFileRepository)(nil)

View File

@ -21,7 +21,7 @@ func TestPersistence(t *testing.T) {
tests.Init(t, true) tests.Init(t, true)
//os.Remove("./test-123.db") //os.Remove("./test-123.db")
//conf.Server.Path = "./test-123.db" //conf.Server.DbPath = "./test-123.db"
conf.Server.DbPath = "file::memory:?cache=shared" conf.Server.DbPath = "file::memory:?cache=shared"
_ = orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath) _ = orm.RegisterDataBase("default", db.Driver, conf.Server.DbPath)
db.EnsureLatestVersion() db.EnsureLatestVersion()

View File

@ -96,3 +96,12 @@ func (r sqlRepository) cleanAnnotations() error {
} }
return nil return nil
} }
func (r sqlRepository) updateAnnotations(id string, m interface{}) error {
ans := m.(model.AnnotatedModel).GetAnnotations()
err := r.SetStar(ans.Starred, id)
if err != nil {
return err
}
return r.SetRating(ans.Rating, id)
}

View File

@ -112,6 +112,8 @@ func (r sqlRepository) executeSQL(sq Sqlizer) (int64, error) {
return res.RowsAffected() return res.RowsAffected()
} }
// Note: Due to a bug in the QueryRow, this method does not map any embedded structs (ex: annotations)
// In this case, use the queryAll method and get the first item of the returned list
func (r sqlRepository) queryOne(sq Sqlizer, response interface{}) error { func (r sqlRepository) queryOne(sq Sqlizer, response interface{}) error {
query, args, err := sq.ToSql() query, args, err := sq.ToSql()
if err != nil { if err != nil {
@ -169,7 +171,10 @@ func (r sqlRepository) put(id string, m interface{}) (newId string, err error) {
return "", err return "", err
} }
if count > 0 { if count > 0 {
return id, nil if _, ok := m.(model.AnnotatedModel); ok {
err = r.updateAnnotations(id, m)
}
return id, err
} }
} }
// If does not have an id OR could not update (new record with predefined id) // If does not have an id OR could not update (new record with predefined id)

View File

@ -31,6 +31,7 @@ func ServeIndex(ds model.DataStore, fs http.FileSystem) http.HandlerFunc {
"loginBackgroundURL": conf.Server.UILoginBackgroundURL, "loginBackgroundURL": conf.Server.UILoginBackgroundURL,
"enableTranscodingConfig": conf.Server.EnableTranscodingConfig, "enableTranscodingConfig": conf.Server.EnableTranscodingConfig,
"enablePlaylists": conf.Server.DevEnableUIPlaylists, "enablePlaylists": conf.Server.DevEnableUIPlaylists,
"enableStarred": conf.Server.DevEnableUIStarred,
} }
j, err := json.Marshal(appConfig) j, err := json.Marshal(appConfig)
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ const defaultConfig = {
loginBackgroundURL: 'https://source.unsplash.com/random/1600x900?music', loginBackgroundURL: 'https://source.unsplash.com/random/1600x900?music',
enableTranscodingConfig: true, enableTranscodingConfig: true,
enablePlaylists: true, enablePlaylists: true,
enableStarred: true,
} }
let config let config

View File

@ -1,16 +1,29 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { useTranslate } from 'react-admin' import { useUpdate, useTranslate, useRefresh, useNotify } from 'react-admin'
import { IconButton, Menu, MenuItem } from '@material-ui/core' import { IconButton, Menu, MenuItem } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import MoreVertIcon from '@material-ui/icons/MoreVert' import MoreVertIcon from '@material-ui/icons/MoreVert'
import StarIcon from '@material-ui/icons/Star'
import StarBorderIcon from '@material-ui/icons/StarBorder'
import NestedMenuItem from 'material-ui-nested-menu-item'
import { addTracks, setTrack } from '../audioplayer' import { addTracks, setTrack } from '../audioplayer'
import { AddToPlaylistMenu } from '../common' import { AddToPlaylistMenu } from '../common'
import NestedMenuItem from 'material-ui-nested-menu-item' import config from '../config'
import PropTypes from 'prop-types'
export const SongContextMenu = ({ record, onAddToPlaylist }) => { const useStyles = makeStyles({
noWrap: {
whiteSpace: 'nowrap',
},
})
export const SongContextMenu = ({ className, record, onAddToPlaylist }) => {
const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const translate = useTranslate() const translate = useTranslate()
const notify = useNotify()
const refresh = useRefresh()
const [anchorEl, setAnchorEl] = useState(null) const [anchorEl, setAnchorEl] = useState(null)
const options = { const options = {
playNow: { playNow: {
@ -41,10 +54,39 @@ export const SongContextMenu = ({ record, onAddToPlaylist }) => {
e.stopPropagation() e.stopPropagation()
} }
const [toggleStar, { toggling: loading }] = useUpdate(
'albumSong',
record.id,
record,
{
undoable: false,
onFailure: (error) => {
console.log(error)
notify('ra.page.error', 'warning')
refresh()
},
}
)
const handleToggleStar = (e, record) => {
record.starred = !record.starred
toggleStar()
e.stopPropagation()
}
const open = Boolean(anchorEl) const open = Boolean(anchorEl)
return ( return (
<> <span className={`${classes.noWrap} ${className}`}>
{config.enableStarred && (
<IconButton
onClick={(e) => handleToggleStar(e, record)}
size={'small'}
disabled={loading}
>
{record.starred ? <StarIcon /> : <StarBorderIcon />}
</IconButton>
)}
<IconButton onClick={handleClick} size={'small'}> <IconButton onClick={handleClick} size={'small'}>
<MoreVertIcon /> <MoreVertIcon />
</IconButton> </IconButton>
@ -70,7 +112,7 @@ export const SongContextMenu = ({ record, onAddToPlaylist }) => {
/> />
</NestedMenuItem> </NestedMenuItem>
</Menu> </Menu>
</> </span>
) )
} }