mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
infinite loading via useSWR, close - Pagination #2
This commit is contained in:
+77
-20
@@ -12,7 +12,8 @@ import dynamic from 'next/dynamic'
|
||||
|
||||
import { getExtension, getFileIcon, hasKey } from '../utils/getFileIcon'
|
||||
import { extensions, preview } from '../utils/getPreviewType'
|
||||
import { getBaseUrl, useStaleSWR } from '../utils/tools'
|
||||
import { getBaseUrl, useProtectedSWRInfinite } from '../utils/tools'
|
||||
|
||||
import { VideoPreview } from './previews/VideoPreview'
|
||||
import { AudioPreview } from './previews/AudioPreview'
|
||||
import Loading from './Loading'
|
||||
@@ -106,7 +107,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
|
||||
const path = queryToPath(query)
|
||||
|
||||
const { data, error } = useStaleSWR(`/api?path=${path}`, path)
|
||||
const { data, error, size, setSize } = useProtectedSWRInfinite(path)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -133,9 +134,15 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
return false
|
||||
}
|
||||
|
||||
if ('folderData' in data) {
|
||||
const { value } = data.folderData
|
||||
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 } = {}
|
||||
@@ -145,7 +152,10 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
let renderReadme = false
|
||||
let readmeFile = null
|
||||
|
||||
value.forEach((c: any) => {
|
||||
// Expand list of API returns into flattened file data
|
||||
const children = [].concat(...responses.map(r => r.folder.value))
|
||||
|
||||
children.forEach((c: any) => {
|
||||
if (fileIsImage(c.name)) {
|
||||
imagesInFolder.push({
|
||||
src: c['@microsoft.graph.downloadUrl'],
|
||||
@@ -217,7 +227,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
/>
|
||||
)}
|
||||
|
||||
{value.map((c: any) => (
|
||||
{children.map((c: any) => (
|
||||
<div className="hover:bg-gray-100 dark:hover:bg-gray-850 grid grid-cols-12" key={c.id}>
|
||||
<div
|
||||
className="col-span-11"
|
||||
@@ -271,6 +281,54 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!onlyOnePage && (
|
||||
<div>
|
||||
<div className="p-3 font-mono text-sm text-center text-gray-400 border-b">
|
||||
- showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : children.length} files -
|
||||
</div>
|
||||
<button
|
||||
className={`flex items-center justify-center w-full p-3 space-x-2 ${
|
||||
isLoadingMore || isReachingEnd ? 'opacity-60' : 'hover:bg-gray-100'
|
||||
}`}
|
||||
onClick={() => setSize(size + 1)}
|
||||
disabled={isLoadingMore || isReachingEnd}
|
||||
>
|
||||
{isLoadingMore ? (
|
||||
<>
|
||||
<span>Loading ...</span>{' '}
|
||||
<svg
|
||||
className="animate-spin w-5 h-5 mr-3 -ml-1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
</>
|
||||
) : isReachingEnd ? (
|
||||
<span>No more files</span>
|
||||
) : (
|
||||
<>
|
||||
<span>Load more</span>
|
||||
<FontAwesomeIcon icon="chevron-circle-down" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderReadme && (
|
||||
<div className="dark:border-gray-700 border-t">
|
||||
<MarkdownPreview file={readmeFile} path={path} standalone={false} />
|
||||
@@ -280,10 +338,10 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
)
|
||||
}
|
||||
|
||||
if ('identityData' in data) {
|
||||
const { identityData } = data
|
||||
const downloadUrl = identityData['@microsoft.graph.downloadUrl']
|
||||
const fileName = data.identityData.name
|
||||
if ('file' in responses[0] && responses.length === 1) {
|
||||
const { file } = responses[0]
|
||||
const downloadUrl = file['@microsoft.graph.downloadUrl']
|
||||
const fileName = file.name
|
||||
const fileExtension = fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase()
|
||||
|
||||
if (hasKey(extensions, fileExtension)) {
|
||||
@@ -297,25 +355,25 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
)
|
||||
|
||||
case preview.text:
|
||||
return <TextPreview file={identityData} />
|
||||
return <TextPreview file={file} />
|
||||
|
||||
case preview.code:
|
||||
return <CodePreview file={identityData} />
|
||||
return <CodePreview file={file} />
|
||||
|
||||
case preview.markdown:
|
||||
return <MarkdownPreview file={identityData} path={path} />
|
||||
return <MarkdownPreview file={file} path={path} />
|
||||
|
||||
case preview.video:
|
||||
return <VideoPreview file={identityData} />
|
||||
return <VideoPreview file={file} />
|
||||
|
||||
case preview.audio:
|
||||
return <AudioPreview file={identityData} />
|
||||
return <AudioPreview file={file} />
|
||||
|
||||
case preview.pdf:
|
||||
return <PDFPreview file={identityData} />
|
||||
return <PDFPreview file={file} />
|
||||
|
||||
case preview.office:
|
||||
return <OfficePreview file={identityData} />
|
||||
return <OfficePreview file={file} />
|
||||
|
||||
default:
|
||||
return <div className="dark:bg-gray-900 bg-white rounded shadow">{fileName}</div>
|
||||
@@ -326,7 +384,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
<>
|
||||
<div className="dark:bg-gray-900 p-3 bg-white rounded shadow">
|
||||
<FourOhFour
|
||||
errorMsg={`Preview for file ${identityData.name} is not available, download directly with the button below.`}
|
||||
errorMsg={`Preview for file ${fileName} is not available, download directly with the button below.`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
@@ -338,9 +396,8 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
|
||||
|
||||
return (
|
||||
<div className="dark:bg-gray-900 p-3 bg-white rounded shadow">
|
||||
<FourOhFour errorMsg={`Cannot preview ${path}.`} />
|
||||
<FourOhFour errorMsg={`Cannot preview ${path}`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileListing
|
||||
|
||||
+3
-1
@@ -35,6 +35,7 @@ import {
|
||||
faKey,
|
||||
faSignOutAlt,
|
||||
faCloud,
|
||||
faChevronCircleDown,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub, faMarkdown, faTelegramPlane } from '@fortawesome/free-brands-svg-icons'
|
||||
|
||||
@@ -72,7 +73,8 @@ library.add(
|
||||
faSignOutAlt,
|
||||
faEnvelope,
|
||||
faCloud,
|
||||
faTelegramPlane
|
||||
faTelegramPlane,
|
||||
faChevronCircleDown
|
||||
)
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
|
||||
+6
-6
@@ -43,7 +43,7 @@ const getAccessToken = async () => {
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { path = '/', raw = false, t = '' } = req.query
|
||||
const { path = '/', raw = false, next = '' } = req.query
|
||||
if (path === '[...path]') {
|
||||
res.status(400).json({ error: 'No path specified.' })
|
||||
return
|
||||
@@ -125,11 +125,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if ('folder' in identityData) {
|
||||
const { data: folderData } = await axios.get(`${requestUrl}:/children`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
params: t
|
||||
params: next
|
||||
? {
|
||||
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
|
||||
top: siteConfig.maxItems,
|
||||
$skipToken: t,
|
||||
$skipToken: next,
|
||||
}
|
||||
: {
|
||||
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file',
|
||||
@@ -144,13 +144,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// Return paging token if specified
|
||||
if (nextPage) {
|
||||
res.status(200).json({ path, identityData, folderData, nextPage })
|
||||
res.status(200).json({ folder: folderData, next: nextPage })
|
||||
} else {
|
||||
res.status(200).json({ path, identityData, folderData })
|
||||
res.status(200).json({ folder: folderData })
|
||||
}
|
||||
return
|
||||
}
|
||||
res.status(200).json({ path, identityData })
|
||||
res.status(200).json({ file: identityData })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+42
-5
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import sha256 from 'crypto-js/sha256'
|
||||
import useSWR, { cache, Key } from 'swr'
|
||||
import useSWR, { cache, Key, useSWRInfinite } from 'swr'
|
||||
|
||||
import siteConfig from '../config/site.json'
|
||||
|
||||
@@ -38,17 +38,54 @@ export const useStaleSWR = (url: Key, path: string = '') => {
|
||||
revalidateOnReconnect: true,
|
||||
}
|
||||
|
||||
const storedToken =
|
||||
typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(matchProtectedRoute(path)) as string) : ''
|
||||
const hashedToken = storedToken ? encryptToken(storedToken) : null
|
||||
|
||||
const hashedToken = getStoredToken(path)
|
||||
return useSWR([url, hashedToken], fetcher, revalidationOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Paging with useSWRInfinite + protected token support
|
||||
* @param path Current query directory path
|
||||
* @returns useSWRInfinite API
|
||||
*/
|
||||
export const useProtectedSWRInfinite = (path: string = '') => {
|
||||
const hashedToken = getStoredToken(path)
|
||||
|
||||
/**
|
||||
* Next page infinite loading for useSWR
|
||||
* @param pageIdx The index of this paging collection
|
||||
* @param prevPageData Previous page information
|
||||
* @param path Directory path
|
||||
* @returns API to the next page
|
||||
*/
|
||||
const getNextKey = (pageIndex, previousPageData) => {
|
||||
// Reached the end of the collection
|
||||
if (previousPageData && !previousPageData.folder) return null
|
||||
|
||||
// First page with no prevPageData
|
||||
if (pageIndex === 0) return [`/api?path=${path}`, hashedToken]
|
||||
|
||||
// Add nextPage token to API endpoint
|
||||
return [`/api?path=${path}&next=${previousPageData.next}`, hashedToken]
|
||||
}
|
||||
|
||||
const revalidationOptions = {
|
||||
revalidateOnMount: !(cache.has(`arg@"/api?path=${path}"@null`) || cache.has(`/api?path=${path}`)),
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: true,
|
||||
}
|
||||
return useSWRInfinite(getNextKey, fetcher, revalidationOptions)
|
||||
}
|
||||
|
||||
// Hash password token with SHA256
|
||||
const encryptToken = (token: string) => {
|
||||
return sha256(token).toString()
|
||||
}
|
||||
// Fetch stored token from localStorage and encrypt with SHA256
|
||||
const getStoredToken = (path: string) => {
|
||||
const storedToken =
|
||||
typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(matchProtectedRoute(path)) as string) : ''
|
||||
return storedToken ? encryptToken(storedToken) : null
|
||||
}
|
||||
/**
|
||||
* Compares the hash of .password and od-protected-token header
|
||||
* @param odTokenHeader od-protected-token header (sha256 hashed token)
|
||||
|
||||
Reference in New Issue
Block a user