Add Album comment to Album details

This commit is contained in:
Deluan
2020-11-13 21:11:24 -05:00
parent 08f96639f4
commit b0ea517fdd
8 changed files with 236 additions and 84 deletions
+187 -8
View File
@@ -1,15 +1,170 @@
import React from 'react' import React, { useMemo } from 'react'
import { Card, CardContent, CardMedia, Typography } from '@material-ui/core' import {
Card,
CardContent,
CardMedia,
Typography,
Collapse,
makeStyles,
IconButton,
Fab,
useMediaQuery,
} from '@material-ui/core'
import classnames from 'classnames'
import { useTranslate } from 'react-admin' import { useTranslate } from 'react-admin'
import Lightbox from 'react-image-lightbox' import Lightbox from 'react-image-lightbox'
import 'react-image-lightbox/style.css' import 'react-image-lightbox/style.css'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { DurationField, StarButton, SizeField } from '../common' import {
import { ArtistLinkField, formatRange } from '../common' DurationField,
StarButton,
SizeField,
ArtistLinkField,
formatRange,
MultiLineTextField,
} from '../common'
const AlbumDetails = ({ classes, record }) => { const useStyles = makeStyles((theme) => ({
container: {
[theme.breakpoints.down('xs')]: {
padding: '0.7em',
minWidth: '24em',
},
[theme.breakpoints.up('sm')]: {
padding: '1em',
minWidth: '32em',
},
},
starButton: {
bottom: theme.spacing(-1.5),
right: theme.spacing(-1.5),
},
albumCover: {
display: 'inline-flex',
justifyContent: 'flex-end',
alignItems: 'flex-end',
cursor: 'pointer',
[theme.breakpoints.down('xs')]: {
height: '8em',
width: '8em',
},
[theme.breakpoints.up('sm')]: {
height: '10em',
width: '10em',
},
[theme.breakpoints.up('lg')]: {
height: '15em',
width: '15em',
},
},
albumDetails: {
display: 'inline-block',
verticalAlign: 'top',
[theme.breakpoints.down('xs')]: {
width: '14em',
},
[theme.breakpoints.up('sm')]: {
width: '26em',
},
[theme.breakpoints.up('lg')]: {
width: '43em',
},
},
albumTitle: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
comment: {
whiteSpace: 'nowrap',
marginTop: '1em',
display: 'inline-block',
[theme.breakpoints.down('xs')]: {
width: '10em',
},
[theme.breakpoints.up('sm')]: {
width: '10em',
},
[theme.breakpoints.up('lg')]: {
width: '10em',
},
},
commentFirstLine: {
float: 'left',
marginRight: '5px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
expand: {
marginTop: '-5px',
transform: 'rotate(0deg)',
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
},
expandOpen: {
transform: 'rotate(180deg)',
},
}))
const AlbumComment = ({ classes, record, commentNumLines }) => {
const [expanded, setExpanded] = React.useState(false)
const handleExpandClick = React.useCallback(() => {
commentNumLines > 1 && setExpanded(!expanded)
}, [expanded, setExpanded, commentNumLines])
return (
<div className={classes.comment}>
<div onClick={handleExpandClick}>
<MultiLineTextField
record={record}
source={'comment'}
maxLines={1}
className={classes.commentFirstLine}
/>
</div>
{commentNumLines > 1 && (
<IconButton
size={'small'}
className={classnames(classes.expand, {
[classes.expandOpen]: expanded,
})}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
<ExpandMoreIcon />
</IconButton>
)}
<Collapse in={expanded} timeout="auto" unmountOnExit>
<MultiLineTextField
record={record}
source={'comment'}
firstLine={1}
className={classes.commentFirstLine}
/>
</Collapse>
</div>
)
}
const AlbumDetails = ({ record }) => {
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('lg'))
const classes = useStyles()
const [isLightboxOpen, setLightboxOpen] = React.useState(false) const [isLightboxOpen, setLightboxOpen] = React.useState(false)
const translate = useTranslate() const translate = useTranslate()
const commentNumLines = useMemo(
() => record.comment && record.comment.split('\n').length,
[record]
)
const genreYear = (record) => { const genreYear = (record) => {
let genreDateLine = [] let genreDateLine = []
if (record.genre) { if (record.genre) {
@@ -38,14 +193,23 @@ const AlbumDetails = ({ classes, record }) => {
() => setLightboxOpen(false), () => setLightboxOpen(false),
[] []
) )
return ( return (
<Card className={classes.container}> <Card className={classes.container}>
<CardMedia <CardMedia
image={imageUrl} image={imageUrl}
className={classes.albumCover} className={classes.albumCover}
onClick={handleOpenLightbox} onClick={handleOpenLightbox}
></CardMedia> >
<StarButton
className={classes.starButton}
record={record}
resource={'album'}
size={isDesktop ? 'default' : 'small'}
aria-label="star"
color="primary"
component={Fab}
/>
</CardMedia>
<CardContent className={classes.albumDetails}> <CardContent className={classes.albumDetails}>
<Typography variant="h5" className={classes.albumTitle}> <Typography variant="h5" className={classes.albumTitle}>
{record.name} {record.name}
@@ -60,8 +224,23 @@ const AlbumDetails = ({ classes, record }) => {
{' · '} <DurationField record={record} source={'duration'} /> {' · '} {' · '} <DurationField record={record} source={'duration'} /> {' · '}
<SizeField record={record} source="size" /> <SizeField record={record} source="size" />
</Typography> </Typography>
<StarButton record={record} resource={'album'} size={'large'} /> {isDesktop && record['comment'] && (
<AlbumComment
classes={classes}
record={record}
commentNumLines={commentNumLines}
/>
)}
</CardContent> </CardContent>
{!isDesktop && record['comment'] && (
<div>
<AlbumComment
classes={classes}
record={record}
commentNumLines={commentNumLines}
/>
</div>
)}
{isLightboxOpen && ( {isLightboxOpen && (
<Lightbox <Lightbox
+3 -3
View File
@@ -12,16 +12,16 @@ import {
} from 'react-admin' } from 'react-admin'
import { useMediaQuery } from '@material-ui/core' import { useMediaQuery } from '@material-ui/core'
import StarBorderIcon from '@material-ui/icons/StarBorder' import StarBorderIcon from '@material-ui/icons/StarBorder'
import { makeStyles } from '@material-ui/core/styles'
import { import {
ArtistLinkField, ArtistLinkField,
DurationField, DurationField,
RangeField, RangeField,
SimpleList, SimpleList,
SizeField, SizeField,
MultiLineTextField,
AlbumContextMenu,
} from '../common' } from '../common'
import { AlbumContextMenu } from '../common'
import { makeStyles } from '@material-ui/core/styles'
import MultiLineTextField from '../common/MultiLineTextField'
const useStyles = makeStyles({ const useStyles = makeStyles({
columnIcon: { columnIcon: {
+1 -3
View File
@@ -2,13 +2,11 @@ import React from 'react'
import { useGetOne } from 'react-admin' import { useGetOne } from 'react-admin'
import AlbumDetails from './AlbumDetails' import AlbumDetails from './AlbumDetails'
import { Title } from '../common' import { Title } from '../common'
import { useStyles } from './styles'
import { SongBulkActions } from '../common' import { SongBulkActions } from '../common'
import AlbumActions from './AlbumActions' import AlbumActions from './AlbumActions'
import AlbumSongs from './AlbumSongs' import AlbumSongs from './AlbumSongs'
const AlbumShow = (props) => { const AlbumShow = (props) => {
const classes = useStyles()
const { data: record, loading, error } = useGetOne('album', props.id) const { data: record, loading, error } = useGetOne('album', props.id)
if (loading) { if (loading) {
@@ -21,7 +19,7 @@ const AlbumShow = (props) => {
return ( return (
<> <>
<AlbumDetails {...props} classes={classes} record={record} /> <AlbumDetails {...props} record={record} />
<AlbumSongs <AlbumSongs
{...props} {...props}
albumId={props.id} albumId={props.id}
-54
View File
@@ -1,54 +0,0 @@
import { makeStyles } from '@material-ui/core/styles'
export const useStyles = makeStyles((theme) => ({
container: {
[theme.breakpoints.down('xs')]: {
padding: '0.7em',
minWidth: '24em',
},
[theme.breakpoints.up('sm')]: {
padding: '1em',
minWidth: '32em',
},
},
albumCover: {
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
[theme.breakpoints.down('xs')]: {
height: '8em',
width: '8em',
},
[theme.breakpoints.up('sm')]: {
height: '10em',
width: '10em',
},
[theme.breakpoints.up('lg')]: {
height: '15em',
width: '15em',
},
'&:hover $playButton': {
opacity: 1,
},
},
albumDetails: {
display: 'inline-block',
verticalAlign: 'top',
[theme.breakpoints.down('xs')]: {
width: '14em',
},
[theme.breakpoints.up('sm')]: {
width: '26em',
},
[theme.breakpoints.up('lg')]: {
width: '43em',
},
},
albumTitle: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}))
+28 -12
View File
@@ -4,10 +4,22 @@ import Typography from '@material-ui/core/Typography'
import sanitizeFieldRestProps from './sanitizeFieldRestProps' import sanitizeFieldRestProps from './sanitizeFieldRestProps'
import md5 from 'md5-hex' import md5 from 'md5-hex'
const MultiLineTextField = memo( export const MultiLineTextField = memo(
({ className, emptyText, source, record = {}, stripTags, ...rest }) => { ({
className,
emptyText,
source,
record,
firstLine,
maxLines,
addLabel,
...rest
}) => {
const value = get(record, source) const value = get(record, source)
const lines = value ? value.split('\n') : [] let lines = value ? value.split('\n') : []
if (maxLines || firstLine) {
lines = lines.slice(firstLine, maxLines)
}
return ( return (
<Typography <Typography
@@ -18,20 +30,24 @@ const MultiLineTextField = memo(
> >
{lines.length === 0 && emptyText {lines.length === 0 && emptyText
? emptyText ? emptyText
: lines.map((line, idx) => ( : lines.map((line, idx) =>
<div line === '' ? (
data-testid={`${source}.${idx}`} <br key={md5(line + idx)} />
key={md5(line)} ) : (
dangerouslySetInnerHTML={{ __html: line }} <div
/> data-testid={`${source}.${idx}`}
))} key={md5(line + idx)}
dangerouslySetInnerHTML={{ __html: line }}
/>
)
)}
</Typography> </Typography>
) )
} }
) )
MultiLineTextField.defaultProps = { MultiLineTextField.defaultProps = {
record: {},
addLabel: true, addLabel: true,
firstLine: 0,
} }
export default MultiLineTextField
+1 -1
View File
@@ -8,7 +8,7 @@ import TableRow from '@material-ui/core/TableRow'
import { BooleanField, DateField, TextField, useTranslate } from 'react-admin' import { BooleanField, DateField, TextField, useTranslate } from 'react-admin'
import inflection from 'inflection' import inflection from 'inflection'
import { BitrateField, SizeField } from './index' import { BitrateField, SizeField } from './index'
import MultiLineTextField from './MultiLineTextField' import { MultiLineTextField } from './MultiLineTextField'
export const SongDetails = (props) => { export const SongDetails = (props) => {
const translate = useTranslate() const translate = useTranslate()
+15 -3
View File
@@ -15,7 +15,16 @@ const useStyles = makeStyles({
}, },
}) })
export const StarButton = ({ resource, record, color, visible, size }) => { export const StarButton = ({
resource,
record,
color,
visible,
size,
component: Button,
addLabel,
...rest
}) => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const classes = useStyles({ color, visible, starred: record.starred }) const classes = useStyles({ color, visible, starred: record.starred })
const notify = useNotify() const notify = useNotify()
@@ -56,18 +65,19 @@ export const StarButton = ({ resource, record, color, visible, size }) => {
} }
return ( return (
<IconButton <Button
onClick={handleToggleStar} onClick={handleToggleStar}
size={'small'} size={'small'}
disabled={loading} disabled={loading}
className={classes.star} className={classes.star}
{...rest}
> >
{record.starred ? ( {record.starred ? (
<StarIcon fontSize={size} /> <StarIcon fontSize={size} />
) : ( ) : (
<StarBorderIcon fontSize={size} /> <StarBorderIcon fontSize={size} />
)} )}
</IconButton> </Button>
) )
} }
@@ -77,6 +87,7 @@ StarButton.propTypes = {
visible: PropTypes.bool, visible: PropTypes.bool,
color: PropTypes.string, color: PropTypes.string,
size: PropTypes.string, size: PropTypes.string,
component: PropTypes.object,
} }
StarButton.defaultProps = { StarButton.defaultProps = {
@@ -85,4 +96,5 @@ StarButton.defaultProps = {
visible: true, visible: true,
size: 'small', size: 'small',
color: 'inherit', color: 'inherit',
component: IconButton,
} }
+1
View File
@@ -6,6 +6,7 @@ export * from './ContextMenus'
export * from './DocLink' export * from './DocLink'
export * from './DurationField' export * from './DurationField'
export * from './List' export * from './List'
export * from './MultiLineTextField'
export * from './Pagination' export * from './Pagination'
export * from './PlayButton' export * from './PlayButton'
export * from './QuickFilter' export * from './QuickFilter'