Skip to content

Commit

Permalink
Merge pull request #6784 from LedgerHQ/refactor/medias_nft_gallery
Browse files Browse the repository at this point in the history
refactor(lld): nfts gallery medias in newArch
  • Loading branch information
LucasWerey committed May 10, 2024
2 parents 796f7d8 + b3d56f7 commit 69dfce5
Show file tree
Hide file tree
Showing 16 changed files with 508 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/weak-avocados-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": patch
---

Moove and refactor medias from nft gallery to newArch. Create a FF to switch between old and new arch
10 changes: 9 additions & 1 deletion apps/ledger-live-desktop/.unimportedrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@
"extensions": [".ts", ".js", ".jsx", ".tsx"],
"ignorePatterns": ["**/node_modules/**"],
"ignoreUnresolved": ["unzip-crx-3", "../../../../release-notes.json"],
"ignoreUnimported": ["**/*.test.*", "**/*.spec.*", "**/*.d.ts", "**/types.js", "**/types.*", "src/generate-cryptoassets-md.ts"],
"ignoreUnimported": [
"**/*.test.*",
"**/*.spec.*",
"**/*.d.ts",
"**/types.js",
"**/types.*",
"src/generate-cryptoassets-md.ts",
"src/newArch/Collectibles/**"
],
"ignoreUnused": ["@types/semver", "@types/qrcode", "@types/react-key-handler", "prop-types"],
"aliases": {
"~/*": ["./src/*"]
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { memo } from "react";
import { useNftCollectionMetadata } from "@ledgerhq/live-nft-react";
import { Account, ProtoNFT } from "@ledgerhq/types-live";
import NFTCollectionContextMenu from "~/renderer/components/ContextMenu/NFTCollectionContextMenu";
import { Skeleton } from "LLD/Collectibles/components";
import styled from "styled-components";
import { IconsLegacy } from "@ledgerhq/react-ui";

const Dots = styled.div`
justify-content: flex-end;
display: flex;
align-items: center;
cursor: pointer;
padding: 5px;
color: ${p => p.theme.colors.palette.text.shade50};
&:hover {
color: ${p => p.theme.colors.palette.text.shade80};
}
`;
const Container = styled.div`
display: flex;
column-gap: 10px;
`;

type Props = {
nft?: ProtoNFT;
fallback?: string;
account?: Account;
showHideMenu?: boolean;
}; // TODO Make me pretty

const CollectionNameComponent: React.FC<Props> = ({ nft, fallback, account, showHideMenu }) => {
const { status, metadata } = useNftCollectionMetadata(nft?.contract, nft?.currencyId);
const { tokenName } = metadata || {};
const loading = status === "loading";
const isComponentReady = account && showHideMenu && nft;

return (
<Skeleton width={80} minHeight={24} barHeight={10} show={loading}>
<Container>
{tokenName || fallback || "-"}
{isComponentReady && (
<NFTCollectionContextMenu
collectionName={tokenName || fallback || "-"}
collectionAddress={nft.contract || ""}
account={account}
leftClick={true}
>
<Dots>
<IconsLegacy.OthersMedium size={20} />
</Dots>
</NFTCollectionContextMenu>
)}
</Container>
</Skeleton>
);
};
export const CollectionName = memo<Props>(CollectionNameComponent);
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useState } from "react";
import { ImageProps } from "LLD/Collectibles/types/Media";
import styled from "styled-components";
import { Skeleton, Placeholder } from "LLD/Collectibles/components";
/**
* Nb: This image component can be used for small listings, large gallery rendering,
* and even tokens without an image where it will fallback to a generative image
* based on the token metadata and some hue changes.
*
* The text in the fallback image is only visible if we are in `full` mode, since list
* mode is not large enough for the text to be readable.
*/

const Wrapper = styled.div<{
full?: ImageProps["full"];
size?: ImageProps["size"];
loaded: boolean;
square: ImageProps["square"];
maxHeight?: ImageProps["maxHeight"];
objectFit?: ImageProps["objectFit"];
error?: boolean;
}>`
width: ${({ full, size }) => (full ? "100%" : `${size}px`)};
height: ${({ full }) => full && "100%"};
aspect-ratio: ${({ square }) => (square ? "1 / 1" : "initial")};
max-height: ${({ maxHeight }) => maxHeight && `${maxHeight}px`};
border-radius: 4px;
overflow: hidden;
background-size: contain;
display: flex;
align-items: center;
justify-content: center;
& > *:nth-child(1) {
display: ${({ loaded, error }) => (loaded || error ? "none" : "block")};
}
& > img {
display: ${({ loaded, error }) => (loaded || error ? "block" : "none")};
${({ objectFit }) =>
objectFit === "cover"
? `width: 100%;
height: 100%;`
: `max-width: 100%;
max-height: 100%;`}
object-fit: ${p => p.objectFit ?? "cover"};
border-radius: 4px;
user-select: none;
}
`;

const ImageComponent: React.FC<ImageProps> = ({
uri,
metadata,
full = false,
size = 32,
tokenId,
maxHeight,
onClick,
square = true,
objectFit = "cover",
setUseFallback,
isFallback,
}) => {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
const isImageReady = uri && !error;

return (
<Wrapper
full={full}
size={size}
loaded={loaded}
error={error || !uri}
square={square}
maxHeight={maxHeight}
objectFit={objectFit}
>
<Skeleton full />
{isImageReady ? (
<img
// This prevent a bug where the change of props from isFallback
// would not unbind onError and would not trigger it again in case of error
key={isFallback?.toString()}
onClick={onClick}
onLoad={() => setLoaded(true)}
onError={() => (isFallback ? setError(true) : setUseFallback(true))}
src={uri}
/>
) : (
<Placeholder tokenId={tokenId} metadata={metadata} full={full} />
)}
</Wrapper>
);
};

export const Image = ImageComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { memo } from "react";
import { PlaceholderProps } from "LLD/Collectibles/types/Media";
import styled from "styled-components";
import { centerEllipsis } from "~/renderer/styles/helpers";
import Fallback from "~/renderer/images/nftFallback.jpg";

const randomHueForTokenId = (tokenId = "") => parseInt(tokenId.substr(-8)) % 360;

const StyledPlaceholder = styled.div<{
tokenId?: PlaceholderProps["tokenId"];
metadata?: PlaceholderProps["metadata"];
}>`
--hue: ${p => randomHueForTokenId(p.tokenId)};
background-image: url("${Fallback}");
background-size: contain;
border-radius: 4px;
width: 100%;
@@ -27,7 +22,6 @@ const StyledPlaceholder = styled.div<{ tokenId?: string; full?: boolean; metadat
aspect-ratio: 1;
&:after {
content: "${p => p?.metadata?.nftName || centerEllipsis(p?.tokenId || "-")}";
font-size: 16px;
font-size: 1vw;
@@ -42,10 +36,11 @@ const StyledPlaceholder = styled.div<{ tokenId?: string; full?: boolean; metadat
height: 100%;
}
`;

const PlaceholderComponent = memo<PlaceholderProps>(({ metadata, tokenId }) => (
<StyledPlaceholder metadata={metadata} tokenId={tokenId} />
));

PlaceholderComponent.displayName = "Placeholder";

export const Placeholder = PlaceholderComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useState } from "react";
import { VideoProps } from "LLD/Collectibles/types/Media";
import { Skeleton } from "LLD/Collectibles/components";
import styled from "styled-components";

const Wrapper = styled.div<{
full?: VideoProps["full"];
size?: VideoProps["size"];
loaded: boolean;
square: VideoProps["square"];
maxHeight?: VideoProps["maxHeight"];
objectFit?: VideoProps["objectFit"];
error?: boolean;
}>`
width: ${({ full, size }) => (full ? "100%" : `${size}px`)};
height: ${({ full }) => full && "100%"};
aspect-ratio: ${({ square, error }) => (square || error ? "1 / 1" : "initial")};
max-height: ${({ maxHeight }) => maxHeight && `${maxHeight}px`};
border-radius: 4px;
overflow: hidden;
background-size: contain;
display: flex;
align-items: center;
justify-content: center;
& > *:nth-child(1) {
display: ${({ loaded, error }) => (loaded || error ? "none" : "block")};
}
& > video {
display: ${({ loaded, error }) => (loaded || error ? "block" : "none")};
${({ objectFit }) =>
objectFit === "cover"
? `width: 100%;
height: 100%;`
: `max-width: 100%;
max-height: 100%;`}
object-fit: ${p => p.objectFit ?? "contain"};
border-radius: 4px;
user-select: none;
position: relative;
z-index: 10;
}
`;

const VideoComponent: React.FC<VideoProps> = ({
uri,
mediaType,
full = false,
size = 32,
maxHeight,
square = true,
objectFit = "contain",
setUseFallback,
}) => {
const [loaded, setLoaded] = useState(false);

return (
<Wrapper
full={full}
size={size}
loaded={loaded}
square={square}
maxHeight={maxHeight}
objectFit={objectFit}
>
<Skeleton full />
<video
onError={() => {
setUseFallback(true);
}}
onLoadedData={() => setLoaded(true)}
autoPlay
loop
controls
disablePictureInPicture
>
<source src={uri} type={mediaType} />
</video>
</Wrapper>
);
};

export const Video = VideoComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react";
import { MediaProps } from "LLD/Collectibles/types/Media";
import useMedia from "LLD/Collectibles/hooks/useMedia";
import { Placeholder, Image, Video } from "LLD/Collectibles/components";

const MediaComponent: React.FC<MediaProps> = props => {
const { useFallback, contentType, uri, mediaType, squareWithDefault, setUseFallback } =
useMedia(props);
const Component = contentType === "video" && !useFallback ? Video : Image;

return uri ? (
<Component
{...props}
uri={uri}
mediaType={mediaType}
square={squareWithDefault}
isFallback={useFallback}
setUseFallback={setUseFallback}
/>
) : (
<Placeholder metadata={props.metadata} tokenId={props.tokenId} />
);
};

export const Media = MediaComponent;

0 comments on commit 69dfce5

Please sign in to comment.