Merge pull request #78 from metaplex-foundation/feature/video

Feature/video
This commit is contained in:
B 2021-06-27 20:57:37 -05:00 committed by GitHub
commit e922c3ba95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 319 additions and 205 deletions

View File

@ -51,7 +51,12 @@ export enum MetadataCategory {
VR = 'vr',
}
type FileOrString = File | string;
export type MetadataFile = {
uri: string;
type: string;
};
export type FileOrString = MetadataFile | string;
export interface IMetadataExtension {
name: string;
@ -61,6 +66,8 @@ export interface IMetadataExtension {
description: string;
// preview image absolute URI
image: string;
animation_url?: string;
// stores link to item on meta
external_url: string;
@ -72,7 +79,6 @@ export interface IMetadataExtension {
maxSupply?: number;
creators?: {
address: string;
verified: boolean;
shares: number;
}[];
};

View File

@ -11,6 +11,7 @@ import {
Data,
Creator,
findProgramAddress,
MetadataCategory,
} from '@oyster/common';
import React from 'react';
import { MintLayout, Token } from '@solana/spl-token';
@ -48,6 +49,7 @@ export const mintNFT = async (
symbol: string;
description: string;
image: string | undefined;
animation_url: string | undefined;
external_url: string;
properties: any;
creators: Creator[] | null;
@ -60,28 +62,31 @@ export const mintNFT = async (
if (!wallet?.publicKey) {
return;
}
const metadataContent = {
name: metadata.name,
symbol: metadata.symbol,
description: metadata.description,
seller_fee_basis_points: metadata.sellerFeeBasisPoints,
image: metadata.image,
animation_url: metadata.animation_url,
external_url: metadata.external_url,
properties: {
...metadata.properties,
creators: metadata.creators?.map(creator => {
return {
address: creator.address.toBase58(),
share: creator.share,
};
}),
},
};
const realFiles: File[] = [
...files,
new File(
[
JSON.stringify({
name: metadata.name,
symbol: metadata.symbol,
description: metadata.description,
seller_fee_basis_points: metadata.sellerFeeBasisPoints,
image: metadata.image,
external_url: metadata.external_url,
properties: {
...metadata.properties,
creators: metadata.creators?.map(creator => {
return {
address: creator.address.toBase58(),
verified: creator.verified,
share: creator.share,
};
}),
},
}),
JSON.stringify(metadataContent),
],
'metadata.json',
),
@ -139,22 +144,11 @@ export const mintNFT = async (
mintKey,
);
instructions.push(
Token.createMintToInstruction(
TOKEN_PROGRAM_ID,
mintKey,
recipientKey,
payerPublicKey,
[],
1,
),
);
const metadataAccount = await createMetadata(
new Data({
symbol: metadata.symbol,
name: metadata.name,
uri: `https://-------.---/rfX69WKd7Bin_RTbcnH4wM3BuWWsR_ZhWSSqZBLYdMY`,
uri: ' '.repeat(64), // size of url for arweave
sellerFeeBasisPoints: metadata.sellerFeeBasisPoints,
creators: metadata.creators,
}),
@ -212,8 +206,8 @@ export const mintNFT = async (
await fetch(
// TODO: add CNAME
env.startsWith('mainnet-beta')
? 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFileProd-1'
: 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile-1',
? 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFileProd2'
: 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile2',
{
method: 'POST',
body: data,
@ -246,7 +240,18 @@ export const mintNFT = async (
metadataAccount,
);
// // This mint, which allows limited editions to be made, stays with user's wallet.
updateInstructions.push(
Token.createMintToInstruction(
TOKEN_PROGRAM_ID,
mintKey,
recipientKey,
payerPublicKey,
[],
1,
),
);
// This mint, which allows limited editions to be made, stays with user's wallet.
const printingMint = createMint(
updateInstructions,
payerPublicKey,

View File

@ -51,7 +51,7 @@
font-size: 20px;
font-weight: 600;
padding: 24px 24px 0 24px;
min-height: 120px;
min-height: 55px;
}
.ant-avatar.ant-avatar-circle {

View File

@ -12,9 +12,12 @@ const { Meta } = Card;
export interface ArtCardProps extends CardProps {
pubkey?: PublicKey;
image?: string;
file?: string;
animationURL?: string;
category?: MetadataCategory;
name?: string;
symbol?: string;
description?: string;
@ -22,7 +25,7 @@ export interface ArtCardProps extends CardProps {
preview?: boolean;
small?: boolean;
close?: () => void;
endAuctionAt?: number;
height?: number;
width?: number;
}
@ -33,7 +36,7 @@ export const ArtCard = (props: ArtCardProps) => {
small,
category,
image,
file,
animationURL,
name,
preview,
creators,
@ -79,8 +82,8 @@ export const ArtCard = (props: ArtCardProps) => {
<ArtContent
pubkey={pubkey}
extension={file || image}
uri={image}
animationURL={animationURL}
category={category}
preview={preview}

View File

@ -1,25 +1,27 @@
import React, { Ref, useCallback, useEffect, useRef, useState } from 'react';
import { Image } from 'antd';
import { MetadataCategory } from '@oyster/common';
import { MetadataCategory, MetadataFile } from '@oyster/common';
import { MeshViewer } from '../MeshViewer';
import { ThreeDots } from '../MyLoader';
import { useCachedImage, useExtendedArt } from '../../hooks';
import { Stream, StreamPlayerApi } from '@cloudflare/stream-react';
import { useInView } from 'react-intersection-observer';
import { PublicKey } from '@solana/web3.js';
import { getLast } from '../../utils/utils';
const MeshArtContent = ({
uri,
animationUrl,
className,
style,
files,
}: {
uri?: string;
animationUrl?: string;
className?: string;
style?: React.CSSProperties;
files?: string[];
files?: (MetadataFile | string)[];
}) => {
const renderURL = files && files.length > 0 ? files[0] : uri;
const renderURL = files && files.length > 0 && typeof files[0] === 'string' ? files[0] : animationUrl;
const { isLoading } = useCachedImage(renderURL || '', true);
if (isLoading) {
@ -62,16 +64,18 @@ const CachedImageContent = ({
}
const VideoArtContent = ({
extension,
className,
style,
files,
uri,
animationURL,
active,
}: {
extension?: string;
className?: string;
style?: React.CSSProperties;
files?: string[];
files?: (MetadataFile | string)[];
uri?: string;
animationURL?: string;
active?: boolean;
}) => {
const [playerApi, setPlayerApi] = useState<StreamPlayerApi>();
@ -90,9 +94,13 @@ const VideoArtContent = ({
}, [active, playerApi]);
const likelyVideo = (files || []).filter((f, index, arr) => {
if(typeof f !== 'string') {
return false;
}
// TODO: filter by fileType
return arr.length >= 2 ? index === 1 : index === 0;
})[0];
})?.[0] as string;
const content = (
likelyVideo && likelyVideo.startsWith('https://watch.videodelivery.net/') ? (
@ -116,15 +124,17 @@ const VideoArtContent = ({
<video
className={className}
playsInline={true}
autoPlay={false}
autoPlay={true}
muted={true}
controls={true}
controlsList="nodownload"
style={style}
loop={true}
poster={extension}
poster={uri}
>
<source src={likelyVideo} type="video/mp4" style={style} />
{likelyVideo && <source src={likelyVideo} type="video/mp4" style={style} />}
{animationURL && <source src={animationURL} type="video/mp4" style={style} />}
{files?.filter(f => typeof f !== 'string').map((f: any) => <source src={f.uri} type={f.type} style={style} />)}
</video>
)
);
@ -145,7 +155,7 @@ export const ArtContent = ({
pubkey,
uri,
extension,
animationURL,
files,
}: {
category?: MetadataCategory;
@ -158,24 +168,29 @@ export const ArtContent = ({
active?: boolean;
allowMeshRender?: boolean;
pubkey?: PublicKey | string,
extension?: string;
uri?: string;
files?: string[];
animationURL?: string;
files?: (MetadataFile | string)[];
}) => {
const id = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58() || '';
const { ref, data } = useExtendedArt(id);
if(pubkey && data) {
files = data.properties.files?.filter(f => typeof f === 'string') as string[];
files = data.properties.files;
uri = data.image;
animationURL = data.animation_url;
category = data.properties.category;
}
if (allowMeshRender&& (extension?.endsWith('.glb') || category === 'vr')) {
animationURL = animationURL || '';
const animationUrlExt = new URLSearchParams(getLast(animationURL.split("?"))).get("ext");
if (allowMeshRender && (category === 'vr' || animationUrlExt === 'glb' || animationUrlExt === 'gltf')) {
return <MeshArtContent
uri={uri}
animationUrl={animationURL}
className={className}
style={style}
files={files}/>;
@ -183,10 +198,11 @@ export const ArtContent = ({
const content = category === 'video' ? (
<VideoArtContent
extension={extension}
className={className}
style={style}
files={files}
uri={uri}
animationURL={animationURL}
active={active}
/>
) : (

View File

@ -92,7 +92,7 @@
font-weight: 600;
padding: 24px 24px 0px 24px;
white-space: normal !important;
min-height: 120px;
min-height: 55px;
}
.ant-avatar.ant-avatar-circle,

View File

@ -6,16 +6,18 @@ import { AuctionView, useArt } from '../../hooks';
import { ArtContent } from '../ArtContent';
import { AuctionCard } from '../AuctionCard';
import { Link } from 'react-router-dom';
import { useMeta } from '../../contexts';
interface IPreSaleBanner {
auction?: AuctionView;
}
export const PreSaleBanner = ({ auction }: IPreSaleBanner) => {
const { isLoading } = useMeta();
const id = auction?.thumbnail.metadata.pubkey;
const art = useArt();
if (!auction) {
if (isLoading) {
return <Skeleton />;
}
@ -31,7 +33,7 @@ export const PreSaleBanner = ({ auction }: IPreSaleBanner) => {
<h2 className="art-title">
{art.title}
</h2>
<AuctionCard
{auction && <AuctionCard
auctionView={auction}
style={{
background: 'transparent',
@ -54,7 +56,7 @@ export const PreSaleBanner = ({ auction }: IPreSaleBanner) => {
</Link>
</>
}
/>
/>}
</Col>
</Row>
);

View File

@ -22,5 +22,12 @@
"image": "https://pbs.twimg.com/profile_images/1393353972371623938/ZMWvvptg_400x400.jpg",
"description": "",
"background": ""
},
"SoL351y4uKWtbH14AU1Rhiao96aBM4u57bMi5Vj2XJc": {
"name": "Solana",
"image": "https://pbs.twimg.com/profile_images/1299400345144049665/sPxnVXa7_400x400.jpg",
"description": "Account used by Solana to mint official NFTs.",
"background": ""
}
}

View File

@ -27,7 +27,6 @@ import {
Vault,
setProgramIds,
useConnectionConfig,
useWallet,
AuctionDataExtended,
MAX_AUCTION_DATA_EXTENDED_SIZE,
AuctionDataExtendedParser,
@ -373,18 +372,19 @@ export function MetaProvider({ children = null as any }) {
isMetadataPartOfStore(result, store, whitelistedCreatorsByCreator)
) {
await result.info.init();
updateStateValue(
'metadataByMasterEdition',
result.info.masterEdition?.toBase58() || '',
result,
);
setState((data) => ({
...data,
metadata: [...data.metadata.filter(m => m.pubkey.equals(pubkey)), result],
metadataByMasterEdition: {
...data.metadataByMasterEdition,
[result.info.masterEdition?.toBase58() || '']: result,
},
metadataByMint: {
...data.metadataByMint,
[result.info.mint.toBase58()]: result
}
}));
}
// TODO: BL
// setMetadataByMint(latest => {
// updateMints(latest);
// return latest;
// });
},
);
@ -414,6 +414,7 @@ export function MetaProvider({ children = null as any }) {
}, [
connection,
updateStateValue,
setState,
updateMints,
store,
whitelistedCreatorsByCreator,
@ -503,7 +504,7 @@ const queryExtendedMetadata = async (
MintParser,
false,
) as ParsedAccount<MintInfo>;
if (mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) {
if (!mint.info.supply.eqn(1) || mint.info.decimals !== 0) {
// naive not NFT check
delete mintToMetadata[key];
} else {

View File

@ -1,3 +1,15 @@
export const cleanName = (name: string): string => {
export const cleanName = (name?: string): string | undefined => {
if (!name) {
return undefined;
}
return name.replaceAll(' ', '-');
};
export const getLast = <T>(arr: T[]) => {
if (arr.length <= 0) {
return undefined;
}
return arr[arr.length - 1];
};

View File

@ -5,7 +5,7 @@ import { useArt, useExtendedArt } from './../../hooks';
import './index.less';
import { ArtContent } from '../../components/ArtContent';
import { shortenAddress, useConnection, useWallet } from '@oyster/common';
import { shortenAddress, TokenAccount, useConnection, useUserAccounts, useWallet } from '@oyster/common';
import { MetaAvatar } from '../../components/MetaAvatar';
import { sendSignMetadata } from '../../actions/sendSignMetadata';
import { PublicKey } from '@solana/web3.js';
@ -15,10 +15,18 @@ const { Content } = Layout;
export const ArtView = () => {
const { id } = useParams<{ id: string }>();
const { wallet } = useWallet();
const connection = useConnection();
const art = useArt(id);
const { ref, data } = useExtendedArt(id);
// const { userAccounts } = useUserAccounts();
// const accountByMint = userAccounts.reduce((prev, acc) => {
// prev.set(acc.info.mint.toBase58(), acc);
// return prev;
// }, new Map<string, TokenAccount>());
const description = data?.description;
const pubkey = wallet?.publicKey?.toBase58() || '';
@ -55,6 +63,7 @@ export const ArtView = () => {
className="artwork-image"
pubkey={id}
active={true}
allowMeshRender={true}
/>
</Col>
{/* <Divider /> */}
@ -79,6 +88,31 @@ export const ArtView = () => {
<span className="creator-name">
{creator.name || shortenAddress(creator.address || '')}
</span>
{/* <Button
onClick={async () => {
if(!art.mint) {
return;
}
const mint = new PublicKey(art.mint);
const account = accountByMint.get(art.mint);
if(!account) {
return;
}
const owner = wallet?.publicKey;
if(!owner) {
return;
}
const instructions: any[] = [];
await updateMetadata(undefined, undefined, true, mint, owner, instructions)
sendTransaction(connection, wallet, instructions, [], true);
}}
>
Mark as Sold
</Button> */}
<div style={{ marginLeft: 10 }}>
{!creator.verified &&
(creator.address === pubkey ? (

View File

@ -30,12 +30,13 @@ import {
shortenAddress,
MetaplexModal,
MetaplexOverlay,
MetadataFile,
} from '@oyster/common';
import { getAssetCostToStore, LAMPORT_MULTIPLIER } from '../../utils/assets';
import { Connection, PublicKey } from '@solana/web3.js';
import { MintLayout } from '@solana/spl-token';
import { useHistory, useParams } from 'react-router-dom';
import { cleanName } from '../../utils/utils';
import { cleanName, getLast } from '../../utils/utils';
import { AmountLabel } from '../../components/AmountLabel';
import useWindowDimensions from '../../utils/layout';
@ -56,12 +57,14 @@ export const ArtCreateView = () => {
const [progress, setProgress] = useState<number>(0);
const [nft, setNft] =
useState<{ metadataAccount: PublicKey } | undefined>(undefined);
const [files, setFiles] = useState<File[]>([]);
const [attributes, setAttributes] = useState<IMetadataExtension>({
name: '',
symbol: '',
description: '',
external_url: '',
image: '',
animation_url: undefined,
seller_fee_basis_points: 0,
creators: [],
properties: {
@ -85,22 +88,17 @@ export const ArtCreateView = () => {
// store files
const mint = async () => {
const fileNames = (attributes?.properties?.files || []).map(f =>
typeof f === 'string' ? f : f.name,
);
const files = (attributes?.properties?.files || []).filter(
f => typeof f !== 'string',
) as File[];
const metadata = {
name: attributes.name,
symbol: attributes.symbol,
creators: attributes.creators,
description: attributes.description,
sellerFeeBasisPoints: attributes.seller_fee_basis_points,
image: fileNames && fileNames?.[0] && fileNames[0],
image: attributes.image,
animation_url: attributes.animation_url,
external_url: attributes.external_url,
properties: {
files: fileNames,
files: attributes.properties.files,
category: attributes.properties?.category,
},
};
@ -165,6 +163,8 @@ export const ArtCreateView = () => {
<UploadStep
attributes={attributes}
setAttributes={setAttributes}
files={files}
setFiles={setFiles}
confirm={() => gotoStep(2)}
/>
)}
@ -172,6 +172,7 @@ export const ArtCreateView = () => {
{step === 2 && (
<InfoStep
attributes={attributes}
files={files}
setAttributes={setAttributes}
confirm={() => gotoStep(3)}
/>
@ -186,6 +187,7 @@ export const ArtCreateView = () => {
{step === 4 && (
<LaunchStep
attributes={attributes}
files={files}
confirm={() => gotoStep(5)}
connection={connection}
/>
@ -283,14 +285,16 @@ const CategoryStep = (props: {
const UploadStep = (props: {
attributes: IMetadataExtension;
setAttributes: (attr: IMetadataExtension) => void;
files: File[],
setFiles: (files: File[]) => void,
confirm: () => void;
}) => {
const [mainFile, setMainFile] = useState<any>();
const [coverFile, setCoverFile] = useState<any>();
const [image, setImage] = useState<string>('');
const [imageURL, setImageURL] = useState<string>('');
const [imageURLErr, setImageURLErr] = useState<string>('');
const disableContinue = (!mainFile && !image) || !!imageURLErr;
const [coverFile, setCoverFile] = useState<File | undefined>(props.files?.[0]);
const [mainFile, setMainFile] = useState<File | undefined>(props.files?.[1]);
const [customURL, setCustomURL] = useState<string>('');
const [customURLErr, setCustomURLErr] = useState<string>('');
const disableContinue = (!coverFile) || !!customURLErr;
useEffect(() => {
props.setAttributes({
@ -344,46 +348,63 @@ const UploadStep = (props: {
very first time.
</p>
</Row>
<Row className="content-action" style={{ marginBottom: 5 }}>
<h3>{uploadMsg(props.attributes.properties?.category)}</h3>
<Dragger
accept={acceptableFiles(props.attributes.properties?.category)}
style={{ padding: 20, background: 'rgba(255, 255, 255, 0.08)' }}
multiple={false}
customRequest={info => {
// dont upload files here, handled outside of the control
info?.onSuccess?.({}, null as any);
}}
fileList={mainFile ? [mainFile] : []}
onChange={async info => {
const file = info.file.originFileObj;
<Row className="content-action">
<h3>
Upload a cover image (PNG, JPG, GIF)
</h3>
<Dragger
accept=".png,.jpg,.gif,.mp4"
style={{ padding: 20 }}
multiple={false}
customRequest={info => {
// dont upload files here, handled outside of the control
info?.onSuccess?.({}, null as any);
}}
fileList={coverFile ? [coverFile as any] : []}
onChange={async info => {
const file = info.file.originFileObj;
if (file) setCoverFile(file);
}}
>
<div className="ant-upload-drag-icon">
<h3 style={{ fontWeight: 700 }}>
Upload your cover image (PNG, JPG, GIF)
</h3>
</div>
<p className="ant-upload-text">Drag and drop, or click to browse</p>
</Dragger>
</Row>
{(props.attributes.properties?.category !== MetadataCategory.Image) && (
<Row className="content-action" style={{ marginBottom: 5, marginTop: 30 }}>
<h3>{uploadMsg(props.attributes.properties?.category)}</h3>
<Dragger
accept={acceptableFiles(props.attributes.properties?.category)}
style={{ padding: 20, background: 'rgba(255, 255, 255, 0.08)' }}
multiple={false}
customRequest={info => {
// dont upload files here, handled outside of the control
info?.onSuccess?.({}, null as any);
}}
fileList={mainFile ? [mainFile as any] : []}
onChange={async info => {
const file = info.file.originFileObj;
// Reset image URL
setImageURL('');
setImageURLErr('');
// Reset image URL
setCustomURL('');
setCustomURLErr('');
if (file) setMainFile(file);
if (
props.attributes.properties?.category !== MetadataCategory.Audio
) {
const reader = new FileReader();
reader.onload = function (event) {
setImage((event.target?.result as string) || '');
};
if (file) reader.readAsDataURL(file);
}
}}
onRemove={() => {
setMainFile(null);
setImage('');
}}
>
<div className="ant-upload-drag-icon">
<h3 style={{ fontWeight: 700 }}>Upload your creation</h3>
</div>
<p className="ant-upload-text">Drag and drop, or click to browse</p>
</Dragger>
</Row>
if (file) setMainFile(file);
}}
onRemove={() => {
setMainFile(undefined);
}}
>
<div className="ant-upload-drag-icon">
<h3 style={{ fontWeight: 700 }}>Upload your creation</h3>
</div>
<p className="ant-upload-text">Drag and drop, or click to browse</p>
</Dragger>
</Row>)}
<Form.Item
style={{
width: '100%',
@ -394,72 +415,33 @@ const UploadStep = (props: {
label={<h3>OR use absolute URL to content</h3>}
labelAlign="left"
colon={false}
validateStatus={imageURLErr ? 'error' : 'success'}
help={imageURLErr}
validateStatus={customURLErr ? 'error' : 'success'}
help={customURLErr}
>
<Input
disabled={!!mainFile}
placeholder="http://example.com/path/to/image"
value={imageURL}
onChange={ev => setImageURL(ev.target.value)}
onFocus={() => setImageURLErr('')}
value={customURL}
onChange={ev => setCustomURL(ev.target.value)}
onFocus={() => setCustomURLErr('')}
onBlur={() => {
if (!imageURL) {
setImageURLErr('');
if (!customURL) {
setCustomURLErr('');
return;
}
try {
// Validate URL and save
new URL(imageURL);
setImage(imageURL);
new URL(customURL);
setCustomURL(customURL);
setCustomURLErr('');
} catch (e) {
console.error(e);
setImageURLErr('Please enter a valid absolute URL');
setCustomURLErr('Please enter a valid absolute URL');
}
}}
/>
</Form.Item>
{props.attributes.properties?.category === MetadataCategory.Audio && (
<Row className="content-action">
<h3>
Optionally, you can upload a cover image or video (PNG, JPG, GIF,
MP4)
</h3>
<Dragger
accept=".png,.jpg,.gif,.mp4"
style={{ padding: 20 }}
multiple={false}
customRequest={info => {
// dont upload files here, handled outside of the control
info?.onSuccess?.({}, null as any);
}}
fileList={coverFile ? [coverFile] : []}
onChange={async info => {
const file = info.file.originFileObj;
if (file) setCoverFile(file);
if (
props.attributes.properties?.category === MetadataCategory.Audio
) {
const reader = new FileReader();
reader.onload = function (event) {
setImage((event.target?.result as string) || '');
};
if (file) reader.readAsDataURL(file);
}
}}
>
<div className="ant-upload-drag-icon">
<h3 style={{ fontWeight: 700 }}>
Upload your cover image or video (PNG, JPG, GIF, MP4)
</h3>
</div>
<p className="ant-upload-text">Drag and drop, or click to browse</p>
</Dragger>
<h3 style={{ marginTop: 30 }}>OR use absolute URL to content</h3>
<Input />
</Row>
)}
<Row>
<Button
type="primary"
@ -470,16 +452,24 @@ const UploadStep = (props: {
...props.attributes,
properties: {
...props.attributes.properties,
files: imageURL
? [imageURL]
: [mainFile, coverFile]
files: [coverFile, mainFile, customURL]
.filter(f => f)
.map(
f => new File([f], cleanName(f.name), { type: f.type }),
f => {
const uri = typeof f === 'string' ? f : (cleanName(f?.name) || '');
const type = typeof f === 'string' || !f ? 'unknown' : f.type || (getLast(f.name.split('.')) || 'unknown');
return ({
uri,
type
}) as MetadataFile;
},
),
},
image: imageURL || image,
image: cleanName(coverFile?.name) || '',
animation_url: cleanName(mainFile && mainFile.name),
});
props.setFiles([coverFile, mainFile].filter(f => f) as File[]);
props.confirm();
}}
style={{ marginTop: 24 }}
@ -497,17 +487,55 @@ interface Royalty {
amount: number;
}
const useArtworkFiles = (files: File[], attributes: IMetadataExtension) => {
const [data, setData] = useState<{ image: string, animation_url: string }>({ image: '', animation_url: '' });
useEffect(() => {
if(attributes.image) {
const file = files.find(f => f.name === attributes.image);
if(file) {
const reader = new FileReader();
reader.onload = function (event) {
setData((data: any) => {
return {
...(data || {}),
image: (event.target?.result as string) || '',
}
});
};
if (file) reader.readAsDataURL(file);
}
}
if(attributes.animation_url) {
const file = files.find(f => f.name === attributes.animation_url);
if(file) {
const reader = new FileReader();
reader.onload = function (event) {
setData((data: any) => {
return {
...(data || {}),
animation_url: (event.target?.result as string) || '',
}
});
};
if (file) reader.readAsDataURL(file);
}
}
}, [files, attributes]);
return data;
}
const InfoStep = (props: {
attributes: IMetadataExtension;
files: File[],
setAttributes: (attr: IMetadataExtension) => void;
confirm: () => void;
}) => {
const [creators, setCreators] = useState<Array<UserValue>>([]);
const [royalties, setRoyalties] = useState<Array<Royalty>>([]);
const { wallet } = useWallet();
const file = props.attributes.properties.files?.[0];
const fileName = typeof file === 'string' ? file : file?.name;
const { image, animation_url } = useArtworkFiles(props.files, props.attributes);
useEffect(() => {
setRoyalties(
@ -530,8 +558,8 @@ const InfoStep = (props: {
<Col>
{props.attributes.image && (
<ArtCard
image={props.attributes.image}
file={fileName || ''}
image={image}
animationURL={animation_url}
category={props.attributes.properties?.category}
name={props.attributes.name}
symbol={props.attributes.symbol}
@ -893,19 +921,13 @@ const RoyaltiesStep = (props: {
const LaunchStep = (props: {
confirm: () => void;
attributes: IMetadataExtension;
files: File[],
connection: Connection;
}) => {
const files = (props.attributes.properties?.files || []).filter(
f => typeof f !== 'string',
) as File[];
const fileNames = (props.attributes.properties?.files || []).map(f =>
typeof f === 'string' ? f : f?.name,
);
const metadata = {
...(props.attributes as any),
files: fileNames,
};
const [cost, setCost] = useState(0);
const { image, animation_url } = useArtworkFiles(props.files, props.attributes);
const files = props.files;
const metadata = props.attributes;
useEffect(() => {
const rentCall = Promise.all([
props.connection.getMinimumBalanceForRentExemption(MintLayout.span),
@ -932,7 +954,7 @@ const LaunchStep = (props: {
// TODO: add fees based on number of transactions and signers
setCost(sol + additionalSol);
});
}, [files, setCost]);
}, [files, metadata, setCost]);
return (
<>
@ -947,8 +969,8 @@ const LaunchStep = (props: {
<Col>
{props.attributes.image && (
<ArtCard
image={props.attributes.image}
file={fileNames?.[0] || ''}
image={image}
animationURL={animation_url}
category={props.attributes.properties?.category}
name={props.attributes.name}
symbol={props.attributes.symbol}

View File

@ -416,6 +416,7 @@ export const InnerBillingView = ({
<ArtContent
pubkey={id}
className="artwork-image"
allowMeshRender={true}
/>
</Col>
<Col span={12}>

View File

@ -64,6 +64,7 @@ export const AuctionItem = ({
className="artwork-image stack-item"
style={style}
active={active}
allowMeshRender={true}
/>
</div>
);

View File

@ -32,7 +32,11 @@ import {
PriceFloorType,
IPartialCreateAuctionArgs,
} from '@oyster/common';
import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
import {
Connection,
LAMPORTS_PER_SOL,
PublicKey,
} from '@solana/web3.js';
import { MintLayout } from '@solana/spl-token';
import { useHistory, useParams } from 'react-router-dom';
import { capitalize } from 'lodash';