refactor(ui): rename cover artwork components

This commit is contained in:
Deluan 2026-07-25 18:35:12 -04:00
parent 19d89143f7
commit 57a95c0dfd
14 changed files with 42 additions and 47 deletions

View File

@ -20,7 +20,7 @@ import Lightbox from 'react-image-lightbox'
import config from '../config'
import 'react-image-lightbox/style.css'
import subsonic from '../subsonic'
import { CoverImage } from '../common/CoverImage'
import { Artwork } from '../common/Artwork'
import {
ArtistLinkField,
CollapsibleComment,
@ -248,7 +248,7 @@ const AlbumDetails = (props) => {
<Card className={classes.root}>
<div className={classes.cardContents}>
<div className={classes.coverParent}>
<CoverImage
<Artwork
record={record}
fit="contain"
className={classes.cover}

View File

@ -21,7 +21,7 @@ import {
import { DraggableTypes } from '../consts'
import clsx from 'clsx'
import { AlbumDatesField } from './AlbumDatesField.jsx'
import { CoverImage } from '../common/CoverImage'
import { Artwork } from '../common/Artwork'
const useStyles = makeStyles(
(theme) => ({
@ -136,7 +136,7 @@ const Cover = withContentRect('bounds')(({
return (
<div ref={measureRef} className={classes.coverContainer}>
<div ref={dragAlbumRef}>
<CoverImage
<Artwork
record={record}
square
className={classes.cover}

View File

@ -14,7 +14,7 @@ import { makeStyles } from '@material-ui/core/styles'
import { useDrag } from 'react-dnd'
import {
ArtistLinkField,
CoverArtAvatar,
ArtworkAvatar,
DurationField,
RangeField,
SimpleList,
@ -164,7 +164,7 @@ const AlbumTableView = ({
)}
leftIcon={(r) => (
<span style={{ marginRight: '8px' }}>
<CoverArtAvatar record={r} variant="square" />
<ArtworkAvatar record={r} variant="square" />
</span>
)}
linkType={'show'}
@ -173,7 +173,7 @@ const AlbumTableView = ({
/>
) : (
<AlbumDatagrid rowClick={'show'} classes={{ row: classes.row }} {...rest}>
<CoverArtAvatar source="id" variant="square" />
<ArtworkAvatar source="id" variant="square" />
<TextField source="name" />
{columns}
<AlbumContextMenu

View File

@ -22,7 +22,7 @@ import { useDrag } from 'react-dnd'
import clsx from 'clsx'
import {
ArtistContextMenu,
CoverArtAvatar,
ArtworkAvatar,
List,
useGetHandleArtistClick,
RatingField,
@ -173,7 +173,7 @@ const ArtistListView = ({ hasShow, hasEdit, hasList, width, ...rest }) => {
/>
) : (
<ArtistDatagrid rowClick={handleArtistLink} classes={{ row: classes.row }}>
<CoverArtAvatar source="id" />
<ArtworkAvatar source="id" />
<TextField source="name" />
<FunctionField
source="albumCount"

View File

@ -8,7 +8,7 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
import ListItemText from '@material-ui/core/ListItemText'
import { makeStyles } from '@material-ui/core/styles'
import { sanitizeListRestProps } from 'react-admin'
import { ArtistContextMenu, CoverArtAvatar, RatingField } from '../common'
import { ArtistContextMenu, ArtworkAvatar, RatingField } from '../common'
import config from '../config'
const useStyles = makeStyles(
@ -49,7 +49,7 @@ const ArtistSimpleList = ({
<span key={id} onClick={() => linkType(id)}>
<ListItem className={classes.listItem} button={true}>
<ListItemAvatar>
<CoverArtAvatar record={data[id]} />
<ArtworkAvatar record={data[id]} />
</ListItemAvatar>
<ListItemText
style={{ marginLeft: '8px' }}

View File

@ -11,7 +11,7 @@ import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
import AlbumInfo from '../album/AlbumInfo'
import subsonic from '../subsonic'
import { SafeHTML } from '../common/SafeHTML'
import { CoverImage } from '../common/CoverImage'
import { Artwork } from '../common/Artwork'
const useStyles = makeStyles(
(theme) => ({
@ -90,7 +90,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => {
<Card className={classes.artistDetail}>
<Card className={classes.artistImage}>
{artistInfo && (
<CoverImage
<Artwork
record={record}
className={classes.cover}
title={title}

View File

@ -7,7 +7,7 @@ import { LoveButton, RatingField, ImageUploadOverlay } from '../common'
import Lightbox from 'react-image-lightbox'
import subsonic from '../subsonic'
import { SafeHTML } from '../common/SafeHTML'
import { CoverImage } from '../common/CoverImage'
import { Artwork } from '../common/Artwork'
const useStyles = makeStyles(
(theme) => ({
@ -93,7 +93,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => {
<div className={classes.bgContainer}>
<Card className={classes.artistImage}>
{artistInfo && (
<CoverImage
<Artwork
record={record}
className={classes.cover}
title={title}

View File

@ -34,10 +34,10 @@ const useStyles = makeStyles({
imgInstant: { opacity: 1, transition: 'none' },
})
// CoverImage renders an entity's cover through the shared useImageUrl blob cache, so it survives
// Artwork renders an entity's cover through the shared useImageUrl blob cache, so it survives
// React remounts without re-fetching. The blurhash is the loading placeholder; the image is only
// mounted once its blob is ready, so an unresolved cover never renders as a broken <img>.
export const CoverImage = ({
export const Artwork = ({
record,
size = config.uiCoverArtSize,
square = false,
@ -107,7 +107,7 @@ export const CoverImage = ({
)
}
CoverImage.propTypes = {
Artwork.propTypes = {
record: PropTypes.object,
size: PropTypes.number,
square: PropTypes.bool,

View File

@ -8,7 +8,7 @@ vi.mock('../subsonic', () => ({
vi.mock('../config', () => ({ default: { uiCoverArtSize: 300 } }))
import { useImageUrl } from './useImageUrl'
import { CoverImage } from './CoverImage'
import { Artwork } from './Artwork'
const withArt = {
id: 'al-1',
@ -16,7 +16,7 @@ const withArt = {
blurHash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj',
}
describe('CoverImage', () => {
describe('Artwork', () => {
beforeEach(() => {
vi.clearAllMocks()
// jsdom has no 2D context; stub it so BlurHashCanvas bails cleanly without console noise
@ -25,29 +25,27 @@ describe('CoverImage', () => {
it('renders nothing without a record', () => {
useImageUrl.mockReturnValue({ imgUrl: null, loading: false })
const { container } = render(<CoverImage record={null} />)
const { container } = render(<Artwork record={null} />)
expect(container.firstChild).toBeNull()
})
it('shows the blurhash and no <img> while loading', () => {
useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
const { container } = render(<CoverImage record={withArt} title="Album" />)
const { container } = render(<Artwork record={withArt} title="Album" />)
expect(container.querySelector('canvas')).not.toBeNull()
expect(container.querySelector('img')).toBeNull()
})
it('shows neither a broken <img> nor a canvas while loading a record with no blurhash', () => {
useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
const { container } = render(
<CoverImage record={{ id: 'al-2', name: 'X' }} />,
)
const { container } = render(<Artwork record={{ id: 'al-2', name: 'X' }} />)
expect(container.querySelector('img')).toBeNull()
expect(container.querySelector('canvas')).toBeNull()
})
it('mounts the image only once its blob is ready', () => {
useImageUrl.mockReturnValue({ imgUrl: 'blob:abc', loading: false })
const { container } = render(<CoverImage record={withArt} title="Album" />)
const { container } = render(<Artwork record={withArt} title="Album" />)
const img = container.querySelector('img')
expect(img).not.toBeNull()
expect(img.getAttribute('src')).toBe('blob:abc')
@ -57,11 +55,11 @@ describe('CoverImage', () => {
vi.useFakeTimers()
try {
useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
const { container, rerender } = render(<CoverImage record={withArt} />)
const { container, rerender } = render(<Artwork record={withArt} />)
// Blob arrives: the image mounts transparent, with the blurhash still behind it.
useImageUrl.mockReturnValue({ imgUrl: 'blob:abc', loading: false })
rerender(<CoverImage record={withArt} />)
rerender(<Artwork record={withArt} />)
const img = container.querySelector('img')
expect(img).not.toBeNull()
expect(container.querySelector('canvas')).not.toBeNull()
@ -87,7 +85,7 @@ describe('CoverImage', () => {
it('does not fade an image that was already cached on mount', () => {
useImageUrl.mockReturnValue({ imgUrl: 'blob:abc', loading: false })
const { container } = render(<CoverImage record={withArt} />)
const { container } = render(<Artwork record={withArt} />)
// No placeholder to cross-fade from, so it paints at once.
expect(container.querySelector('canvas')).toBeNull()
expect(container.querySelector('img').className).toContain('imgInstant')
@ -95,9 +93,9 @@ describe('CoverImage', () => {
it('keeps the blurhash visible when the image never decodes', () => {
useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
const { container, rerender } = render(<CoverImage record={withArt} />)
const { container, rerender } = render(<Artwork record={withArt} />)
useImageUrl.mockReturnValue({ imgUrl: 'blob:abc', loading: false })
rerender(<CoverImage record={withArt} />)
rerender(<Artwork record={withArt} />)
expect(container.querySelector('canvas')).not.toBeNull()
})
@ -106,13 +104,13 @@ describe('CoverImage', () => {
const onClick = vi.fn()
useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
const { container, rerender } = render(
<CoverImage record={withArt} onClick={onClick} />,
<Artwork record={withArt} onClick={onClick} />,
)
container.firstChild.click()
expect(onClick).not.toHaveBeenCalled()
useImageUrl.mockReturnValue({ imgUrl: 'blob:abc', loading: false })
rerender(<CoverImage record={withArt} onClick={onClick} />)
rerender(<Artwork record={withArt} onClick={onClick} />)
container.firstChild.click()
expect(onClick).toHaveBeenCalledTimes(1)
})

View File

@ -2,7 +2,7 @@ import { useRecordContext } from 'react-admin'
import { makeStyles } from '@material-ui/core/styles'
import clsx from 'clsx'
import config from '../config'
import { CoverImage } from './CoverImage'
import { Artwork } from './Artwork'
const useStyles = makeStyles({
avatar: {
@ -17,17 +17,14 @@ const useStyles = makeStyles({
},
})
export const CoverArtAvatar = ({
record: recordProp,
variant = 'circular',
}) => {
export const ArtworkAvatar = ({ record: recordProp, variant = 'circular' }) => {
const classes = useStyles()
const recordContext = useRecordContext()
const record = recordProp || recordContext
if (!record) return null
const square = variant !== 'circular'
return (
<CoverImage
<Artwork
record={record}
size={config.uiCoverArtSize}
square={square}
@ -40,4 +37,4 @@ export const CoverArtAvatar = ({
)
}
CoverArtAvatar.defaultProps = { label: '', sortable: false }
ArtworkAvatar.defaultProps = { label: '', sortable: false }

View File

@ -45,6 +45,6 @@ export * from './ParticipantsInfo'
export * from './OverflowTooltip'
export * from './useSearchRefocus'
export * from './ImageUploadOverlay'
export * from './CoverArtAvatar'
export * from './ArtworkAvatar'
export * from './useImageLoadingState'
export * from './useImageUrl'

View File

@ -14,7 +14,7 @@ import {
OverflowTooltip,
} from '../common'
import subsonic from '../subsonic'
import { CoverImage } from '../common/CoverImage'
import { Artwork } from '../common/Artwork'
const useStyles = makeStyles(
(theme) => ({
@ -106,7 +106,7 @@ const PlaylistDetails = (props) => {
<Card className={classes.root}>
<div className={classes.cardContents}>
<div className={classes.coverParent}>
<CoverImage
<Artwork
record={record}
square
fit="contain"

View File

@ -20,7 +20,7 @@ import Switch from '@material-ui/core/Switch'
import { makeStyles } from '@material-ui/core/styles'
import { useMediaQuery } from '@material-ui/core'
import {
CoverArtAvatar,
ArtworkAvatar,
DurationField,
List,
LoveButton,
@ -197,7 +197,7 @@ const PlaylistList = (props) => {
bulkActionButtons={!isXsmall && <PlaylistListBulkActions />}
>
<Datagrid rowClick="show" isRowSelectable={(r) => isWritable(r?.ownerId)}>
<CoverArtAvatar source="id" variant="square" />
<ArtworkAvatar source="id" variant="square" />
<TextField source="name" />
{columns}
<Writable>

View File

@ -21,7 +21,7 @@ import {
ToggleFieldsMenu,
useSelectedFields,
} from '../common'
import { CoverImage } from '../common/CoverImage'
import { Artwork } from '../common/Artwork'
import { StreamField } from './StreamField'
import { setTrack } from '../actions'
import { songFromRadio } from './helper'
@ -92,7 +92,7 @@ const CoverArtField = ({ record }) => {
// Radios resolve art only from an uploaded image; otherwise show the generic radio icon.
if (record.uploadedImage) {
return (
<CoverImage
<Artwork
record={record}
size={40}
square