mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-05 21:47:36 +03:00
Add Genre filters to UI
This commit is contained in:
parent
c56c7c865e
commit
20b7e5c49b
13 changed files with 108 additions and 32 deletions
|
@ -2,9 +2,9 @@ package model
|
||||||
|
|
||||||
type Genre struct {
|
type Genre struct {
|
||||||
ID string `json:"id" orm:"column(id)"`
|
ID string `json:"id" orm:"column(id)"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
SongCount int `json:"-"`
|
SongCount int `json:"-"`
|
||||||
AlbumCount int `json:"-"`
|
AlbumCount int `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Genres []Genre
|
type Genres []Genre
|
|
@ -82,7 +82,12 @@ func artistFilter(field string, value interface{}) Sqlizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||||
return r.count(r.selectAlbum(), options...)
|
sql := r.selectAlbum()
|
||||||
|
sql = sql.LeftJoin("album_genres ag on album.id = ag.album_id").
|
||||||
|
LeftJoin("genre on ag.genre_id = genre.id").
|
||||||
|
GroupBy("album.id")
|
||||||
|
|
||||||
|
return r.count(sql, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *albumRepository) Exists(id string) (bool, error) {
|
func (r *albumRepository) Exists(id string) (bool, error) {
|
||||||
|
|
|
@ -49,7 +49,11 @@ func (r *artistRepository) selectArtist(options ...model.QueryOptions) SelectBui
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *artistRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
func (r *artistRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||||
return r.count(r.newSelectWithAnnotation("artist.id"), options...)
|
sql := r.newSelectWithAnnotation("artist.id")
|
||||||
|
sql = sql.LeftJoin("artist_genres ag on artist.id = ag.artist_id").
|
||||||
|
LeftJoin("genre on ag.genre_id = genre.id").
|
||||||
|
GroupBy("artist.id")
|
||||||
|
return r.count(sql, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *artistRepository) Exists(id string) (bool, error) {
|
func (r *artistRepository) Exists(id string) (bool, error) {
|
||||||
|
|
|
@ -22,6 +22,9 @@ func NewGenreRepository(ctx context.Context, o orm.Ormer) model.GenreRepository
|
||||||
r.ctx = ctx
|
r.ctx = ctx
|
||||||
r.ormer = o
|
r.ormer = o
|
||||||
r.tableName = "genre"
|
r.tableName = "genre"
|
||||||
|
r.filterMappings = map[string]filterFunc{
|
||||||
|
"name": containsFilter,
|
||||||
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,11 @@ func NewMediaFileRepository(ctx context.Context, o orm.Ormer) *mediaFileReposito
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||||
return r.count(r.newSelectWithAnnotation("media_file.id"), options...)
|
sql := r.newSelectWithAnnotation("media_file.id")
|
||||||
|
sql = sql.LeftJoin("media_file_genres mfg on media_file.id = mfg.media_file_id").
|
||||||
|
LeftJoin("genre on mfg.genre_id = genre.id").
|
||||||
|
GroupBy("media_file.id")
|
||||||
|
return r.count(sql, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mediaFileRepository) Exists(id string) (bool, error) {
|
func (r *mediaFileRepository) Exists(id string) (bool, error) {
|
||||||
|
|
|
@ -88,6 +88,8 @@ func (s *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRe
|
||||||
return s.Album(ctx).(model.ResourceRepository)
|
return s.Album(ctx).(model.ResourceRepository)
|
||||||
case model.MediaFile:
|
case model.MediaFile:
|
||||||
return s.MediaFile(ctx).(model.ResourceRepository)
|
return s.MediaFile(ctx).(model.ResourceRepository)
|
||||||
|
case model.Genre:
|
||||||
|
return s.Genre(ctx).(model.ResourceRepository)
|
||||||
case model.Playlist:
|
case model.Playlist:
|
||||||
return s.Playlist(ctx).(model.ResourceRepository)
|
return s.Playlist(ctx).(model.ResourceRepository)
|
||||||
case model.Share:
|
case model.Share:
|
||||||
|
|
|
@ -37,6 +37,7 @@ func (n *Router) routes() http.Handler {
|
||||||
n.R(r, "/song", model.MediaFile{}, true)
|
n.R(r, "/song", model.MediaFile{}, true)
|
||||||
n.R(r, "/album", model.Album{}, true)
|
n.R(r, "/album", model.Album{}, true)
|
||||||
n.R(r, "/artist", model.Artist{}, true)
|
n.R(r, "/artist", model.Artist{}, true)
|
||||||
|
n.R(r, "/genre", model.Genre{}, true)
|
||||||
n.R(r, "/player", model.Player{}, true)
|
n.R(r, "/player", model.Player{}, true)
|
||||||
n.R(r, "/playlist", model.Playlist{}, true)
|
n.R(r, "/playlist", model.Playlist{}, true)
|
||||||
n.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig)
|
n.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig)
|
||||||
|
|
|
@ -108,6 +108,7 @@ const Admin = (props) => {
|
||||||
<Resource name="transcoding" />
|
<Resource name="transcoding" />
|
||||||
),
|
),
|
||||||
<Resource name="translation" />,
|
<Resource name="translation" />,
|
||||||
|
<Resource name="genre" />,
|
||||||
<Resource name="playlistTrack" />,
|
<Resource name="playlistTrack" />,
|
||||||
<Resource name="keepalive" />,
|
<Resource name="keepalive" />,
|
||||||
<Player />,
|
<Player />,
|
||||||
|
|
|
@ -42,6 +42,15 @@ const AlbumFilter = (props) => {
|
||||||
>
|
>
|
||||||
<AutocompleteInput emptyText="-- None --" />
|
<AutocompleteInput emptyText="-- None --" />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
|
<ReferenceInput
|
||||||
|
label={translate('resources.album.fields.genre')}
|
||||||
|
source="genre_id"
|
||||||
|
reference="genre"
|
||||||
|
sort={{ field: 'name', order: 'ASC' }}
|
||||||
|
filterToQuery={(searchText) => ({ name: [searchText] })}
|
||||||
|
>
|
||||||
|
<AutocompleteInput emptyText="-- None --" />
|
||||||
|
</ReferenceInput>
|
||||||
<NullableBooleanInput source="compilation" />
|
<NullableBooleanInput source="compilation" />
|
||||||
<NumberInput source="year" />
|
<NumberInput source="year" />
|
||||||
{config.enableFavourites && (
|
{config.enableFavourites && (
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
|
AutocompleteInput,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
Filter,
|
Filter,
|
||||||
NumberField,
|
NumberField,
|
||||||
|
ReferenceInput,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
TextField,
|
TextField,
|
||||||
|
useTranslate,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { useMediaQuery, withWidth } from '@material-ui/core'
|
import { useMediaQuery, withWidth } from '@material-ui/core'
|
||||||
import FavoriteIcon from '@material-ui/icons/Favorite'
|
import FavoriteIcon from '@material-ui/icons/Favorite'
|
||||||
|
@ -49,18 +52,30 @@ const useStyles = makeStyles({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const ArtistFilter = (props) => (
|
const ArtistFilter = (props) => {
|
||||||
<Filter {...props} variant={'outlined'}>
|
const translate = useTranslate()
|
||||||
<SearchInput source="name" alwaysOn />
|
return (
|
||||||
{config.enableFavourites && (
|
<Filter {...props} variant={'outlined'}>
|
||||||
<QuickFilter
|
<SearchInput source="name" alwaysOn />
|
||||||
source="starred"
|
<ReferenceInput
|
||||||
label={<FavoriteIcon fontSize={'small'} />}
|
label={translate('resources.artist.fields.genre')}
|
||||||
defaultValue={true}
|
source="genre_id"
|
||||||
/>
|
reference="genre"
|
||||||
)}
|
sort={{ field: 'name', order: 'ASC' }}
|
||||||
</Filter>
|
filterToQuery={(searchText) => ({ name: [searchText] })}
|
||||||
)
|
>
|
||||||
|
<AutocompleteInput emptyText="-- None --" />
|
||||||
|
</ReferenceInput>
|
||||||
|
{config.enableFavourites && (
|
||||||
|
<QuickFilter
|
||||||
|
source="starred"
|
||||||
|
label={<FavoriteIcon fontSize={'small'} />}
|
||||||
|
defaultValue={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Filter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
|
const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
|
@ -1,13 +1,29 @@
|
||||||
import React from 'react'
|
import React, { cloneElement } from 'react'
|
||||||
import { sanitizeListRestProps, TopToolbar } from 'react-admin'
|
import { sanitizeListRestProps, TopToolbar } from 'react-admin'
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
import { ToggleFieldsMenu } from '../common'
|
import { ToggleFieldsMenu } from '../common'
|
||||||
|
|
||||||
const ArtistListActions = ({ className, ...rest }) => {
|
const ArtistListActions = ({
|
||||||
|
className,
|
||||||
|
filters,
|
||||||
|
resource,
|
||||||
|
showFilter,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||||
|
{filters &&
|
||||||
|
cloneElement(filters, {
|
||||||
|
resource,
|
||||||
|
showFilter,
|
||||||
|
displayedFilters,
|
||||||
|
filterValues,
|
||||||
|
context: 'button',
|
||||||
|
})}
|
||||||
{isNotSmall && <ToggleFieldsMenu resource="artist" />}
|
{isNotSmall && <ToggleFieldsMenu resource="artist" />}
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
)
|
)
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
"albumCount": "Album Count",
|
"albumCount": "Album Count",
|
||||||
"songCount": "Song Count",
|
"songCount": "Song Count",
|
||||||
"playCount": "Plays",
|
"playCount": "Plays",
|
||||||
|
"genre": "Genre",
|
||||||
"rating": "Rating"
|
"rating": "Rating"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
|
AutocompleteInput,
|
||||||
Filter,
|
Filter,
|
||||||
FunctionField,
|
FunctionField,
|
||||||
NumberField,
|
NumberField,
|
||||||
|
ReferenceInput,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
TextField,
|
TextField,
|
||||||
|
useTranslate,
|
||||||
} from 'react-admin'
|
} from 'react-admin'
|
||||||
import { useMediaQuery } from '@material-ui/core'
|
import { useMediaQuery } from '@material-ui/core'
|
||||||
import FavoriteIcon from '@material-ui/icons/Favorite'
|
import FavoriteIcon from '@material-ui/icons/Favorite'
|
||||||
|
@ -55,18 +58,30 @@ const useStyles = makeStyles({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const SongFilter = (props) => (
|
const SongFilter = (props) => {
|
||||||
<Filter {...props} variant={'outlined'}>
|
const translate = useTranslate()
|
||||||
<SearchInput source="title" alwaysOn />
|
return (
|
||||||
{config.enableFavourites && (
|
<Filter {...props} variant={'outlined'}>
|
||||||
<QuickFilter
|
<SearchInput source="title" alwaysOn />
|
||||||
source="starred"
|
<ReferenceInput
|
||||||
label={<FavoriteIcon fontSize={'small'} />}
|
label={translate('resources.song.fields.genre')}
|
||||||
defaultValue={true}
|
source="genre_id"
|
||||||
/>
|
reference="genre"
|
||||||
)}
|
sort={{ field: 'name', order: 'ASC' }}
|
||||||
</Filter>
|
filterToQuery={(searchText) => ({ name: [searchText] })}
|
||||||
)
|
>
|
||||||
|
<AutocompleteInput emptyText="-- None --" />
|
||||||
|
</ReferenceInput>
|
||||||
|
{config.enableFavourites && (
|
||||||
|
<QuickFilter
|
||||||
|
source="starred"
|
||||||
|
label={<FavoriteIcon fontSize={'small'} />}
|
||||||
|
defaultValue={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Filter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SongList = (props) => {
|
const SongList = (props) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue