Skip to content

Commit cbd33dc

Browse files
LevilkTheRealLevente Kiss
andauthored
feat: vod display (#391)
* feat: add preview for all html5 media types + alignments for bee dashboard compatibility * fix: make the media check more strict --------- Co-authored-by: Levente Kiss <levente.kiss@solarpunk.bzz>
1 parent 1e5fe03 commit cbd33dc

11 files changed

Lines changed: 159 additions & 92 deletions

File tree

src/components/AssetPreview.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Box, Grid, Typography } from '@material-ui/core'
2-
import { ReactElement } from 'react'
2+
import { ReactElement, useMemo } from 'react'
33
import { File, Folder, Monitor } from 'react-feather'
44
import { AssetIcon } from './AssetIcon'
55
import { FitImage } from './FitImage'
@@ -8,6 +8,7 @@ import { shortenHash } from '../utils/hash'
88
import { PREVIEW_DIMENSIONS } from '../constants'
99

1010
import text from '../translations'
11+
import { FitVideo } from './FitVideo'
1112

1213
interface Props {
1314
assetName?: string
@@ -17,24 +18,45 @@ interface Props {
1718

1819
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
1920

20-
export function AssetPreview({ previewUri, metadata }: Props): ReactElement {
21-
let previewComponent = <File />
22-
let type = mimeToKind(metadata?.type)
21+
/* eslint-disable react/display-name */
22+
const getPreviewComponent = (previewUri?: string, metadata?: Metadata) => {
23+
if (metadata?.isVideo) {
24+
return () => <FitVideo src={previewUri} />
25+
}
26+
27+
if (metadata?.isImage) {
28+
return () => <FitImage alt="Upload Preview" src={previewUri} />
29+
}
2330

2431
if (metadata?.isWebsite) {
25-
previewComponent = <Monitor />
26-
type = text.uploadFile.headerWebsite
27-
} else if (metadata?.type === 'folder') {
28-
previewComponent = <Folder />
29-
type = text.uploadFile.headerFolder
32+
return () => <AssetIcon icon={<Monitor />} />
3033
}
3134

35+
if (metadata?.type === 'folder') {
36+
return () => <AssetIcon icon={<Folder />} />
37+
}
38+
39+
return () => <AssetIcon icon={<File />} />
40+
}
41+
42+
const getType = (metadata?: Metadata) => {
43+
if (metadata?.isWebsite) return text.uploadFile.headerWebsite
44+
45+
if (metadata?.type === 'folder') return text.uploadFile.headerFolder
46+
47+
return mimeToKind(metadata?.type)
48+
}
49+
50+
export function AssetPreview({ previewUri, metadata }: Props): ReactElement {
51+
const PreviewAssetComponent = useMemo(() => getPreviewComponent(previewUri, metadata), [metadata, previewUri])
52+
const type = useMemo(() => getType(metadata), [metadata])
53+
3254
return (
3355
<Box mb={0.25}>
3456
<Box bgcolor="background.paper">
3557
<Grid container direction="row">
3658
<div style={{ width: PREVIEW_DIMENSIONS.maxWidth, height: PREVIEW_DIMENSIONS.maxHeight }}>
37-
{previewUri ? <FitImage alt="Upload Preview" src={previewUri} /> : <AssetIcon icon={previewComponent} />}
59+
<PreviewAssetComponent />
3860
</div>
3961
<Box p={2} textAlign="left">
4062
{metadata?.hash && <Typography>Swarm Hash: {shortenHash(metadata.hash)}</Typography>}

src/components/FitVideo.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createStyles, makeStyles } from '@material-ui/core'
2+
import { ReactElement } from 'react'
3+
4+
const useStyles = makeStyles(() =>
5+
createStyles({
6+
video: {
7+
width: '100%',
8+
height: '100%',
9+
objectFit: 'scale-down',
10+
},
11+
}),
12+
)
13+
14+
interface VideoProps {
15+
src: string | undefined
16+
}
17+
18+
export function FitVideo(props: VideoProps): ReactElement {
19+
const classes = useStyles()
20+
21+
return <video className={classes.video} src={props.src} controls />
22+
}

src/constants.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ export const POSTAGE_STAMP =
55
(process.env.REACT_APP_POSTAGE_STAMP as Reference | undefined) ||
66
('0000000000000000000000000000000000000000000000000000000000000000' as Reference)
77

8-
export const META_FILE_NAME = '.swarmgatewaymeta.json'
9-
export const PREVIEW_FILE_NAME = '.swarmgatewaypreview.jpeg'
8+
export const META_FILE_NAME = 'metadata'
109
export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 }
1110

1211
const url = window.location.origin

src/pages/Share/Upload.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const SharePage = ({
5656

5757
if (metadata?.isWebsite) header = text.uploadFile.headerWebsite
5858

59-
const reachedSizeLimit = Boolean(metadata && metadata?.size > UPLOAD_SIZE_LIMIT)
59+
const reachedSizeLimit = Boolean(metadata?.size && metadata.size > UPLOAD_SIZE_LIMIT)
6060

6161
return (
6262
<Layout

src/pages/Share/index.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,43 @@ export default function ShareGeneral(): ReactElement {
1414
const [uploadReference, setUploadReference] = useState('')
1515
const [isUploadingFile, setIsUploadingFile] = useState<boolean>(false)
1616
const [uploadError, setUploadError] = useState<boolean>(false)
17-
const [preview, setPreview] = useState<string | undefined>(undefined)
17+
const [previewUri, setPreviewUri] = useState<string | undefined>(undefined)
1818
const [previewBlob, setPreviewBlob] = useState<Blob | undefined>(undefined)
1919
const [metadata, setMetadata] = useState<Metadata | undefined>()
2020
const { upload } = useContext(Context)
2121

2222
useEffect(() => {
23-
setMetadata(getMetadata(files))
23+
const metadata = getMetadata(files)
24+
setMetadata(metadata)
2425

25-
if (preview) {
26-
URL.revokeObjectURL(preview) // Clear the preview from memory
27-
setPreview(undefined)
26+
if (previewUri) {
27+
URL.revokeObjectURL(previewUri) // Clear the preview from memory
28+
setPreviewUri(undefined)
2829
setPreviewBlob(undefined)
2930
}
3031

31-
if (files.length !== 1 || !files[0].type.startsWith('image')) return
32+
if (files.length !== 1) return
3233

33-
resize(files[0], PREVIEW_DIMENSIONS.maxWidth, PREVIEW_DIMENSIONS.maxHeight).then(blob => {
34-
setPreview(URL.createObjectURL(blob)) // NOTE: Until it is cleared with URL.revokeObjectURL, the file stays allocated in memory
35-
setPreviewBlob(blob)
36-
})
34+
if (metadata.isVideo) {
35+
const videoFile = files[0]
36+
const videoBlob = new Blob([videoFile], { type: videoFile.type })
37+
setPreviewUri(URL.createObjectURL(videoBlob))
38+
setPreviewBlob(videoBlob)
39+
}
40+
41+
if (metadata.isImage) {
42+
resize(files[0], PREVIEW_DIMENSIONS.maxWidth, PREVIEW_DIMENSIONS.maxHeight).then(blob => {
43+
setPreviewUri(URL.createObjectURL(blob)) // NOTE: Until it is cleared with URL.revokeObjectURL, the file stays allocated in memory
44+
setPreviewBlob(blob)
45+
})
46+
}
3747

3848
return () => {
39-
if (preview) {
40-
URL.revokeObjectURL(preview)
49+
if (previewUri) {
50+
URL.revokeObjectURL(previewUri)
4151
}
4252
}
43-
}, [files]) //eslint-disable-line react-hooks/exhaustive-deps
53+
}, [files]) // eslint-disable-line react-hooks/exhaustive-deps
4454

4555
const uploadFile = () => {
4656
if (files.length === 0 || !metadata) return
@@ -67,7 +77,7 @@ export default function ShareGeneral(): ReactElement {
6777
uploadError={uploadError}
6878
setFiles={setFiles}
6979
metadata={metadata}
70-
preview={preview}
80+
preview={previewUri}
7181
files={files}
7282
uploadFile={uploadFile}
7383
isUploadingFile={isUploadingFile}

src/providers/bee.tsx

Lines changed: 30 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { saveAs } from 'file-saver'
33
import JSZip from 'jszip'
44
import { createContext, ReactChild, ReactElement } from 'react'
55

6-
import { BEE_HOSTS, META_FILE_NAME, POSTAGE_STAMP, PREVIEW_FILE_NAME } from '../constants'
6+
import { BEE_HOSTS, META_FILE_NAME, POSTAGE_STAMP } from '../constants'
77
import { detectIndexHtml } from '../utils/file'
88
import { ManifestJs } from '../utils/manifest'
99
import { packageFile } from '../utils/SwarmFile'
@@ -45,37 +45,21 @@ function hashToIndex(hash: Reference | string) {
4545
}
4646

4747
export function Provider({ children }: Props): ReactElement {
48-
const upload = async (files: SwarmFile[], metadata: Metadata, preview?: Blob) => {
48+
const upload = async (files: SwarmFile[], metadata: Metadata) => {
4949
const fls = files.map(packageFile) // Apart from packaging, this is needed to not modify the original files array as it can trigger effects
5050
let indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(files) || undefined
5151

5252
// TODO: Remove once this is fixed in bee-js https://github.com/ethersphere/bee-js/issues/531
5353
if (indexDocument) indexDocument = unescape(encodeURIComponent(indexDocument))
54-
const lastModified = files[0].lastModified
55-
56-
// We want to store only some metadata
57-
const mtd: SwarmMetadata = {
58-
name: metadata.name,
59-
size: metadata.size,
60-
}
61-
62-
// Type of the file only makes sense for a single file
63-
if (files.length === 1) mtd.type = metadata.type
6454

55+
const lastModified = files[0].lastModified
6556
fls.push(
66-
new File([JSON.stringify(mtd)], META_FILE_NAME, {
57+
new File([JSON.stringify(metadata)], META_FILE_NAME, {
6758
type: 'application/json',
6859
lastModified,
6960
}),
7061
)
7162

72-
if (preview) {
73-
const previewFile = new File([preview], PREVIEW_FILE_NAME, {
74-
lastModified,
75-
})
76-
fls.push(previewFile)
77-
}
78-
7963
const { reference } = await randomBee.uploadFiles(POSTAGE_STAMP, fls, { indexDocument })
8064
const hashIndex = hashToIndex(reference)
8165

@@ -110,51 +94,48 @@ export function Provider({ children }: Props): ReactElement {
11094
preview?: string
11195
entries: Record<string, string>
11296
}> => {
113-
let metadata: Metadata | undefined = { size: 0, type: 'unknown', name: hash, isWebsite: false }
97+
let metadata: Metadata | undefined
11498
let entries: Record<string, string> = {}
115-
let preview
99+
let indexDocument: string | null = null
100+
let preview: string | undefined
116101

117102
const hashIndex = hashToIndex(hash)
118103
const bee = new Bee(BEE_HOSTS[hashIndex])
119104

120-
try {
121-
const mtdt = await bee.downloadFile(hash, META_FILE_NAME)
122-
metadata = { ...metadata, ...(JSON.parse(mtdt.data.text()) as Metadata) }
123-
} catch (e) {} // eslint-disable-line no-empty
124-
125105
try {
126106
const manifestJs = new ManifestJs(bee)
127107
const isManifest = await manifestJs.isManifest(hash)
128108

129109
if (!isManifest) throw Error('The specified hash does not contain valid content.')
130-
entries = await manifestJs.getHashes(hash)
131-
132-
const indexDocument = await manifestJs.getIndexDocumentPath(hash)
133-
134-
metadata.isWebsite = Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)) // only html documents can be websites
135110

136-
preview = getPreview(entries, hash)
137-
138-
// Erase the files added by the gateway
139-
delete entries[META_FILE_NAME]
140-
delete entries[PREVIEW_FILE_NAME]
141-
142-
metadata.count = Object.keys(entries).length
143-
144-
if (metadata.count > 1) metadata.type = 'folder'
111+
entries = await manifestJs.getHashes(hash, { exclude: [META_FILE_NAME] })
112+
indexDocument = await manifestJs.getIndexDocumentPath(hash)
145113
} catch (e) {} // eslint-disable-line no-empty
146114

147-
metadata.hash = hash
148-
149-
return { metadata, preview, entries }
150-
}
115+
try {
116+
const remoteMetadata = await bee.downloadFile(hash, META_FILE_NAME)
117+
const formattedMetadata = JSON.parse(remoteMetadata.data.text()) as Metadata
151118

152-
const getPreview = (entries: Record<string, string>, hash: Reference | string): string | undefined => {
153-
if (!entries[PREVIEW_FILE_NAME]) return
119+
if (formattedMetadata.isVideo || formattedMetadata.isImage) {
120+
preview = `${BEE_HOSTS[hashIndex]}/bzz/${hash}`
121+
}
154122

155-
const hashIndex = hashToIndex(hash)
123+
metadata = { ...formattedMetadata, hash }
124+
} catch (e) {
125+
const count = Object.keys(entries).length
126+
metadata = {
127+
hash,
128+
type: count > 1 ? 'folder' : 'unknown',
129+
name: hash,
130+
count,
131+
isWebsite: Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)),
132+
isVideo: Boolean(indexDocument && /.*\.(mp4|webm|ogg|mp3|ogg|wav)$/i.test(indexDocument)),
133+
isImage: Boolean(indexDocument && /.*\.(jpg|jpeg|png|gif|webp|svg)$/i.test(indexDocument)),
134+
// naive assumption based on indexDocument, we don't want to donwload the whole manifest
135+
}
136+
}
156137

157-
return `${BEE_HOSTS[hashIndex]}/bzz/${hash}/${PREVIEW_FILE_NAME}`
138+
return { metadata, preview, entries }
158139
}
159140

160141
const getChunk = (hash: Reference | string): Promise<Data> => {

src/react-app-env.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
/// <reference types="react-scripts" />
22

33
interface SwarmMetadata {
4-
size: number
4+
size?: number
55
name: string
66
type?: string
77
}
88

99
interface Metadata extends SwarmMetadata {
1010
type: string
11-
isWebsite: boolean
1211
count?: number
1312
hash?: string
13+
isWebsite?: boolean
14+
isVideo?: boolean
15+
isImage?: boolean
1416
}
1517

1618
type FilePath = File & { path?: string; fullPath?: string }

src/utils/file.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DragEvent } from 'react'
22
import { convertSwarmFile } from './SwarmFile'
3+
import { isSupportedVideoType } from './video'
4+
import { isSupportedImageType } from './image'
35

46
const indexHtmls = ['index.html', 'index.htm']
57

@@ -31,12 +33,14 @@ export function detectIndexHtml(files: SwarmFile[]): string | false {
3133

3234
export function getMetadata(files: SwarmFile[]): Metadata {
3335
const size = files.reduce((total, item) => total + item.size, 0)
34-
const isWebsite = Boolean(detectIndexHtml(files))
3536
const name = getAssetNameFromFiles(files)
3637
const type = files.length === 1 ? files[0].type : 'folder'
3738
const count = files.length
39+
const isWebsite = Boolean(detectIndexHtml(files))
40+
const isVideo = isSupportedVideoType(type)
41+
const isImage = isSupportedImageType(type)
3842

39-
return { size, name, type, isWebsite, count }
43+
return { size, name, type, isWebsite, count, isVideo, isImage }
4044
}
4145

4246
export function getAssetNameFromFiles(files: SwarmFile[]): string {

src/utils/image.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@ export function getDimensions(imgWidth: number, imgHeight: number, maxWidth?: nu
2525
return { width: imgWidth / ratio, height: imgHeight / ratio }
2626
}
2727

28+
function getAllowedTypes(): string[] {
29+
return [
30+
'image/bmp',
31+
'image/gif',
32+
'image/vnd.microsoft.icon',
33+
'image/jpeg',
34+
'image/png',
35+
'image/svg+xml',
36+
'image/tiff',
37+
'image/webp',
38+
]
39+
}
40+
41+
/**
42+
* Check if the image type is supported
43+
*
44+
* @param type Image MIME type
45+
*
46+
* @returns True if the type is supported, false otherwise
47+
*/
48+
export const isSupportedImageType = (type: string): boolean => getAllowedTypes().includes(type)
49+
2850
/**
2951
* Resize image passed to fit in the bounding box defined with maxWidth and maxHeight.
3052
* 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
3759
*/
3860
export function resize(file: File, maxWidth?: number, maxHeight?: number): Promise<Blob> {
3961
return new Promise((resolve, reject) => {
40-
const allowedTypes = [
41-
'image/bmp',
42-
'image/gif',
43-
'image/vnd.microsoft.icon',
44-
'image/jpeg',
45-
'image/png',
46-
'image/svg+xml',
47-
'image/tiff',
48-
'image/webp',
49-
]
62+
const allowedTypes = getAllowedTypes()
5063

5164
if (!file.size || !file.type || !allowedTypes.includes(file.type)) return reject('File not supported!')
5265

0 commit comments

Comments
 (0)