refactor: repo (#1070)

This commit is contained in:
Mo
2022-06-07 07:18:41 -05:00
committed by GitHub
parent 4c65784421
commit f4ef63693c
1102 changed files with 5786 additions and 3366 deletions

View File

@@ -0,0 +1,18 @@
import { FileItem } from '@standardnotes/snjs'
import { MutableRefObject } from 'react'
export const createObjectURLWithRef = (
type: FileItem['mimeType'],
bytes: Uint8Array,
ref: MutableRefObject<string | undefined>,
) => {
const objectURL = URL.createObjectURL(
new Blob([bytes], {
type,
}),
)
ref.current = objectURL
return objectURL
}

View File

@@ -0,0 +1,81 @@
import { WebApplication } from '@/Application/Application'
import { concatenateUint8Arrays } from '@/Utils'
import { FileItem } from '@standardnotes/snjs'
import { useEffect, useMemo, useState } from 'react'
import FilePreviewError from './FilePreviewError'
import { isFileTypePreviewable } from './isFilePreviewable'
import PreviewComponent from './PreviewComponent'
type Props = {
application: WebApplication
file: FileItem
}
const FilePreview = ({ file, application }: Props) => {
const isFilePreviewable = useMemo(() => {
return isFileTypePreviewable(file.mimeType)
}, [file.mimeType])
const [isDownloading, setIsDownloading] = useState(true)
const [downloadProgress, setDownloadProgress] = useState(0)
const [downloadedBytes, setDownloadedBytes] = useState<Uint8Array>()
useEffect(() => {
if (!isFilePreviewable) {
setIsDownloading(false)
setDownloadProgress(0)
setDownloadedBytes(undefined)
return
}
const downloadFileForPreview = async () => {
if (downloadedBytes) {
return
}
setIsDownloading(true)
try {
const chunks: Uint8Array[] = []
setDownloadProgress(0)
await application.files.downloadFile(file, async (decryptedChunk, progress) => {
chunks.push(decryptedChunk)
if (progress) {
setDownloadProgress(Math.round(progress.percentComplete))
}
})
const finalDecryptedBytes = concatenateUint8Arrays(chunks)
setDownloadedBytes(finalDecryptedBytes)
} catch (error) {
console.error(error)
} finally {
setIsDownloading(false)
}
}
void downloadFileForPreview()
}, [application.files, downloadedBytes, file, isFilePreviewable])
return isDownloading ? (
<div className="flex flex-col justify-center items-center flex-grow">
<div className="flex items-center">
<div className="sk-spinner w-5 h-5 spinner-info mr-3"></div>
<div className="text-base font-semibold">{downloadProgress}%</div>
</div>
<span className="mt-3">Loading file...</span>
</div>
) : downloadedBytes ? (
<PreviewComponent file={file} bytes={downloadedBytes} />
) : (
<FilePreviewError
file={file}
filesController={application.getViewControllerManager().filesController}
tryAgainCallback={() => {
setDownloadedBytes(undefined)
}}
isFilePreviewable={isFilePreviewable}
/>
)
}
export default FilePreview

View File

@@ -0,0 +1,62 @@
import { FilesController } from '@/Controllers/FilesController'
import { NoPreviewIllustration } from '@standardnotes/icons'
import { FileItem } from '@standardnotes/snjs'
import Button from '../Button/Button'
type Props = {
file: FileItem
filesController: FilesController
isFilePreviewable: boolean
tryAgainCallback: () => void
}
const FilePreviewError = ({ file, filesController, isFilePreviewable, tryAgainCallback }: Props) => {
return (
<div className="flex flex-col justify-center items-center flex-grow">
<NoPreviewIllustration className="w-30 h-30 mb-4" />
<div className="font-bold text-base mb-2">This file can't be previewed.</div>
{isFilePreviewable ? (
<>
<div className="text-sm text-center color-passive-0 mb-4 max-w-35ch">
There was an error loading the file. Try again, or download the file and open it using another application.
</div>
<div className="flex items-center">
<Button
variant="primary"
className="mr-3"
onClick={() => {
tryAgainCallback()
}}
>
Try again
</Button>
<Button
variant="normal"
onClick={() => {
filesController.downloadFile(file).catch(console.error)
}}
>
Download
</Button>
</div>
</>
) : (
<>
<div className="text-sm text-center color-passive-0 mb-4 max-w-35ch">
To view this file, download it and open it using another application.
</div>
<Button
variant="primary"
onClick={() => {
filesController.downloadFile(file).catch(console.error)
}}
>
Download
</Button>
</>
)}
</div>
)
}
export default FilePreviewError

View File

@@ -0,0 +1,39 @@
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import { FunctionComponent } from 'react'
import Icon from '@/Components/Icon/Icon'
type Props = {
file: FileItem
}
const FilePreviewInfoPanel: FunctionComponent<Props> = ({ file }) => {
return (
<div className="flex flex-col min-w-70 p-4 border-0 border-l-1px border-solid border-main">
<div className="flex items-center mb-4">
<Icon type="info" className="mr-2" />
<div className="font-semibold">File information</div>
</div>
<div className="mb-3">
<span className="font-semibold">Type:</span> {file.mimeType}
</div>
<div className="mb-3">
<span className="font-semibold">Decrypted Size:</span> {formatSizeToReadableString(file.decryptedSize)}
</div>
<div className="mb-3">
<span className="font-semibold">Encrypted Size:</span> {formatSizeToReadableString(file.encryptedSize)}
</div>
<div className="mb-3">
<span className="font-semibold">Created:</span> {file.created_at.toLocaleString()}
</div>
<div className="mb-3">
<span className="font-semibold">Last Modified:</span> {file.userModifiedDate.toLocaleString()}
</div>
<div>
<span className="font-semibold">File ID:</span> {file.uuid}
</div>
</div>
)
}
export default FilePreviewInfoPanel

View File

@@ -0,0 +1,134 @@
import { WebApplication } from '@/Application/Application'
import { DialogContent, DialogOverlay } from '@reach/dialog'
import { FunctionComponent, KeyboardEventHandler, useCallback, useMemo, useRef, useState } from 'react'
import { getFileIconComponent } from '@/Components/AttachedFilesPopover/getFileIconComponent'
import Icon from '@/Components/Icon/Icon'
import FilePreviewInfoPanel from './FilePreviewInfoPanel'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { KeyboardKey } from '@/Services/IOService'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import FilePreview from './FilePreview'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const FilePreviewModal: FunctionComponent<Props> = observer(({ application, viewControllerManager }) => {
const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController
if (!currentFile) {
return null
}
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
const closeButtonRef = useRef<HTMLButtonElement>(null)
const keyDownHandler: KeyboardEventHandler = useCallback(
(event) => {
const hasNotPressedLeftOrRightKeys = event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right
if (hasNotPressedLeftOrRightKeys) {
return
}
event.preventDefault()
const currentFileIndex = otherFiles.findIndex((fileFromArray) => fileFromArray.uuid === currentFile.uuid)
switch (event.key) {
case KeyboardKey.Left: {
const previousFileIndex = currentFileIndex - 1 >= 0 ? currentFileIndex - 1 : otherFiles.length - 1
const previousFile = otherFiles[previousFileIndex]
if (previousFile) {
setCurrentFile(previousFile)
}
break
}
case KeyboardKey.Right: {
const nextFileIndex = currentFileIndex + 1 < otherFiles.length ? currentFileIndex + 1 : 0
const nextFile = otherFiles[nextFileIndex]
if (nextFile) {
setCurrentFile(nextFile)
}
break
}
}
},
[currentFile.uuid, otherFiles, setCurrentFile],
)
const IconComponent = useMemo(
() =>
getFileIconComponent(
application.iconsController.getIconForFileType(currentFile.mimeType),
'w-6 h-6 flex-shrink-0',
),
[application.iconsController, currentFile.mimeType],
)
return (
<DialogOverlay
className="sn-component"
aria-label="File preview modal"
onDismiss={dismiss}
initialFocusRef={closeButtonRef}
dangerouslyBypassScrollLock
>
<DialogContent
aria-label="File preview modal"
className="flex flex-col rounded shadow-overlay"
style={{
width: '90%',
maxWidth: '90%',
minHeight: '90%',
background: 'var(--modal-background-color)',
}}
>
<div
className="flex flex-shrink-0 justify-between items-center min-h-6 px-4 py-3 border-0 border-b-1 border-solid border-main focus:shadow-none"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
onKeyDown={keyDownHandler}
>
<div className="flex items-center">
<div className="w-6 h-6">{IconComponent}</div>
<span className="ml-3 font-medium">{currentFile.name}</span>
</div>
<div className="flex items-center">
<button
className="flex p-1.5 mr-4 bg-transparent hover:bg-contrast border-solid border-main border-1 cursor-pointer rounded"
onClick={() => setShowFileInfoPanel((show) => !show)}
>
<Icon type="info" className="color-neutral" />
</button>
<button
ref={closeButtonRef}
onClick={dismiss}
aria-label="Close modal"
className="flex p-1 bg-transparent hover:bg-contrast border-0 cursor-pointer rounded"
>
<Icon type="close" className="color-neutral" />
</button>
</div>
</div>
<div className="flex flex-grow min-h-0">
<div className="flex flex-grow items-center justify-center relative max-w-full">
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
</div>
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
</div>
</DialogContent>
</DialogOverlay>
)
})
FilePreviewModal.displayName = 'FilePreviewModal'
const FilePreviewModalWrapper: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
return viewControllerManager.filePreviewModalController.isOpen ? (
<FilePreviewModal application={application} viewControllerManager={viewControllerManager} />
) : null
}
export default observer(FilePreviewModalWrapper)

