diff --git a/cspell.json b/cspell.json index b4f83540523b..069b320a239f 100644 --- a/cspell.json +++ b/cspell.json @@ -358,7 +358,8 @@ "Enjin", "poap", "irss", - "CELO" + "CELO", + "flac" ], "ignoreRegExpList": ["/@servie/"], "overrides": [ diff --git a/packages/icons/brands/MaskGrey.tsx b/packages/icons/brands/MaskGrey.tsx index a87c3fe85961..7ab13dd429ab 100644 --- a/packages/icons/brands/MaskGrey.tsx +++ b/packages/icons/brands/MaskGrey.tsx @@ -1,7 +1,7 @@ -import { createIcon } from '../utils' +import { createPaletteAwareIcon } from '../utils' import type { SvgIcon } from '@mui/material' -export const MaskGreyIcon: typeof SvgIcon = createIcon( +export const MaskGreyIcon: typeof SvgIcon = createPaletteAwareIcon( 'Mask', @@ -12,6 +12,15 @@ export const MaskGreyIcon: typeof SvgIcon = createIcon( clipRule="evenodd" /> , + + + + , '0 0 120 120', [130, 40], ) diff --git a/packages/icons/general/AssetLoading.tsx b/packages/icons/general/AssetLoading.tsx new file mode 100644 index 000000000000..e8eb7f46b9f6 --- /dev/null +++ b/packages/icons/general/AssetLoading.tsx @@ -0,0 +1,16 @@ +import { createIcon } from '../utils' + +export const AssetLoadingIcon = createIcon( + 'AssetLoadingIcon', + + + + + , + '0 0 36 52', +) diff --git a/packages/icons/general/index.ts b/packages/icons/general/index.ts index acc65abe0d1a..a4f47b110fe6 100644 --- a/packages/icons/general/index.ts +++ b/packages/icons/general/index.ts @@ -81,3 +81,4 @@ export * from './Retweet' export * from './Drop' export * from './Right' export * from './Tutorial' +export * from './AssetLoading' diff --git a/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx b/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx index c395e666196d..5821a424dd76 100644 --- a/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx +++ b/packages/mask/src/plugins/Collectible/SNSAdaptor/ArticleTab.tsx @@ -1,9 +1,8 @@ -import type { FC } from 'react' -import { Link } from '@mui/material' import { makeStyles } from '@masknet/theme' -import { Video } from '../../../components/shared/Video' import { CollectibleTab } from './CollectibleTab' import { CollectibleState } from '../hooks/useCollectibleState' +import { AssetPlayer } from '@masknet/shared' +import { useMemo } from 'react' const useStyles = makeStyles()({ body: { @@ -15,44 +14,38 @@ const useStyles = makeStyles()({ maxHeight: '100%', border: 'none', }, + errorPlaceholder: { + padding: '82px 0', + }, + loadingPlaceholder: { + padding: '74px 0', + }, }) -interface AssetPlayerProps { - src?: string - alt: string -} - -// opensea supports: JPG, PNG, GIF, SVG, MP4, WEBM, MP3, WAV, OGG, GLB, GLTF. -const AssetPlayer: FC = ({ src, alt }) => { - const { classes } = useStyles() - if (!src) return null - const isVideo = src.match(/\.(mp4|webm)$/i) - return isVideo ? ( - - ) : ( - - ) -} - export interface ArticleTabProps {} export function ArticleTab(props: ArticleTabProps) { const { classes } = useStyles() const { asset } = CollectibleState.useContainer() - if (!asset.value) return null - const resourceUrl = asset.value.image_url || asset.value.animation_url - return ( - - - {asset.value.animation_url ? ( - - - - ) : ( - - )} - - - ) + return useMemo(() => { + if (!asset.value) return null + const resourceUrl = asset.value.animation_url || asset.value.image_url + return ( + + + + + + ) + }, [asset.value?.animation_url, asset.value?.image_url, classes]) } diff --git a/packages/shared/package.json b/packages/shared/package.json index 3775e8fac304..1ab2a8b44544 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -11,9 +11,12 @@ "anchorme": "^2.1.2", "bignumber.js": "^9.0.2", "classnames": "^2.3.1", + "iframe-resizer-react": "^1.1.0", + "lodash-es": "^4.17.21", "qrcode": "^1.5.0", "react-feather": "^2.0.9", "react-use": "^17.3.1", + "urlcat": "^2.0.4", "use-subscription": "^1.5.1", "uuid": "^8.3.2" } diff --git a/packages/shared/src/UI/components/AssetPlayer/index.tsx b/packages/shared/src/UI/components/AssetPlayer/index.tsx new file mode 100644 index 000000000000..fd14b8718891 --- /dev/null +++ b/packages/shared/src/UI/components/AssetPlayer/index.tsx @@ -0,0 +1,136 @@ +import { memo, useRef, useCallback, useState } from 'react' +import IframeResizer, { IFrameComponent } from 'iframe-resizer-react' +import { mediaViewerUrl } from '../../../constants' +import { useTimeoutFn, useUpdateEffect } from 'react-use' +import { makeStyles, useStylesExtends } from '@masknet/theme' +import { Box, SvgIconProps } from '@mui/material' +import { AssetLoadingIcon, MaskGreyIcon } from '@masknet/icons' + +interface AssetPlayerProps + extends withClasses<'errorPlaceholder' | 'errorIcon' | 'loadingPlaceholder' | 'loadingIcon'> { + url?: string + type?: string + options?: { + autoPlay?: boolean + controls?: boolean + playsInline?: boolean + loop?: boolean + muted?: boolean + } + iconProps?: SvgIconProps +} +const useStyles = makeStyles()((theme) => ({ + errorPlaceholder: { + backgroundColor: theme.palette.background.default, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 12, + width: '100%', + }, + errorIcon: { + width: 36, + height: 36, + }, + loadingPlaceholder: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + }, + loadingIcon: { + width: 36, + height: 52, + }, +})) + +enum AssetPlayerState { + LOADING = 0, + INIT = 1, + NORMAL = 2, + ERROR = 3, +} + +const TIMEOUT = 10000 + +export const AssetPlayer = memo(({ url, type, options, iconProps, ...props }) => { + const ref = useRef(null) + const classes = useStylesExtends(useStyles(), props) + const [playerState, setPlayerState] = useState(url ? AssetPlayerState.LOADING : AssetPlayerState.ERROR) + + //#region If onResized is not triggered within the specified time, set player state to error + const [, cancel, reset] = useTimeoutFn(() => { + if (playerState !== AssetPlayerState.NORMAL) { + setPlayerState(AssetPlayerState.ERROR) + } + }, TIMEOUT) + //#endregion + + //#region setup iframe when url and options be changed + const setIframe = useCallback(() => { + // if iframe isn't be init or the load error has been existed + if (!ref.current || playerState === AssetPlayerState.ERROR) return + if (!url) { + setPlayerState(AssetPlayerState.ERROR) + return + } + if (playerState === AssetPlayerState.INIT || playerState === AssetPlayerState.NORMAL) { + reset() + ref.current.iFrameResizer.sendMessage({ + url, + type, + ...options, + }) + return + } + }, [url, type, options, playerState]) + //endregion + + //#region resource loaded error + const onMessage = useCallback(({ message }: { message: { name: string } }) => { + if (message?.name === 'Error') { + setPlayerState(AssetPlayerState.ERROR) + } + }, []) + //#endregion + + useUpdateEffect(() => { + setIframe() + }, [setIframe]) + + return ( + <> + + {playerState === AssetPlayerState.ERROR ? ( + + ) : ( + + )} + + { + ref.current = iframe + setPlayerState(AssetPlayerState.INIT) + setIframe() + }} + onResized={({ type }) => { + if (type !== 'init') { + cancel() + setPlayerState(AssetPlayerState.NORMAL) + } + }} + style={{ width: playerState !== AssetPlayerState.NORMAL ? 0 : undefined }} + checkOrigin={false} + onMessage={onMessage} + frameBorder="0" + allow="autoplay" + allowFullScreen + /> + > + ) +}) diff --git a/packages/shared/src/UI/components/index.ts b/packages/shared/src/UI/components/index.ts index c1412f9d9393..5eba5c52a1a8 100644 --- a/packages/shared/src/UI/components/index.ts +++ b/packages/shared/src/UI/components/index.ts @@ -11,3 +11,4 @@ export * from './LoadingAnimation' export * from './MiniNetworkSelector' export * from './AddressViewer' export * from './I18NextProviderHMR' +export * from './AssetPlayer' diff --git a/packages/shared/src/constants.tsx b/packages/shared/src/constants.tsx index 194afdaf6285..cd50d3028e19 100644 --- a/packages/shared/src/constants.tsx +++ b/packages/shared/src/constants.tsx @@ -7,3 +7,5 @@ export const SOCIAL_MEDIA_ICON_MAPPING: Record = { 'minds.com': , 'instagram.com': , } + +export const mediaViewerUrl = 'https://dimensiondev.github.io/Media-Viewer/index.html' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb8edd94993a..e68beec2b560 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -816,9 +816,12 @@ importers: anchorme: ^2.1.2 bignumber.js: ^9.0.2 classnames: ^2.3.1 + iframe-resizer-react: ^1.1.0 + lodash-es: ^4.17.21 qrcode: ^1.5.0 react-feather: ^2.0.9 react-use: ^17.3.1 + urlcat: ^2.0.4 use-subscription: ^1.5.1 uuid: ^8.3.2 dependencies: @@ -830,9 +833,12 @@ importers: anchorme: 2.1.2 bignumber.js: 9.0.2 classnames: 2.3.1 + iframe-resizer-react: 1.1.0_757a802188413a36d4f24237d13b8e90 + lodash-es: 4.17.21 qrcode: 1.5.0 react-feather: 2.0.9_react@18.0.0-rc.0 react-use: 17.3.1_757a802188413a36d4f24237d13b8e90 + urlcat: 2.0.4 use-subscription: 1.5.1_react@18.0.0-rc.0 uuid: 8.3.2