Skip to content

Commit 84969a5

Browse files
alikhereSgtPooki
andauthored
feat: add close button to file viewer for improved navigation (#2401)
* feat: add close button to file preview * fix: close button uses parentPath * chore: fix type error * chore: proper null check --------- Co-authored-by: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com>
1 parent b33775a commit 84969a5

4 files changed

Lines changed: 120 additions & 53 deletions

File tree

src/bundles/files/actions.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { IGNORED_FILES, ACTIONS } from './consts.js'
2626
* @property {CID} cid
2727
* @property {string} name
2828
* @property {string} path
29+
* @property {string} parentPath
2930
* @property {boolean} pinned
3031
* @property {boolean|void} isParent
3132
*
@@ -41,15 +42,22 @@ import { IGNORED_FILES, ACTIONS } from './consts.js'
4142
* @param {string} [prefix]
4243
* @returns {FileStat}
4344
*/
44-
const fileFromStats = ({ cumulativeSize, type, size, cid, name, path, pinned, isParent }, prefix = '/ipfs') => ({
45-
size: cumulativeSize || size || 0,
46-
type: type === 'dir' ? 'directory' : type,
47-
cid,
48-
name: name || path.split('/').pop() || cid.toString(),
49-
path: path || `${prefix}/${cid.toString()}`,
50-
pinned: Boolean(pinned),
51-
isParent
52-
})
45+
const fileFromStats = ({ cumulativeSize, type, size, cid, name, path, pinned, isParent }, prefix = '/ipfs') => {
46+
const pathParts = path.split('/')
47+
const pathFileName = pathParts.pop()
48+
const parentPath = pathParts.join('/') || '/'
49+
const file = {
50+
size: cumulativeSize || size || 0,
51+
type: type === 'dir' ? 'directory' : type,
52+
cid,
53+
name: name || pathFileName || cid.toString(),
54+
path: path || `${prefix}/${cid.toString()}`,
55+
parentPath,
56+
pinned: Boolean(pinned),
57+
isParent
58+
}
59+
return file
60+
}
5361

5462
/**
5563
* @param {IPFSService} ipfs
@@ -717,6 +725,7 @@ const importFiles = (ipfs, files) => {
717725
* @param {string} options.path
718726
* @param {boolean} [options.isRoot]
719727
* @param {import('./utils').Sorting} options.sorting
728+
* @returns {Promise<import('./protocol').DirectoryContent>}
720729
*/
721730
const dirStats = async (ipfs, cid, { path, isRoot, sorting }) => {
722731
const entries = await all(ipfs.ls(cid)) || []
@@ -746,6 +755,7 @@ const dirStats = async (ipfs, cid, { path, isRoot, sorting }) => {
746755
}
747756

748757
let parent = null
758+
let parentPathForDir = '/'
749759

750760
if (!isRoot) {
751761
const parentPath = dirname(path)
@@ -764,11 +774,13 @@ const dirStats = async (ipfs, cid, { path, isRoot, sorting }) => {
764774
name: '..',
765775
isParent: true
766776
})
777+
parentPathForDir = parentInfo.path
767778
}
768779
}
769780