View File

@@ -0,0 +1,73 @@
import { IconType } from '@standardnotes/snjs'
import { FunctionComponent, useRef, useState } from 'react'
import IconButton from '../Button/IconButton'
type Props = {
objectUrl: string
}
const ImagePreview: FunctionComponent<Props> = ({ objectUrl }) => {
const initialImgHeightRef = useRef<number>()
const [imageZoomPercent, setImageZoomPercent] = useState(100)
return (
<div className="flex items-center justify-center w-full h-full min-h-0">
<div className="flex items-center justify-center w-full h-full relative overflow-auto">
<img
src={objectUrl}
style={{
height: `${imageZoomPercent}%`,
...(imageZoomPercent <= 100
? {
minWidth: '100%',
objectFit: 'contain',
}
: {
position: 'absolute',
top: 0,
left: 0,
margin: 'auto',
}),
}}
ref={(imgElement) => {
if (!initialImgHeightRef.current) {
initialImgHeightRef.current = imgElement?.height
}
}}
/>
</div>
<div className="flex items-center absolute left-1/2 -translate-x-1/2 bottom-6 py-1 px-3 bg-default border-1 border-solid border-main rounded">
<span className="mr-1.5">Zoom:</span>
<IconButton
className="hover:bg-contrast p-1 rounded"
icon={'subtract' as IconType}
title="Zoom Out"
focusable={true}
onClick={() => {
setImageZoomPercent((percent) => {
const newPercent = percent - 10
if (newPercent >= 10) {
return newPercent
} else {
return percent
}
})
}}
/>
<span className="mx-2">{imageZoomPercent}%</span>
<IconButton
className="hover:bg-contrast p-1 rounded"
icon="add"
title="Zoom In"
focusable={true}
onClick={() => {
setImageZoomPercent((percent) => percent + 10)
}}
/>
</div>
</div>
)
}
export default ImagePreview

