mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
Merge pull request #350 from spencerwooo/breadcrumb-grid-view
This commit is contained in:
+40
-20
@@ -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 (
|
||||||
|
<Link href="/">
|
||||||
|
<a>
|
||||||
|
<FontAwesomeIcon className="h-3 w-3" icon={['far', 'flag']} />
|
||||||
|
<span className="ml-2 font-medium">Home</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Breadcrumb: React.FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
const Breadcrumb: React.FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
||||||
if (query) {
|
if (query) {
|
||||||
const { path } = query
|
const { path } = query
|
||||||
if (Array.isArray(path)) {
|
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 (
|
return (
|
||||||
<div className="dark:text-gray-300 no-scrollbar flex pb-4 overflow-x-scroll text-sm text-gray-600 -mx-1">
|
<ol className="no-scrollbar inline-flex flex-row-reverse items-center gap-1 overflow-x-scroll text-sm text-gray-600 dark:text-gray-300 md:gap-3">
|
||||||
<div className="hover:opacity-80 flex-shrink-0 px-1 transition-all duration-75">
|
{path
|
||||||
<Link href="/">🚩 Home</Link>
|
.slice(0)
|
||||||
</div>
|
.reverse()
|
||||||
{path.map((q: string, i: number) => (
|
.map((p: string, i: number) => (
|
||||||
<div key={i} className="flex items-center flex-shrink-0">
|
<li key={i} className="flex flex-shrink-0 items-center">
|
||||||
<div>/</div>
|
<FontAwesomeIcon className="h-3 w-3" icon="angle-right" />
|
||||||
<div className="hover:opacity-80 px-1 transition-all duration-75">
|
|
||||||
<Link
|
<Link
|
||||||
href={`/${path
|
href={`/${path
|
||||||
.slice(0, i + 1)
|
.slice(0, path.length - i)
|
||||||
.map(p => encodeURIComponent(p))
|
.map(p => encodeURIComponent(p))
|
||||||
.join('/')}`}
|
.join('/')}`}
|
||||||
|
passHref
|
||||||
>
|
>
|
||||||
{q}
|
<a
|
||||||
|
className={`ml-1 transition-all duration-75 hover:opacity-70 md:ml-3 ${
|
||||||
|
i == 0 && 'pointer-events-none opacity-80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
))}
|
||||||
))}
|
<li className="flex-shrink-0 transition-all duration-75 hover:opacity-80">
|
||||||
</div>
|
<HomeCrumb />
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:text-gray-300 hover:opacity-80 pb-4 text-sm text-gray-600 transition-all duration-75">
|
<div className="text-sm text-gray-600 transition-all duration-75 hover:opacity-80 dark:text-gray-300">
|
||||||
<div>
|
<HomeCrumb />
|
||||||
<Link href="/">🚩 Home</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-305
@@ -1,20 +1,17 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import toast, { Toaster } from 'react-hot-toast'
|
import toast, { Toaster } from 'react-hot-toast'
|
||||||
import emojiRegex from 'emoji-regex'
|
import emojiRegex from 'emoji-regex'
|
||||||
import { useClipboard } from 'use-clipboard-copy'
|
|
||||||
|
|
||||||
import { ParsedUrlQuery } from 'querystring'
|
import { ParsedUrlQuery } from 'querystring'
|
||||||
import { FC, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react'
|
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 dynamic from 'next/dynamic'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { humanFileSize, formatModifiedDateTime } from '../utils/fileDetails'
|
import useLocalStorage from '../utils/useLocalStorage'
|
||||||
import { getExtension, getFileIcon } from '../utils/getFileIcon'
|
|
||||||
import { getPreviewType, preview } from '../utils/getPreviewType'
|
import { getPreviewType, preview } from '../utils/getPreviewType'
|
||||||
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
|
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
|
||||||
import { getBaseUrl } from '../utils/getBaseUrl'
|
import { getFileIcon } from '../utils/getFileIcon'
|
||||||
import {
|
import {
|
||||||
DownloadingToast,
|
DownloadingToast,
|
||||||
downloadMultipleFiles,
|
downloadMultipleFiles,
|
||||||
@@ -22,6 +19,7 @@ import {
|
|||||||
traverseFolder,
|
traverseFolder,
|
||||||
} from './MultiFileDownloader'
|
} from './MultiFileDownloader'
|
||||||
|
|
||||||
|
import { layouts } from './SwitchLayout'
|
||||||
import Loading, { LoadingIcon } from './Loading'
|
import Loading, { LoadingIcon } from './Loading'
|
||||||
import FourOhFour from './FourOhFour'
|
import FourOhFour from './FourOhFour'
|
||||||
import Auth from './Auth'
|
import Auth from './Auth'
|
||||||
@@ -33,14 +31,15 @@ import AudioPreview from './previews/AudioPreview'
|
|||||||
import VideoPreview from './previews/VideoPreview'
|
import VideoPreview from './previews/VideoPreview'
|
||||||
import PDFPreview from './previews/PDFPreview'
|
import PDFPreview from './previews/PDFPreview'
|
||||||
import URLPreview from './previews/URLPreview'
|
import URLPreview from './previews/URLPreview'
|
||||||
|
import ImagePreview from './previews/ImagePreview'
|
||||||
import DefaultPreview from './previews/DefaultPreview'
|
import DefaultPreview from './previews/DefaultPreview'
|
||||||
import { DownloadBtnContainer, PreviewContainer } from './previews/Containers'
|
import { PreviewContainer } from './previews/Containers'
|
||||||
import DownloadButtonGroup from './DownloadBtnGtoup'
|
|
||||||
|
|
||||||
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)
|
// Disabling SSR for some previews
|
||||||
const ReactViewer = dynamic(() => import('react-viewer'), { ssr: false })
|
|
||||||
const EPUBPreview = dynamic(() => import('./previews/EPUBPreview'), {
|
const EPUBPreview = dynamic(() => import('./previews/EPUBPreview'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
})
|
})
|
||||||
@@ -61,36 +60,25 @@ const queryToPath = (query?: ParsedUrlQuery) => {
|
|||||||
return '/'
|
return '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileListItem: FC<{ fileContent: OdFolderObject['value'][number] }> = ({ fileContent: c }) => {
|
// 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 emojiIcon = emojiRegex().exec(c.name)
|
const renderEmoji = (name: string) => {
|
||||||
const renderEmoji = emojiIcon && !emojiIcon.index
|
const emoji = emojiRegex().exec(name)
|
||||||
|
return { render: emoji && !emoji.index, emoji }
|
||||||
return (
|
}
|
||||||
<div className="grid items-center grid-cols-10 px-3 py-2.5 space-x-2 cursor-pointer">
|
export const formatChildName = (name: string) => {
|
||||||
<div className="md:col-span-6 flex items-center col-span-10 space-x-2 truncate">
|
const { render, emoji } = renderEmoji(name)
|
||||||
{/* <div>{c.file ? c.file.mimeType : 'folder'}</div> */}
|
return render ? name.replace(emoji ? emoji[0] : '', '').trim() : name
|
||||||
<div className="flex-shrink-0 w-5 text-center">
|
}
|
||||||
{renderEmoji ? (
|
export const ChildIcon: FC<{ child: OdFolderChildren }> = ({ child }) => {
|
||||||
<span>{emojiIcon ? emojiIcon[0] : '📁'}</span>
|
const { render, emoji } = renderEmoji(child.name)
|
||||||
) : (
|
return render ? (
|
||||||
<FontAwesomeIcon icon={c.file ? getFileIcon(c.name, { video: Boolean(c.video) }) : ['far', 'folder']} />
|
<span>{emoji ? emoji[0] : '📁'}</span>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<FontAwesomeIcon icon={child.file ? getFileIcon(child.name, { video: Boolean(child.video) }) : ['far', 'folder']} />
|
||||||
<div className="truncate">
|
|
||||||
{renderEmoji ? c.name.replace(emojiIcon ? emojiIcon[0] : '', '').trim() : c.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="md:block dark:text-gray-500 flex-shrink-0 hidden col-span-3 font-mono text-sm text-gray-700">
|
|
||||||
{formatModifiedDateTime(c.lastModifiedDateTime)}
|
|
||||||
</div>
|
|
||||||
<div className="md:block dark:text-gray-500 flex-shrink-0 hidden col-span-1 font-mono text-sm text-gray-700 truncate">
|
|
||||||
{humanFileSize(c.size)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Checkbox: FC<{
|
export const Checkbox: FC<{
|
||||||
checked: 0 | 1 | 2
|
checked: 0 | 1 | 2
|
||||||
onChange: () => void
|
onChange: () => void
|
||||||
title: string
|
title: string
|
||||||
@@ -120,7 +108,7 @@ const Checkbox: FC<{
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
title={title}
|
title={title}
|
||||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 p-1.5 inline-flex items-center rounded cursor-pointer"
|
className="inline-flex cursor-pointer items-center rounded p-1.5 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -135,21 +123,19 @@ const Checkbox: FC<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Downloading: FC<{ title: string }> = ({ title }) => {
|
export const Downloading: FC<{ title: string }> = ({ title }) => {
|
||||||
return (
|
return (
|
||||||
<span title={title} className="p-2 rounded" role="status">
|
<span title={title} className="rounded p-2" role="status">
|
||||||
<LoadingIcon
|
<LoadingIcon
|
||||||
// Use fontawesome far theme via class `svg-inline--fa` to get style `vertical-align` only
|
// Use fontawesome far theme via class `svg-inline--fa` to get style `vertical-align` only
|
||||||
// for consistent icon alignment, as class `align-*` cannot satisfy it
|
// for consistent icon alignment, as class `align-*` cannot satisfy it
|
||||||
className="animate-spin w-4 h-4 inline-block svg-inline--fa"
|
className="svg-inline--fa inline-block h-4 w-4 animate-spin"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
||||||
const [imageViewerVisible, setImageViewerVisibility] = useState(false)
|
|
||||||
const [activeImageIdx, setActiveImageIdx] = useState(0)
|
|
||||||
const [selected, setSelected] = useState<{ [key: string]: boolean }>({})
|
const [selected, setSelected] = useState<{ [key: string]: boolean }>({})
|
||||||
const [totalSelected, setTotalSelected] = useState<0 | 1 | 2>(0)
|
const [totalSelected, setTotalSelected] = useState<0 | 1 | 2>(0)
|
||||||
const [totalGenerating, setTotalGenerating] = useState<boolean>(false)
|
const [totalGenerating, setTotalGenerating] = useState<boolean>(false)
|
||||||
@@ -158,19 +144,17 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const clipboard = useClipboard()
|
const [layout, _] = useLocalStorage('preferredLayout', layouts[0])
|
||||||
|
|
||||||
const path = queryToPath(query)
|
const path = queryToPath(query)
|
||||||
|
|
||||||
const { data, error, size, setSize } = useProtectedSWRInfinite(path)
|
const { data, error, size, setSize } = useProtectedSWRInfinite(path)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// If error includes 403 which means the user has not completed initial setup, redirect to OAuth page
|
// If error includes 403 which means the user has not completed initial setup, redirect to OAuth page
|
||||||
if (error.status === 403) {
|
if (error.status === 403) {
|
||||||
router.push('/onedrive-vercel-index-oauth/step-1')
|
router.push('/onedrive-vercel-index-oauth/step-1')
|
||||||
return <div></div>
|
return <div />
|
||||||
}
|
}
|
||||||
|
|
||||||
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 responses: any[] = data ? [].concat(...data) : []
|
||||||
|
|
||||||
const isLoadingInitialData = !data && !error
|
const isLoadingInitialData = !data && !error
|
||||||
@@ -204,37 +180,14 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
const onlyOnePage = data && typeof data[0].next === 'undefined'
|
const onlyOnePage = data && typeof data[0].next === 'undefined'
|
||||||
|
|
||||||
if ('folder' in responses[0]) {
|
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
|
// 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 => {
|
// Find README.md file to render
|
||||||
if (fileIsImage(c.name)) {
|
const readmeFile = folderChildren.find(c => c.name.toLowerCase() === 'readme.md')
|
||||||
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
|
// 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
|
// File selection
|
||||||
const genTotalSelected = (selected: { [key: string]: boolean }) => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:bg-gray-900 dark:text-gray-100 bg-white rounded">
|
<Toaster />
|
||||||
<div className="dark:border-gray-500/30 grid items-center grid-cols-12 px-3 space-x-2 border-b border-gray-900/10">
|
|
||||||
<div className="md:col-span-6 col-span-12 font-bold py-2 text-gray-600 dark:text-gray-300 uppercase tracking-widest text-xs">
|
|
||||||
Name
|
|
||||||
</div>
|
|
||||||
<div className="md:block hidden col-span-3 font-bold text-gray-600 dark:text-gray-300 uppercase tracking-widest text-xs">
|
|
||||||
Last Modified
|
|
||||||
</div>
|
|
||||||
<div className="md:block hidden font-bold text-gray-600 dark:text-gray-300 uppercase tracking-widest text-xs">
|
|
||||||
Size
|
|
||||||
</div>
|
|
||||||
<div className="md:block hidden font-bold text-gray-600 dark:text-gray-300 uppercase tracking-widest text-xs">
|
|
||||||
Actions
|
|
||||||
</div>
|
|
||||||
<div className="md:block hidden font-bold text-gray-600 dark:text-gray-300 uppercase tracking-widest text-xs">
|
|
||||||
<div className="md:flex dark:text-gray-400 hidden p-1.5 text-gray-700">
|
|
||||||
<Checkbox
|
|
||||||
checked={totalSelected}
|
|
||||||
onChange={toggleTotalSelected}
|
|
||||||
indeterminate={true}
|
|
||||||
title={'Select files'}
|
|
||||||
/>
|
|
||||||
{totalGenerating ? (
|
|
||||||
<Downloading title="Downloading selected files, refresh page to cancel" />
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
title="Download selected files"
|
|
||||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 p-1.5 rounded cursor-pointer disabled:text-gray-400 disabled:dark:text-gray-600 disabled:hover:bg-white disabled:hover:dark:bg-gray-900 disabled:cursor-not-allowed"
|
|
||||||
disabled={totalSelected === 0}
|
|
||||||
onClick={handleSelectedDownload}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} size="lg" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Toaster />
|
{layout.name === 'Grid' ? <FolderGridLayout {...folderProps} /> : <FolderListLayout {...folderProps} />}
|
||||||
|
|
||||||
{imagesInFolder.length !== 0 && (
|
{!onlyOnePage && (
|
||||||
<ReactViewer
|
<div className="rounded-b bg-white dark:bg-gray-900 dark:text-gray-100">
|
||||||
zIndex={99}
|
<div className="border-b border-gray-200 p-3 text-center font-mono text-sm text-gray-400 dark:border-gray-700">
|
||||||
visible={imageViewerVisible}
|
- showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : folderChildren.length} files -
|
||||||
activeIndex={activeImageIdx}
|
</div>
|
||||||
images={imagesInFolder}
|
<button
|
||||||
drag={false}
|
className={`flex w-full items-center justify-center space-x-2 p-3 disabled:cursor-not-allowed ${
|
||||||
rotatable={false}
|
isLoadingMore || isReachingEnd ? 'opacity-60' : 'hover:bg-gray-100 dark:hover:bg-gray-850'
|
||||||
noClose={true}
|
}`}
|
||||||
scalable={false}
|
onClick={() => setSize(size + 1)}
|
||||||
zoomSpeed={0.2}
|
disabled={isLoadingMore || isReachingEnd}
|
||||||
downloadable={true}
|
>
|
||||||
downloadInNewWindow={true}
|
{isLoadingMore ? (
|
||||||
onMaskClick={() => {
|
<>
|
||||||
setImageViewerVisibility(false)
|
<LoadingIcon className="inline-block h-4 w-4 animate-spin" />
|
||||||
}}
|
<span>Loading ...</span>{' '}
|
||||||
customToolbar={toolbars => {
|
</>
|
||||||
toolbars[0].render = <FontAwesomeIcon icon="plus" />
|
) : isReachingEnd ? (
|
||||||
toolbars[1].render = <FontAwesomeIcon icon="minus" />
|
<span>No more files</span>
|
||||||
toolbars[2].render = <FontAwesomeIcon icon="arrow-left" />
|
|
||||||
toolbars[3].render = <FontAwesomeIcon icon="undo" />
|
|
||||||
toolbars[4].render = <FontAwesomeIcon icon="arrow-right" />
|
|
||||||
toolbars[9].render = <FontAwesomeIcon icon="download" />
|
|
||||||
return toolbars.concat([
|
|
||||||
{
|
|
||||||
key: 'copy',
|
|
||||||
render: <FontAwesomeIcon icon={['fas', 'copy']} />,
|
|
||||||
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 => (
|
|
||||||
<div className="hover:bg-gray-100 dark:hover:bg-gray-850 grid grid-cols-12" key={c.id}>
|
|
||||||
<div
|
|
||||||
className="col-span-10"
|
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (!c.folder && fileIsImage(c.name)) {
|
|
||||||
setActiveImageIdx(imageIndexDict[c.id])
|
|
||||||
setImageViewerVisibility(true)
|
|
||||||
} else {
|
|
||||||
router.push(`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FileListItem fileContent={c} />
|
|
||||||
</div>
|
|
||||||
{c.folder ? (
|
|
||||||
<div className="md:flex dark:text-gray-400 hidden p-1.5 text-gray-700">
|
|
||||||
<span
|
|
||||||
title="Copy folder permalink"
|
|
||||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 px-1.5 py-1 rounded cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.copy(`${getBaseUrl()}${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`)
|
|
||||||
toast('Copied folder permalink.', { icon: '👌' })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={['far', 'copy']} />
|
|
||||||
</span>
|
|
||||||
{folderGenerating[c.id] ? (
|
|
||||||
<Downloading title="Downloading folder, refresh page to cancel" />
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
title="Download folder"
|
|
||||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 px-1.5 py-1 rounded cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const p = `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
|
|
||||||
handleFolderDownload(p, c.id, c.name)()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="md:flex dark:text-gray-400 hidden p-1.5 text-gray-700">
|
<>
|
||||||
<span
|
<span>Load more</span>
|
||||||
title="Copy raw file permalink"
|
<FontAwesomeIcon icon="chevron-circle-down" />
|
||||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 px-1.5 py-1 rounded cursor-pointer"
|
</>
|
||||||
onClick={() => {
|
|
||||||
clipboard.copy(
|
|
||||||
`${getBaseUrl()}/api?path=${path === '/' ? '' : path}/${encodeURIComponent(c.name)}&raw=true`
|
|
||||||
)
|
|
||||||
toast.success('Copied raw file permalink.')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={['far', 'copy']} />
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
title="Download file"
|
|
||||||
className="hover:bg-gray-300 dark:hover:bg-gray-600 px-1.5 py-1 rounded cursor-pointer"
|
|
||||||
href={c['@microsoft.graph.downloadUrl']}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="md:flex dark:text-gray-400 hidden p-1.5 text-gray-700">
|
</button>
|
||||||
{!c.folder && !(c.name === '.password') && (
|
</div>
|
||||||
<Checkbox
|
)}
|
||||||
checked={selected[c.id] ? 2 : 0}
|
|
||||||
onChange={() => toggleItemSelected(c.id)}
|
|
||||||
title="Select file"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!onlyOnePage && (
|
{readmeFile && (
|
||||||
<div>
|
|
||||||
<div className="dark:border-gray-700 p-3 font-mono text-sm text-center text-gray-400 border-b border-gray-200">
|
|
||||||
- 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 disabled:cursor-not-allowed ${
|
|
||||||
isLoadingMore || isReachingEnd ? 'opacity-60' : 'hover:bg-gray-100 dark:hover:bg-gray-850'
|
|
||||||
}`}
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{renderReadme && (
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<MarkdownPreview file={readmeFile} path={path} standalone={false} />
|
<MarkdownPreview file={readmeFile} path={path} standalone={false} />
|
||||||
</div>
|
</div>
|
||||||
@@ -558,29 +353,12 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
const fileName = file.name
|
const fileName = file.name
|
||||||
const fileExtension = fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase()
|
const fileExtension = fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase()
|
||||||
|
|
||||||
const previewType = getPreviewType(fileExtension, {
|
const previewType = getPreviewType(fileExtension, { video: Boolean(file.video) })
|
||||||
video: Boolean(file.video),
|
|
||||||
})
|
|
||||||
if (previewType) {
|
if (previewType) {
|
||||||
switch (previewType) {
|
switch (previewType) {
|
||||||
case preview.image:
|
case preview.image:
|
||||||
return (
|
return <ImagePreview file={file} />
|
||||||
<>
|
|
||||||
<PreviewContainer>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
className="mx-auto"
|
|
||||||
src={downloadUrl}
|
|
||||||
alt={fileName}
|
|
||||||
width={file.image?.width}
|
|
||||||
height={file.image?.height}
|
|
||||||
/>
|
|
||||||
</PreviewContainer>
|
|
||||||
<DownloadBtnContainer>
|
|
||||||
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
|
||||||
</DownloadBtnContainer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
case preview.text:
|
case preview.text:
|
||||||
return <TextPreview file={file} />
|
return <TextPreview file={file} />
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-32 overflow-hidden rounded border border-gray-900/10 dark:border-gray-500/30">
|
||||||
|
{thumbnail && !brokenThumbnail ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
className="h-full w-full object-cover object-top"
|
||||||
|
src={thumbnail.url}
|
||||||
|
alt={c.name}
|
||||||
|
onError={() => setBrokenThumbnail(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative flex h-full w-full items-center justify-center rounded-lg">
|
||||||
|
<ChildIcon child={c} />
|
||||||
|
<span className="absolute bottom-0 right-0 m-1 font-medium text-gray-700 dark:text-gray-500">
|
||||||
|
{c.folder?.childCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start justify-center space-x-2">
|
||||||
|
<span className="w-5 flex-shrink-0 text-center">
|
||||||
|
<ChildIcon child={c} />
|
||||||
|
</span>
|
||||||
|
<span className="overflow-hidden truncate">{formatChildName(c.name)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="truncate text-center font-mono text-xs text-gray-700 dark:text-gray-500">
|
||||||
|
{formatModifiedDateTime(c.lastModifiedDateTime)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FolderGridLayout = ({
|
||||||
|
path,
|
||||||
|
folderChildren,
|
||||||
|
selected,
|
||||||
|
toggleItemSelected,
|
||||||
|
totalSelected,
|
||||||
|
toggleTotalSelected,
|
||||||
|
totalGenerating,
|
||||||
|
handleSelectedDownload,
|
||||||
|
folderGenerating,
|
||||||
|
handleFolderDownload,
|
||||||
|
toast,
|
||||||
|
}) => {
|
||||||
|
const clipboard = useClipboard()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100">
|
||||||
|
<div className="flex items-center border-b border-gray-900/10 px-3 text-xs font-bold uppercase tracking-widest text-gray-600 dark:border-gray-500/30 dark:text-gray-400">
|
||||||
|
<div className="flex-1">{folderChildren.length} items</div>
|
||||||
|
<div className="flex p-1.5 text-gray-700 dark:text-gray-400">
|
||||||
|
<Checkbox
|
||||||
|
checked={totalSelected}
|
||||||
|
onChange={toggleTotalSelected}
|
||||||
|
indeterminate={true}
|
||||||
|
title={'Select all files'}
|
||||||
|
/>
|
||||||
|
{totalGenerating ? (
|
||||||
|
<Downloading title="Downloading selected files, refresh page to cancel" />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
title="Download selected files"
|
||||||
|
className="cursor-pointer rounded p-1.5 hover:bg-gray-300 disabled:cursor-not-allowed disabled:text-gray-400 disabled:hover:bg-white dark:hover:bg-gray-600 disabled:dark:text-gray-600 disabled:hover:dark:bg-gray-900"
|
||||||
|
disabled={totalSelected === 0}
|
||||||
|
onClick={handleSelectedDownload}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} size="lg" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 p-3 md:grid-cols-4">
|
||||||
|
{folderChildren.map((c: OdFolderChildren) => (
|
||||||
|
<div
|
||||||
|
key={c.id}
|
||||||
|
className="group relative overflow-hidden rounded transition-all duration-100 hover:bg-gray-100 dark:hover:bg-gray-850"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 z-10 m-1 rounded bg-white/50 py-0.5 opacity-0 transition-all duration-100 group-hover:opacity-100 dark:bg-gray-900/50">
|
||||||
|
{c.folder ? (
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
title="Copy folder permalink"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
clipboard.copy(`${getBaseUrl()}${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`)
|
||||||
|
toast('Copied folder permalink.', { icon: '👌' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||||
|
</span>
|
||||||
|
{folderGenerating[c.id] ? (
|
||||||
|
<Downloading title="Downloading folder, refresh page to cancel" />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
title="Download folder"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
const p = `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
|
||||||
|
handleFolderDownload(p, c.id, c.name)()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
title="Copy raw file permalink"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
clipboard.copy(
|
||||||
|
`${getBaseUrl()}/api?path=${path === '/' ? '' : path}/${encodeURIComponent(c.name)}&raw=true`
|
||||||
|
)
|
||||||
|
toast.success('Copied raw file permalink.')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
title="Download file"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
href={c['@microsoft.graph.downloadUrl']}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
selected[c.id] ? 'opacity-100' : 'opacity-0'
|
||||||
|
} absolute top-0 left-0 z-10 m-1 rounded bg-white/50 py-0.5 group-hover:opacity-100 dark:bg-gray-900/50`}
|
||||||
|
>
|
||||||
|
{!c.folder && !(c.name === '.password') && (
|
||||||
|
<Checkbox
|
||||||
|
checked={selected[c.id] ? 2 : 0}
|
||||||
|
onChange={() => toggleItemSelected(c.id)}
|
||||||
|
title="Select file"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href={`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`} passHref>
|
||||||
|
<a>
|
||||||
|
<GridItem c={c} />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FolderGridLayout
|
||||||
@@ -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 (
|
||||||
|
<div className="grid cursor-pointer grid-cols-10 items-center space-x-2 px-3 py-2.5">
|
||||||
|
<div className="col-span-10 flex items-center space-x-2 truncate md:col-span-6" title={c.name}>
|
||||||
|
<div className="w-5 flex-shrink-0 text-center">
|
||||||
|
<ChildIcon child={c} />
|
||||||
|
</div>
|
||||||
|
<div className="truncate">{formatChildName(c.name)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 hidden flex-shrink-0 font-mono text-sm text-gray-700 dark:text-gray-500 md:block">
|
||||||
|
{formatModifiedDateTime(c.lastModifiedDateTime)}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 hidden flex-shrink-0 truncate font-mono text-sm text-gray-700 dark:text-gray-500 md:block">
|
||||||
|
{humanFileSize(c.size)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FolderListLayout = ({
|
||||||
|
path,
|
||||||
|
folderChildren,
|
||||||
|
selected,
|
||||||
|
toggleItemSelected,
|
||||||
|
totalSelected,
|
||||||
|
toggleTotalSelected,
|
||||||
|
totalGenerating,
|
||||||
|
handleSelectedDownload,
|
||||||
|
folderGenerating,
|
||||||
|
handleFolderDownload,
|
||||||
|
toast,
|
||||||
|
}) => {
|
||||||
|
const clipboard = useClipboard()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded bg-white dark:bg-gray-900 dark:text-gray-100">
|
||||||
|
<div className="grid grid-cols-12 items-center space-x-2 border-b border-gray-900/10 px-3 dark:border-gray-500/30">
|
||||||
|
<div className="col-span-12 py-2 text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:col-span-6">
|
||||||
|
Name
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
|
||||||
|
Last Modified
|
||||||
|
</div>
|
||||||
|
<div className="hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
|
||||||
|
Size
|
||||||
|
</div>
|
||||||
|
<div className="hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
<div className="hidden text-xs font-bold uppercase tracking-widest text-gray-600 dark:text-gray-300 md:block">
|
||||||
|
<div className="hidden p-1.5 text-gray-700 dark:text-gray-400 md:flex">
|
||||||
|
<Checkbox
|
||||||
|
checked={totalSelected}
|
||||||
|
onChange={toggleTotalSelected}
|
||||||
|
indeterminate={true}
|
||||||
|
title={'Select files'}
|
||||||
|
/>
|
||||||
|
{totalGenerating ? (
|
||||||
|
<Downloading title="Downloading selected files, refresh page to cancel" />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
title="Download selected files"
|
||||||
|
className="cursor-pointer rounded p-1.5 hover:bg-gray-300 disabled:cursor-not-allowed disabled:text-gray-400 disabled:hover:bg-white dark:hover:bg-gray-600 disabled:dark:text-gray-600 disabled:hover:dark:bg-gray-900"
|
||||||
|
disabled={totalSelected === 0}
|
||||||
|
onClick={handleSelectedDownload}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} size="lg" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{folderChildren.map((c: OdFolderChildren) => (
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-12 transition-all duration-100 hover:bg-gray-100 dark:hover:bg-gray-850"
|
||||||
|
key={c.id}
|
||||||
|
>
|
||||||
|
<Link href={`${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`} passHref>
|
||||||
|
<a className="col-span-10">
|
||||||
|
<FileListItem fileContent={c} />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{c.folder ? (
|
||||||
|
<div className="hidden p-1.5 text-gray-700 dark:text-gray-400 md:flex">
|
||||||
|
<span
|
||||||
|
title="Copy folder permalink"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
clipboard.copy(`${getBaseUrl()}${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`)
|
||||||
|
toast('Copied folder permalink.', { icon: '👌' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||||
|
</span>
|
||||||
|
{folderGenerating[c.id] ? (
|
||||||
|
<Downloading title="Downloading folder, refresh page to cancel" />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
title="Download folder"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
const p = `${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`
|
||||||
|
handleFolderDownload(p, c.id, c.name)()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="hidden p-1.5 text-gray-700 dark:text-gray-400 md:flex">
|
||||||
|
<span
|
||||||
|
title="Copy raw file permalink"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
clipboard.copy(
|
||||||
|
`${getBaseUrl()}/api?path=${path === '/' ? '' : path}/${encodeURIComponent(c.name)}&raw=true`
|
||||||
|
)
|
||||||
|
toast.success('Copied raw file permalink.')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'copy']} />
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
title="Download file"
|
||||||
|
className="cursor-pointer rounded px-1.5 py-1 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||||
|
href={c['@microsoft.graph.downloadUrl']}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={['far', 'arrow-alt-circle-down']} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="hidden p-1.5 text-gray-700 dark:text-gray-400 md:flex">
|
||||||
|
{!c.folder && !(c.name === '.password') && (
|
||||||
|
<Checkbox
|
||||||
|
checked={selected[c.id] ? 2 : 0}
|
||||||
|
onChange={() => toggleItemSelected(c.id)}
|
||||||
|
title="Select file"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FolderListLayout
|
||||||
+21
-21
@@ -51,34 +51,34 @@ const Navbar = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-900 dark:border-gray-500/30 sticky top-0 bg-opacity-80 border-b border-gray-900/10 backdrop-blur-md z-[100]">
|
<div className="sticky top-0 z-[100] border-b border-gray-900/10 bg-white bg-opacity-80 backdrop-blur-md dark:border-gray-500/30 dark:bg-gray-900">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
||||||
<SearchModal searchOpen={searchOpen} setSearchOpen={setSearchOpen} />
|
<SearchModal searchOpen={searchOpen} setSearchOpen={setSearchOpen} />
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 justify-between w-full mx-auto px-4 py-1">
|
<div className="mx-auto flex w-full items-center justify-between space-x-4 px-4 py-1">
|
||||||
<Link href="/" passHref>
|
<Link href="/" passHref>
|
||||||
<a className="dark:text-white hover:opacity-80 flex items-center py-2 md:p-2 space-x-2">
|
<a className="flex items-center space-x-2 py-2 hover:opacity-80 dark:text-white md:p-2">
|
||||||
<Image src={siteConfig.icon} alt="icon" width="25" height="25" priority />
|
<Image src={siteConfig.icon} alt="icon" width="25" height="25" priority />
|
||||||
<span className="sm:block hidden font-bold">{siteConfig.title}</span>
|
<span className="hidden font-bold sm:block">{siteConfig.title}</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center flex-1 md:flex-initial space-x-4 text-gray-700">
|
<div className="flex flex-1 items-center space-x-4 text-gray-700 md:flex-initial">
|
||||||
<button
|
<button
|
||||||
className="flex-1 flex items-center justify-between rounded-lg bg-gray-100 dark:bg-gray-800 md:w-48 px-2.5 py-1.5 dark:text-white hover:opacity-80"
|
className="flex flex-1 items-center justify-between rounded-lg bg-gray-100 px-2.5 py-1.5 hover:opacity-80 dark:bg-gray-800 dark:text-white md:w-48"
|
||||||
onClick={openSearchBox}
|
onClick={openSearchBox}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<FontAwesomeIcon icon="search" />
|
<FontAwesomeIcon className="h-4 w-4" icon="search" />
|
||||||
<span className="text-sm font-medium">Search ...</span>
|
<span className="text-sm font-medium">Search ...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<div className="px-2 py-1 rounded-lg bg-gray-200 dark:bg-gray-700 font-medium text-xs">
|
<div className="rounded-lg bg-gray-200 px-2 py-1 text-xs font-medium dark:bg-gray-700">
|
||||||
{os === 'mac' ? '⌘' : 'Ctrl'}
|
{os === 'mac' ? '⌘' : 'Ctrl'}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-1 rounded-lg bg-gray-200 dark:bg-gray-700 font-medium text-xs">K</div>
|
<div className="rounded-lg bg-gray-200 px-2 py-1 text-xs font-medium dark:bg-gray-700">K</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -89,23 +89,23 @@ const Navbar = () => {
|
|||||||
href={l.link}
|
href={l.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={['fab', l.name.toLowerCase() as IconName]} />
|
<FontAwesomeIcon icon={['fab', l.name.toLowerCase() as IconName]} />
|
||||||
<span className="text-sm font-medium hidden md:inline-block">{l.name}</span>
|
<span className="hidden text-sm font-medium md:inline-block">{l.name}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{siteConfig.email && (
|
{siteConfig.email && (
|
||||||
<a href={siteConfig.email} className="flex items-center space-x-2 dark:text-white hover:opacity-80">
|
<a href={siteConfig.email} className="flex items-center space-x-2 hover:opacity-80 dark:text-white">
|
||||||
<FontAwesomeIcon icon={['far', 'envelope']} />
|
<FontAwesomeIcon icon={['far', 'envelope']} />
|
||||||
<span className="text-sm font-medium hidden md:inline-block">Email</span>
|
<span className="hidden text-sm font-medium md:inline-block">Email</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tokenPresent && (
|
{tokenPresent && (
|
||||||
<button
|
<button
|
||||||
className="dark:text-white flex items-center p-2 space-x-2 hover:opacity-80"
|
className="flex items-center space-x-2 p-2 hover:opacity-80 dark:text-white"
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium">Logout</span>
|
<span className="text-sm font-medium">Logout</span>
|
||||||
@@ -127,7 +127,7 @@ const Navbar = () => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Dialog.Overlay className="bg-gray-50 dark:bg-gray-800 fixed inset-0" />
|
<Dialog.Overlay className="fixed inset-0 bg-gray-50 dark:bg-gray-800" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
{/* This element is to trick the browser into centering the modal contents. */}
|
{/* This element is to trick the browser into centering the modal contents. */}
|
||||||
@@ -143,8 +143,8 @@ const Navbar = () => {
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<div className="dark:bg-gray-900 inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white rounded-lg">
|
<div className="my-8 inline-block w-full max-w-md transform overflow-hidden rounded-lg bg-white p-6 text-left align-middle transition-all dark:bg-gray-900">
|
||||||
<Dialog.Title className="dark:text-gray-100 text-lg font-bold text-gray-900">
|
<Dialog.Title className="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
Clear all tokens?
|
Clear all tokens?
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -154,7 +154,7 @@ const Navbar = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dark:text-gray-100 max-h-32 mt-4 overflow-y-scroll font-mono text-sm">
|
<div className="mt-4 max-h-32 overflow-y-scroll font-mono text-sm dark:text-gray-100">
|
||||||
{siteConfig.protectedRoutes.map((r, i) => (
|
{siteConfig.protectedRoutes.map((r, i) => (
|
||||||
<div key={i} className="flex items-center space-x-1">
|
<div key={i} className="flex items-center space-x-1">
|
||||||
<FontAwesomeIcon icon="key" />
|
<FontAwesomeIcon icon="key" />
|
||||||
@@ -163,15 +163,15 @@ const Navbar = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end mt-8">
|
<div className="mt-8 flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
className="focus:outline-none focus:ring focus:ring-blue-300 hover:bg-blue-400 inline-flex items-center justify-center px-4 py-2 mr-3 space-x-2 text-white bg-blue-500 rounded"
|
className="mr-3 inline-flex items-center justify-center space-x-2 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="focus:outline-none focus:ring focus:ring-red-300 hover:bg-red-400 inline-flex items-center justify-center px-4 py-2 space-x-2 text-white bg-red-500 rounded"
|
className="inline-flex items-center justify-center space-x-2 rounded bg-red-500 px-4 py-2 text-white hover:bg-red-400 focus:outline-none focus:ring focus:ring-red-300"
|
||||||
onClick={() => clearTokens()}
|
onClick={() => clearTokens()}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={['far', 'trash-alt']} />
|
<FontAwesomeIcon icon={['far', 'trash-alt']} />
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Fragment } from 'react'
|
||||||
|
import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { Listbox, Transition } from '@headlessui/react'
|
||||||
|
|
||||||
|
import useLocalStorage from '../utils/useLocalStorage'
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
const [preferredLayout, setPreferredLayout] = useLocalStorage('preferredLayout', layouts[0])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-24 flex-shrink-0 text-sm text-gray-600 dark:text-gray-300 md:w-28">
|
||||||
|
<Listbox value={preferredLayout} onChange={setPreferredLayout}>
|
||||||
|
<Listbox.Button className="relative w-full cursor-pointer rounded pl-2">
|
||||||
|
<span className="pointer-events-none flex items-center">
|
||||||
|
<FontAwesomeIcon className="mr-2 h-3 w-3" icon={preferredLayout.icon} />
|
||||||
|
<span>{preferredLayout.name}</span>
|
||||||
|
</span>
|
||||||
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<FontAwesomeIcon className="h-3 w-3" icon="chevron-down" />
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
|
||||||
|
<Listbox.Options className="absolute right-0 z-20 mt-1 w-32 overflow-auto rounded border border-gray-900/10 bg-white py-1 shadow-lg focus:outline-none dark:border-gray-500/30 dark:bg-gray-800">
|
||||||
|
{layouts.map(layout => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={layout.id}
|
||||||
|
className={`${
|
||||||
|
layout.name === preferredLayout.name &&
|
||||||
|
'bg-blue-50 text-blue-700 dark:bg-blue-600/10 dark:text-blue-400'
|
||||||
|
} relative flex cursor-pointer select-none items-center py-1.5 pl-3 text-gray-600 hover:opacity-80 dark:text-gray-300`}
|
||||||
|
value={layout}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="mr-2 h-3 w-3" icon={layout.icon} />
|
||||||
|
<span className={layout.name === preferredLayout.name ? 'font-medium' : 'font-normal'}>
|
||||||
|
{layout.name}
|
||||||
|
</span>
|
||||||
|
{layout.name === preferredLayout.name && (
|
||||||
|
<span className="absolute inset-y-0 right-3 flex items-center">
|
||||||
|
<FontAwesomeIcon className="h-3 w-3" icon="check" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,9 +14,9 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
<div>
|
<div>
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
<div className="items-center px-5 py-4 md:flex md:space-x-8">
|
<div className="items-center px-5 py-4 md:flex md:space-x-8">
|
||||||
<div className="rounded-lg border border-gray-900/10 px-10 py-20 text-center dark:border-gray-500/30">
|
<div className="rounded-lg border border-gray-900/10 px-8 py-20 text-center dark:border-gray-500/30">
|
||||||
<FontAwesomeIcon icon={getFileIcon(file.name, { video: Boolean(file.video) })} />
|
<FontAwesomeIcon icon={getFileIcon(file.name, { video: Boolean(file.video) })} />
|
||||||
<div className="mt-6 overflow-hidden truncate text-sm font-medium md:w-20">{file.name}</div>
|
<div className="mt-6 text-sm font-medium line-clamp-3 md:w-28">{file.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-2 py-4 md:flex-1">
|
<div className="flex flex-col space-y-2 py-4 md:flex-1">
|
||||||
@@ -32,7 +32,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="py-2 text-xs font-medium uppercase opacity-80">MIME type</div>
|
<div className="py-2 text-xs font-medium uppercase opacity-80">MIME type</div>
|
||||||
<div>{file.file.mimeType}</div>
|
<div>{file.file?.mimeType || 'Unavailable'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -44,7 +44,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
Quick XOR
|
Quick XOR
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
||||||
{file.file.hashes.quickXorHash}
|
{file.file.hashes?.quickXorHash || 'Unavailable'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
||||||
@@ -52,7 +52,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
SHA1
|
SHA1
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
||||||
{file.file.hashes.sha1Hash}
|
{file.file.hashes?.sha1Hash || 'Unavailable'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
<tr className="border-y bg-white dark:border-gray-700 dark:bg-gray-900">
|
||||||
@@ -60,7 +60,7 @@ const DefaultPreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
|||||||
SHA256
|
SHA256
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
<td className="whitespace-nowrap py-1 px-3 font-mono text-gray-500 dark:text-gray-400">
|
||||||
{file.file.hashes.sha256Hash}
|
{file.file.hashes?.sha256Hash || 'Unavailable'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { OdFileObject } from '../../types'
|
||||||
|
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
import { PreviewContainer, DownloadBtnContainer } from './Containers'
|
||||||
|
import DownloadButtonGroup from '../DownloadBtnGtoup'
|
||||||
|
|
||||||
|
const ImagePreview: FC<{ file: OdFileObject }> = ({ file }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PreviewContainer>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
className="mx-auto"
|
||||||
|
src={file['@microsoft.graph.downloadUrl']}
|
||||||
|
alt={file.name}
|
||||||
|
width={file.image?.width}
|
||||||
|
height={file.image?.height}
|
||||||
|
/>
|
||||||
|
</PreviewContainer>
|
||||||
|
<DownloadBtnContainer>
|
||||||
|
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||||
|
</DownloadBtnContainer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImagePreview
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"@headlessui/react": "^1.4.0",
|
"@headlessui/react": "^1.4.0",
|
||||||
|
"@tailwindcss/line-clamp": "^0.3.1",
|
||||||
"awesome-debounce-promise": "^2.1.0",
|
"awesome-debounce-promise": "^2.1.0",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
+8
-4
@@ -6,20 +6,24 @@ import Navbar from '../components/Navbar'
|
|||||||
import FileListing from '../components/FileListing'
|
import FileListing from '../components/FileListing'
|
||||||
import Footer from '../components/Footer'
|
import Footer from '../components/Footer'
|
||||||
import Breadcrumb from '../components/Breadcrumb'
|
import Breadcrumb from '../components/Breadcrumb'
|
||||||
|
import { SwitchLayout } from '../components/SwitchLayout'
|
||||||
|
|
||||||
export default function Folders() {
|
export default function Folders() {
|
||||||
const { query } = useRouter()
|
const { query } = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-gray-900 flex flex-col items-center justify-center min-h-screen bg-white">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{siteConfig.title}</title>
|
<title>{siteConfig.title}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<main className="bg-gray-50 dark:bg-gray-800 flex flex-col flex-1 w-full">
|
<main className="flex w-full flex-1 flex-col bg-gray-50 dark:bg-gray-800">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="w-full max-w-5xl p-4 mx-auto">
|
<div className="mx-auto w-full max-w-5xl p-4">
|
||||||
<Breadcrumb query={query} />
|
<nav className="mb-4 flex items-center justify-between pl-1 space-x-3">
|
||||||
|
<Breadcrumb query={query} />
|
||||||
|
<SwitchLayout />
|
||||||
|
</nav>
|
||||||
<FileListing query={query} />
|
<FileListing query={query} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
faArrowAltCircleDown,
|
faArrowAltCircleDown,
|
||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
|
faFlag,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
faPlus,
|
faPlus,
|
||||||
faMinus,
|
faMinus,
|
||||||
faCopy as faCopySolid,
|
faCopy as faCopySolid,
|
||||||
|
faAngleRight,
|
||||||
faDownload,
|
faDownload,
|
||||||
faMusic,
|
faMusic,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
@@ -39,10 +41,14 @@ import {
|
|||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
faCloud,
|
faCloud,
|
||||||
faChevronCircleDown,
|
faChevronCircleDown,
|
||||||
|
faChevronDown,
|
||||||
faLink,
|
faLink,
|
||||||
faExternalLinkAlt,
|
faExternalLinkAlt,
|
||||||
faExclamationCircle,
|
faExclamationCircle,
|
||||||
faExclamationTriangle,
|
faExclamationTriangle,
|
||||||
|
faTh,
|
||||||
|
faThLarge,
|
||||||
|
faThList,
|
||||||
faHome,
|
faHome,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import * as Icons from '@fortawesome/free-brands-svg-icons'
|
import * as Icons from '@fortawesome/free-brands-svg-icons'
|
||||||
@@ -67,10 +73,12 @@ library.add(
|
|||||||
faFileCode,
|
faFileCode,
|
||||||
faFileAlt,
|
faFileAlt,
|
||||||
faFile,
|
faFile,
|
||||||
|
faFlag,
|
||||||
faFolder,
|
faFolder,
|
||||||
faMusic,
|
faMusic,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
|
faAngleRight,
|
||||||
faFileDownload,
|
faFileDownload,
|
||||||
faCopy,
|
faCopy,
|
||||||
faCopySolid,
|
faCopySolid,
|
||||||
@@ -94,6 +102,10 @@ library.add(
|
|||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faSearch,
|
faSearch,
|
||||||
|
faChevronDown,
|
||||||
|
faTh,
|
||||||
|
faThLarge,
|
||||||
|
faThList,
|
||||||
...iconList
|
...iconList
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -220,11 +220,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
params: next
|
params: next
|
||||||
? {
|
? {
|
||||||
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image',
|
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image',
|
||||||
|
$expand: 'thumbnails',
|
||||||
top: siteConfig.maxItems,
|
top: siteConfig.maxItems,
|
||||||
$skipToken: next,
|
$skipToken: next,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image',
|
select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file,video,image',
|
||||||
|
$expand: 'thumbnails',
|
||||||
top: siteConfig.maxItems,
|
top: siteConfig.maxItems,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+8
-4
@@ -5,18 +5,22 @@ import Navbar from '../components/Navbar'
|
|||||||
import FileListing from '../components/FileListing'
|
import FileListing from '../components/FileListing'
|
||||||
import Footer from '../components/Footer'
|
import Footer from '../components/Footer'
|
||||||
import Breadcrumb from '../components/Breadcrumb'
|
import Breadcrumb from '../components/Breadcrumb'
|
||||||
|
import { SwitchLayout } from '../components/SwitchLayout'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-gray-900 flex flex-col items-center justify-center min-h-screen bg-white">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{siteConfig.title}</title>
|
<title>{siteConfig.title}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<main className="bg-gray-50 dark:bg-gray-800 flex flex-col flex-1 w-full">
|
<main className="flex w-full flex-1 flex-col bg-gray-50 dark:bg-gray-800">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="w-full max-w-5xl p-4 mx-auto">
|
<div className="mx-auto w-full max-w-5xl p-4">
|
||||||
<Breadcrumb />
|
<nav className="mb-4 flex items-center justify-between pl-1">
|
||||||
|
<Breadcrumb />
|
||||||
|
<SwitchLayout />
|
||||||
|
</nav>
|
||||||
<FileListing />
|
<FileListing />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Generated
+10
@@ -7,6 +7,7 @@ specifiers:
|
|||||||
'@fortawesome/free-solid-svg-icons': ^5.15.3
|
'@fortawesome/free-solid-svg-icons': ^5.15.3
|
||||||
'@fortawesome/react-fontawesome': ^0.1.14
|
'@fortawesome/react-fontawesome': ^0.1.14
|
||||||
'@headlessui/react': ^1.4.0
|
'@headlessui/react': ^1.4.0
|
||||||
|
'@tailwindcss/line-clamp': ^0.3.1
|
||||||
'@types/cors': ^2.8.12
|
'@types/cors': ^2.8.12
|
||||||
'@types/crypto-js': ^4.0.2
|
'@types/crypto-js': ^4.0.2
|
||||||
'@types/ioredis': ^4.28.5
|
'@types/ioredis': ^4.28.5
|
||||||
@@ -62,6 +63,7 @@ dependencies:
|
|||||||
'@fortawesome/free-solid-svg-icons': 5.15.4
|
'@fortawesome/free-solid-svg-icons': 5.15.4
|
||||||
'@fortawesome/react-fontawesome': 0.1.17_f515edce028694561ceb456e3dba224c
|
'@fortawesome/react-fontawesome': 0.1.17_f515edce028694561ceb456e3dba224c
|
||||||
'@headlessui/react': 1.4.3_react-dom@17.0.2+react@17.0.2
|
'@headlessui/react': 1.4.3_react-dom@17.0.2+react@17.0.2
|
||||||
|
'@tailwindcss/line-clamp': 0.3.1_tailwindcss@3.0.18
|
||||||
awesome-debounce-promise: 2.1.0
|
awesome-debounce-promise: 2.1.0
|
||||||
axios: 0.25.0
|
axios: 0.25.0
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
@@ -376,6 +378,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==}
|
resolution: {integrity: sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@tailwindcss/line-clamp/0.3.1_tailwindcss@3.0.18:
|
||||||
|
resolution: {integrity: sha512-pNr0T8LAc3TUx/gxCfQZRe9NB2dPEo/cedPHzUGIPxqDMhgjwNm6jYxww4W5l0zAsAddxr+XfZcqttGiFDgrGg==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 3.0.18_833e1018ad0d7954aa80c53675939269
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/cors/2.8.12:
|
/@types/cors/2.8.12:
|
||||||
resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==}
|
resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
+3
-1
@@ -35,5 +35,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
require('@tailwindcss/line-clamp'),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-1
@@ -13,12 +13,15 @@ export type OdFolderObject = {
|
|||||||
name: string
|
name: string
|
||||||
size: number
|
size: number
|
||||||
lastModifiedDateTime: string
|
lastModifiedDateTime: string
|
||||||
file?: { mimeType: string; hashes: { quickXorHash: string; sha1Hash?: string; sha256Hash?: string } }
|
file?: { mimeType: string; hashes: { quickXorHash?: string; sha1Hash?: string; sha256Hash?: string } }
|
||||||
folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } }
|
folder?: { childCount: number; view: { sortBy: string; sortOrder: 'ascending'; viewType: 'thumbnails' } }
|
||||||
image?: OdImageFile
|
image?: OdImageFile
|
||||||
video?: OdVideoFile
|
video?: OdVideoFile
|
||||||
|
'thumbnails@odata.context'?: string
|
||||||
|
thumbnails?: Array<OdThumbnail>
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
export type OdFolderChildren = OdFolderObject['value'][number]
|
||||||
// A file object returned from the OneDrive API. This object may contain 'video' if the file is a video.
|
// A file object returned from the OneDrive API. This object may contain 'video' if the file is a video.
|
||||||
export type OdFileObject = {
|
export type OdFileObject = {
|
||||||
'@microsoft.graph.downloadUrl': string
|
'@microsoft.graph.downloadUrl': string
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ const extensions = {
|
|||||||
|
|
||||||
txt: icons.text,
|
txt: icons.text,
|
||||||
rtf: icons.text,
|
rtf: icons.text,
|
||||||
|
vtt: icons.text,
|
||||||
|
srt: icons.text,
|
||||||
|
log: icons.text,
|
||||||
|
diff: icons.text,
|
||||||
|
|
||||||
md: icons.markdown,
|
md: icons.markdown,
|
||||||
|
|
||||||
epub: icons.book,
|
epub: icons.book,
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export const extensions = {
|
|||||||
toml: preview.code,
|
toml: preview.code,
|
||||||
|
|
||||||
txt: preview.text,
|
txt: preview.text,
|
||||||
|
vtt: preview.text,
|
||||||
|
srt: preview.text,
|
||||||
|
log: preview.text,
|
||||||
|
diff: preview.text,
|
||||||
|
|
||||||
mp4: preview.video,
|
mp4: preview.video,
|
||||||
flv: preview.video,
|
flv: preview.video,
|
||||||
|
|||||||
Reference in New Issue
Block a user