diff --git a/components/Breadcrumb.tsx b/components/Breadcrumb.tsx index b939401..726ccf1 100644 --- a/components/Breadcrumb.tsx +++ b/components/Breadcrumb.tsx @@ -1,41 +1,61 @@ -import Link from 'next/link' +import type { ParsedUrlQuery } from 'querystring' -import { ParsedUrlQuery } from 'querystring' +import Link from 'next/link' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +const HomeCrumb = () => { + return ( + + + + Home + + + ) +} const Breadcrumb: React.FC<{ query?: ParsedUrlQuery }> = ({ query }) => { if (query) { const { path } = query if (Array.isArray(path)) { + // We are rendering the path in reverse, so that the browser automatically scrolls to the end of the breadcrumb + // https://stackoverflow.com/questions/18614301/keep-overflow-div-scrolled-to-bottom-unless-user-scrolls-up/18614561 return ( -
-
- 🚩 Home -
- {path.map((q: string, i: number) => ( -
-
/
-
+
    + {path + .slice(0) + .reverse() + .map((p: string, i: number) => ( +
  1. + encodeURIComponent(p)) .join('/')}`} + passHref > - {q} + + {p} + -
-
- ))} -
+ + ))} +
  • + +
  • + ) } } return ( -
    -
    - 🚩 Home -
    +
    +
    ) } diff --git a/components/FileListing.tsx b/components/FileListing.tsx index 9ffff95..6dbfa07 100644 --- a/components/FileListing.tsx +++ b/components/FileListing.tsx @@ -1,20 +1,17 @@ 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 { useRouter } from 'next/router' -import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails' -import { getExtension, getFileIcon } from '../utils/getFileIcon' +import useLocalStorage from '../utils/useLocalStorage' import { getPreviewType, preview } from '../utils/getPreviewType' import { useProtectedSWRInfinite } from '../utils/fetchWithSWR' -import { getBaseUrl } from '../utils/getBaseUrl' +import { getFileIcon } from '../utils/getFileIcon' import { DownloadingToast, downloadMultipleFiles, @@ -22,6 +19,7 @@ import { traverseFolder, } from './MultiFileDownloader' +import { layouts } from './SwitchLayout' import Loading, { LoadingIcon } from './Loading' import FourOhFour from './FourOhFour' import Auth from './Auth' @@ -33,14 +31,15 @@ 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 { DownloadBtnContainer, PreviewContainer } from './previews/Containers' -import DownloadButtonGroup from './DownloadBtnGtoup' +import { PreviewContainer } from './previews/Containers' -import type { OdFileObject, OdFolderObject } from '../types' +import type { OdFileObject, OdFolderChildren, OdFolderObject } from '../types' +import FolderListLayout from './FolderListLayout' +import FolderGridLayout from './FolderGridLayout' -// Disabling SSR for some previews (image gallery view, and PDF view) -const ReactViewer = dynamic(() => import('react-viewer'), { ssr: false }) +// Disabling SSR for some previews const EPUBPreview = dynamic(() => import('./previews/EPUBPreview'), { ssr: false, }) @@ -61,36 +60,25 @@ const queryToPath = (query?: ParsedUrlQuery) => { 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)} -
    -
    +// 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 } +} +export const formatChildName = (name: string) => { + const { render, emoji } = renderEmoji(name) + return render ? name.replace(emoji ? emoji[0] : '', '').trim() : name +} +export const ChildIcon: FC<{ child: OdFolderChildren }> = ({ child }) => { + const { render, emoji } = renderEmoji(child.name) + return render ? ( + {emoji ? emoji[0] : '📁'} + ) : ( + ) } -const Checkbox: FC<{ +export const Checkbox: FC<{ checked: 0 | 1 | 2 onChange: () => void title: string @@ -120,7 +108,7 @@ const Checkbox: FC<{ return ( = ({ title }) => { +export 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) @@ -158,19 +144,17 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { }>({}) const router = useRouter() - const clipboard = useClipboard() + const [layout, _] = useLocalStorage('preferredLayout', layouts[0]) 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
    } return ( @@ -187,14 +171,6 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { ) } - 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 @@ -204,37 +180,14 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { 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'] + const folderChildren = [].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 - } - }) + // Find README.md file to render + const readmeFile = folderChildren.find(c => c.name.toLowerCase() === 'readme.md') // Filtered file list helper - const getFiles = () => children.filter(c => !c.folder && c.name !== '.password') + const getFiles = () => folderChildren.filter(c => !c.folder && c.name !== '.password') // File selection const genTotalSelected = (selected: { [key: string]: boolean }) => { @@ -335,215 +288,57 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { }) } + // Folder layout component props + const folderProps = { + toast, + path, + folderChildren, + selected, + toggleItemSelected, + totalSelected, + toggleTotalSelected, + totalGenerating, + handleSelectedDownload, + folderGenerating, + handleFolderDownload, + } + return ( <> -
    -
    -
    - Name -
    -
    - Last Modified -
    -
    - Size -
    -
    - Actions -
    -
    -
    - - {totalGenerating ? ( - - ) : ( - - )} -
    -
    -
    + - + {layout.name === 'Grid' ? : } - {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)() - }} - > - - - )} -
    + {!onlyOnePage && ( +
    +
    + - showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : folderChildren.length} files - +
    +
    - ))} + +
    + )} - {!onlyOnePage && ( -
    -
    - - showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : children.length} files - -
    - -
    - )} -
    - {renderReadme && ( + {readmeFile && (
    @@ -558,29 +353,12 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { const fileName = file.name const fileExtension = fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase() - const previewType = getPreviewType(fileExtension, { - video: Boolean(file.video), - }) + 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} - - - - - - ) + return case preview.text: return diff --git a/components/FolderGridLayout.tsx b/components/FolderGridLayout.tsx new file mode 100644 index 0000000..7f32807 --- /dev/null +++ b/components/FolderGridLayout.tsx @@ -0,0 +1,179 @@ +import type { OdFolderChildren } from '../types' + +import Link from 'next/link' +import { useState } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useClipboard } from 'use-clipboard-copy' + +import { getBaseUrl } from '../utils/getBaseUrl' +import { formatModifiedDateTime } from '../utils/fileDetails' +import { Checkbox, ChildIcon, Downloading, formatChildName } from './FileListing' + +const GridItem = ({ c }: { c: OdFolderChildren }) => { + // We use the generated medium thumbnail for rendering preview images + const thumbnail = c.thumbnails && c.thumbnails.length > 0 ? c.thumbnails[0].medium : null + + // Some thumbnails are broken, so we check for onerror event in the image component + const [brokenThumbnail, setBrokenThumbnail] = useState(false) + + return ( +
    +
    + {thumbnail && !brokenThumbnail ? ( + // eslint-disable-next-line @next/next/no-img-element + {c.name} setBrokenThumbnail(true)} + /> + ) : ( +
    + + + {c.folder?.childCount} + +
    + )} +
    + +
    + + + + {formatChildName(c.name)} +
    +
    + {formatModifiedDateTime(c.lastModifiedDateTime)} +
    +
    + ) +} + +const FolderGridLayout = ({ + path, + folderChildren, + selected, + toggleItemSelected, + totalSelected, + toggleTotalSelected, + totalGenerating, + handleSelectedDownload, + folderGenerating, + handleFolderDownload, + toast, +}) => { + const clipboard = useClipboard() + + return ( +
    +
    +
    {folderChildren.length} items
    +
    + + {totalGenerating ? ( + + ) : ( + + )} +
    +
    + +
    + {folderChildren.map((c: OdFolderChildren) => ( +
    +
    + {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" + /> + )} +
    + + + + + + +
    + ))} +
    +
    + ) +} + +export default FolderGridLayout diff --git a/components/FolderListLayout.tsx b/components/FolderListLayout.tsx new file mode 100644 index 0000000..897cd70 --- /dev/null +++ b/components/FolderListLayout.tsx @@ -0,0 +1,162 @@ +import type { OdFolderChildren } from '../types' + +import Link from 'next/link' +import { FC } from 'react' +import { useClipboard } from 'use-clipboard-copy' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +import { getBaseUrl } from '../utils/getBaseUrl' +import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails' + +import { Downloading, Checkbox, formatChildName, ChildIcon } from './FileListing' + +const FileListItem: FC<{ fileContent: OdFolderChildren }> = ({ fileContent: c }) => { + return ( +
    +
    +
    + +
    +
    {formatChildName(c.name)}
    +
    +
    + {formatModifiedDateTime(c.lastModifiedDateTime)} +
    +
    + {humanFileSize(c.size)} +
    +
    + ) +} + +const FolderListLayout = ({ + path, + folderChildren, + selected, + toggleItemSelected, + totalSelected, + toggleTotalSelected, + totalGenerating, + handleSelectedDownload, + folderGenerating, + handleFolderDownload, + toast, +}) => { + const clipboard = useClipboard() + + return ( +
    +
    +
    + Name +
    +
    + Last Modified +
    +
    + Size +
    +
    + Actions +
    +
    +
    + + {totalGenerating ? ( + + ) : ( + + )} +
    +
    +
    + + {folderChildren.map((c: OdFolderChildren) => ( +
    + + + + + + + {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" + /> + )} +
    +
    + ))} +
    + ) +} + +export default FolderListLayout diff --git a/components/Navbar.tsx b/components/Navbar.tsx index ac94bad..a2d6236 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -51,34 +51,34 @@ const Navbar = () => { } return ( -
    +
    -
    +
    - + icon - {siteConfig.title} + {siteConfig.title} -
    +
    @@ -89,23 +89,23 @@ const Navbar = () => { href={l.link} target="_blank" rel="noopener noreferrer" - className="flex items-center space-x-2 dark:text-white hover:opacity-80" + className="flex items-center space-x-2 hover:opacity-80 dark:text-white" > - {l.name} + {l.name} ))} {siteConfig.email && ( - + - Email + Email )} {tokenPresent && (