View File

@@ -0,0 +1,54 @@
import { FileItem } from '@standardnotes/snjs'
import { FunctionComponent, useEffect, useMemo, useRef } from 'react'
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
import ImagePreview from './ImagePreview'
import { PreviewableTextFileTypes } from './isFilePreviewable'
import TextPreview from './TextPreview'
type Props = {
file: FileItem
bytes: Uint8Array
}
const PreviewComponent: FunctionComponent<Props> = ({ file, bytes }) => {
const objectUrlRef = useRef<string>()
const objectUrl = useMemo(() => {
return createObjectURLWithRef(file.mimeType, bytes, objectUrlRef)
}, [bytes, file.mimeType])
useEffect(() => {
const objectUrl = objectUrlRef.current
return () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl)
objectUrlRef.current = ''
}
}
}, [])
if (file.mimeType.startsWith('image/')) {
return <ImagePreview objectUrl={objectUrl} />
}
if (file.mimeType.startsWith('video/')) {
return <video className="w-full h-full" src={objectUrl} controls autoPlay />
}
if (file.mimeType.startsWith('audio/')) {
return (
<div className="w-full h-full flex items-center justify-center">
<audio src={objectUrl} controls />
</div>
)
}
if (PreviewableTextFileTypes.includes(file.mimeType)) {
return <TextPreview bytes={bytes} />
}
return <object className="w-full h-full" data={objectUrl} />
}
export default PreviewComponent

View File

@@ -0,0 +1,26 @@
import { ElementIds } from '@/Constants/ElementIDs'
import { useMemo } from 'react'
type Props = {
bytes: Uint8Array
}
const TextPreview = ({ bytes }: Props) => {
const text = useMemo(() => {
const textDecoder = new TextDecoder()
return textDecoder.decode(bytes)
}, [bytes])
return (
<textarea
autoComplete="off"
className="w-full h-full flex-grow font-editor focus:shadow-none focus:outline-none"
dir="auto"
id={ElementIds.FileTextPreview}
defaultValue={text}
readOnly={true}
></textarea>
)
}
export default TextPreview

View File

@@ -0,0 +1,15 @@
export const PreviewableTextFileTypes = ['text/plain', 'application/json']
export const isFileTypePreviewable = (fileType: string) => {
const isImage = fileType.startsWith('image/')
const isVideo = fileType.startsWith('video/')
const isAudio = fileType.startsWith('audio/')
const isPdf = fileType === 'application/pdf'
const isText = PreviewableTextFileTypes.includes(fileType)
if (isImage || isVideo || isAudio || isText || isPdf) {
return true
}
return false
}