Artist page improvements (#1391)

* Seperate mobile desktop components

* Fix err

* Rename classes and fix some styles

* Add lastFM button and remove console log

* Add Mbiz Icon

* render bio as dangerouslySetInnerHTML and remove unused css classes

* Add Fav and Stars

* Remove unstandardised class selector

* Remove ext link from m view

* Fix naming and simplify rounded styling

* Refactor ArtistShow:

- Extracted DesktopArtistDetails to its own file
- Removed album count as it was incorrect, it is not considering compilations
- Show bio and image from Native API, if it is available, before calling `getArtistInfo`

Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Dnouv 2021-10-16 06:32:11 +05:30 committed by GitHub
parent 7505b5c554
commit 1d742cf8c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 343 additions and 257 deletions

View File

@ -0,0 +1,50 @@
import React from 'react'
import { useTranslate } from 'react-admin'
import { IconButton, Tooltip, Link, useMediaQuery } from '@material-ui/core'
import { ImLastfm2 } from 'react-icons/im'
import MusicBrainz from '../icons/MusicBrainz'
import { intersperse } from '../utils'
const ArtistExternalLinks = ({ artistInfo, record }) => {
const translate = useTranslate()
let links = []
let linkButtons = []
const lastFMlink = artistInfo?.biography?.match(
/<a\s+(?:[^>]*?\s+)?href=(["'])(.*?)\1/
)
if (lastFMlink) {
links.push(lastFMlink[2])
}
if (artistInfo && artistInfo.musicBrainzId) {
links.push(`https://musicbrainz.org/artist/${artistInfo.musicBrainzId}`)
}
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('sm'))
const addLink = (url, title, icon) => {
const translatedTitle = translate(title)
const link = (
<Link href={url} target="_blank" rel="noopener noreferrer">
<Tooltip title={translatedTitle}>
<IconButton size={'small'} aria-label={translatedTitle}>
{icon}
</IconButton>
</Tooltip>
</Link>
)
const id = linkButtons.length
linkButtons.push(<span key={`link-${record.id}-${id}`}>{link}</span>)
}
isDesktop && addLink(links[0], 'message.openIn.lastfm', <ImLastfm2 />)
isDesktop &&
artistInfo?.musicBrainzId &&
addLink(links[1], 'message.openIn.musicbrainz', <MusicBrainz />)
return isDesktop && <div>{intersperse(linkButtons, ' ')}</div>
}
export default ArtistExternalLinks

View File

@ -1,11 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'
import { Typography, Collapse, Link } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import Card from '@material-ui/core/Card'
import CardContent from '@material-ui/core/CardContent'
import CardMedia from '@material-ui/core/CardMedia'
import React, { useState, createElement, useEffect } from 'react'
import { useMediaQuery } from '@material-ui/core'
import {
useTranslate,
useShowController,
ShowContextProvider,
useRecordContext,
@ -14,152 +9,22 @@ import {
} from 'react-admin'
import subsonic from '../subsonic'
import AlbumGridView from '../album/AlbumGridView'
import MobileArtistDetails from './MobileArtistDetails'
import DesktopArtistDetails from './DesktopArtistDetails'
const useStyles = makeStyles(
(theme) => ({
root: {
display: 'flex',
padding: '1em',
'& .MuiTypography-h5': {
wordBreak: 'break-word',
},
[theme.breakpoints.down('xs')]: {
padding: 'unset',
background: ({ img }) => `url(${img})`,
},
},
bgContainer: {
display: 'flex',
width: '100%',
[theme.breakpoints.down('xs')]: {
height: '15rem',
width: '100vw',
padding: 'unset',
backdropFilter: 'blur(1px)',
backgroundPosition: '50% 30%',
background: `linear-gradient(to bottom, rgba(52 52 52 / 72%), rgba(21 21 21))`,
},
},
albumList: {
margin: '20px',
display: 'grid',
},
details: {
display: 'flex',
flex: '1',
flexDirection: 'column',
},
bioBlock: {
display: 'inline-block',
marginTop: '1em',
float: 'left',
wordBreak: 'break-word',
cursor: 'pointer',
},
link: {
margin: '1px',
},
mdetails: {
display: 'none',
[theme.breakpoints.down('xs')]: {
display: 'flex',
alignItems: 'center',
width: '7rem',
marginLeft: '0.5rem',
flex: '1',
},
},
mbio: {
display: 'none',
[theme.breakpoints.down('xs')]: {
display: 'flex',
marginLeft: '3%',
marginRight: '3%',
zIndex: '1',
'& p': {
whiteSpace: ({ expanded }) => (expanded ? 'unset' : 'nowrap'),
overflow: 'hidden',
width: '95vw',
textOverflow: 'ellipsis',
},
},
},
content: {
flex: '1 0 auto',
},
cover: {
width: 151,
boxShadow: '0px 0px 6px 0px #565656',
borderRadius: '5px',
[theme.breakpoints.up('sm')]: {
borderRadius: '7em',
},
},
martImage: {
marginLeft: '1em',
maxHeight: '10rem',
backgroundColor: 'inherit',
display: 'none',
[theme.breakpoints.down('xs')]: {
marginTop: '4rem',
maxHeight: '7rem',
width: '7rem',
display: 'flex',
},
},
artImage: {
maxHeight: '9.5rem',
backgroundColor: 'inherit',
display: 'flex',
[theme.breakpoints.down('xs')]: {
marginTop: '4rem',
maxHeight: '7rem',
width: '7rem',
},
},
artDetail: {
flex: '1',
padding: '3%',
display: 'flex',
minHeight: '10rem',
'& .MuiPaper-elevation1': {
boxShadow: 'none',
padding: '4px',
},
[theme.breakpoints.down('xs')]: {
display: 'none',
},
},
artistSummary: {
marginBottom: '1em',
},
}),
{ name: 'NDArtistPage' }
)
const ArtistDetails = () => {
const ArtistDetails = (props) => {
const record = useRecordContext(props)
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('sm'))
const [artistInfo, setArtistInfo] = useState()
const [expanded, setExpanded] = useState(false)
const record = useRecordContext()
const artistId = record?.id
const title = record.name
let completeBioLink = ''
const link = artistInfo?.biography?.match(
/<a\s+(?:[^>]*?\s+)?href=(["'])(.*?)\1/
)
if (link) {
completeBioLink = link[2]
}
const biography = artistInfo?.biography?.replace(new RegExp('<.*>', 'g'), '')
const translate = useTranslate()
const img = artistInfo?.largeImageUrl
const classes = useStyles({ img, expanded })
const biography =
artistInfo?.biography?.replace(new RegExp('<.*>', 'g'), '') ||
record.biography
const img = artistInfo?.largeImageUrl || record.largeImageUrl
useEffect(() => {
subsonic
.getArtistInfo(artistId)
.getArtistInfo(record.id)
.then((resp) => resp.json['subsonic-response'])
.then((data) => {
if (data.status === 'ok') {
@ -169,106 +34,21 @@ const ArtistDetails = () => {
.catch((e) => {
console.error('error on artist page', e)
})
}, [artistId, record])
const handleExpandClick = useCallback(() => {
setExpanded(!expanded)
}, [expanded, setExpanded])
}, [record])
const component = isDesktop ? DesktopArtistDetails : MobileArtistDetails
return (
<>
<div className={classes.root}>
<div className={classes.bgContainer}>
<Card className={classes.martImage}>
{artistInfo && (
<CardMedia
className={classes.cover}
image={`${artistInfo.mediumImageUrl}`}
title={title}
/>
)}
</Card>
<div className={classes.mdetails}>
<Typography component="h5" variant="h5">
{title}
</Typography>
</div>
<Card className={classes.artDetail}>
<Card className={classes.artImage}>
{artistInfo && (
<CardMedia
className={classes.cover}
image={`${artistInfo.mediumImageUrl}`}
title={title}
/>
)}
</Card>
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography component="h5" variant="h5">
{title}
</Typography>
<Collapse
collapsedHeight={'4.5em'}
in={expanded}
timeout={'auto'}
className={classes.bioBlock}
>
<Typography variant={'body1'} onClick={handleExpandClick}>
<span dangerouslySetInnerHTML={{ __html: biography }} />
{completeBioLink !== '' && (
<Link
href={completeBioLink}
className={classes.link}
target="_blank"
rel="nofollow"
>
{translate('message.lastfmLink')}
</Link>
)}
</Typography>
</Collapse>
</CardContent>
</div>
</Card>
</div>
</div>
<div className={classes.mbio}>
<Collapse collapsedHeight={'1.5em'} in={expanded} timeout={'auto'}>
<Typography variant={'body1'} onClick={handleExpandClick}>
{biography}
<Link
href={completeBioLink}
className={classes.link}
target="_blank"
rel="nofollow"
>
{translate('message.lastfmLink')}
</Link>
</Typography>
</Collapse>
</div>
{createElement(component, {
img,
artistInfo,
record,
biography,
})}
</>
)
}
const ArtistAlbums = ({ ...props }) => {
const { ids } = props
const classes = useStyles()
const translate = useTranslate()
return (
<div className={classes.albumList}>
<div className={classes.artistSummary}>
{ids.length +
' ' +
translate('resources.album.name', { smart_count: ids.length })}
</div>
<AlbumGridView {...props} />
</div>
)
}
const AlbumShowLayout = (props) => {
const showContext = useShowContext(props)
const record = useRecordContext()
@ -287,7 +67,7 @@ const AlbumShowLayout = (props) => {
perPage={0}
pagination={null}
>
<ArtistAlbums />
<AlbumGridView {...props} />
</ReferenceManyField>
)}
</>

View File

@ -0,0 +1,134 @@
import React, { useState } from 'react'
import { Typography, Collapse } from '@material-ui/core'
import { makeStyles } from '@material-ui/core'
import Card from '@material-ui/core/Card'
import CardContent from '@material-ui/core/CardContent'
import CardMedia from '@material-ui/core/CardMedia'
import ArtistExternalLinks from './ArtistExternalLink'
import config from '../config'
import { LoveButton, RatingField } from '../common'
const useStyles = makeStyles(
(theme) => ({
root: {
display: 'flex',
padding: '1em',
},
details: {
display: 'flex',
flex: '1',
flexDirection: 'column',
},
biography: {
display: 'inline-block',
marginTop: '1em',
float: 'left',
wordBreak: 'break-word',
cursor: 'pointer',
},
content: {
flex: '1 0 auto',
},
cover: {
width: 151,
borderRadius: '6em',
},
artistImage: {
maxHeight: '9.5rem',
backgroundColor: 'inherit',
display: 'flex',
boxShadow: 'none',
},
artistDetail: {
flex: '1',
padding: '3%',
display: 'flex',
minHeight: '10rem',
},
button: {
marginLeft: '0.9em',
},
loveButton: {
top: theme.spacing(-0.2),
left: theme.spacing(0.5),
},
rating: {
marginTop: '5px',
},
artistName: {
wordBreak: 'break-word',
},
}),
{ name: 'NDDesktopArtistDetails' }
)
const DesktopArtistDetails = ({ img, artistInfo, record, biography }) => {
const [expanded, setExpanded] = useState(false)
const classes = useStyles({ img, expanded })
const title = record.name
return (
<div className={classes.root}>
<Card className={classes.artistDetail}>
<Card className={classes.artistImage}>
{artistInfo && (
<CardMedia
className={classes.cover}
image={`${artistInfo.mediumImageUrl}`}
title={title}
/>
)}
</Card>
<div className={classes.details}>
<CardContent className={classes.content}>
<Typography
component="h5"
variant="h5"
className={classes.artistName}
>
{title}
{config.enableFavourites && (
<LoveButton
className={classes.loveButton}
record={record}
resource={'artist'}
size={'default'}
aria-label="love"
color="primary"
/>
)}
</Typography>
{config.enableStarRating && (
<div>
<RatingField
record={record}
resource={'artist'}
size={'small'}
className={classes.rating}
/>
</div>
)}
<Collapse
collapsedHeight={'4.5em'}
in={expanded}
timeout={'auto'}
className={classes.biography}
>
<Typography
variant={'body1'}
onClick={() => setExpanded(!expanded)}
>
<span dangerouslySetInnerHTML={{ __html: biography }} />
</Typography>
</Collapse>
</CardContent>
<Typography component={'div'} className={classes.button}>
<ArtistExternalLinks artistInfo={artistInfo} record={record} />
</Typography>
</div>
</Card>
</div>
)
}
export default DesktopArtistDetails

View File

@ -0,0 +1,134 @@
import React, { useState } from 'react'
import { Typography, Collapse } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import Card from '@material-ui/core/Card'
import CardMedia from '@material-ui/core/CardMedia'
import config from '../config'
import { LoveButton, RatingField } from '../common'
const useStyles = makeStyles(
(theme) => ({
root: {
display: 'flex',
background: ({ img }) => `url(${img})`,
},
bgContainer: {
display: 'flex',
height: '15rem',
width: '100vw',
padding: 'unset',
backdropFilter: 'blur(1px)',
backgroundPosition: '50% 30%',
background: `linear-gradient(to bottom, rgba(52 52 52 / 72%), rgba(21 21 21))`,
},
link: {
margin: '1px',
},
details: {
display: 'flex',
alignItems: 'flex-start',
flexDirection: 'column',
justifyContent: 'center',
marginLeft: '0.5rem',
},
biography: {
display: 'flex',
marginLeft: '3%',
marginRight: '3%',
marginTop: '-2em',
zIndex: '1',
'& p': {
whiteSpace: ({ expanded }) => (expanded ? 'unset' : 'nowrap'),
overflow: 'hidden',
width: '95vw',
textOverflow: 'ellipsis',
},
},
cover: {
width: 151,
boxShadow: '0px 0px 6px 0px #565656',
borderRadius: '5px',
},
artistImage: {
marginLeft: '1em',
maxHeight: '7rem',
backgroundColor: 'inherit',
marginTop: '4rem',
width: '7rem',
minWidth: '7rem',
display: 'flex',
borderRadius: '5em',
},
loveButton: {
top: theme.spacing(-0.2),
left: theme.spacing(0.5),
},
rating: {
marginTop: '5px',
},
artistName: {
wordBreak: 'break-word',
},
}),
{ name: 'NDMobileArtistDetails' }
)
const MobileArtistDetails = ({ img, artistInfo, biography, record }) => {
const [expanded, setExpanded] = useState(false)
const classes = useStyles({ img, expanded })
const title = record.name
return (
<>
<div className={classes.root}>
<div className={classes.bgContainer}>
<Card className={classes.artistImage}>
{artistInfo && (
<CardMedia
className={classes.cover}
image={`${artistInfo.mediumImageUrl}`}
title={title}
/>
)}
</Card>
<div className={classes.details}>
<Typography
component="h5"
variant="h5"
className={classes.artistName}
>
{title}
{config.enableFavourites && (
<LoveButton
className={classes.loveButton}
record={record}
resource={'artist'}
size={'small'}
aria-label="love"
color="primary"
/>
)}
</Typography>
{config.enableStarRating && (
<RatingField
record={record}
resource={'artist'}
size={'small'}
className={classes.rating}
/>
)}
</div>
</div>
</div>
<div className={classes.biography}>
<Collapse collapsedHeight={'1.5em'} in={expanded} timeout={'auto'}>
<Typography variant={'body1'} onClick={() => setExpanded(!expanded)}>
<span dangerouslySetInnerHTML={{ __html: biography }} />
</Typography>
</Collapse>
</div>
</>
)
}
export default MobileArtistDetails

View File

@ -32,14 +32,11 @@ export default {
boxShadow: '3px 3px 5px #000000a3',
},
},
NDArtistPage: {
NDMobileArtistDetails: {
bgContainer: {
background:
'linear-gradient(to bottom, rgba(52 52 52 / 72%), rgb(48 48 48))!important',
},
more: {
boxShadow: '-10px 0px 18px 5px #303030!important',
},
},
},
player: {

View File

@ -27,14 +27,11 @@ export default {
color: '#eee',
},
},
NDArtistPage: {
NDMobileArtistDetails: {
bgContainer: {
background:
'linear-gradient(to bottom, rgba(52 52 52 / 72%), rgb(48 48 48))!important',
},
more: {
boxShadow: '-10px 0px 18px 5px #303030!important',
},
},
},
player: {

View File

@ -359,14 +359,11 @@ export default {
marginTop: '-50px',
},
},
NDArtistPage: {
NDMobileArtistDetails: {
bgContainer: {
background:
'linear-gradient(to bottom, rgb(255 255 255 / 51%), rgb(240 242 245))!important',
},
more: {
boxShadow: '-10px 0px 18px 5px #f0f2f5!important',
},
},
RaLayout: {
content: {

View File

@ -46,14 +46,11 @@ export default {
color: '#0085ff',
},
},
NDArtistPage: {
NDMobileArtistDetails: {
bgContainer: {
background:
'linear-gradient(to bottom, rgb(255 255 255 / 51%), rgb(250 250 250))!important',
},
more: {
boxShadow: '-10px 0px 18px 5px #fafafa!important',
},
},
},
player: {