diff --git a/packages/common/src/adapters/collection.ts b/packages/common/src/adapters/collection.ts index 68a3f86eb2f..831267c0f12 100644 --- a/packages/common/src/adapters/collection.ts +++ b/packages/common/src/adapters/collection.ts @@ -190,7 +190,7 @@ export const playlistMetadataForCreateWithSDK = ( parentalWarningType: input.parental_warning_type ?? undefined, ...('cover_art_sizes' in input ? { - coverArtCid: input.cover_art_sizes ?? '', + playlistImageSizesMultihash: input.cover_art_sizes ?? '', isImageAutogenerated: input.is_image_autogenerated ?? false } : {}) @@ -211,7 +211,7 @@ export const playlistMetadataForUpdateWithSDK = ( : undefined, playlistName: input.playlist_name ?? '', description: input.description ?? '', - coverArtCid: input.cover_art_sizes ?? '', + playlistImageSizesMultihash: input.cover_art_sizes ?? '', isPrivate: input.is_private ?? false } } diff --git a/packages/mobile/src/components/image/CollectionImage.tsx b/packages/mobile/src/components/image/CollectionImage.tsx index 54ad8828a07..4369e516bb6 100644 --- a/packages/mobile/src/components/image/CollectionImage.tsx +++ b/packages/mobile/src/components/image/CollectionImage.tsx @@ -1,21 +1,36 @@ +import { useState } from 'react' + import { useCollection } from '@audius/common/api' import { useImageSize } from '@audius/common/hooks' import type { SquareSizes, ID } from '@audius/common/models' import { reachabilitySelectors } from '@audius/common/store' import type { Maybe } from '@audius/common/utils' +import type { LayoutChangeEvent } from 'react-native' +import { View } from 'react-native' import { useSelector } from 'react-redux' -import { Artwork, preload } from '@audius/harmony-native' +import { Artwork, IconImage, preload } from '@audius/harmony-native' import type { ImageProps } from '@audius/harmony-native' -import imageEmpty from 'app/assets/images/imageBlank2x.png' import { getLocalCollectionCoverArtPath } from 'app/services/offline-downloader' import { getCollectionDownloadStatus } from 'app/store/offline-downloads/selectors' import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' +import { useThemeColors } from 'app/utils/theme' import { primitiveToImageSource } from './primitiveToImageSource' const { getIsReachable } = reachabilitySelectors +const EMPTY_ICON_MIN = 12 +const EMPTY_ICON_MAX = 128 +const EMPTY_ICON_RATIO = 0.35 + +const hasValidArtwork = (artwork: unknown): boolean => + !!artwork && + typeof artwork === 'object' && + Object.entries(artwork as Record).some( + ([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0 + ) + export const useLocalCollectionImageUri = (collectionId: Maybe) => { const collectionImageUri = useSelector((state) => { if (!collectionId) return null @@ -45,9 +60,17 @@ export const useCollectionImage = ({ collectionId: Maybe size: SquareSizes }) => { - const { data: artwork } = useCollection(collectionId, { - select: (collection) => collection.artwork + const { data: artworkData } = useCollection(collectionId, { + select: (collection) => + collection != null + ? { + artwork: collection.artwork, + hasNoArtwork: !hasValidArtwork(collection.artwork) + } + : undefined }) + const artwork = artworkData?.artwork + const hasNoArtwork = artworkData?.hasNoArtwork ?? false const { imageUrl, onError: onImageError } = useImageSize({ artwork, targetSize: size, @@ -57,30 +80,25 @@ export const useCollectionImage = ({ } }) - if (imageUrl === '') { - return { - source: imageEmpty, - isFallbackImage: true, - onError: onImageError - } + if (hasNoArtwork || artworkData === undefined) { + return { source: undefined, hasNoArtwork: true, onError: onImageError } } - // Return edited artwork from this session, if it exists - // TODO(PAY-3588) Update field once we've switched to another property name - // for local changes to artwork - // @ts-ignore if (artwork?.url) { return { - // @ts-ignore source: primitiveToImageSource(artwork.url), - isFallbackImage: false, + hasNoArtwork: false, onError: onImageError } } + if (imageUrl === '') { + return { source: undefined, hasNoArtwork: true, onError: onImageError } + } + return { source: primitiveToImageSource(imageUrl), - isFallbackImage: false, + hasNoArtwork: false, onError: onImageError } } @@ -97,11 +115,39 @@ type CollectionImageProps = { export const CollectionImage = (props: CollectionImageProps) => { const { collectionId, size, style, onLoad, onError, ...other } = props + const { staticWhite } = useThemeColors() + const [containerSize, setContainerSize] = useState({ w: 0, h: 0 }) const localCollectionImageUri = useLocalCollectionImageUri(collectionId) const collectionImageSource = useCollectionImage({ collectionId, size }) - const { source: loadedSource, onError: onImageError } = collectionImageSource - - const source = loadedSource ?? localCollectionImageUri + const { + source: loadedSource, + onError: onImageError, + hasNoArtwork + } = collectionImageSource + + const onEmptyStateLayout = (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout + setContainerSize((prev) => + prev.w === width && prev.h === height ? prev : { w: width, h: height } + ) + } + const emptyIconSize = + containerSize.w > 0 && containerSize.h > 0 + ? Math.round( + Math.min( + EMPTY_ICON_MAX, + Math.max( + EMPTY_ICON_MIN, + Math.min(containerSize.w, containerSize.h) * EMPTY_ICON_RATIO + ) + ) + ) + : EMPTY_ICON_MIN + + const source = + hasNoArtwork === true + ? undefined + : (loadedSource ?? localCollectionImageUri) const handleError = (error: { nativeEvent: { error: string } }) => { if (source && typeof source === 'object' && 'uri' in source) { @@ -117,6 +163,29 @@ export const CollectionImage = (props: CollectionImageProps) => { onLoad={onLoad} onError={handleError} style={style} - /> + > + {hasNoArtwork ? ( + + + + ) : null} + ) } diff --git a/packages/mobile/src/components/image/TrackImage.tsx b/packages/mobile/src/components/image/TrackImage.tsx index af2c0ca6429..ce2bec5967a 100644 --- a/packages/mobile/src/components/image/TrackImage.tsx +++ b/packages/mobile/src/components/image/TrackImage.tsx @@ -1,21 +1,36 @@ +import { useState } from 'react' + import { useTrack } from '@audius/common/api' import { useImageSize } from '@audius/common/hooks' import type { SquareSizes, ID } from '@audius/common/models' import { reachabilitySelectors } from '@audius/common/store' import type { Maybe } from '@audius/common/utils' +import type { LayoutChangeEvent } from 'react-native' +import { View } from 'react-native' import { useSelector } from 'react-redux' import type { CornerRadiusOptions, ImageProps } from '@audius/harmony-native' -import { Artwork, preload } from '@audius/harmony-native' -import imageEmpty from 'app/assets/images/imageBlank2x.png' +import { Artwork, IconImage, preload } from '@audius/harmony-native' import { getLocalTrackCoverArtPath } from 'app/services/offline-downloader' import { getTrackDownloadStatus } from 'app/store/offline-downloads/selectors' import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' +import { useThemeColors } from 'app/utils/theme' import { primitiveToImageSource } from './primitiveToImageSource' const { getIsReachable } = reachabilitySelectors +const EMPTY_ICON_MIN = 12 +const EMPTY_ICON_MAX = 128 +const EMPTY_ICON_RATIO = 0.35 + +const hasValidArtwork = (artwork: unknown): boolean => + !!artwork && + typeof artwork === 'object' && + Object.entries(artwork as Record).some( + ([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0 + ) + const useLocalTrackImageUri = (trackId: Maybe) => { const trackImageUri = useSelector((state) => { if (!trackId) return null @@ -40,11 +55,17 @@ export const useTrackImage = ({ trackId?: ID size: SquareSizes }) => { - const { data: artwork } = useTrack(trackId, { - select: (track) => { - return track.artwork - } + const { data: artworkData } = useTrack(trackId, { + select: (track) => + track != null + ? { + artwork: track.artwork, + hasNoArtwork: !hasValidArtwork(track.artwork) + } + : undefined }) + const artwork = artworkData?.artwork + const hasNoArtwork = artworkData?.hasNoArtwork ?? false const { imageUrl, onError: onImageError } = useImageSize({ artwork, targetSize: size, @@ -54,29 +75,29 @@ export const useTrackImage = ({ } }) - if (imageUrl === '') { - return { - source: imageEmpty, - isFallbackImage: true - } + // When track has no artwork or track not loaded yet, don't pass a URL so we never show stale image + if (hasNoArtwork || artworkData === undefined) { + return { source: undefined, hasNoArtwork: true } } // Return edited artwork from this session, if it exists - // TODO(PAY-3588) Update field once we've switched to another property name - // for local changes to artwork - // @ts-ignore + // @ts-expect-error - url is added for in-session edits if (artwork?.url) { return { - // @ts-ignore + // @ts-expect-error - url is added for in-session edits source: primitiveToImageSource(artwork.url), - isFallbackImage: false, + hasNoArtwork: false, onError: onImageError } } + if (imageUrl === '') { + return { source: undefined, hasNoArtwork: true } + } + return { source: primitiveToImageSource(imageUrl), - isFallbackImage: false, + hasNoArtwork: false, onError: onImageError } } @@ -102,11 +123,37 @@ export const TrackImage = (props: TrackImageProps) => { children } = props + const { staticWhite } = useThemeColors() + const [containerSize, setContainerSize] = useState({ w: 0, h: 0 }) const localTrackImageUri = useLocalTrackImageUri(trackId) const trackImageSource = useTrackImage({ trackId, size }) - const { source: loadedSource, onError: onImageError } = trackImageSource - - const source = loadedSource ?? localTrackImageUri + const { + source: loadedSource, + onError: onImageError, + hasNoArtwork + } = trackImageSource + + const onEmptyStateLayout = (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout + setContainerSize((prev) => + prev.w === width && prev.h === height ? prev : { w: width, h: height } + ) + } + const emptyIconSize = + containerSize.w > 0 && containerSize.h > 0 + ? Math.round( + Math.min( + EMPTY_ICON_MAX, + Math.max( + EMPTY_ICON_MIN, + Math.min(containerSize.w, containerSize.h) * EMPTY_ICON_RATIO + ) + ) + ) + : EMPTY_ICON_MIN + + const source = + hasNoArtwork === true ? undefined : (loadedSource ?? localTrackImageUri) const handleError = (error: any) => { try { @@ -134,6 +181,28 @@ export const TrackImage = (props: TrackImageProps) => { borderRadius={borderRadius} style={style} > + {hasNoArtwork ? ( + + + + ) : null} {children} ) diff --git a/packages/mobile/src/components/now-playing-drawer/Artwork.tsx b/packages/mobile/src/components/now-playing-drawer/Artwork.tsx index 988ff1ea873..a46803b93ec 100644 --- a/packages/mobile/src/components/now-playing-drawer/Artwork.tsx +++ b/packages/mobile/src/components/now-playing-drawer/Artwork.tsx @@ -78,6 +78,7 @@ export const Artwork = ({ track }: ArtworkProps) => { return ( { borderRadius={borderRadius} border='default' shadow={shadow} + w='100%' + h='100%' style={{ borderWidth }} > {isLoading && hasImageSource ? ( @@ -102,7 +104,7 @@ export const Artwork = (props: ArtworkProps) => { style={{ backgroundColor: !hasImageSource && children - ? color.neutral.n400 + ? color.neutral.n100 : color.background.surface2 }} /> diff --git a/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx b/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx index 35bfbdc01f5..cf440f68f60 100644 --- a/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx +++ b/packages/web/src/components/add-to-collection/desktop/AddToCollectionModal.tsx @@ -219,7 +219,7 @@ const CollectionItem = ({ collection, collectionType }: CollectionItemProps) => { - const image = useCollectionCoverArt({ + const { imageUrl: image } = useCollectionCoverArt({ collectionId: collection.playlist_id, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/collection/CollectionImage.tsx b/packages/web/src/components/collection/CollectionImage.tsx index 134d2c215b4..c4f12e37b93 100644 --- a/packages/web/src/components/collection/CollectionImage.tsx +++ b/packages/web/src/components/collection/CollectionImage.tsx @@ -11,7 +11,7 @@ type CollectionImageProps = { export const CollectionImage = (props: CollectionImageProps) => { const { collectionId, size, ...other } = props - const imageSource = useCollectionCoverArt({ + const { imageUrl: imageSource } = useCollectionCoverArt({ collectionId, size }) diff --git a/packages/web/src/components/collection/desktop/Artwork.tsx b/packages/web/src/components/collection/desktop/Artwork.tsx index 5447a4314a0..57cd1b798c3 100644 --- a/packages/web/src/components/collection/desktop/Artwork.tsx +++ b/packages/web/src/components/collection/desktop/Artwork.tsx @@ -31,7 +31,7 @@ export const Artwork = (props: ArtworkProps) => { }) const { is_image_autogenerated, permalink } = partialCollection ?? {} - const image = useCollectionCoverArt({ + const { imageUrl: image } = useCollectionCoverArt({ collectionId, size: SquareSizes.SIZE_1000_BY_1000 }) diff --git a/packages/web/src/components/collection/mobile/CollectionHeader.tsx b/packages/web/src/components/collection/mobile/CollectionHeader.tsx index babd024b2f8..e06863c8b9d 100644 --- a/packages/web/src/components/collection/mobile/CollectionHeader.tsx +++ b/packages/web/src/components/collection/mobile/CollectionHeader.tsx @@ -166,7 +166,7 @@ const CollectionHeader = ({ onClickMobileOverflow?.(collectionId, overflowActions) } - const image = useCollectionCoverArt({ + const { imageUrl: image } = useCollectionCoverArt({ collectionId, size: SquareSizes.SIZE_1000_BY_1000 }) diff --git a/packages/web/src/components/dynamic-image/DynamicImage.tsx b/packages/web/src/components/dynamic-image/DynamicImage.tsx index 41fadca5535..b19703ac1d9 100644 --- a/packages/web/src/components/dynamic-image/DynamicImage.tsx +++ b/packages/web/src/components/dynamic-image/DynamicImage.tsx @@ -50,6 +50,7 @@ const moveBehind = (ref: RefObject) => { ref.current.style.animation = 'none' ref.current.style.zIndex = '1' ref.current.style.backgroundColor = 'unset' + ref.current.style.backgroundImage = 'none' } } diff --git a/packages/web/src/components/nav/desktop/NowPlayingArtworkTile.tsx b/packages/web/src/components/nav/desktop/NowPlayingArtworkTile.tsx index 11fd474c986..e60519eefb4 100644 --- a/packages/web/src/components/nav/desktop/NowPlayingArtworkTile.tsx +++ b/packages/web/src/components/nav/desktop/NowPlayingArtworkTile.tsx @@ -4,6 +4,7 @@ import { useCurrentUserId, useTrack } from '@audius/common/api' import { SquareSizes } from '@audius/common/models' import { playerSelectors } from '@audius/common/store' import { + IconImage, IconWaveForm as IconVisualizer, IconButton, useTheme, @@ -56,7 +57,7 @@ export const NowPlayingArtworkTile = () => { }) const { title, isStreamGated, permalink, isOwner } = partialTrack ?? {} - const trackCoverArtImage = useTrackCoverArt({ + const { imageUrl: trackCoverArtImage, hasNoArtwork } = useTrackCoverArt({ trackId: trackId ?? undefined, size: SquareSizes.SIZE_480_BY_480 }) @@ -100,7 +101,26 @@ export const NowPlayingArtworkTile = () => { style={slideInProps} > - + + {hasNoArtwork ? ( + + + + ) : null} { const { track, hideTitle = false } = props - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId: track.track_id, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/now-playing/NowPlaying.module.css b/packages/web/src/components/now-playing/NowPlaying.module.css index 3e5e1be3d28..2d647b75234 100644 --- a/packages/web/src/components/now-playing/NowPlaying.module.css +++ b/packages/web/src/components/now-playing/NowPlaying.module.css @@ -84,6 +84,22 @@ overflow: hidden; } +/* Empty artwork: white image icon on gray (no skeleton) */ +.emptyArtworkIcon { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; +} +.emptyArtworkIcon path { + fill: var(--harmony-static-white); +} + .info { text-align: center; width: 100%; diff --git a/packages/web/src/components/now-playing/NowPlaying.tsx b/packages/web/src/components/now-playing/NowPlaying.tsx index 4518a947a66..e52f2e6bb87 100644 --- a/packages/web/src/components/now-playing/NowPlaying.tsx +++ b/packages/web/src/components/now-playing/NowPlaying.tsx @@ -34,7 +34,11 @@ import { PurchaseableContentType } from '@audius/common/store' import { Genre, route } from '@audius/common/utils' -import { IconCaretRight as IconCaret, Scrubber } from '@audius/harmony' +import { + IconCaretRight as IconCaret, + IconImage, + Scrubber +} from '@audius/harmony' import { Location } from 'history' import { connect, useSelector } from 'react-redux' import { Dispatch } from 'redux' @@ -213,7 +217,7 @@ const NowPlaying = g( } = track const { name, handle } = user - const image = useTrackCoverArt({ + const { imageUrl: image, hasNoArtwork } = useTrackCoverArt({ trackId: track_id, size: SquareSizes.SIZE_480_BY_480 }) @@ -403,7 +407,13 @@ const NowPlaying = g( style={artworkAverageColor} > - + + {hasNoArtwork ? ( +
+ +
+ ) : null} +
) : ( @@ -415,7 +425,13 @@ const NowPlaying = g( style={artworkAverageColor} > - + + {hasNoArtwork ? ( +
+ +
+ ) : null} +
)} diff --git a/packages/web/src/components/play-bar/mobile/PlayBar.module.css b/packages/web/src/components/play-bar/mobile/PlayBar.module.css index 4265978009b..24ce9aba614 100644 --- a/packages/web/src/components/play-bar/mobile/PlayBar.module.css +++ b/packages/web/src/components/play-bar/mobile/PlayBar.module.css @@ -46,6 +46,24 @@ overflow: hidden; } +.imageEmpty { + background-color: var(--harmony-n-100); +} + +.emptyArtworkIcon { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; +} +.emptyArtworkIcon path { + fill: var(--harmony-static-white); +} + .info { flex: 1 1 auto; display: flex; diff --git a/packages/web/src/components/play-bar/mobile/PlayBar.tsx b/packages/web/src/components/play-bar/mobile/PlayBar.tsx index 4dede0e6937..1ced9ddb77d 100644 --- a/packages/web/src/components/play-bar/mobile/PlayBar.tsx +++ b/packages/web/src/components/play-bar/mobile/PlayBar.tsx @@ -15,7 +15,8 @@ import { tracksSocialActions, playerSelectors } from '@audius/common/store' -import { IconLock } from '@audius/harmony' +import { IconImage, IconLock } from '@audius/harmony' +import cn from 'classnames' import { connect, useSelector } from 'react-redux' import { Dispatch } from 'redux' @@ -84,7 +85,7 @@ const PlayBar = ({ return () => clearInterval(seekInterval) }) - const image = useTrackCoverArt({ + const { imageUrl: image, hasNoArtwork } = useTrackCoverArt({ trackId: track ? track.track_id : undefined, size: SquareSizes.SIZE_150_BY_150, defaultImage: '' @@ -166,11 +167,18 @@ const PlayBar = ({ id={track?.track_id} >
+ {hasNoArtwork ? ( +
+ +
+ ) : null} {shouldShowPreviewLock ? (
diff --git a/packages/web/src/components/remix-card/ConnectedRemixCard.tsx b/packages/web/src/components/remix-card/ConnectedRemixCard.tsx index 5801fcf2453..c153a5d2542 100644 --- a/packages/web/src/components/remix-card/ConnectedRemixCard.tsx +++ b/packages/web/src/components/remix-card/ConnectedRemixCard.tsx @@ -44,7 +44,7 @@ const ConnectedRemixCard = ({ userId: partialUser?.user_id, size: SquareSizes.SIZE_150_BY_150 }) - const coverArtImage = useTrackCoverArt({ + const { imageUrl: coverArtImage } = useTrackCoverArt({ trackId, size: SquareSizes.SIZE_480_BY_480 }) diff --git a/packages/web/src/components/search-bar/SearchBarResult.tsx b/packages/web/src/components/search-bar/SearchBarResult.tsx index 04ab39bb9f4..3fc40a0e9ef 100644 --- a/packages/web/src/components/search-bar/SearchBarResult.tsx +++ b/packages/web/src/components/search-bar/SearchBarResult.tsx @@ -202,7 +202,7 @@ export const TrackResult = ({ }: TrackResultProps) => { const { data: track } = useTrack(trackId) const { data: user } = useUser(track?.owner_id) - const trackArtwork = useTrackCoverArt({ + const { imageUrl: trackArtwork } = useTrackCoverArt({ trackId, size: SquareSizes.SIZE_150_BY_150 }) @@ -238,7 +238,7 @@ export const CollectionResult = ({ const { data: user } = useUser( collection ? collection.playlist_owner_id : null ) - const collectionArtwork = useCollectionCoverArt({ + const { imageUrl: collectionArtwork } = useCollectionCoverArt({ collectionId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/suggested-tracks/SuggestedTracks.tsx b/packages/web/src/components/suggested-tracks/SuggestedTracks.tsx index fd739d39295..5d942db8435 100644 --- a/packages/web/src/components/suggested-tracks/SuggestedTracks.tsx +++ b/packages/web/src/components/suggested-tracks/SuggestedTracks.tsx @@ -47,7 +47,7 @@ const SuggestedTrackRow = (props: SuggestedTrackProps) => { const { track_id, title, owner_id } = track const { data: user } = useUser(owner_id) const { data: collection } = useCollection(collectionId) - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId: track_id, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/track/Artwork.module.css b/packages/web/src/components/track/Artwork.module.css index 10951fed684..3b90b21715f 100644 --- a/packages/web/src/components/track/Artwork.module.css +++ b/packages/web/src/components/track/Artwork.module.css @@ -28,6 +28,15 @@ opacity: 0; transition: all ease-in-out 0.18s; } + +/* Empty artwork state: gray background + visible image icon (no skeleton) */ +.artworkWrapper.artworkEmpty .artworkIcon { + opacity: 1; + background-color: transparent; +} +.artworkWrapper.artworkEmpty .artworkIcon path { + fill: var(--harmony-static-white); +} .artworkWrapper .artworkIcon > svg, .artworkWrapper .artworkIcon > div { position: absolute; diff --git a/packages/web/src/components/track/Artwork.tsx b/packages/web/src/components/track/Artwork.tsx index 3bf58292d3c..bf76600c950 100644 --- a/packages/web/src/components/track/Artwork.tsx +++ b/packages/web/src/components/track/Artwork.tsx @@ -2,6 +2,7 @@ import { memo } from 'react' import { SquareSizes, ID } from '@audius/common/models' import { + IconImage, IconLock, IconPlaybackPlay as IconPlay, IconPlaybackPause as IconPause @@ -39,16 +40,20 @@ export const ArtworkIcon = ({ isPlaying, artworkIconClassName, hasStreamAccess, - isTrack + isTrack, + hasNoArtwork }: { isBuffering: boolean isPlaying: boolean artworkIconClassName?: string hasStreamAccess?: boolean isTrack?: boolean + hasNoArtwork?: boolean }) => { let artworkIcon - if (isTrack && !hasStreamAccess) { + if (hasNoArtwork) { + artworkIcon = + } else if (isTrack && !hasStreamAccess) { artworkIcon = } else if (isBuffering) { artworkIcon = @@ -72,6 +77,7 @@ type ArtworkProps = TileArtworkProps & { image: any label?: string isTrack?: boolean + hasNoArtwork?: boolean } const Artwork = memo( @@ -88,19 +94,22 @@ const Artwork = memo( label, hasStreamAccess, isTrack, - noShimmer + noShimmer, + hasNoArtwork }: ArtworkProps) => { const imageElement = ( {showArtworkIcon && ( )} @@ -131,19 +141,21 @@ const Artwork = memo( ) export const TrackArtwork = memo((props: TileArtworkProps) => { - const image = useTrackCoverArt({ + const { imageUrl: image, hasNoArtwork } = useTrackCoverArt({ trackId: props.id, size: SquareSizes.SIZE_150_BY_150 }) - return + return ( + + ) }) export const CollectionArtwork = memo((props: TileArtworkProps) => { - const image = useCollectionCoverArt({ + const { imageUrl: image, hasNoArtwork } = useCollectionCoverArt({ collectionId: props.id, size: SquareSizes.SIZE_150_BY_150 }) - return + return }) diff --git a/packages/web/src/components/track/GiantArtwork.module.css b/packages/web/src/components/track/GiantArtwork.module.css index b6d39b31845..fecd5f7cf34 100644 --- a/packages/web/src/components/track/GiantArtwork.module.css +++ b/packages/web/src/components/track/GiantArtwork.module.css @@ -15,6 +15,22 @@ height: 338px; } +/* Empty artwork: subdued image icon on gray background (no skeleton) */ +.emptyArtworkIcon { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; +} +.emptyArtworkIcon path { + fill: var(--harmony-static-white); +} + .iconLeftWrapper { position: absolute; top: 16px; diff --git a/packages/web/src/components/track/GiantArtwork.tsx b/packages/web/src/components/track/GiantArtwork.tsx index 1ca807df142..bee06018f24 100644 --- a/packages/web/src/components/track/GiantArtwork.tsx +++ b/packages/web/src/components/track/GiantArtwork.tsx @@ -2,7 +2,7 @@ import { memo, useEffect } from 'react' import { SquareSizes, Remix } from '@audius/common/models' import { Nullable } from '@audius/common/utils' -import { IconArrowLeft } from '@audius/harmony' +import { IconArrowLeft, IconImage } from '@audius/harmony' import DynamicImage from 'components/dynamic-image/DynamicImage' import TrackFlair from 'components/track-flair/TrackFlair' @@ -24,25 +24,31 @@ const messages = { const GiantArtwork = (props: GiantArtworkProps) => { const { trackId, callback, onIconLeftClick } = props - const image = useTrackCoverArt({ + const { imageUrl: image, hasNoArtwork } = useTrackCoverArt({ trackId, size: SquareSizes.SIZE_1000_BY_1000 }) useEffect(() => { - if (image) callback() - }, [image, callback]) + if (image || hasNoArtwork) callback() + }, [image, hasNoArtwork, callback]) const imageElement = ( - {onIconLeftClick && ( + {hasNoArtwork ? ( +
+ +
+ ) : null} + {onIconLeftClick && !hasNoArtwork ? (
- )} + ) : null}
) diff --git a/packages/web/src/components/track/LockedContentDetailsTile.tsx b/packages/web/src/components/track/LockedContentDetailsTile.tsx index 74c65198726..f744798813b 100644 --- a/packages/web/src/components/track/LockedContentDetailsTile.tsx +++ b/packages/web/src/components/track/LockedContentDetailsTile.tsx @@ -53,11 +53,11 @@ export const LockedContentDetailsTile = ({ const title = isAlbum ? metadata.playlist_name : metadata.title const isDownloadGated = !isAlbum && metadata.is_download_gated - const trackArt = useTrackCoverArt({ + const { imageUrl: trackArt } = useTrackCoverArt({ trackId: contentId, size: SquareSizes.SIZE_150_BY_150 }) - const albumArt = useCollectionCoverArt({ + const { imageUrl: albumArt } = useCollectionCoverArt({ collectionId: contentId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/track/TrackArtwork.tsx b/packages/web/src/components/track/TrackArtwork.tsx index 89038be9840..79a98bec7df 100644 --- a/packages/web/src/components/track/TrackArtwork.tsx +++ b/packages/web/src/components/track/TrackArtwork.tsx @@ -23,7 +23,7 @@ export const TrackArtwork = (props: TrackArtworkProps) => { ...other } = props - const imageSource = useTrackCoverArt({ trackId, size }) + const { imageUrl: imageSource } = useTrackCoverArt({ trackId, size }) const artworkElement = ( diff --git a/packages/web/src/components/track/mobile/TrackListItem.tsx b/packages/web/src/components/track/mobile/TrackListItem.tsx index 9494a52a6e6..7a32e9552a8 100644 --- a/packages/web/src/components/track/mobile/TrackListItem.tsx +++ b/packages/web/src/components/track/mobile/TrackListItem.tsx @@ -69,7 +69,7 @@ const Artwork = ({ isLoading, coverArtSizes }: ArtworkProps) => { - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/track/mobile/TrackTileArt.module.css b/packages/web/src/components/track/mobile/TrackTileArt.module.css index 72eabf97927..84e472fc078 100644 --- a/packages/web/src/components/track/mobile/TrackTileArt.module.css +++ b/packages/web/src/components/track/mobile/TrackTileArt.module.css @@ -7,3 +7,12 @@ border-radius: 4px; overflow: hidden; } + +/* Empty artwork: image icon always visible on gray (no skeleton) */ +.artworkIconVisible { + opacity: 1 !important; + background-color: transparent !important; +} +.artworkIconVisible path { + fill: var(--harmony-static-white); +} diff --git a/packages/web/src/components/track/mobile/TrackTileArt.tsx b/packages/web/src/components/track/mobile/TrackTileArt.tsx index 6b00be99d1f..611f13186d8 100644 --- a/packages/web/src/components/track/mobile/TrackTileArt.tsx +++ b/packages/web/src/components/track/mobile/TrackTileArt.tsx @@ -40,7 +40,7 @@ const TrackTileArt = ({ artworkIconClassName, callback }: TrackTileArtProps) => { - const image = useTrackCoverArt({ + const { imageUrl: image, hasNoArtwork } = useTrackCoverArt({ trackId: id, size: SquareSizes.SIZE_150_BY_150 }) @@ -48,9 +48,15 @@ const TrackTileArt = ({ const imageProps = { image: showSkeleton ? '' : image, noShimmer, + useSkeleton: !hasNoArtwork, wrapperClassName: coSign ? styles.imageWrapper - : cn(styles.container, styles.imageWrapper, className), + : cn( + styles.container, + styles.imageWrapper, + className, + hasNoArtwork && styles.artworkEmpty + ), 'aria-label': label, onLoad: callback } @@ -60,7 +66,11 @@ const TrackTileArt = ({ ) @@ -88,7 +98,7 @@ const CollectionTileArt = ({ artworkIconClassName, callback }: TrackTileArtProps) => { - const image = useCollectionCoverArt({ + const { imageUrl: image, hasNoArtwork } = useCollectionCoverArt({ collectionId: id, size: SquareSizes.SIZE_150_BY_150 }) @@ -96,9 +106,15 @@ const CollectionTileArt = ({ const imageProps = { image: showSkeleton ? '' : image, noShimmer, + useSkeleton: !hasNoArtwork, wrapperClassName: coSign ? styles.imageWrapper - : cn(styles.container, styles.imageWrapper, className), + : cn( + styles.container, + styles.imageWrapper, + className, + hasNoArtwork && styles.artworkEmpty + ), 'aria-label': label, onLoad: callback } @@ -108,7 +124,11 @@ const CollectionTileArt = ({ ) diff --git a/packages/web/src/components/usdc-purchase-details-modal/components/AlbumPurchaseModalContent.tsx b/packages/web/src/components/usdc-purchase-details-modal/components/AlbumPurchaseModalContent.tsx index bbd853ef7bb..cae4e642ca7 100644 --- a/packages/web/src/components/usdc-purchase-details-modal/components/AlbumPurchaseModalContent.tsx +++ b/packages/web/src/components/usdc-purchase-details-modal/components/AlbumPurchaseModalContent.tsx @@ -23,7 +23,7 @@ export const AlbumPurchaseModalContent = ({ select: (collection) => pick(collection, ['playlist_name', 'permalink']) }) const { playlist_name, permalink } = partialAlbum ?? {} - const albumArtwork = useCollectionCoverArt({ + const { imageUrl: albumArtwork } = useCollectionCoverArt({ collectionId: contentId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/usdc-purchase-details-modal/components/AlbumSaleModalContent.tsx b/packages/web/src/components/usdc-purchase-details-modal/components/AlbumSaleModalContent.tsx index eba00518001..049e1b2638d 100644 --- a/packages/web/src/components/usdc-purchase-details-modal/components/AlbumSaleModalContent.tsx +++ b/packages/web/src/components/usdc-purchase-details-modal/components/AlbumSaleModalContent.tsx @@ -23,7 +23,7 @@ export const AlbumSaleModalContent = ({ select: (collection) => pick(collection, ['playlist_name', 'permalink']) }) const { playlist_name, permalink } = partialAlbum ?? {} - const albumArtwork = useCollectionCoverArt({ + const { imageUrl: albumArtwork } = useCollectionCoverArt({ collectionId: contentId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/usdc-purchase-details-modal/components/TrackPurchaseModalContent.tsx b/packages/web/src/components/usdc-purchase-details-modal/components/TrackPurchaseModalContent.tsx index fd650ce4372..8bc2459dc59 100644 --- a/packages/web/src/components/usdc-purchase-details-modal/components/TrackPurchaseModalContent.tsx +++ b/packages/web/src/components/usdc-purchase-details-modal/components/TrackPurchaseModalContent.tsx @@ -21,7 +21,7 @@ export const TrackPurchaseModalContent = ({ const { data: partialTrack } = useTrack(contentId, { select: (track) => pick(track, ['title', 'permalink']) }) - const trackArtwork = useTrackCoverArt({ + const { imageUrl: trackArtwork } = useTrackCoverArt({ trackId: contentId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/components/usdc-purchase-details-modal/components/TrackSaleModalContent.tsx b/packages/web/src/components/usdc-purchase-details-modal/components/TrackSaleModalContent.tsx index a46d9e99ccb..9a94e78295d 100644 --- a/packages/web/src/components/usdc-purchase-details-modal/components/TrackSaleModalContent.tsx +++ b/packages/web/src/components/usdc-purchase-details-modal/components/TrackSaleModalContent.tsx @@ -21,7 +21,7 @@ export const TrackSaleModalContent = ({ const { data: partialTrack } = useTrack(contentId, { select: (track) => pick(track, ['title', 'permalink']) }) - const trackArtwork = useTrackCoverArt({ + const { imageUrl: trackArtwork } = useTrackCoverArt({ trackId: contentId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/hooks/useCollectionCoverArt.ts b/packages/web/src/hooks/useCollectionCoverArt.ts index 2f987392f7b..733bc2d4f58 100644 --- a/packages/web/src/hooks/useCollectionCoverArt.ts +++ b/packages/web/src/hooks/useCollectionCoverArt.ts @@ -6,6 +6,13 @@ import { Maybe } from '@audius/common/utils' import { preload } from 'utils/image' +const hasValidArtwork = (artwork: unknown): boolean => + !!artwork && + typeof artwork === 'object' && + Object.entries(artwork).some( + ([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0 + ) + export const useCollectionCoverArt = ({ collectionId, size, @@ -15,9 +22,17 @@ export const useCollectionCoverArt = ({ size: SquareSizes defaultImage?: string }) => { - const { data: artwork } = useCollection(collectionId, { - select: (collection) => collection.artwork + const { data: artworkData } = useCollection(collectionId, { + select: (collection) => + collection != null + ? { + artwork: collection.artwork, + hasNoArtwork: !hasValidArtwork(collection.artwork) + } + : undefined }) + const artwork = artworkData?.artwork + const hasNoArtwork = artworkData?.hasNoArtwork ?? false const { imageUrl } = useImageSize({ artwork, targetSize: size, @@ -26,10 +41,10 @@ export const useCollectionCoverArt = ({ }) // Return edited artwork from this session, if it exists - // TODO(PAY-3588) Update field once we've switched to another property name - // for local changes to artwork - // @ts-ignore - if (artwork?.url) return artwork.url + if (artwork?.url) return { imageUrl: artwork.url, hasNoArtwork: false } - return imageUrl + return { + imageUrl, + hasNoArtwork: hasNoArtwork && !artwork?.url + } } diff --git a/packages/web/src/hooks/useTrackCoverArt.ts b/packages/web/src/hooks/useTrackCoverArt.ts index e58e9b437ad..b597f0be3fd 100644 --- a/packages/web/src/hooks/useTrackCoverArt.ts +++ b/packages/web/src/hooks/useTrackCoverArt.ts @@ -14,6 +14,13 @@ import { preload } from 'utils/image' import { dominantColor } from 'utils/imageProcessingUtil' import { useSelector } from 'utils/reducer' +const hasValidArtwork = (artwork: unknown): boolean => + !!artwork && + typeof artwork === 'object' && + Object.entries(artwork).some( + ([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0 + ) + export const useTrackCoverArt = ({ trackId, size, @@ -23,9 +30,17 @@ export const useTrackCoverArt = ({ size: SquareSizes defaultImage?: string }) => { - const { data: artwork } = useTrack(trackId, { - select: (track) => track?.artwork + const { data: artworkData } = useTrack(trackId, { + select: (track) => + track != null + ? { + artwork: track.artwork, + hasNoArtwork: !hasValidArtwork(track.artwork) + } + : undefined }) + const artwork = artworkData?.artwork + const hasNoArtwork = artworkData?.hasNoArtwork ?? false const { imageUrl } = useImageSize({ artwork, targetSize: size, @@ -36,10 +51,19 @@ export const useTrackCoverArt = ({ // Return edited artwork from this session, if it exists // TODO(PAY-3588) Update field once we've switched to another property name // for local changes to artwork - // @ts-ignore - if (artwork?.url) return artwork.url + // @ts-expect-error - url is added for in-session edits, not on CoverArtSizesWithMirror type + if (artwork?.url) return { imageUrl: artwork.url, hasNoArtwork: false } - return imageUrl + // @ts-expect-error - url is added for in-session edits + const noArtwork = hasNoArtwork && !artwork?.url + // Don't pass a URL when track has no artwork, or when track data isn't loaded yet + // (artworkData undefined), so we never show the previous track's image. + const safeImageUrl = + noArtwork || artworkData === undefined ? undefined : imageUrl + return { + imageUrl: safeImageUrl, + hasNoArtwork: noArtwork + } } export const useTrackCoverArtDominantColors = ({ @@ -49,7 +73,7 @@ export const useTrackCoverArtDominantColors = ({ }) => { const dispatch = useDispatch() - const trackCoverArtImage = useTrackCoverArt({ + const { imageUrl: trackCoverArtImage } = useTrackCoverArt({ trackId: trackId ?? undefined, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/pages/chat-page/components/ChatBlastHeader.tsx b/packages/web/src/pages/chat-page/components/ChatBlastHeader.tsx index 363718912bb..60a10a17291 100644 --- a/packages/web/src/pages/chat-page/components/ChatBlastHeader.tsx +++ b/packages/web/src/pages/chat-page/components/ChatBlastHeader.tsx @@ -23,11 +23,11 @@ export const ChatBlastHeader = ({ chat }: { chat: ChatBlast }) => { chat }) const decodedId = OptionalHashId.parse(audienceContentId) - const albumArtwork = useCollectionCoverArt({ + const { imageUrl: albumArtwork } = useCollectionCoverArt({ collectionId: decodedId, size: SquareSizes.SIZE_150_BY_150 }) - const trackArtwork = useTrackCoverArt({ + const { imageUrl: trackArtwork } = useTrackCoverArt({ trackId: decodedId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/pages/chat-page/components/ComposePreviewInfo.tsx b/packages/web/src/pages/chat-page/components/ComposePreviewInfo.tsx index a875efa1cc0..f80ff3283b7 100644 --- a/packages/web/src/pages/chat-page/components/ComposePreviewInfo.tsx +++ b/packages/web/src/pages/chat-page/components/ComposePreviewInfo.tsx @@ -44,7 +44,7 @@ type ComposerTrackInfoProps = { export const ComposerTrackInfo = (props: ComposerTrackInfoProps) => { const { trackId } = props - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId, size: SquareSizes.SIZE_150_BY_150 }) @@ -73,7 +73,7 @@ type ComposerCollectionInfoProps = { export const ComposerCollectionInfo = (props: ComposerCollectionInfoProps) => { const { collectionId } = props - const image = useCollectionCoverArt({ + const { imageUrl: image } = useCollectionCoverArt({ collectionId, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/pages/deleted-page/components/desktop/DeletedPage.tsx b/packages/web/src/pages/deleted-page/components/desktop/DeletedPage.tsx index ed64cccb528..6bbdc651b8d 100644 --- a/packages/web/src/pages/deleted-page/components/desktop/DeletedPage.tsx +++ b/packages/web/src/pages/deleted-page/components/desktop/DeletedPage.tsx @@ -52,7 +52,7 @@ const messages = { } const TrackArt = ({ trackId }: { trackId: ID }) => { - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId, size: SquareSizes.SIZE_480_BY_480 }) @@ -60,7 +60,7 @@ const TrackArt = ({ trackId }: { trackId: ID }) => { } const CollectionArt = ({ collectionId }: { collectionId: ID }) => { - const image = useCollectionCoverArt({ + const { imageUrl: image } = useCollectionCoverArt({ collectionId, size: SquareSizes.SIZE_480_BY_480 }) diff --git a/packages/web/src/pages/deleted-page/components/mobile/DeletedPage.tsx b/packages/web/src/pages/deleted-page/components/mobile/DeletedPage.tsx index 75e3ab5f8fc..c80391c09d0 100644 --- a/packages/web/src/pages/deleted-page/components/mobile/DeletedPage.tsx +++ b/packages/web/src/pages/deleted-page/components/mobile/DeletedPage.tsx @@ -48,7 +48,7 @@ const messages = { } const TrackArt = ({ trackId }: { trackId: ID }) => { - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId, size: SquareSizes.SIZE_480_BY_480 }) @@ -56,7 +56,7 @@ const TrackArt = ({ trackId }: { trackId: ID }) => { } const CollectionArt = ({ collectionId }: { collectionId: ID }) => { - const image = useCollectionCoverArt({ + const { imageUrl: image } = useCollectionCoverArt({ collectionId, size: SquareSizes.SIZE_480_BY_480 }) diff --git a/packages/web/src/pages/edit-collection-page/desktop/EditCollectionPage.tsx b/packages/web/src/pages/edit-collection-page/desktop/EditCollectionPage.tsx index 2c4ea97ed86..b3f475c060e 100644 --- a/packages/web/src/pages/edit-collection-page/desktop/EditCollectionPage.tsx +++ b/packages/web/src/pages/edit-collection-page/desktop/EditCollectionPage.tsx @@ -53,7 +53,7 @@ export const EditCollectionPage = () => { const { data: tracks, isLoading: isTracksLoading } = useCollectionTracks(playlist_id) - const artworkUrl = useCollectionCoverArt({ + const { imageUrl: artworkUrl } = useCollectionCoverArt({ collectionId: playlist_id, size: SquareSizes.SIZE_1000_BY_1000 }) diff --git a/packages/web/src/pages/edit-collection-page/mobile/EditCollectionPage.tsx b/packages/web/src/pages/edit-collection-page/mobile/EditCollectionPage.tsx index 14977ef7301..d93d1a60043 100644 --- a/packages/web/src/pages/edit-collection-page/mobile/EditCollectionPage.tsx +++ b/packages/web/src/pages/edit-collection-page/mobile/EditCollectionPage.tsx @@ -121,7 +121,7 @@ const EditCollectionPage = g( } }, [setReorderedTracks, reorderedTracks, tracks]) - const artworkUrl = useCollectionCoverArt({ + const { imageUrl: artworkUrl } = useCollectionCoverArt({ collectionId: playlist_id, size: SquareSizes.SIZE_1000_BY_1000 }) diff --git a/packages/web/src/pages/edit-page/EditTrackPage.tsx b/packages/web/src/pages/edit-page/EditTrackPage.tsx index efd7e715d86..5acddd11166 100644 --- a/packages/web/src/pages/edit-page/EditTrackPage.tsx +++ b/packages/web/src/pages/edit-page/EditTrackPage.tsx @@ -93,7 +93,7 @@ export const EditTrackPage = (props: EditPageProps) => { } } - const coverArtUrl = useTrackCoverArt({ + const { imageUrl: coverArtUrl } = useTrackCoverArt({ trackId: track?.track_id, size: SquareSizes.SIZE_1000_BY_1000 }) diff --git a/packages/web/src/pages/pay-and-earn-page/components/TrackNameWithArtwork.tsx b/packages/web/src/pages/pay-and-earn-page/components/TrackNameWithArtwork.tsx index cc99296598c..c44ffa36e83 100644 --- a/packages/web/src/pages/pay-and-earn-page/components/TrackNameWithArtwork.tsx +++ b/packages/web/src/pages/pay-and-earn-page/components/TrackNameWithArtwork.tsx @@ -24,11 +24,11 @@ export const TrackNameWithArtwork = ({ enabled: !isTrack, select: (collection) => collection.playlist_name }) - const trackArtwork = useTrackCoverArt({ + const { imageUrl: trackArtwork } = useTrackCoverArt({ trackId: id, size: SquareSizes.SIZE_150_BY_150 }) - const albumArtwork = useCollectionCoverArt({ + const { imageUrl: albumArtwork } = useCollectionCoverArt({ collectionId: id, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/pages/search-page/RecentSearches.tsx b/packages/web/src/pages/search-page/RecentSearches.tsx index 2a08e33d55c..4e5bc0a36fd 100644 --- a/packages/web/src/pages/search-page/RecentSearches.tsx +++ b/packages/web/src/pages/search-page/RecentSearches.tsx @@ -112,7 +112,7 @@ const RecentSearchTrack = (props: { searchItem: SearchItem }) => { }) const { data: user } = useUser(partialTrack?.owner_id) - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId: partialTrack?.track_id, size: SquareSizes.SIZE_150_BY_150 }) @@ -185,7 +185,7 @@ const RecentSearchCollection = (props: { searchItem: SearchItem }) => { const { playlist_id, playlist_name, permalink, playlist_owner_id, is_album } = partialCollection ?? {} - const image = useCollectionCoverArt({ + const { imageUrl: image } = useCollectionCoverArt({ collectionId: playlist_id, size: SquareSizes.SIZE_150_BY_150 }) diff --git a/packages/web/src/pages/track-page/DesktopServerTrackPage.tsx b/packages/web/src/pages/track-page/DesktopServerTrackPage.tsx index 5a880e1a96c..f7f26e427c0 100644 --- a/packages/web/src/pages/track-page/DesktopServerTrackPage.tsx +++ b/packages/web/src/pages/track-page/DesktopServerTrackPage.tsx @@ -19,6 +19,7 @@ import { Paper } from '@audius/harmony/src/components/layout/Paper' import { Tag } from '@audius/harmony/src/components/tag' import { Text } from '@audius/harmony/src/components/text' import { TextLink } from '@audius/harmony/src/components/text-link' +import { IconImage } from '@audius/harmony/src/icons/individual/IconImage' import { Link } from 'react-router' import { ServerUserGeneratedText } from 'components/user-generated-text/ServerUserGeneratedText' @@ -79,6 +80,13 @@ export const DesktopServerTrackPage = ({ field_visibility, artwork } = track + + const hasArtwork = + artwork && + Object.entries(artwork).some( + ([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0 + ) + const artworkSrc = hasArtwork ? artwork['480x480'] : undefined const { handle, name, cover_photo, profile_picture } = user // Use user cover photo as primary, fallback to profile picture with blur @@ -130,12 +138,25 @@ export const DesktopServerTrackPage = ({ > - + {artworkSrc ? ( + + ) : ( + + + + )} Track diff --git a/packages/web/src/pages/track-page/MobileServerTrackPage.tsx b/packages/web/src/pages/track-page/MobileServerTrackPage.tsx index eb30ad76506..28274932b45 100644 --- a/packages/web/src/pages/track-page/MobileServerTrackPage.tsx +++ b/packages/web/src/pages/track-page/MobileServerTrackPage.tsx @@ -19,6 +19,7 @@ import { Paper } from '@audius/harmony/src/components/layout/Paper' import { Tag } from '@audius/harmony/src/components/tag' import { Text } from '@audius/harmony/src/components/text' import { TextLink } from '@audius/harmony/src/components/text-link' +import { IconImage } from '@audius/harmony/src/icons/individual/IconImage' import { Link } from 'react-router' import { ServerUserGeneratedText } from 'components/user-generated-text/ServerUserGeneratedText' @@ -79,6 +80,13 @@ export const MobileServerTrackPage = ({ field_visibility, artwork } = track + + const hasArtwork = + artwork && + Object.entries(artwork).some( + ([k, v]) => k !== 'mirrors' && typeof v === 'string' && v.length > 0 + ) + const artworkSrc = hasArtwork ? artwork['480x480'] : undefined const { handle, name, cover_photo, profile_picture } = user // Use user cover photo as primary, fallback to profile picture with blur @@ -113,12 +121,25 @@ export const MobileServerTrackPage = ({ Track - + {artworkSrc ? ( + + ) : ( + + + + )} {title} diff --git a/packages/web/src/pages/visualizer/VisualizerProvider.tsx b/packages/web/src/pages/visualizer/VisualizerProvider.tsx index 58a9884dcde..ac6ccd87e92 100644 --- a/packages/web/src/pages/visualizer/VisualizerProvider.tsx +++ b/packages/web/src/pages/visualizer/VisualizerProvider.tsx @@ -42,7 +42,7 @@ const { getTheme } = themeSelectors const Artwork = ({ track }: { track?: Track | null }) => { const { track_id } = track || {} - const image = useTrackCoverArt({ + const { imageUrl: image } = useTrackCoverArt({ trackId: track_id, size: SquareSizes.SIZE_480_BY_480 })