From 2c22c17ddd11656797a6b759cee3f3f99235c8e7 Mon Sep 17 00:00:00 2001 From: spencerwooo Date: Sat, 5 Feb 2022 17:25:46 +0800 Subject: [PATCH] grid layout with thumbnails --- components/FileListing.tsx | 244 ++++++++------------------------ components/FolderGridLayout.tsx | 83 +++++++++++ components/FolderListLayout.tsx | 174 +++++++++++++++++++++++ components/SwitchLayout.tsx | 6 +- pages/_app.tsx | 2 + pages/api/index.ts | 2 + types/index.d.ts | 2 + 7 files changed, 324 insertions(+), 189 deletions(-) create mode 100644 components/FolderGridLayout.tsx create mode 100644 components/FolderListLayout.tsx diff --git a/components/FileListing.tsx b/components/FileListing.tsx index 7635353..9e0fd0b 100644 --- a/components/FileListing.tsx +++ b/components/FileListing.tsx @@ -6,11 +6,15 @@ import { useClipboard } from 'use-clipboard-copy' import { ParsedUrlQuery } from 'querystring' import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react' -import { useRouter } from 'next/router' +import Link from 'next/link' import dynamic from 'next/dynamic' +import { useRouter } from 'next/router' +import { layouts } from './SwitchLayout' + +import useLocalStorage from '../utils/useLocalStorage' import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails' -import { getExtension, getFileIcon } from '../utils/getFileIcon' +import { getFileIcon } from '../utils/getFileIcon' import { getPreviewType, preview } from '../utils/getPreviewType' import { useProtectedSWRInfinite } from '../utils/fetchWithSWR' import { getBaseUrl } from '../utils/getBaseUrl' @@ -35,11 +39,12 @@ import URLPreview from './previews/URLPreview' import DefaultPreview from './previews/DefaultPreview' import { DownloadBtnContainer, PreviewContainer } from './previews/Containers' import DownloadButtonGroup from './DownloadBtnGtoup' +import FolderListLayout from './FolderListLayout' import type { OdFileObject, OdFolderObject } from '../types' -import Link from 'next/link' +import FolderGridLayout from './FolderGridLayout' -// Disabling SSR for some previews (image gallery view, and PDF view) +// Disabling SSR for some previews const EPUBPreview = dynamic(() => import('./previews/EPUBPreview'), { ssr: false, }) @@ -60,36 +65,7 @@ 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)} -
-
- ) -} - -const Checkbox: FC<{ +export const Checkbox: FC<{ checked: 0 | 1 | 2 onChange: () => void title: string @@ -134,7 +110,7 @@ const Checkbox: FC<{ ) } -const Downloading: FC<{ title: string }> = ({ title }) => { +export const Downloading: FC<{ title: string }> = ({ title }) => { return ( = ({ title }) => { } 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) @@ -157,19 +131,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 ( @@ -186,14 +158,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,13 +168,13 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => { if ('folder' in responses[0]) { // 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'] // Find README.md file to render - const readmeFile = children.find(c => c.name.toLowerCase() === 'readme.md') + 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 }) => { @@ -311,147 +275,55 @@ 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' ? : } - {children.map(c => ( -
- - - - - - - {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 - -
- -
- )} -
+ +
+ )} {readmeFile && (
diff --git a/components/FolderGridLayout.tsx b/components/FolderGridLayout.tsx new file mode 100644 index 0000000..503256d --- /dev/null +++ b/components/FolderGridLayout.tsx @@ -0,0 +1,83 @@ +import type { OdFolderObject } from '../types' + +import Link from 'next/link' +import emojiRegex from 'emoji-regex' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +import { getFileIcon } from '../utils/getFileIcon' +import { formatModifiedDateTime } from '../utils/fileDetails' + +type OdFolderChildren = OdFolderObject['value'][number] + +const GridItem = ({ c }: { c: OdFolderChildren }) => { + const emojiIcon = emojiRegex().exec(c.name) + const renderEmoji = emojiIcon && !emojiIcon.index + + const ChildIcon = () => + renderEmoji ? ( + {emojiIcon ? emojiIcon[0] : '📁'} + ) : ( + + ) + + // We use the generated medium thumbnail for rendering preview images + const thumbnail = c.thumbnails && c.thumbnails.length > 0 ? c.thumbnails[0].medium : null + + return ( +
+
+ {thumbnail ? ( + // eslint-disable-next-line @next/next/no-img-element + {c.name} + ) : ( +
+ + + {c.folder?.childCount} + +
+ )} +
+ +
+ + + + + {renderEmoji ? c.name.replace(emojiIcon ? emojiIcon[0] : '', '').trim() : c.name} + +
+
+ {formatModifiedDateTime(c.lastModifiedDateTime)} +
+
+ ) +} + +const FolderGridLayout = ({ + path, + folderChildren, + selected, + toggleItemSelected, + totalSelected, + toggleTotalSelected, + totalGenerating, + handleSelectedDownload, + folderGenerating, + handleFolderDownload, + toast, +}) => { + return ( +
+ {folderChildren.map((c: OdFolderChildren) => ( + + + + + + ))} +
+ ) +} + +export default FolderGridLayout diff --git a/components/FolderListLayout.tsx b/components/FolderListLayout.tsx new file mode 100644 index 0000000..ed541ec --- /dev/null +++ b/components/FolderListLayout.tsx @@ -0,0 +1,174 @@ +import type { OdFolderObject } from '../types' + +import { FC } from 'react' +import emojiRegex from 'emoji-regex' +import { useClipboard } from 'use-clipboard-copy' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +import Link from 'next/link' + +import { getBaseUrl } from '../utils/getBaseUrl' +import { getFileIcon } from '../utils/getFileIcon' +import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails' + +import { Downloading, Checkbox } from './FileListing' + +type OdFolderChildren = OdFolderObject['value'][number] + +const FileListItem: FC<{ fileContent: OdFolderChildren }> = ({ 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 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/SwitchLayout.tsx b/components/SwitchLayout.tsx index fbe70ac..fbdbe5c 100644 --- a/components/SwitchLayout.tsx +++ b/components/SwitchLayout.tsx @@ -5,9 +5,9 @@ import { Listbox, Transition } from '@headlessui/react' import useLocalStorage from '../utils/useLocalStorage' -const layouts: Array<{ id: number; name: 'Grid' | 'List'; icon: IconProp }> = [ - { id: 1, name: 'Grid', icon: 'th' }, - { id: 2, name: 'List', icon: 'th-list' }, +export const layouts: Array<{ id: number; name: 'Grid' | 'List'; icon: IconProp }> = [ + { id: 1, name: 'List', icon: 'th-list' }, + { id: 2, name: 'Grid', icon: 'th' }, ] export const SwitchLayout = () => { diff --git a/pages/_app.tsx b/pages/_app.tsx index c5ef125..9e4f4eb 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -47,6 +47,7 @@ import { faExclamationCircle, faExclamationTriangle, faTh, + faThLarge, faThList, faHome, } from '@fortawesome/free-solid-svg-icons' @@ -103,6 +104,7 @@ library.add( faSearch, faChevronDown, faTh, + faThLarge, faThList, ...iconList ) diff --git a/pages/api/index.ts b/pages/api/index.ts index 5addae6..e4fb79b 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -220,11 +220,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) params: next ? { select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image', + $expand: 'thumbnails', top: siteConfig.maxItems, $skipToken: next, } : { select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image', + $expand: 'thumbnails', top: siteConfig.maxItems, }, }) diff --git a/types/index.d.ts b/types/index.d.ts index 1872f2f..f3dae85 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,6 +17,8 @@ export type OdFolderObject = { folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } } image?: OdImageFile video?: OdVideoFile + 'thumbnails@odata.context'?: string + thumbnails?: Array }> } // A file object returned from the OneDrive API. This object may contain 'video' if the file is a video.