From 5d379e4a5f09eef8663fce05248c17b35e2cf1e6 Mon Sep 17 00:00:00 2001 From: MarcoMadera Date: Sun, 17 Dec 2023 01:43:13 -0700 Subject: [PATCH] images fixes --- .../BrowseCategories/BrowseCategories.tsx | 4 +- components/CardContent/CardContent.tsx | 12 +- components/CardTrack/CardTrack.tsx | 16 +- components/CardTrack/TrackDetails.tsx | 4 +- components/CardTrack/TrackImage.tsx | 8 +- .../EditPlaylistDetails.tsx | 2 +- components/EpisodeCard/EpisodeCard.tsx | 9 +- .../FirstTrackContainer.tsx | 6 +- components/NowPlaying/NowPlaying.tsx | 12 +- components/PageHeader/PageHeader.tsx | 5 +- .../RemoveTracksModal/RemoveTracksModal.tsx | 9 +- components/SideBar/SideBar.tsx | 11 +- .../SingleTrackCard/SingleTrackCard.tsx | 4 +- components/TopBar/TopBar.tsx | 6 +- .../VirtualizedList/VirtualizedList.tsx | 11 +- layouts/FullScreenPlayer.tsx | 5 +- layouts/playlist/index.tsx | 104 +++++----- pages/album/[albumId].tsx | 8 +- pages/api/top-tracks-cover.tsx | 22 +- pages/artist/[artistId].tsx | 8 +- pages/episode/[episodeId].tsx | 8 +- pages/show/[show].tsx | 8 +- pages/track/[id].tsx | 10 +- pages/user/[userId].tsx | 8 +- types/spotify.ts | 4 +- utils/__tests__/chooseImage.spec.ts | 194 ++++++++++++++++++ utils/chooseImage.ts | 74 +++++++ utils/generateSrcSet.ts | 6 + utils/index.ts | 2 + utils/playButton.ts | 10 +- 30 files changed, 425 insertions(+), 165 deletions(-) create mode 100644 utils/__tests__/chooseImage.spec.ts create mode 100644 utils/chooseImage.ts create mode 100644 utils/generateSrcSet.ts diff --git a/components/BrowseCategories/BrowseCategories.tsx b/components/BrowseCategories/BrowseCategories.tsx index 06c1bd55..49797dab 100644 --- a/components/BrowseCategories/BrowseCategories.tsx +++ b/components/BrowseCategories/BrowseCategories.tsx @@ -3,7 +3,7 @@ import { ReactElement } from "react"; import Link from "next/link"; import { Grid, Heading } from "components"; -import { colors } from "utils"; +import { chooseImage, colors } from "utils"; interface IBrowseCategoriesProps { categories: SpotifyApi.PagingObject | null; @@ -23,7 +23,7 @@ export default function BrowseCategories({ className="BrowseCategories-category" > {/* eslint-disable-next-line @next/next/no-img-element */} - {name} + {name} {name} diff --git a/components/CardContent/CardContent.tsx b/components/CardContent/CardContent.tsx index a0d554a9..e74c9974 100644 --- a/components/CardContent/CardContent.tsx +++ b/components/CardContent/CardContent.tsx @@ -4,8 +4,7 @@ import { decode } from "html-entities"; import { useRouter } from "next/router"; import { useContextMenu, useOnScreen } from "hooks"; -import { handleAsyncError } from "utils"; -import { getSiteUrl } from "utils/environment"; +import { chooseImage, handleAsyncError } from "utils"; export enum CardType { SIMPLE = "simple", @@ -96,14 +95,7 @@ export default function CardContent({ > {images && ( // eslint-disable-next-line @next/next/no-img-element - {title} + {title} )}
{title} diff --git a/components/CardTrack/CardTrack.tsx b/components/CardTrack/CardTrack.tsx index c6b708ad..34ce1274 100644 --- a/components/CardTrack/CardTrack.tsx +++ b/components/CardTrack/CardTrack.tsx @@ -24,6 +24,7 @@ import { import { ITrack } from "types/spotify"; import { ContentType, + getIdFromUri, handlePlayCurrentTrackError, playCurrentTrack, templateReplace, @@ -165,10 +166,17 @@ function CardTrack({ const source = pageDetails?.uri; const isCollection = source?.split(":")?.[3]; - setPlaylistPlayingId(isSingleTrack ? undefined : pageDetails?.id); + setPlaylistPlayingId( + isSingleTrack ? undefined : getIdFromUri(pageDetails?.uri, "id") + ); setPlayedSource( - isCollection && pageDetails?.type && pageDetails?.id - ? `spotify:${pageDetails.type}:${pageDetails.id}` + isCollection && + pageDetails?.type && + getIdFromUri(pageDetails?.uri, "id") + ? `spotify:${pageDetails.type}:${getIdFromUri( + pageDetails?.uri, + "id" + )}` : source ?? track?.uri ); } catch (error) { @@ -216,7 +224,7 @@ function CardTrack({ if (isPlaying && isTheSameAsCurrentlyPlaying) { player?.pause(); setIsPlaying(false); - setPlaylistPlayingId(pageDetails?.id); + setPlaylistPlayingId(getIdFromUri(pageDetails?.uri, "id")); } else if (isPlayable) { if (isPremium) { (player as Spotify.Player)?.activateElement(); diff --git a/components/CardTrack/TrackDetails.tsx b/components/CardTrack/TrackDetails.tsx index 9732db32..ac4d477d 100644 --- a/components/CardTrack/TrackDetails.tsx +++ b/components/CardTrack/TrackDetails.tsx @@ -7,7 +7,7 @@ import { CardType } from "./CardTrack"; import { ArtistList, ExplicitSign } from "components"; import { useOnScreen } from "hooks"; import { ITrack } from "types/spotify"; -import { getTimeAgo, spanishCountries } from "utils"; +import { chooseImage, getTimeAgo, spanishCountries } from "utils"; interface ITrackDetails { track: ITrack; @@ -35,7 +35,7 @@ export function TrackDetails({ // eslint-disable-next-line @next/next/no-img-element {` button.playButton { background-image: ${type === "presentation" - ? `url(${ - track.album?.images?.[2]?.url ?? - track.album?.images?.[1]?.url ?? - `${getSiteUrl()}/defaultSongCover.jpeg` - })` + ? `url(${chooseImage(track.album?.images, 50).url})` : "unset"}; } button.playButton { diff --git a/components/EditPlaylistDetails/EditPlaylistDetails.tsx b/components/EditPlaylistDetails/EditPlaylistDetails.tsx index 53044d7e..56767675 100644 --- a/components/EditPlaylistDetails/EditPlaylistDetails.tsx +++ b/components/EditPlaylistDetails/EditPlaylistDetails.tsx @@ -147,7 +147,7 @@ export default function EditPlaylistDetails({ aria-hidden="false" draggable="false" loading="eager" - src={imgUrl || "/images/playlist-default.png"} + src={imgUrl || "/defaultSongCover.jpeg"} alt="Playlist cover" id="edit-cover-image" sizes="(min-width: 1280px) 232px, 192px" diff --git a/components/EpisodeCard/EpisodeCard.tsx b/components/EpisodeCard/EpisodeCard.tsx index d981a374..d5436cf7 100644 --- a/components/EpisodeCard/EpisodeCard.tsx +++ b/components/EpisodeCard/EpisodeCard.tsx @@ -13,8 +13,10 @@ import { } from "hooks"; import { AsType } from "types/heading"; import { + chooseImage, ContentType, formatTime, + getIdFromUri, getSiteUrl, getTimeAgo, handlePlayCurrentTrackError, @@ -99,9 +101,10 @@ export default function EpisodeCard({ const source = pageDetails?.uri; const isCollection = source?.split(":")?.[3]; setPlaylistPlayingId(undefined); + const id = getIdFromUri(pageDetails?.uri, "id"); setPlayedSource( - isCollection && pageDetails?.type && pageDetails?.id - ? `spotify:${pageDetails.type}:${pageDetails.id}` + isCollection && pageDetails?.type && id + ? `spotify:${pageDetails.type}:${id}` : source ?? item.uri ); } catch (error) { @@ -132,7 +135,7 @@ export default function EpisodeCard({
{/* eslint-disable-next-line @next/next/no-img-element */} - {item.name} + {item.name}
diff --git a/components/FirstTrackContainer/FirstTrackContainer.tsx b/components/FirstTrackContainer/FirstTrackContainer.tsx index 9fed0a7e..c09694ed 100644 --- a/components/FirstTrackContainer/FirstTrackContainer.tsx +++ b/components/FirstTrackContainer/FirstTrackContainer.tsx @@ -6,7 +6,7 @@ import { useRouter } from "next/router"; import { ArtistList, Heading, PlayButton } from "components"; import { useAuth, useContextMenu, useOnScreen } from "hooks"; import { ITrack } from "types/spotify"; -import { getMainColorFromImage } from "utils"; +import { chooseImage, getMainColorFromImage } from "utils"; interface FirstTrackContainerProps { preview?: string | null; @@ -68,7 +68,7 @@ export default function FirstTrackContainer({ > {/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */} {currentlyPlaying.album?.name} {/* eslint-disable-next-line @next/next/no-img-element */} {currentlyPlaying.album?.name} { return tracks.filter((_, i) => { if (indexes.includes(i)) { diff --git a/components/SideBar/SideBar.tsx b/components/SideBar/SideBar.tsx index 02a1e292..7e89a34a 100644 --- a/components/SideBar/SideBar.tsx +++ b/components/SideBar/SideBar.tsx @@ -13,6 +13,7 @@ import { useTranslations, } from "hooks"; import { + chooseImage, ContentType, getAllMyPlaylists, templateReplace, @@ -168,10 +169,7 @@ export default function SideBar(): ReactElement { {/* eslint-disable-next-line @next/next/no-img-element */} {currentlyPlaying.album?.name} @@ -179,10 +177,7 @@ export default function SideBar(): ReactElement { <> {/* eslint-disable-next-line @next/next/no-img-element */} {currentlyPlaying.album?.name} diff --git a/components/SingleTrackCard/SingleTrackCard.tsx b/components/SingleTrackCard/SingleTrackCard.tsx index 8734cb7a..0829eef9 100644 --- a/components/SingleTrackCard/SingleTrackCard.tsx +++ b/components/SingleTrackCard/SingleTrackCard.tsx @@ -6,7 +6,7 @@ import { useRouter } from "next/router"; import { PlayButton } from "components"; import { useContextMenu, useHeader, useSpotify } from "hooks"; import { ITrack } from "types/spotify"; -import { getMainColorFromImage } from "utils"; +import { chooseImage, getMainColorFromImage } from "utils"; interface ISingleTrackCard { track: ITrack; @@ -47,7 +47,7 @@ export default function SingleTrackCard({ > {/* eslint-disable-next-line @next/next/no-img-element */} {track.name} diff --git a/components/TopBar/TopBar.tsx b/components/TopBar/TopBar.tsx index 3a288926..e3f651cb 100644 --- a/components/TopBar/TopBar.tsx +++ b/components/TopBar/TopBar.tsx @@ -9,6 +9,7 @@ import { DisplayInFullScreen } from "types/spotify"; import { calculateBannerOpacity, calculateHeaderOpacity, + chooseImage, isFullScreen, isServer, setOpacityStyles, @@ -133,7 +134,10 @@ export default function TopBar({ appRef }: TopBarProps): ReactElement { ) : null} {user ? ( - + ) : (
)} diff --git a/components/VirtualizedList/VirtualizedList.tsx b/components/VirtualizedList/VirtualizedList.tsx index 8bc44c75..b0c5a900 100644 --- a/components/VirtualizedList/VirtualizedList.tsx +++ b/components/VirtualizedList/VirtualizedList.tsx @@ -12,7 +12,12 @@ import { CardTrack } from "components"; import { CardType } from "components/CardTrack/CardTrack"; import { useAuth, useSpotify } from "hooks"; import { ITrack } from "types/spotify"; -import { getTracksFromLibrary, isServer, mapPlaylistItems } from "utils"; +import { + getIdFromUri, + getTracksFromLibrary, + isServer, + mapPlaylistItems, +} from "utils"; import { checkTracksInLibrary, getTracksFromPlaylist, @@ -72,7 +77,7 @@ export default function VirtualizedList({ const data = isLibrary ? await getTracksFromLibrary(startIndex, accessToken) : await getTracksFromPlaylist( - pageDetails?.id ?? "", + getIdFromUri(pageDetails?.uri, "id") ?? "", startIndex, accessToken ); @@ -91,7 +96,7 @@ export default function VirtualizedList({ addTracksToPlaylists, isGeneratedPlaylist, isLibrary, - pageDetails?.id, + pageDetails?.uri, ] ); diff --git a/layouts/FullScreenPlayer.tsx b/layouts/FullScreenPlayer.tsx index deec0cdc..697d8a20 100644 --- a/layouts/FullScreenPlayer.tsx +++ b/layouts/FullScreenPlayer.tsx @@ -26,6 +26,7 @@ import { } from "hooks"; import { DisplayInFullScreen } from "types/spotify"; import { + chooseImage, ContentType, exitFullScreen, getMainColorFromImage, @@ -121,7 +122,7 @@ export default function FullScreenPlayer(): ReactElement | null {
{/* eslint-disable-next-line @next/next/no-img-element */} {nextTracks?.[0].name} {/* eslint-disable-next-line @next/next/no-img-element */} {currentlyPlaying.name} diff --git a/layouts/playlist/index.tsx b/layouts/playlist/index.tsx index 88889300..9890ea9a 100644 --- a/layouts/playlist/index.tsx +++ b/layouts/playlist/index.tsx @@ -30,7 +30,13 @@ import { import { PlaylistProps } from "pages/playlist/[playlist]"; import { HeaderType } from "types/pageHeader"; import { ITrack } from "types/spotify"; -import { ContentType, getSiteUrl, templateReplace, ToastMessage } from "utils"; +import { + chooseImage, + ContentType, + getIdFromUri, + templateReplace, + ToastMessage, +} from "utils"; import { addItemsToPlaylist, checkUsersFollowAPlaylist, @@ -79,13 +85,13 @@ const Playlist: NextPage< async function fetchData() { const userFollowThisPlaylist = await checkUsersFollowAPlaylist( [user?.id ?? ""], - pageDetails?.id, + getIdFromUri(pageDetails?.uri, "id"), accessToken ); setIsFollowingThisPlaylist(!!userFollowThisPlaylist?.[0]); } fetchData(); - }, [accessToken, pageDetails?.id, user?.id, isMyPlaylist]); + }, [accessToken, pageDetails?.uri, user?.id, isMyPlaylist]); useEffect(() => { if (!pageDetails) { @@ -112,7 +118,7 @@ const Playlist: NextPage< ?.getCurrentState() .then((e) => { if (e?.context.uri === pageDetails?.uri) { - setPlaylistPlayingId(pageDetails?.id); + setPlaylistPlayingId(getIdFromUri(pageDetails?.uri, "id")); } }) .catch((err) => { @@ -170,11 +176,7 @@ const Playlist: NextPage< } title={pageDetails?.name ?? ""} description={pageDetails?.description ?? ""} - coverImg={ - pageDetails?.images?.[0]?.url ?? - pageDetails?.images?.[1]?.url ?? - `${getSiteUrl()}/defaultSongCover.jpeg` - } + coverImg={chooseImage(pageDetails?.images, 300).url} ownerDisplayName={pageDetails?.owner?.display_name ?? ""} ownerId={pageDetails?.owner?.id ?? ""} totalFollowers={pageDetails?.followers?.total ?? 0} @@ -218,7 +220,7 @@ const Playlist: NextPage< active={isFollowingThisPlaylist} handleLike={async () => { const followRes = await followPlaylist( - pageDetails?.id + getIdFromUri(pageDetails?.uri, "id") ); if (followRes) { setIsFollowingThisPlaylist(true); @@ -238,7 +240,7 @@ const Playlist: NextPage< }} handleDislike={async () => { const unfollowRes = await unfollowPlaylist( - pageDetails?.id + getIdFromUri(pageDetails?.uri, "id") ); if (unfollowRes) { setIsFollowingThisPlaylist(false); @@ -320,25 +322,24 @@ const Playlist: NextPage< playlistUri="" track={track} onClickAdd={() => { - if (!pageDetails?.id) return; - addItemsToPlaylist(pageDetails.id, [track.uri]).then( - (res) => { - if (res?.snapshot_id) { - setAllTracks((prev) => { - return [ - ...prev, - { - ...track, - position: prev.length, - images: track.album.images, - duration: track.duration_ms, - added_at: Date.now(), - }, - ]; - }); - } + const id = getIdFromUri(pageDetails?.uri, "id"); + if (!id) return; + addItemsToPlaylist(id, [track.uri]).then((res) => { + if (res?.snapshot_id) { + setAllTracks((prev) => { + return [ + ...prev, + { + ...track, + position: prev.length, + images: track.album.images, + duration: track.duration_ms, + added_at: Date.now(), + }, + ]; + }); } - ); + }); }} key={track.id} type={CardType.Presentation} @@ -364,32 +365,31 @@ const Playlist: NextPage< playlistUri="" track={track} onClickAdd={() => { - if (!pageDetails?.id) return; - addItemsToPlaylist(pageDetails.id, [track.uri]).then( - (res) => { - if (res?.snapshot_id) { - setAllTracks((prev) => { - return [ - ...prev, - { - ...track, - position: prev.length, + const id = getIdFromUri(pageDetails?.uri, "id"); + if (!id) return; + addItemsToPlaylist(id, [track.uri]).then((res) => { + if (res?.snapshot_id) { + setAllTracks((prev) => { + return [ + ...prev, + { + ...track, + position: prev.length, + images: track.images, + duration: track.duration_ms, + added_at: Date.now(), + album: { images: track.images, - duration: track.duration_ms, - added_at: Date.now(), - album: { - images: track.images, - name: track.name, - uri: track.uri, - id: track.id, - type: "episode", - }, + name: track.name, + uri: track.uri, + id: track.id, + type: "episode", }, - ]; - }); - } + }, + ]; + }); } - ); + }); }} key={track.id} type={CardType.Presentation} diff --git a/pages/album/[albumId].tsx b/pages/album/[albumId].tsx index 0fdd96be..ab12e304 100644 --- a/pages/album/[albumId].tsx +++ b/pages/album/[albumId].tsx @@ -18,9 +18,9 @@ import { useAnalytics, useAuth, useHeader, useSpotify, useToast } from "hooks"; import { HeaderType } from "types/pageHeader"; import { ITrack } from "types/spotify"; import { + chooseImage, ContentType, getAuth, - getSiteUrl, getTranslations, isCorruptedTrack, Page, @@ -123,11 +123,7 @@ const AlbumPage: NextPage = ({ - + {imageUrl && ( + + )}
), diff --git a/pages/artist/[artistId].tsx b/pages/artist/[artistId].tsx index 00703cb3..d21a8d49 100644 --- a/pages/artist/[artistId].tsx +++ b/pages/artist/[artistId].tsx @@ -29,12 +29,12 @@ import { import { HeaderType } from "types/pageHeader"; import { ArtistScrobbleInfo, + chooseImage, fullFilledValue, getArtistInfo, getAuth, getCarouselItems, getSetLists, - getSiteUrl, getTranslations, Page, serverRedirect, @@ -196,11 +196,7 @@ export default function ArtistPage({ diff --git a/pages/show/[show].tsx b/pages/show/[show].tsx index 5969ed52..bd56e06b 100644 --- a/pages/show/[show].tsx +++ b/pages/show/[show].tsx @@ -23,9 +23,9 @@ import { AsType } from "types/heading"; import { HeaderType } from "types/pageHeader"; import { ITrack } from "types/spotify"; import { + chooseImage, ContentType, getAuth, - getSiteUrl, getTranslations, Page, serverRedirect, @@ -135,11 +135,7 @@ const Shows: NextPage = ({ show }) => { diff --git a/pages/track/[id].tsx b/pages/track/[id].tsx index bab93e76..f6434de4 100644 --- a/pages/track/[id].tsx +++ b/pages/track/[id].tsx @@ -25,10 +25,10 @@ import { } from "hooks"; import { HeaderType } from "types/pageHeader"; import { + chooseImage, ContentType, getAuth, getLyrics, - getSiteUrl, getTranslations, LyricsAction, Page, @@ -158,11 +158,7 @@ export default function TrackPage({ = ({ { + it("should return the first image that matches the target width and height criteria", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: 100, height: 100, url: "1" }, + { width: 200, height: 200, url: "2" }, + { width: 300, height: 300, url: "3" }, + ]; + const targetWidth = 200; + const targetHeight = 200; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[1]); + }); + + it("should return the largest image if no images match the criteria", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: 100, height: 100, url: "1" }, + { width: 200, height: 200, url: "2" }, + { width: 300, height: 300, url: "3" }, + ]; + const targetWidth = 400; + const targetHeight = 400; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[2]); + }); + + it("should return the only image if it matches the criteria", () => { + expect.assertions(1); + const images: Spotify.Image[] = [{ width: 200, height: 200, url: "1" }]; + const targetWidth = 200; + const targetHeight = 200; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[0]); + }); + + it("should return an object with default url if the input images list is empty", () => { + expect.assertions(1); + const images: Spotify.Image[] = []; + const targetWidth = 200; + const targetHeight = 200; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual({ + url: "http://localhost:3000/defaultSongCover.jpeg", + }); + }); + + it("should return the only image if it is smaller than the target width and height criteria", () => { + expect.assertions(1); + const images: Spotify.Image[] = [{ width: 100, height: 100, url: "1" }]; + const targetWidth = 200; + const targetHeight = 200; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[0]); + }); + + it("should return the largest image if all images are smaller than the target width and height criteria", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: 100, height: 100, url: "1" }, + { width: 150, height: 150, url: "2" }, + { width: 175, height: 175, url: "3" }, + ]; + const targetWidth = 200; + const targetHeight = 200; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[2]); + }); + + it("should return default url if there is no urls in the object", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: 100, height: 100, url: "" }, + { width: 150, height: 150, url: "" }, + { width: 175, height: 175, url: "" }, + ]; + const targetWidth = 200; + const targetHeight = 200; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual({ + url: "http://localhost:3000/defaultSongCover.jpeg", + }); + }); + it("should return the closest down url if there is no url in the selected top object", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: 100, height: 100, url: "1" }, + { width: 150, height: 150, url: "2" }, + { width: 175, height: 175, url: "" }, + ]; + const targetWidth = 200; + const targetHeight = 200; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[1]); + }); + it("should return the closest url of the provided width", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: 300, url: "1" }, + { width: 250, url: "2" }, + { width: 575, url: "3" }, + ]; + const targetWidth = 50; + + const result = chooseImage(images, targetWidth); + + expect(result).toStrictEqual(images[1]); + }); + it("should return the closest top url if there is no url in the selected object", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: 100, height: 100, url: "1" }, + { width: 150, height: 150, url: "" }, + { width: 175, height: 175, url: "3" }, + ]; + const targetWidth = 150; + const targetHeight = 150; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[2]); + }); + it("should return the first element with url if not width and height provided", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: null, height: null, url: "1" }, + { width: null, height: null, url: "" }, + { width: null, height: null, url: "3" }, + ]; + const targetWidth = 150; + const targetHeight = 150; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[0]); + }); + it("should return the selected element with url if not width and height provided", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { width: null, height: null, url: "1" }, + { width: null, height: null, url: "" }, + { width: 120, height: 120, url: "3" }, + ]; + const targetWidth = 100; + const targetHeight = 100; + + const result = chooseImage(images, targetWidth, targetHeight); + + expect(result).toStrictEqual(images[2]); + }); + it("should return a larger element if target is in middle", () => { + expect.assertions(1); + const images: Spotify.Image[] = [ + { + height: 640, + url: "1", + width: 640, + }, + { + height: 300, + url: "2", + width: 300, + }, + { + height: 64, + url: "3", + width: 64, + }, + ]; + const targetWidth = 400; + + const result = chooseImage(images, targetWidth); + + expect(result).toStrictEqual(images[0]); + }); +}); diff --git a/utils/chooseImage.ts b/utils/chooseImage.ts new file mode 100644 index 00000000..61a214b3 --- /dev/null +++ b/utils/chooseImage.ts @@ -0,0 +1,74 @@ +import { getSiteUrl } from "./environment"; +const DEFAULT_URL = `${getSiteUrl()}/defaultSongCover.jpeg`; + +const isValidImage = ( + image: Spotify.Image | undefined +): image is Pick => { + return !!(image?.url && image.url.trim() !== ""); +}; + +const calculateDistance = ( + image: Spotify.Image, + width: number, + height?: number +) => { + return ( + (image?.width ? Math.abs(image.width - width) : 0) + + (height !== undefined && image?.height + ? Math.abs(image.height - height) + : 0) + ); +}; + +function findClosestImage( + images: (Spotify.Image | undefined)[], + targetWidth: number, + targetHeight?: number +) { + let closestValidImage: Spotify.Image | undefined; + let closestDistance = Number.MAX_VALUE; + + for (const image of images) { + if ( + closestValidImage?.url && + closestValidImage?.width && + closestValidImage?.width >= targetWidth && + image?.width && + image?.width < targetWidth + ) { + break; + } + + if (isValidImage(image)) { + const distance = calculateDistance(image, targetWidth, targetHeight); + + if (distance <= closestDistance && image?.width) { + closestDistance = distance; + closestValidImage = image; + } + } + } + + return closestValidImage; +} + +export function chooseImage( + images: (Spotify.Image | undefined)[] | undefined, + targetWidth: number, + targetHeight?: number +): Spotify.Image { + if (!images || images.length === 0) { + return { url: DEFAULT_URL }; + } + + const closestValidImage = findClosestImage(images, targetWidth, targetHeight); + + if (closestValidImage) { + return closestValidImage; + } else { + const imageWithUrl = images.find(isValidImage); + if (imageWithUrl) return imageWithUrl; + } + + return { url: DEFAULT_URL }; +} diff --git a/utils/generateSrcSet.ts b/utils/generateSrcSet.ts new file mode 100644 index 00000000..0d2a6468 --- /dev/null +++ b/utils/generateSrcSet.ts @@ -0,0 +1,6 @@ +export function generateSrcSet(images: Spotify.Image[]): string { + return images + .filter((image) => image.url && image.width) + .map((image) => `${image.url} ${image.width}w`) + .join(", "); +} diff --git a/utils/index.ts b/utils/index.ts index 0e9ef951..f631b753 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -59,3 +59,5 @@ export * from "./wait"; export * from "./within"; export * from "./findIndexOrLast"; export * from "./errors"; +export * from "./generateSrcSet"; +export * from "./chooseImage"; diff --git a/utils/playButton.ts b/utils/playButton.ts index e31d5d01..7264e009 100644 --- a/utils/playButton.ts +++ b/utils/playButton.ts @@ -1,5 +1,6 @@ import { Dispatch, SetStateAction } from "react"; +import { getIdFromUri } from "./getIdFromUri"; import { AudioPlayer } from "hooks/useSpotifyPlayer"; import { IPageDetails, ITrack } from "types/spotify"; import { NewToast } from "types/toast"; @@ -125,15 +126,16 @@ export async function handlePremiumPlay( } if (playRes.ok) { - setPlaylistPlayingId(uriId ?? pageDetails?.id); + const id = getIdFromUri(pageDetails?.uri, "id"); + setPlaylistPlayingId(uriId ?? id); const source = track || isSingle ? track?.uri || pageDetails?.uri : uri ?? pageDetails?.uri; const isCollection = source?.split(":")?.[3]; setPlayedSource( - isCollection && pageDetails?.type && pageDetails?.id - ? `spotify:${pageDetails?.type}:${pageDetails?.id}` + isCollection && pageDetails?.type && id + ? `spotify:${pageDetails?.type}:${id}` : source ); } @@ -168,6 +170,6 @@ export function handleNonPremiumPlay( player.nextTrack(); } if (pageDetails) { - setPlaylistPlayingId(pageDetails.id); + setPlaylistPlayingId(getIdFromUri(pageDetails?.uri, "id")); } }