diff --git a/components/NftFullScreenViewer.js b/components/NftFullScreenViewer.js new file mode 100644 index 000000000..75ddb9e8c --- /dev/null +++ b/components/NftFullScreenViewer.js @@ -0,0 +1,449 @@ +import { useState, useEffect } from 'react' +import { useTranslation } from 'next-i18next' +import Head from 'next/head' + +import { forbid18Plus, stripText } from '../utils' +import { needNftAgeCheck, nftName, nftUrl } from '../utils/nft' + +import LoadingGif from '../public/images/loading.gif' +import { FaCloudDownloadAlt, FaTimes } from 'react-icons/fa' +import ReactPannellum from 'react-pannellum' +import AgeCheck from './UI/AgeCheck' + +const downloadIcon = ( +
+ +
+) + +const isPanorama = (metadata) => { + if (!metadata) return false + + // Check name and description for panorama keywords + const panoramaKeywords = ['360', 'panorama', 'panoramic', 'equirectangular'] + const name = metadata.name?.toString().toLowerCase() || '' + const description = metadata.description?.toString().toLowerCase() || '' + + // Check if name or description contains panorama keywords + const hasPanoramaKeyword = panoramaKeywords.some((keyword) => name.includes(keyword) || description.includes(keyword)) + + // Check for specific camera types known for panoramas + const panoramaCameras = ['gopro fusion', 'insta360', 'ricoh theta'] + const hasPanoramaCamera = panoramaCameras.some((camera) => description.includes(camera.toLowerCase())) + + return hasPanoramaKeyword || hasPanoramaCamera +} + +export default function NftFullScreenViewer({ nft, onClose }) { + const { t } = useTranslation() + const [contentTab, setContentTab] = useState('image') + const [loaded, setLoaded] = useState(false) + const [errored, setErrored] = useState(false) + const [isPanoramic, setIsPanoramic] = useState(false) + const [showAgeCheck, setShowAgeCheck] = useState(false) + + const imageUrl = nftUrl(nft, 'image') + const videoUrl = nftUrl(nft, 'video') + const audioUrl = nftUrl(nft, 'audio') + const modelUrl = nftUrl(nft, 'model') + + const clUrl = { + image: nftUrl(nft, 'image', 'cl'), + video: nftUrl(nft, 'video', 'cl'), + audio: nftUrl(nft, 'audio', 'cl'), + model: nftUrl(nft, 'model', 'cl') + } + + const contentTabList = [] + if (imageUrl) { + contentTabList.push({ value: 'image', label: t('tabs.image') }) + } + if (videoUrl) { + contentTabList.push({ value: 'video', label: t('tabs.video') }) + } + if (modelUrl) { + contentTabList.push({ value: 'model', label: t('tabs.model') }) + } + + let imageStyle = { width: '100%', height: 'auto', maxHeight: '80vh' } + if (imageUrl) { + if (imageUrl.slice(0, 10) === 'data:image') { + imageStyle.imageRendering = 'pixelated' + } + if (nft.deletedAt) { + imageStyle.filter = 'grayscale(1)' + } + } + + let defaultTab = contentTab + let defaultUrl = clUrl[contentTab] + if (!imageUrl && contentTab === 'image') { + if (clUrl['video']) { + defaultTab = 'video' + defaultUrl = clUrl['video'] + } else if (clUrl['model']) { + defaultTab = 'model' + defaultUrl = clUrl['model'] + } + } + + //add attributes for the 3D model viewer + let modelAttr = [] + if (nft.metadata && (nft.metadata['3D_attributes'] || nft.metadata['3d_attributes'])) { + modelAttr = nft.metadata['3D_attributes'] || nft.metadata['3d_attributes'] + const supportedAttr = [ + 'environment-image', + 'exposure', + 'shadow-intensity', + 'shadow-softness', + 'camera-orbit', + 'camera-target', + 'skybox-image', + 'auto-rotate-delay', + 'rotation-per-second', + 'field-of-view', + 'max-camera-orbit', + 'min-camera-orbit', + 'max-field-of-view', + 'min-field-of-view', + 'disable-zoom', + 'orbit-sensitivity', + 'animation-name', + 'animation-crossfade-duration', + 'variant-name', + 'orientation', + 'scale' + ] + if (Array.isArray(modelAttr)) { + for (let i = 0; i < modelAttr.length; i++) { + if (supportedAttr.includes(modelAttr[i].attribute)) { + modelAttr[i].value = stripText(modelAttr[i].value) + } else { + delete modelAttr[i] + } + } + } else if (typeof modelAttr === 'object') { + let metaModelAttr = modelAttr + modelAttr = [] + Object.keys(metaModelAttr).forEach((e) => { + if (supportedAttr.includes(e)) { + modelAttr.push({ + attribute: e, + value: stripText(metaModelAttr[e]) + }) + } + }) + } + } + + useEffect(() => { + if (imageUrl || videoUrl) { + const panoramic = isPanorama(nft.metadata) + setIsPanoramic(panoramic) + if (panoramic) { + setLoaded(true) + } + // if no image, but there is a video, don't show loading spinner + if (!imageUrl) { + setLoaded(true) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [imageUrl, videoUrl]) + + const clickOn18PlusImage = async () => { + const forbid = await forbid18Plus() + if (forbid) return + setShowAgeCheck(true) + } + + const loadingImage = () => { + if (errored) { + return ( +
+ {t('general.load-failed')} +
+
+ ) + } else if (!loaded) { + return ( +
+ +
+ {t('general.loading')} +
+ ) + } + } + + return ( +
+ + {nftName(nft) || 'NFT Viewer'} - Full Screen + + + {/* Header with close button and tabs */} +
+
+

+ {nftName(nft) || 'NFT Viewer'} +

+ + {contentTabList.length > 1 && ( +
+ {contentTabList.map(tab => ( + + ))} +
+ )} +
+ +
+ {defaultUrl && ( + + {t('tabs.' + defaultTab)} {downloadIcon} + + )} + + +
+
+ + {/* Content Area */} +
+ {needNftAgeCheck(nft) ? ( + 18 plus content + ) : ( + <> + {imageUrl && contentTab === 'image' && ( + <> + {loadingImage()} + {isPanoramic ? ( + + ) : ( + { + setLoaded(true) + setErrored(false) + }} + onError={({ currentTarget }) => { + if (currentTarget.src === imageUrl && imageUrl !== clUrl.image) { + currentTarget.src = clUrl.image + } else { + setErrored(true) + } + }} + alt={nftName(nft)} + /> + )} + + )} + + {videoUrl && contentTab === 'video' && ( + <> + {loadingImage()} + {isPanoramic ? ( + + ) : ( + + )} + + )} + + {modelUrl && contentTab === 'model' && ( + <> + +