import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import toast, { Toaster } from 'react-hot-toast' import emojiRegex from 'emoji-regex' import { useClipboard } from 'use-clipboard-copy' import { ParsedUrlQuery } from 'querystring' import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react' import { ImageDecorator } from 'react-viewer/lib/ViewerProps' import { useRouter } from 'next/router' import dynamic from 'next/dynamic' import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails' import { getExtension, getFileIcon } from '../utils/getFileIcon' import { getPreviewType, preview } from '../utils/getPreviewType' import { useProtectedSWRInfinite } from '../utils/fetchWithSWR' import { getBaseUrl } from '../utils/getBaseUrl' import { DownloadingToast, downloadMultipleFiles, downloadTreelikeMultipleFiles, traverseFolder, } from './MultiFileDownloader' import Loading, { LoadingIcon } from './Loading' import FourOhFour from './FourOhFour' import Auth from './Auth' import TextPreview from './previews/TextPreview' import MarkdownPreview from './previews/MarkdownPreview' import CodePreview from './previews/CodePreview' import OfficePreview from './previews/OfficePreview' import AudioPreview from './previews/AudioPreview' import VideoPreview from './previews/VideoPreview' import PDFPreview from './previews/PDFPreview' import URLPreview from './previews/URLPreview' import DefaultPreview from './previews/DefaultPreview' import { DownloadBtnContainer, PreviewContainer } from './previews/Containers' import DownloadButtonGroup from './DownloadBtnGtoup' import { OdFileObject, OdFolderObject } from '../types' // Disabling SSR for some previews (image gallery view, and PDF view) const ReactViewer = dynamic(() => import('react-viewer'), { ssr: false }) const EPUBPreview = dynamic(() => import('./previews/EPUBPreview'), { ssr: false }) /** * Convert url query into path string * * @param query Url query property * @returns Path string */ const queryToPath = (query?: ParsedUrlQuery) => { if (query) { const { path } = query if (!path) return '/' if (typeof path === 'string') return `/${encodeURIComponent(path)}` return `/${path.map(p => encodeURIComponent(p)).join('/')}` } return '/' } const FileListItem: FC<{ fileContent: OdFolderObject['value'][number] }> = ({ fileContent: c }) => { const emojiIcon = emojiRegex().exec(c.name) const renderEmoji = emojiIcon && !emojiIcon.index return (
{/*
{c.file ? c.file.mimeType : 'folder'}
*/}
{renderEmoji ? ( {emojiIcon ? emojiIcon[0] : '📁'} ) : ( )}
{renderEmoji ? c.name.replace(emojiIcon ? emojiIcon[0] : '', '').trim() : c.name}
{formatModifiedDateTime(c.lastModifiedDateTime)}
{humanFileSize(c.size)}
) } const Checkbox: FC<{ checked: 0 | 1 | 2 onChange: () => void title: string indeterminate?: boolean }> = ({ checked, onChange, title, indeterminate }) => { const ref = useRef(null) useEffect(() => { if (ref.current) { ref.current.checked = Boolean(checked) if (indeterminate) { ref.current.indeterminate = checked == 1 } } }, [ref, checked, indeterminate]) const handleClick: MouseEventHandler = e => { if (ref.current) { if (e.target === ref.current) { e.stopPropagation() } else { ref.current.click() } } } return ( ) } const Downloading: FC<{ title: string }> = ({ title }) => { return ( ) } const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { const [imageViewerVisible, setImageViewerVisibility] = useState(false) const [activeImageIdx, setActiveImageIdx] = useState(0) const [selected, setSelected] = useState<{ [key: string]: boolean }>({}) const [totalSelected, setTotalSelected] = useState<0 | 1 | 2>(0) const [totalGenerating, setTotalGenerating] = useState(false) const [folderGenerating, setFolderGenerating] = useState<{ [key: string]: boolean }>({}) const router = useRouter() const clipboard = useClipboard() const path = queryToPath(query) const { data, error, size, setSize } = useProtectedSWRInfinite(path) if (error) { console.log(error) // If error includes 403 which means the user has not completed initial setup, redirect to OAuth page if (error.status === 403) { router.push('/onedrive-vercel-index-oauth/step-1') return
} return ( {error.status === 401 ? : } ) } if (!data) { return ( ) } const fileIsImage = (fileName: string) => { const fileExtension = getExtension(fileName) if (getPreviewType(fileExtension) === preview.image) { return true } return false } const responses: any[] = data ? [].concat(...data) : [] const isLoadingInitialData = !data && !error const isLoadingMore = isLoadingInitialData || (size > 0 && data && typeof data[size - 1] === 'undefined') const isEmpty = data?.[0]?.length === 0 const isReachingEnd = isEmpty || (data && typeof data[data.length - 1]?.next === 'undefined') const onlyOnePage = data && typeof data[0].next === 'undefined' if ('folder' in responses[0]) { // Image preview rendering preparations const imagesInFolder: ImageDecorator[] = [] const imageIndexDict: { [key: string]: number } = {} let imageIndex = 0 // README rendering preparations let renderReadme = false let readmeFile = {} // Expand list of API returns into flattened file data const children = [].concat(...responses.map(r => r.folder.value)) as OdFolderObject['value'] children.forEach(c => { if (fileIsImage(c.name)) { imagesInFolder.push({ src: c['@microsoft.graph.downloadUrl'], alt: c.name, downloadUrl: c['@microsoft.graph.downloadUrl'], }) imageIndexDict[c.id] = imageIndex imageIndex += 1 } if (c.name.toLowerCase() === 'readme.md') { renderReadme = true readmeFile = c } }) // Filtered file list helper const getFiles = () => children.filter(c => !c.folder && c.name !== '.password') // File selection const genTotalSelected = (selected: { [key: string]: boolean }) => { const selectInfo = getFiles().map(c => Boolean(selected[c.id])) const [hasT, hasF] = [selectInfo.some(i => i), selectInfo.some(i => !i)] return hasT && hasF ? 1 : !hasF ? 2 : 0 } const toggleItemSelected = (id: string) => { let val: SetStateAction<{ [key: string]: boolean }> if (selected[id]) { val = { ...selected } delete val[id] } else { val = { ...selected, [id]: true } } setSelected(val) setTotalSelected(genTotalSelected(val)) } const toggleTotalSelected = () => { if (genTotalSelected(selected) == 2) { setSelected({}) setTotalSelected(0) } else { setSelected(Object.fromEntries(getFiles().map(c => [c.id, true]))) setTotalSelected(2) } } // Selected file download const handleSelectedDownload = () => { const folderName = path.substring(path.lastIndexOf('/') + 1) const folder = folderName ? decodeURIComponent(folderName) : undefined const files = getFiles() .filter(c => selected[c.id]) .map(c => ({ name: c.name, url: c['@microsoft.graph.downloadUrl'] })) if (files.length == 1) { const el = document.createElement('a') el.style.display = 'none' document.body.appendChild(el) el.href = files[0].url el.click() el.remove() } else if (files.length > 1) { setTotalGenerating(true) const toastId = toast.loading(DownloadingToast(router)) downloadMultipleFiles({ toastId, router, files, folder }) .then(() => { setTotalGenerating(false) toast.success('Finished downloading selected files.', { id: toastId }) }) .catch(() => { setTotalGenerating(false) toast.error('Failed to download selected files.', { id: toastId }) }) } } // Folder recursive download const handleFolderDownload = (path: string, id: string, name?: string) => () => { const files = (async function* () { for await (const { meta: c, path: p, isFolder, error } of traverseFolder(path)) { if (error) { toast.error(`Failed to download folder ${p}: ${error.status} ${error.message} Skipped it to continue.`) continue } yield { name: c?.name, url: c ? c['@microsoft.graph.downloadUrl'] : undefined, path: p, isFolder, } } })() setFolderGenerating({ ...folderGenerating, [id]: true }) const toastId = toast.loading(DownloadingToast(router)) downloadTreelikeMultipleFiles({ toastId, router, files, basePath: path, folder: name }) .then(() => { setFolderGenerating({ ...folderGenerating, [id]: false }) toast.success('Finished downloading folder.', { id: toastId }) }) .catch(() => { setFolderGenerating({ ...folderGenerating, [id]: false }) toast.error('Failed to download folder.', { id: toastId }) }) } return ( <>
Name
Last Modified
Size
Actions
{totalGenerating ? ( ) : ( )}
{imagesInFolder.length !== 0 && ( { setImageViewerVisibility(false) }} customToolbar={toolbars => { toolbars[0].render = toolbars[1].render = toolbars[2].render = toolbars[3].render = toolbars[4].render = toolbars[9].render = return toolbars.concat([ { key: 'copy', render: , onClick: i => { clipboard.copy(i.alt ? `${getBaseUrl()}/api?path=${path + '/' + i.alt}&raw=true` : '') toast('Copied image permanent link to clipboard.', { icon: '👌' }) }, }, ]) }} /> )} {children.map(c => (
{ e.preventDefault() if (!c.folder && fileIsImage(c.name)) { setActiveImageIdx(imageIndexDict[c.id]) setImageViewerVisibility(true) } else { router.push(`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`) } }} >
{c.folder ? (
{ clipboard.copy(`${getBaseUrl()}${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`) toast('Copied folder permalink.', { icon: '👌' }) }} > {folderGenerating[c.id] ? ( ) : ( { const p = `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}` handleFolderDownload(p, c.id, c.name)() }} > )}
) : (
{ clipboard.copy( `${getBaseUrl()}/api?path=${path === '/' ? '' : path}/${encodeURIComponent(c.name)}&raw=true` ) toast.success('Copied raw file permalink.') }} >
)}
{!c.folder && !(c.name === '.password') && ( toggleItemSelected(c.id)} title="Select file" /> )}
))} {!onlyOnePage && (
- showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : children.length} files -
)}
{renderReadme && (
)} ) } if ('file' in responses[0] && responses.length === 1) { const file = responses[0].file as OdFileObject const downloadUrl = file['@microsoft.graph.downloadUrl'] const fileName = file.name const fileExtension = fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase() const previewType = getPreviewType(fileExtension, { video: Boolean(file.video) }) if (previewType) { switch (previewType) { case preview.image: return ( <> {/* eslint-disable-next-line @next/next/no-img-element */} {fileName} ) case preview.text: return case preview.code: return case preview.markdown: return case preview.video: return case preview.audio: return case preview.pdf: return case preview.office: return case preview.epub: return case preview.url: return default: return } } else { return } } return ( ) } export default FileListing