mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
refactor(ui): rename cover artwork components
This commit is contained in:
parent
19d89143f7
commit
57a95c0dfd
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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' }}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
@ -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)
|
||||
})
|
||||
@ -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 }
|
||||
@ -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'
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user