From 01cf2d291568c7878e7b62375eb8494b6d716824 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 24 Jul 2026 13:53:04 -0400 Subject: [PATCH] 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. --- ui/src/album/AlbumGridView.jsx | 26 ++++--------- ui/src/common/CoverArtAvatar.jsx | 65 +++++--------------------------- ui/src/common/CoverImage.jsx | 1 + ui/src/radio/RadioList.jsx | 44 ++++++++++----------- 4 files changed, 37 insertions(+), 99 deletions(-) diff --git a/ui/src/album/AlbumGridView.jsx b/ui/src/album/AlbumGridView.jsx index aaa1d5a6a..6e35e4b23 100644 --- a/ui/src/album/AlbumGridView.jsx +++ b/ui/src/album/AlbumGridView.jsx @@ -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 (
-
- {showBlurHash && ( - - )} - {record.name} +
diff --git a/ui/src/common/CoverArtAvatar.jsx b/ui/src/common/CoverArtAvatar.jsx index 81c0011ab..d932c9fb2 100644 --- a/ui/src/common/CoverArtAvatar.jsx +++ b/ui/src/common/CoverArtAvatar.jsx @@ -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 = ( - - {/* Empty child prevents default person icon while loading */} - {!imgUrl && } - - ) - - // Show the blurhash behind the transparent avatar until the real image loads. - if (!(loading && record.blurHash)) return avatar + const square = variant !== 'circular' return ( -
- - {avatar} -
+ ) } diff --git a/ui/src/common/CoverImage.jsx b/ui/src/common/CoverImage.jsx index 49f6ac58f..b9ceb03b3 100644 --- a/ui/src/common/CoverImage.jsx +++ b/ui/src/common/CoverImage.jsx @@ -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', }} diff --git a/ui/src/radio/RadioList.jsx b/ui/src/radio/RadioList.jsx index 67d541c6b..2dd00feef 100644 --- a/ui/src/radio/RadioList.jsx +++ b/ui/src/radio/RadioList.jsx @@ -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 = ( - - ) - if (!showBlurHash) return avatar + // Radios resolve art only from an uploaded image; otherwise show the generic radio icon. + if (record.uploadedImage) { + return ( + + ) + } return ( -
- - {avatar} -
+ ) } CoverArtField.defaultProps = { label: '' }