mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(ui): unify list cover surfaces onto the shared CoverImage component
Route the album grid, CoverArtAvatar (artist/playlist lists) and the radio list's cover field through CoverImage instead of each carrying its own useImageUrl + blurhash-overlay wiring. CoverImage gains a default object-fit: cover. Radio keeps its uploaded-image gate and the generic radio placeholder for stations with no art.
This commit is contained in:
parent
44913df403
commit
01cf2d2915
@ -12,19 +12,16 @@ import { Link } from 'react-router-dom'
|
||||
import { linkToRecord, useListContext, Loading } from 'react-admin'
|
||||
import { withContentRect } from 'react-measure'
|
||||
import { useDrag } from 'react-dnd'
|
||||
import subsonic from '../subsonic'
|
||||
import {
|
||||
AlbumContextMenu,
|
||||
PlayButton,
|
||||
ArtistLinkField,
|
||||
OverflowTooltip,
|
||||
useImageUrl,
|
||||
} from '../common'
|
||||
import config from '../config'
|
||||
import { DraggableTypes } from '../consts'
|
||||
import clsx from 'clsx'
|
||||
import { AlbumDatesField } from './AlbumDatesField.jsx'
|
||||
import { BlurHashCanvas } from '../common/BlurHashCanvas'
|
||||
import { CoverImage } from '../common/CoverImage'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
(theme) => ({
|
||||
@ -139,23 +136,14 @@ const Cover = withContentRect('bounds')(({
|
||||
[record],
|
||||
)
|
||||
|
||||
const url = subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)
|
||||
const { imgUrl, loading: imageLoading } = useImageUrl(url)
|
||||
const showBlurHash = imageLoading && record.blurHash
|
||||
|
||||
return (
|
||||
<div ref={measureRef} className={classes.coverContainer}>
|
||||
<div ref={dragAlbumRef} style={{ position: 'relative' }}>
|
||||
{showBlurHash && (
|
||||
<BlurHashCanvas hash={record.blurHash} className={classes.cover} />
|
||||
)}
|
||||
<img
|
||||
src={imgUrl || undefined}
|
||||
alt={record.name}
|
||||
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
|
||||
style={
|
||||
showBlurHash ? { position: 'absolute', left: 0, top: 0 } : undefined
|
||||
}
|
||||
<div ref={dragAlbumRef}>
|
||||
<CoverImage
|
||||
record={record}
|
||||
square
|
||||
className={classes.cover}
|
||||
title={record.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,81 +1,36 @@
|
||||
import { useRecordContext } from 'react-admin'
|
||||
import { Avatar } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import clsx from 'clsx'
|
||||
import config from '../config'
|
||||
import subsonic from '../subsonic'
|
||||
import { useImageUrl } from './useImageUrl'
|
||||
import { BlurHashCanvas } from './BlurHashCanvas'
|
||||
import { CoverImage } from './CoverImage'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
width: '55px',
|
||||
height: '55px',
|
||||
},
|
||||
avatar: {
|
||||
width: '55px',
|
||||
height: '55px',
|
||||
},
|
||||
avatarEmpty: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
square: {
|
||||
borderRadius: '4px',
|
||||
},
|
||||
circular: {
|
||||
borderRadius: '50%',
|
||||
},
|
||||
blur: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
})
|
||||
|
||||
export const CoverArtAvatar = ({
|
||||
record: recordProp,
|
||||
variant = 'circular',
|
||||
}) => {
|
||||
export const CoverArtAvatar = ({ record: recordProp, variant = 'circular' }) => {
|
||||
const classes = useStyles()
|
||||
const recordContext = useRecordContext()
|
||||
const record = recordProp || recordContext
|
||||
const square = variant !== 'circular'
|
||||
const url = record
|
||||
? subsonic.getCoverArtUrl(record, config.uiCoverArtSize, square)
|
||||
: null
|
||||
const { imgUrl, loading } = useImageUrl(url)
|
||||
if (!record) return null
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
src={imgUrl || undefined}
|
||||
variant={variant}
|
||||
className={clsx(
|
||||
classes.avatar,
|
||||
square && classes.square,
|
||||
!imgUrl && classes.avatarEmpty,
|
||||
)}
|
||||
alt={record.name}
|
||||
>
|
||||
{/* Empty child prevents default person icon while loading */}
|
||||
{!imgUrl && <span />}
|
||||
</Avatar>
|
||||
)
|
||||
|
||||
// Show the blurhash behind the transparent avatar until the real image loads.
|
||||
if (!(loading && record.blurHash)) return avatar
|
||||
const square = variant !== 'circular'
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<BlurHashCanvas
|
||||
hash={record.blurHash}
|
||||
className={clsx(classes.blur, square ? classes.square : classes.circular)}
|
||||
/>
|
||||
{avatar}
|
||||
</div>
|
||||
<CoverImage
|
||||
record={record}
|
||||
size={config.uiCoverArtSize}
|
||||
square={square}
|
||||
className={clsx(classes.avatar, square ? classes.square : classes.circular)}
|
||||
title={record.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +45,7 @@ export const CoverImage = ({
|
||||
onClick={handleClick}
|
||||
className={clsx(className, showBlurHash && classes.img)}
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
opacity: loading ? 0.5 : 1,
|
||||
cursor: handleClick ? 'pointer' : 'default',
|
||||
}}
|
||||
|
||||
@ -18,11 +18,10 @@ import {
|
||||
List,
|
||||
defaultRowsPerPageOptions,
|
||||
getStoredPerPage,
|
||||
useImageUrl,
|
||||
ToggleFieldsMenu,
|
||||
useSelectedFields,
|
||||
} from '../common'
|
||||
import { BlurHashCanvas } from '../common/BlurHashCanvas'
|
||||
import { CoverImage } from '../common/CoverImage'
|
||||
import subsonic from '../subsonic'
|
||||
import { StreamField } from './StreamField'
|
||||
import { setTrack } from '../actions'
|
||||
@ -83,33 +82,28 @@ const RadioListActions = ({
|
||||
}
|
||||
|
||||
const avatarStyle = { width: 40, height: 40 }
|
||||
const blurStyle = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '4px',
|
||||
}
|
||||
|
||||
const useCoverStyles = makeStyles({
|
||||
cover: { width: 40, height: 40, borderRadius: '4px' },
|
||||
})
|
||||
|
||||
const CoverArtField = ({ record }) => {
|
||||
const directUrl = record?.uploadedImage
|
||||
? subsonic.getCoverArtUrl(record, 40, true)
|
||||
: null
|
||||
const { imgUrl, loading } = useImageUrl(directUrl)
|
||||
const classes = useCoverStyles()
|
||||
if (!record) return null
|
||||
const showBlurHash = loading && record.blurHash
|
||||
// While the real image loads, prefer the blurhash over the generic radio icon.
|
||||
const src = imgUrl || (showBlurHash ? undefined : RADIO_PLACEHOLDER_IMAGE)
|
||||
const avatar = (
|
||||
<Avatar src={src} variant="rounded" style={avatarStyle} alt={record.name} />
|
||||
)
|
||||
if (!showBlurHash) return avatar
|
||||
// Radios resolve art only from an uploaded image; otherwise show the generic radio icon.
|
||||
if (record.uploadedImage) {
|
||||
return (
|
||||
<CoverImage
|
||||
record={record}
|
||||
size={40}
|
||||
square
|
||||
className={classes.cover}
|
||||
title={record.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{ position: 'relative', display: 'inline-flex', ...avatarStyle }}>
|
||||
<BlurHashCanvas hash={record.blurHash} style={blurStyle} />
|
||||
{avatar}
|
||||
</div>
|
||||
<Avatar src={RADIO_PLACEHOLDER_IMAGE} variant="rounded" style={avatarStyle} alt={record.name} />
|
||||
)
|
||||
}
|
||||
CoverArtField.defaultProps = { label: '' }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user