feat: vod display (#686)
* feat: preview for html5 supported videos * fix: handle out of limit tags * feat: support preview on the donwload screen * refactor: rework meta and preview handling to be more general * fix: missing meta * fix: do not allow maybe or probably types * fix: make the media check more strict --------- Co-authored-by: Levente Kiss <levente.kiss@solarpunk.bzz>
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
import { createStyles, makeStyles } from '@material-ui/core'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
video: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface VideoProps {
|
||||||
|
src: string | undefined
|
||||||
|
maxHeight?: string
|
||||||
|
maxWidth?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FitVideo(props: VideoProps): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const inlineStyles: Record<string, string> = {}
|
||||||
|
|
||||||
|
props.maxHeight && (inlineStyles.maxHeight = props.maxHeight)
|
||||||
|
props.maxWidth && (inlineStyles.maxWidth = props.maxWidth)
|
||||||
|
|
||||||
|
return <video className={classes.video} src={props.src} style={inlineStyles} controls />
|
||||||
|
}
|
||||||
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
export const META_FILE_NAME = '.swarmgatewaymeta.json'
|
export const META_FILE_NAME = 'metadata'
|
||||||
export const PREVIEW_FILE_NAME = '.swarmgatewaypreview.jpeg'
|
|
||||||
export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 }
|
export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 }
|
||||||
export const BZZ_LINK_DOMAIN = 'bzz.link'
|
export const BZZ_LINK_DOMAIN = 'bzz.link'
|
||||||
export const BLOCKCHAIN_EXPLORER_URL = 'https://blockscout.com/xdai/mainnet'
|
export const BLOCKCHAIN_EXPLORER_URL = 'https://blockscout.com/xdai/mainnet'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Grid, Typography } from '@material-ui/core'
|
import { Box, Grid, Typography } from '@material-ui/core'
|
||||||
import { Web } from '@material-ui/icons'
|
import { Web } from '@material-ui/icons'
|
||||||
import { ReactElement } from 'react'
|
import { ReactElement, useMemo } from 'react'
|
||||||
import File from 'remixicon-react/FileLineIcon'
|
import File from 'remixicon-react/FileLineIcon'
|
||||||
import Folder from 'remixicon-react/FolderLineIcon'
|
import Folder from 'remixicon-react/FolderLineIcon'
|
||||||
import { FitImage } from '../../components/FitImage'
|
import { FitImage } from '../../components/FitImage'
|
||||||
@@ -8,35 +8,52 @@ import { shortenText } from '../../utils'
|
|||||||
import { getHumanReadableFileSize } from '../../utils/file'
|
import { getHumanReadableFileSize } from '../../utils/file'
|
||||||
import { shortenHash } from '../../utils/hash'
|
import { shortenHash } from '../../utils/hash'
|
||||||
import { AssetIcon } from './AssetIcon'
|
import { AssetIcon } from './AssetIcon'
|
||||||
|
import { FitVideo } from '../../components/FitVideo'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
previewUri?: string
|
previewUri?: string
|
||||||
metadata?: Metadata
|
metadata?: Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
|
/* eslint-disable react/display-name */
|
||||||
|
const getPreviewComponent = (previewUri?: string, metadata?: Metadata) => {
|
||||||
|
if (metadata?.isVideo) {
|
||||||
|
return () => <FitVideo src={previewUri} maxWidth="250px" maxHeight="175px" />
|
||||||
|
}
|
||||||
|
|
||||||
export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
|
if (metadata?.isImage) {
|
||||||
let previewComponent = <File />
|
return () => <FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
|
||||||
let type = metadata?.type
|
}
|
||||||
|
|
||||||
if (metadata?.isWebsite) {
|
if (metadata?.isWebsite) {
|
||||||
previewComponent = <Web />
|
return () => <AssetIcon icon={<Web />} />
|
||||||
type = 'Website'
|
|
||||||
} else if (metadata?.type === 'folder') {
|
|
||||||
previewComponent = <Folder />
|
|
||||||
type = 'Folder'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata?.type === 'folder') {
|
||||||
|
return () => <AssetIcon icon={<Folder />} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => <AssetIcon icon={<File />} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const getType = (metadata?: Metadata) => {
|
||||||
|
if (metadata?.isWebsite) return 'Website'
|
||||||
|
|
||||||
|
if (metadata?.type === 'folder') return 'Folder'
|
||||||
|
|
||||||
|
return metadata?.type
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
|
||||||
|
export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
|
||||||
|
const PreviewAssetComponent = useMemo(() => getPreviewComponent(previewUri, metadata), [metadata, previewUri])
|
||||||
|
const type = useMemo(() => getType(metadata), [metadata])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<Box bgcolor="background.paper">
|
<Box bgcolor="background.paper">
|
||||||
<Grid container direction="row">
|
<Grid container direction="row">
|
||||||
{previewUri ? (
|
<PreviewAssetComponent />
|
||||||
<FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
|
|
||||||
) : (
|
|
||||||
<AssetIcon icon={previewComponent} />
|
|
||||||
)}
|
|
||||||
<Box p={2}>
|
<Box p={2}>
|
||||||
{metadata?.hash && <Typography>Swarm Hash: {shortenHash(metadata.hash)}</Typography>}
|
{metadata?.hash && <Typography>Swarm Hash: {shortenHash(metadata.hash)}</Typography>}
|
||||||
{metadata?.name && metadata?.name !== metadata?.hash && (
|
{metadata?.name && metadata?.name !== metadata?.hash && (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Box } from '@material-ui/core'
|
|||||||
import { ReactElement, useContext, useEffect, useRef, useState } from 'react'
|
import { ReactElement, useContext, useEffect, useRef, useState } from 'react'
|
||||||
import { DocumentationText } from '../../components/DocumentationText'
|
import { DocumentationText } from '../../components/DocumentationText'
|
||||||
import { LinearProgressWithLabel } from '../../components/ProgressBar'
|
import { LinearProgressWithLabel } from '../../components/ProgressBar'
|
||||||
|
import { Tag } from '@ethersphere/bee-js'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
reference: string
|
reference: string
|
||||||
@@ -16,12 +17,20 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
|||||||
const [syncProgress, setSyncProgress] = useState<number>(0)
|
const [syncProgress, setSyncProgress] = useState<number>(0)
|
||||||
|
|
||||||
const syncCheck = async () => {
|
const syncCheck = async () => {
|
||||||
if (!beeApi) {
|
if (!beeApi) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = await beeApi.getAllTags()
|
let allTags: Tag[] = []
|
||||||
const tag = tags.find(t => t.address === reference)
|
let offset = 0
|
||||||
|
const limit = 1000
|
||||||
|
let tagsBatch
|
||||||
|
|
||||||
|
do {
|
||||||
|
tagsBatch = await beeApi.getAllTags({ limit, offset })
|
||||||
|
allTags = allTags.concat(tagsBatch)
|
||||||
|
offset += limit
|
||||||
|
} while (tagsBatch.length === limit) // Continue if the batch is full, stop if fewer than the limit
|
||||||
|
|
||||||
|
const tag = allTags.find(t => t.address === reference)
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const progress = ((tag.seen + tag.synced) / tag.split) * 100
|
const progress = ((tag.seen + tag.synced) / tag.split) * 100
|
||||||
@@ -51,8 +60,6 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
|||||||
There are instances when it seems that the content isn't synchronized, despite being already available.
|
There are instances when it seems that the content isn't synchronized, despite being already available.
|
||||||
To ensure it's not due to invalid synchronization data,
|
To ensure it's not due to invalid synchronization data,
|
||||||
verify availability from at least 70% using one of the stewardship endpoints.
|
verify availability from at least 70% using one of the stewardship endpoints.
|
||||||
|
|
||||||
TODO: is 70 a good number?
|
|
||||||
*/
|
*/
|
||||||
if (beeApi && !isRetrieveChecking && syncProgress > 10 && syncProgress < 100) {
|
if (beeApi && !isRetrieveChecking && syncProgress > 10 && syncProgress < 100) {
|
||||||
// It's a long running task make sure only one run occurs at a time.
|
// It's a long running task make sure only one run occurs at a time.
|
||||||
|
|||||||
+24
-27
@@ -7,7 +7,7 @@ import { useNavigate, useParams } from 'react-router-dom'
|
|||||||
import { HistoryHeader } from '../../components/HistoryHeader'
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
import { Loading } from '../../components/Loading'
|
import { Loading } from '../../components/Loading'
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
|
import { META_FILE_NAME } from '../../constants'
|
||||||
import { Context as BeeContext } from '../../providers/Bee'
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
import { Context as SettingsContext } from '../../providers/Settings'
|
||||||
import { ROUTES } from '../../routes'
|
import { ROUTES } from '../../routes'
|
||||||
@@ -50,38 +50,35 @@ export function Share(): ReactElement {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const entries = await manifestJs.getHashes(reference)
|
|
||||||
|
const entries = await manifestJs.getHashes(reference, { exclude: [META_FILE_NAME] })
|
||||||
|
setSwarmEntries(entries)
|
||||||
|
|
||||||
const indexDocument = await manifestJs.getIndexDocumentPath(reference)
|
const indexDocument = await manifestJs.getIndexDocumentPath(reference)
|
||||||
setIndexDocument(indexDocument)
|
setIndexDocument(indexDocument)
|
||||||
|
|
||||||
const previewFile = entries[PREVIEW_FILE_NAME]
|
|
||||||
|
|
||||||
delete entries[META_FILE_NAME]
|
|
||||||
delete entries[PREVIEW_FILE_NAME]
|
|
||||||
setSwarmEntries(entries)
|
|
||||||
|
|
||||||
const count = Object.keys(entries).length
|
|
||||||
|
|
||||||
let metadata: Metadata | undefined = {
|
|
||||||
hash,
|
|
||||||
size: 0,
|
|
||||||
type: count > 1 ? 'folder' : 'unknown',
|
|
||||||
name: reference,
|
|
||||||
isWebsite: Boolean(indexDocument) && count > 1,
|
|
||||||
count,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mtdt = await beeApi.downloadFile(reference, META_FILE_NAME)
|
const remoteMetadata = await beeApi.downloadFile(reference, META_FILE_NAME)
|
||||||
const remoteMetadata = mtdt.data.text()
|
const formattedMetadata = JSON.parse(remoteMetadata.data.text()) as Metadata
|
||||||
metadata = { ...metadata, ...(JSON.parse(remoteMetadata) as Metadata) }
|
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
|
||||||
|
|
||||||
if (previewFile) {
|
if (formattedMetadata.isVideo || formattedMetadata.isImage) {
|
||||||
setPreview(`${apiUrl}/bzz/${reference}/${PREVIEW_FILE_NAME}`)
|
setPreview(`${apiUrl}/bzz/${reference}`)
|
||||||
|
}
|
||||||
|
setMetadata({ ...formattedMetadata, hash })
|
||||||
|
} catch (e) {
|
||||||
|
// if metadata is not available or invalid go with the default one
|
||||||
|
const count = Object.keys(entries).length
|
||||||
|
setMetadata({
|
||||||
|
hash,
|
||||||
|
type: count > 1 ? 'folder' : 'unknown',
|
||||||
|
name: reference,
|
||||||
|
count,
|
||||||
|
isWebsite: Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)),
|
||||||
|
isVideo: Boolean(indexDocument && /.*\.(mp4|webm|ogg|mp3|ogg|wav)$/i.test(indexDocument)),
|
||||||
|
isImage: Boolean(indexDocument && /.*\.(jpg|jpeg|png|gif|webp|svg)$/i.test(indexDocument)),
|
||||||
|
// naive assumption based on indexDocument, we don't want to donwload the whole manifest
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setMetadata(metadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { DocumentationText } from '../../components/DocumentationText'
|
|||||||
import { HistoryHeader } from '../../components/HistoryHeader'
|
import { HistoryHeader } from '../../components/HistoryHeader'
|
||||||
import { ProgressIndicator } from '../../components/ProgressIndicator'
|
import { ProgressIndicator } from '../../components/ProgressIndicator'
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants'
|
import { META_FILE_NAME } from '../../constants'
|
||||||
import { Context as BeeContext, CheckState } from '../../providers/Bee'
|
import { Context as BeeContext, CheckState } from '../../providers/Bee'
|
||||||
import { Identity, Context as IdentityContext } from '../../providers/Feeds'
|
import { Identity, Context as IdentityContext } from '../../providers/Feeds'
|
||||||
import { Context as FileContext } from '../../providers/File'
|
import { Context as FileContext } from '../../providers/File'
|
||||||
@@ -33,7 +33,7 @@ export function Upload(): ReactElement {
|
|||||||
|
|
||||||
const { stamps, refresh } = useContext(StampsContext)
|
const { stamps, refresh } = useContext(StampsContext)
|
||||||
const { beeApi } = useContext(SettingsContext)
|
const { beeApi } = useContext(SettingsContext)
|
||||||
const { files, setFiles, uploadOrigin, metadata, previewUri, previewBlob } = useContext(FileContext)
|
const { files, setFiles, uploadOrigin, metadata, previewUri } = useContext(FileContext)
|
||||||
const { identities, setIdentities } = useContext(IdentityContext)
|
const { identities, setIdentities } = useContext(IdentityContext)
|
||||||
const { status } = useContext(BeeContext)
|
const { status } = useContext(BeeContext)
|
||||||
|
|
||||||
@@ -98,31 +98,15 @@ export function Upload(): ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastModified = files[0].lastModified
|
const lastModified = files[0].lastModified
|
||||||
|
|
||||||
// We want to store only some metadata
|
const metafile = new File([JSON.stringify(metadata)], META_FILE_NAME, {
|
||||||
const mtd: SwarmMetadata = {
|
|
||||||
name: metadata.name,
|
|
||||||
size: metadata.size,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type of the file only makes sense for a single file
|
|
||||||
if (files.length === 1) mtd.type = metadata.type
|
|
||||||
|
|
||||||
const metafile = new File([JSON.stringify(mtd)], META_FILE_NAME, {
|
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
lastModified,
|
lastModified,
|
||||||
})
|
})
|
||||||
fls.push(packageFile(metafile))
|
fls.push(packageFile(metafile))
|
||||||
|
|
||||||
if (previewBlob) {
|
|
||||||
const previewFile = new File([previewBlob], PREVIEW_FILE_NAME, {
|
|
||||||
type: 'image/jpeg',
|
|
||||||
lastModified,
|
|
||||||
})
|
|
||||||
fls.push(packageFile(previewFile))
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploading(true)
|
setUploading(true)
|
||||||
|
|
||||||
await waitUntilStampUsable(stamp.batchID, beeApi)
|
await waitUntilStampUsable(stamp.batchID, beeApi)
|
||||||
|
|||||||
+16
-6
@@ -41,7 +41,8 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
const [previewBlob, setPreviewBlob] = useState<Blob | undefined>(undefined)
|
const [previewBlob, setPreviewBlob] = useState<Blob | undefined>(undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMetadata(getMetadata(files))
|
const metadata = getMetadata(files)
|
||||||
|
setMetadata(metadata)
|
||||||
|
|
||||||
if (previewUri) {
|
if (previewUri) {
|
||||||
URL.revokeObjectURL(previewUri) // Clear the preview from memory
|
URL.revokeObjectURL(previewUri) // Clear the preview from memory
|
||||||
@@ -49,12 +50,21 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
setPreviewBlob(undefined)
|
setPreviewBlob(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.length !== 1 || !files[0].type.startsWith('image')) return
|
if (files.length !== 1) return
|
||||||
|
|
||||||
resize(files[0], PREVIEW_DIMENSIONS.maxWidth, PREVIEW_DIMENSIONS.maxHeight).then(blob => {
|
if (metadata.isVideo) {
|
||||||
setPreviewUri(URL.createObjectURL(blob)) // NOTE: Until it is cleared with URL.revokeObjectURL, the file stays allocated in memory
|
const videoFile = files[0]
|
||||||
setPreviewBlob(blob)
|
const videoBlob = new Blob([videoFile], { type: videoFile.type })
|
||||||
})
|
setPreviewUri(URL.createObjectURL(videoBlob))
|
||||||
|
setPreviewBlob(videoBlob)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.isImage) {
|
||||||
|
resize(files[0], PREVIEW_DIMENSIONS.maxWidth, PREVIEW_DIMENSIONS.maxHeight).then(blob => {
|
||||||
|
setPreviewUri(URL.createObjectURL(blob)) // NOTE: Until it is cleared with URL.revokeObjectURL, the file stays allocated in memory
|
||||||
|
setPreviewBlob(blob)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (previewUri) {
|
if (previewUri) {
|
||||||
|
|||||||
Vendored
+4
-2
@@ -6,14 +6,16 @@ interface LatestBeeRelease {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SwarmMetadata {
|
interface SwarmMetadata {
|
||||||
size: number
|
size?: number
|
||||||
name: string
|
name: string
|
||||||
type?: string
|
type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Metadata extends SwarmMetadata {
|
interface Metadata extends SwarmMetadata {
|
||||||
type: string
|
type: string
|
||||||
isWebsite: boolean
|
isWebsite?: boolean
|
||||||
|
isVideo?: boolean
|
||||||
|
isImage?: boolean
|
||||||
count?: number
|
count?: number
|
||||||
hash?: string
|
hash?: string
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -1,3 +1,6 @@
|
|||||||
|
import { isSupportedImageType } from './image'
|
||||||
|
import { isSupportedVideoType } from './video'
|
||||||
|
|
||||||
const indexHtmls = ['index.html', 'index.htm']
|
const indexHtmls = ['index.html', 'index.htm']
|
||||||
|
|
||||||
interface DetectedIndex {
|
interface DetectedIndex {
|
||||||
@@ -83,12 +86,14 @@ export function getAssetNameFromFiles(files: FilePath[]): string {
|
|||||||
|
|
||||||
export function getMetadata(files: FilePath[]): Metadata {
|
export function getMetadata(files: FilePath[]): Metadata {
|
||||||
const size = files.reduce((total, item) => total + item.size, 0)
|
const size = files.reduce((total, item) => total + item.size, 0)
|
||||||
const isWebsite = Boolean(detectIndexHtml(files))
|
|
||||||
const name = getAssetNameFromFiles(files)
|
const name = getAssetNameFromFiles(files)
|
||||||
const type = files.length === 1 ? files[0].type : 'folder'
|
const type = files.length === 1 ? files[0].type : 'folder'
|
||||||
const count = files.length
|
const count = files.length
|
||||||
|
const isWebsite = Boolean(detectIndexHtml(files))
|
||||||
|
const isVideo = isSupportedVideoType(type)
|
||||||
|
const isImage = isSupportedImageType(type)
|
||||||
|
|
||||||
return { size, name, type, isWebsite, count }
|
return { size, name, type, isWebsite, count, isVideo, isImage }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPath(file: FilePath): string {
|
export function getPath(file: FilePath): string {
|
||||||
|
|||||||
+23
-10
@@ -25,6 +25,28 @@ export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: nu
|
|||||||
return { width: imgWidth / ratio, height: imgHeight / ratio }
|
return { width: imgWidth / ratio, height: imgHeight / ratio }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllowedTypes(): string[] {
|
||||||
|
return [
|
||||||
|
'image/bmp',
|
||||||
|
'image/gif',
|
||||||
|
'image/vnd.microsoft.icon',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/tiff',
|
||||||
|
'image/webp',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the image type is supported
|
||||||
|
*
|
||||||
|
* @param type Image MIME type
|
||||||
|
*
|
||||||
|
* @returns True if the type is supported, false otherwise
|
||||||
|
*/
|
||||||
|
export const isSupportedImageType = (type: string): boolean => getAllowedTypes().includes(type)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize image passed to fit in the bounding box defined with maxWidth and maxHeight.
|
* Resize image passed to fit in the bounding box defined with maxWidth and maxHeight.
|
||||||
* Note that one or both of the bounding box dimensions may be omitted
|
* Note that one or both of the bounding box dimensions may be omitted
|
||||||
@@ -37,16 +59,7 @@ export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: nu
|
|||||||
*/
|
*/
|
||||||
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
|
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const allowedTypes = [
|
const allowedTypes = getAllowedTypes()
|
||||||
'image/bmp',
|
|
||||||
'image/gif',
|
|
||||||
'image/vnd.microsoft.icon',
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/svg+xml',
|
|
||||||
'image/tiff',
|
|
||||||
'image/webp',
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!file.size || !file.type || !allowedTypes.includes(file.type)) return reject('File not supported!')
|
if (!file.size || !file.type || !allowedTypes.includes(file.type)) return reject('File not supported!')
|
||||||
|
|
||||||
|
|||||||
@@ -50,14 +50,20 @@ export class ManifestJs {
|
|||||||
/**
|
/**
|
||||||
* Retrieves all paths with the associated hashes from a Swarm manifest
|
* Retrieves all paths with the associated hashes from a Swarm manifest
|
||||||
*/
|
*/
|
||||||
public async getHashes(hash: string): Promise<Record<string, string>> {
|
public async getHashes(hash: string, options?: { exclude: string[] }): Promise<Record<string, string>> {
|
||||||
const data = await this.bee.downloadData(hash)
|
const data = await this.bee.downloadData(hash)
|
||||||
const node = new MantarayNode()
|
const node = new MantarayNode()
|
||||||
node.deserialize(data)
|
node.deserialize(data)
|
||||||
await loadAllNodes(this.load.bind(this), node)
|
await loadAllNodes(this.load.bind(this), node)
|
||||||
const result = {}
|
const result: Record<string, string> = {}
|
||||||
this.extractHashes(result, node)
|
this.extractHashes(result, node)
|
||||||
|
|
||||||
|
if (options?.exclude) {
|
||||||
|
for (const path of options.exclude) {
|
||||||
|
delete result[path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function isSupportedVideoType(type: string) {
|
||||||
|
const video = document.createElement('video')
|
||||||
|
|
||||||
|
const result = video.canPlayType(type)
|
||||||
|
const isDefinitelySupported = result && result !== 'maybe'
|
||||||
|
|
||||||
|
return Boolean(isDefinitelySupported)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user