770781
return {
771782
path,
783+
parentPath: parentPathForDir,
772784
fetched: Date.now(),
773785
type: 'directory',
774786
cid,

src/bundles/files/protocol.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type UnknownContent = {
2525
type: 'unknown',
2626
fetched: Time,
2727
path: string,
28+
parentPath: string,
2829
cid: CID,
2930
size: 0
3031
}
@@ -33,6 +34,7 @@ type FileContent = {
3334
type: 'file',
3435
fetched: Time,
3536
path: string,
37+
parentPath: string,
3638
cid: CID,
3739
size: number,
3840

@@ -44,10 +46,11 @@ export type DirectoryContent = {
4446
type: 'directory',
4547
fetched: Time,
4648
path: string,
49+
parentPath: string,
4750
cid: CID,
4851

4952
content: FileStat[]
50-
upper: void | FileStat,
53+
upper: FileStat | null,
5154
}
5255

5356
export type PageContent =

src/files/FilesPage.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo, useRef, useState } from 'react'
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { findDOMNode } from 'react-dom'
33
import { Helmet } from 'react-helmet'
44
import { connect } from 'redux-bundler-react'
@@ -101,6 +101,13 @@ const FilesPage = ({
101101
doFilesBulkCidImport(raw, root)
102102
}
103103

104+
const onClosePreview = useCallback(() => {
105+
// if the parentPath is / or null then we are at the root of the files page, (preview close button shouldn't even be visible in this case)
106+
if (files?.parentPath == null || files?.parentPath === '/') return
107+
108+
doUpdateHash(files?.parentPath)
109+
}, [files?.parentPath, doUpdateHash])
110+
104111
const onAddByPath = (path, name) => doFilesAddPath(files.path, path, name)
105112
/**
106113
*
@@ -357,7 +364,7 @@ const FilesPage = ({
357364

358365
<MainView t={t} files={files} remotePins={remotePins} pendingPins={pendingPins} failedPins={failedPins} doExploreUserProvidedPath={doExploreUserProvidedPath}/>
359366

360-
<Preview files={files} onDownload={() => onDownload([files])} />
367+
<Preview files={files} onDownload={() => onDownload([files])} onClose={onClosePreview} />
361368

362369
<InfoBoxes isRoot={filesPathInfo.isMfs && filesPathInfo.isRoot}
363370
isCompanion={false}
@@ -393,9 +400,9 @@ const FilesPage = ({
393400
)
394401
}
395402

396-
const Preview = ({ files, onDownload }) => {
403+
const Preview = ({ files, onDownload, onClose }) => {
397404
if (files && files.type === 'file') {
398-
return (<FilePreview {...files} onDownload={onDownload} />)
405+
return (<FilePreview {...files} onDownload={onDownload} onClose={onClose} />)
399406
}
400407
return (<div/>)
401408
}

src/files/file-preview/FilePreview.js

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CID } from 'multiformats/cid'
1010
import { useDrag } from 'react-dnd'
1111
import { toString as fromUint8ArrayToString } from 'uint8arrays'
1212
import Button from '../../components/button/button.tsx'
13+
import GlyphCancel from '../../icons/GlyphCancel.js'
1314

1415
const maxPlainTextPreview = 1024 * 10 // only preview small part of huge files
1516

@@ -24,7 +25,7 @@ const Drag = ({ name, size, cid, path, children }) => {
2425
}
2526

2627
const Preview = (props) => {
27-
const { t, name, cid, size, availableGatewayUrl, publicGateway, read, onDownload } = props
28+
const { t, name, cid, size, availableGatewayUrl, publicGateway, read, onDownload, onClose } = props
2829
const [content, setContent] = useState(null)
2930
const [hasMoreContent, setHasMoreContent] = useState(false)
3031
const [buffer, setBuffer] = useState(null)
@@ -60,39 +61,64 @@ const Preview = (props) => {
6061
const src = `${availableGatewayUrl}/ipfs/${cid}?filename=${encodeURIComponent(name)}`
6162
const className = 'mw-100 mt3 bg-snow-muted pa2 br2 border-box'
6263

64+
// Close button header
65+
const closeButtonHeader = onClose != null && (
66+
<div className="flex items-center justify-between mb3 pb2 bb b--light-gray">
67+
<div className="flex items-center">
68+
<h2 className="ma0 f4 charcoal truncate">{name}</h2>
69+
</div>
70+
<GlyphCancel
71+
onClick={onClose}
72+
style={{ width: '44px', height: '44px', fill: '#244c5a', cursor: 'pointer' }}
73+
/>
74+
</div>
75+
)
76+
6377
switch (type) {
6478
case 'audio':
6579
return (
66-
<Drag {...props}>
67-
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
68-
<audio width='100%' controls>
69-
<source src={safeSubresourceGwUrl(src)} />
70-
</audio>
71-
</Drag>
80+
<div>
81+
{closeButtonHeader}
82+
<Drag {...props}>
83+
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
84+
<audio width='100%' controls>
85+
<source src={safeSubresourceGwUrl(src)} />
86+
</audio>
87+
</Drag>
88+
</div>
7289
)
7390
case 'pdf':
7491
return (
75-
<Drag {...props}>
76-
<object className="FilePreviewPDF w-100" data={safeSubresourceGwUrl(src)} type='application/pdf'>
77-
{t('noPDFSupport')}
78-
<a href={src} download target='_blank' rel='noopener noreferrer' className='underline-hover navy-muted'>{t('downloadPDF')}</a>
79-
</object>
80-
</Drag>
92+
<div>
93+
{closeButtonHeader}
94+
<Drag {...props}>
95+
<object className="FilePreviewPDF w-100" data={safeSubresourceGwUrl(src)} type='application/pdf'>
96+
{t('noPDFSupport')}
97+
<a href={src} download target='_blank' rel='noopener noreferrer' className='underline-hover navy-muted'>{t('downloadPDF')}</a>
98+
</object>
99+
</Drag>
100+
</div>
81101
)
82102
case 'video':
83103
return (
84-
<Drag {...props}>
85-
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
86-
<video controls className={className}>
87-
<source src={safeSubresourceGwUrl(src)} />
88-
</video>
89-
</Drag>
104+
<div>
105+
{closeButtonHeader}
106+
<Drag {...props}>
107+
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
108+
<video controls className={className}>
109+
<source src={safeSubresourceGwUrl(src)} />
110+
</video>
111+
</Drag>
112+
</div>
90113
)
91114
case 'image':
92115
return (
93-
<Drag {...props}>
94-
<img className={className} alt={name} src={safeSubresourceGwUrl(src)} />
95-
</Drag>
116+
<div>
117+
{closeButtonHeader}
118+
<Drag {...props}>
119+
<img className={className} alt={name} src={safeSubresourceGwUrl(src)} />
120+
</Drag>
121+
</div>
96122
)
97123
default: {
98124
const srcPublic = `${publicGateway}/ipfs/${cid}?filename=${encodeURIComponent(name)}`
@@ -114,31 +140,49 @@ const Preview = (props) => {
114140
)
115141

116142
if (content === null) {
117-
return <ComponentLoader />
143+
return (
144+
<div>
145+
{closeButtonHeader}
146+
<ComponentLoader />
147+
</div>
148+
)
118149
}
119150

120151
// a precaution to not render too much, in case we overread
121152
if (content.length > maxPlainTextPreview) {
122-
return cantPreview
153+
return (
154+
<div>
155+
{closeButtonHeader}
156+
{cantPreview}
157+
</div>
158+
)
123159
}
124160

125161
if (isBinary(name, content)) {
126-
return cantPreview
162+
return (
163+
<div>
164+
{closeButtonHeader}
165+
{cantPreview}
166+
</div>
167+
)
127168
}
128169

129-
return <>
130-
<pre className={`${className} overflow-auto monospace`}>
131-
{content}
132-
</pre>
133-
{ hasMoreContent && <div className="w-100 flex items-center justify-center">
134-
<p><Trans i18nKey='previewLimitReached' t={t}>This preview is limited to 10 KiB. Click the download button to access the full file.</Trans></p>
135-
<p>
136-
<Button className="mh2 lh-copy bn justify-center flex " onClick={ onDownload }>
137-
{ t('app:actions.download')}
138-
</Button>
139-
</p>
140-
</div>}
141-
</>
170+
return (
171+
<div>
172+
{closeButtonHeader}
173+
<pre className={`${className} overflow-auto monospace`}>
174+
{content}
175+
</pre>
176+
{ hasMoreContent && <div className="w-100 flex items-center justify-center">
177+
<p><Trans i18nKey='previewLimitReached' t={t}>This preview is limited to 10 KiB. Click the download button to access the full file.</Trans></p>
178+
<p>
179+
<Button className="mh2 lh-copy bn justify-center flex " onClick={ onDownload }>
180+
{ t('app:actions.download')}
181+
</Button>
182+
</p>
183+
</div>}
184+
</div>
185+
)
142186
}
143187
}
144188
}
@@ -151,7 +195,8 @@ Preview.propTypes = {
151195
read: PropTypes.func.isRequired,
152196
content: PropTypes.object,
153197
t: PropTypes.func.isRequired,
154-
tReady: PropTypes.bool.isRequired
198+
tReady: PropTypes.bool.isRequired,
199+
onClose: PropTypes.func
155200
}
156201

157202
export default connect(

0 commit comments

Comments
 (0)