import type { OdFileObject, OdFolderChildren, OdFolderObject } from '../types' import { ParsedUrlQuery } from 'querystring' import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import toast, { Toaster } from 'react-hot-toast' import emojiRegex from 'emoji-regex' import dynamic from 'next/dynamic' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' import useLocalStorage from '../utils/useLocalStorage' import { getPreviewType, preview } from '../utils/getPreviewType' import { useProtectedSWRInfinite } from '../utils/fetchWithSWR' import { getExtension, getRawExtension, getFileIcon } from '../utils/getFileIcon' import { getStoredToken } from '../utils/protectedRouteHandler' import { DownloadingToast, downloadMultipleFiles, downloadTreelikeMultipleFiles, traverseFolder, } from './MultiFileDownloader' import { layouts } from './SwitchLayout' 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 ImagePreview from './previews/ImagePreview' import DefaultPreview from './previews/DefaultPreview' import { PreviewContainer } from './previews/Containers' import FolderListLayout from './FolderListLayout' import FolderGridLayout from './FolderGridLayout' // Disabling SSR for some previews 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 '/' } // Render the icon of a folder child (may be a file or a folder), use emoji if the name of the child contains emoji const renderEmoji = (name: string) => { const emoji = emojiRegex().exec(name) return { render: emoji && !emoji.index, emoji } } const formatChildName = (name: string) => { const { render, emoji } = renderEmoji(name) return render ? name.replace(emoji ? emoji[0] : '', '').trim() : name } export const ChildName: FC<{ name: string; folder?: boolean }> = ({ name, folder }) => { const original = formatChildName(name) const extension = folder ? '' : getRawExtension(original) const prename = folder ? original : original.substring(0, original.length - extension.length) return ( {prename} ) } export const ChildIcon: FC<{ child: OdFolderChildren }> = ({ child }) => { const { render, emoji } = renderEmoji(child.name) return render ? ( {emoji ? emoji[0] : '📁'} ) : ( ) } export 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 ( ) } export const Downloading: FC<{ title: string; style: string }> = ({ title, style }) => { return ( ) } const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { 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 hashedToken = getStoredToken(router.asPath) const [layout, _] = useLocalStorage('preferredLayout', layouts[0]) const { t } = useTranslation() const path = queryToPath(query) const { data, error, size, setSize } = useProtectedSWRInfinite(path) if (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 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]) { // Expand list of API returns into flattened file data const folderChildren = [].concat(...responses.map(r => r.folder.value)) as OdFolderObject['value'] // Find README.md file to render const readmeFile = folderChildren.find(c => c.name.toLowerCase() === 'readme.md') // Filtered file list helper const getFiles = () => folderChildren.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: `/api/raw/?path=${path}/${c.name}${hashedToken ? `&odpt=${hashedToken}` : ''}`, })) 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() downloadMultipleFiles({ toastId, router, files, folder }) .then(() => { setTotalGenerating(false) toast.success(t('Finished downloading selected files.'), { id: toastId, }) }) .catch(() => { setTotalGenerating(false) toast.error(t('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( t('Failed to download folder {{path}}: {{status}} {{message}} Skipped it to continue.', { path: p, status: error.status, message: error.message, }) ) continue } const hashedTokenForPath = getStoredToken(p) yield { name: c?.name, url: `/api/raw/?path=${p}${hashedTokenForPath ? `&odpt=${hashedTokenForPath}` : ''}`, path: p, isFolder, } } })() setFolderGenerating({ ...folderGenerating, [id]: true }) const toastId = toast.loading() downloadTreelikeMultipleFiles({ toastId, router, files, basePath: path, folder: name, }) .then(() => { setFolderGenerating({ ...folderGenerating, [id]: false }) toast.success(t('Finished downloading folder.'), { id: toastId }) }) .catch(() => { setFolderGenerating({ ...folderGenerating, [id]: false }) toast.error(t('Failed to download folder.'), { id: toastId }) }) } // Folder layout component props const folderProps = { toast, path, folderChildren, selected, toggleItemSelected, totalSelected, toggleTotalSelected, totalGenerating, handleSelectedDownload, folderGenerating, handleFolderDownload, } return ( <> {layout.name === 'Grid' ? : } {!onlyOnePage && (
{t('- showing {{count}} page(s) ', { count: size, totalFileNum: isLoadingMore ? '...' : folderChildren.length, }) + (isLoadingMore ? t('of {{count}} file(s) -', { count: folderChildren.length, context: 'loading' }) : t('of {{count}} file(s) -', { count: folderChildren.length, context: 'loaded' }))}
)} {readmeFile && (
)} ) } if ('file' in responses[0] && responses.length === 1) { const file = responses[0].file as OdFileObject const previewType = getPreviewType(getExtension(file.name), { video: Boolean(file.video) }) if (previewType) { switch (previewType) { case preview.image: return 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