diff --git a/components/FileListing.tsx b/components/FileListing.tsx index 84d287f..59c10a7 100644 --- a/components/FileListing.tsx +++ b/components/FileListing.tsx @@ -4,7 +4,7 @@ import emojiRegex from 'emoji-regex' import { useClipboard } from 'use-clipboard-copy' import { ParsedUrlQuery } from 'querystring' -import { FunctionComponent, useState } from 'react' +import { FunctionComponent, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from 'react' import { ImageDecorator } from 'react-viewer/lib/ViewerProps' import { useRouter } from 'next/router' @@ -12,7 +12,7 @@ import dynamic from 'next/dynamic' import { getExtension, getFileIcon, hasKey } from '../utils/getFileIcon' import { extensions, preview } from '../utils/getPreviewType' -import { getBaseUrl, useProtectedSWRInfinite } from '../utils/tools' +import { getBaseUrl, downloadMultipleFiles, useProtectedSWRInfinite } from '../utils/tools' import { VideoPreview } from './previews/VideoPreview' import { AudioPreview } from './previews/AudioPreview' @@ -68,8 +68,8 @@ const FileListItem: FunctionComponent<{ const renderEmoji = emojiIcon && !emojiIcon.index return ( -
-
+
+
{/*
{c.file ? c.file.mimeType : 'folder'}
*/}
{renderEmoji ? ( @@ -99,9 +99,57 @@ const FileListItem: FunctionComponent<{ ) } +const Checkbox: FunctionComponent<{ + checked: 0 | 1 | 2 + onChange: () => void + title: string + indeterminate?: boolean +}> = ({ checked, onChange, title, indeterminate }) => { + const ref = useRef(null) + + useEffect(() => { + if (ref.current) { + ref.current.checked = Boolean(checked) + if (indeterminate) { + ref.current.indeterminate = checked == 1 + } + } + }, [ref, checked, indeterminate]) + + const handleClick: MouseEventHandler = e => { + if (ref.current) { + if (e.target === ref.current) { + e.stopPropagation() + } else { + ref.current.click() + } + } + } + + return ( + + + + ) +} + const FileListing: FunctionComponent<{ 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) const router = useRouter() const clipboard = useClipboard() @@ -173,23 +221,118 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = } }) + // Filtered file list helper + const getFiles = () => children.filter((c: any) => !c.folder && c.name !== '.password') + + // File selection + const genTotalSelected = (selected: { [key: string]: boolean }) => { + const selectInfo = getFiles().map((c: any) => Boolean(selected[c.id])) + const [hasT, hasF] = [selectInfo.some(i => i), selectInfo.some(i => !i)] + return hasT && hasF ? 1 : !hasF ? 2 : 0 + } + + const toggleItemSelected = (id: string) => { + let val: SetStateAction<{ [key: string]: boolean }> + if (selected[id]) { + val = { ...selected } + delete val[id] + } else { + val = { ...selected, [id]: true } + } + setSelected(val) + setTotalSelected(genTotalSelected(val)) + } + + const toggleTotalSelected = () => { + if (genTotalSelected(selected) == 2) { + setSelected({}) + setTotalSelected(0) + } else { + setSelected(Object.fromEntries(getFiles().map((c: any) => [c.id, true]))) + setTotalSelected(2) + } + } + + // Selected file download + const handleSelectedDownload = () => { + const folderName = path.substring(path.lastIndexOf('/') + 1) + const folder = folderName ? decodeURIComponent(folderName) : undefined + const files = getFiles() + .filter((c: any) => selected[c.id]) + .map((c: any) => ({ name: c.name, url: c['@microsoft.graph.downloadUrl'] })) + + if (files.length == 1) { + const el = document.createElement('a') + el.style.display = 'none' + document.body.appendChild(el) + el.href = files[0].url + el.click() + el.remove() + } else if (files.length > 1) { + setTotalGenerating(true) + const toastId = toast.loading('Downloading selected files. Refresh to cancel, this may take some time...') + downloadMultipleFiles(files, folder) + .then(() => { + setTotalGenerating(false) + toast.dismiss(toastId) + toast.success('Finished downloading selected files.') + }) + .catch(() => { + setTotalGenerating(false) + toast.dismiss(toastId) + toast.error('Failed to download selected files.') + }) + } + } + return (
-
-
Name
+
+
Name
Last Modified
Size
Actions
+
+
+ + {totalGenerating ? ( + + + + + + + ) : ( + + )} +
+
- + {imagesInFolder.length !== 0 && ( = ({ query }) = render: , onClick: i => { clipboard.copy(i.alt ? `${getBaseUrl()}/api?path=${path + '/' + i.alt}&raw=true` : '') - toast.success('Copied image permanent link to clipboard.') + toast('Copied image permanent link to clipboard.', { icon: '👌' }) }, }, ]) @@ -231,7 +374,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = {children.map((c: any) => (
{ e.preventDefault() @@ -252,7 +395,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = className="hover:bg-gray-300 dark:hover:bg-gray-600 p-2 rounded cursor-pointer" onClick={() => { clipboard.copy(`${getBaseUrl()}${path === '/' ? '' : path}/${encodeURIComponent(c.name)}`) - toast.success('Copied folder permalink.') + toast('Copied folder permalink.', { icon: '👌' }) }} > @@ -264,7 +407,9 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = title="Copy raw file permalink" className="hover:bg-gray-300 dark:hover:bg-gray-600 p-2 rounded cursor-pointer" onClick={() => { - clipboard.copy(`${getBaseUrl()}/api?path=${path === '/' ? '' : path}/${encodeURIComponent(c.name)}&raw=true`) + clipboard.copy( + `${getBaseUrl()}/api?path=${path === '/' ? '' : path}/${encodeURIComponent(c.name)}&raw=true` + ) toast.success('Copied raw file permalink.') }} > @@ -279,6 +424,17 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
)} +
+ {c.folder || c.name === '.password' ? ( + '' + ) : ( + toggleItemSelected(c.id)} + title="Select file" + /> + )} +
))} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 5003b23..c5bef6e 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -42,7 +42,7 @@ const Navbar = () => { return (
-
+
diff --git a/package-lock.json b/package-lock.json index 2db5352..8a5e4f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "axios": "^0.21.1", "crypto-js": "^4.1.1", "emoji-regex": "^9.2.2", + "jszip": "^3.7.1", "next": "^12.0.7", "preview-office-docs": "^1.0.2", "prismjs": "^1.23.0", diff --git a/package.json b/package.json index 464e7b6..759ba53 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "axios": "^0.21.1", "crypto-js": "^4.1.1", "emoji-regex": "^9.2.2", + "jszip": "^3.7.1", "next": "^12.0.7", "preview-office-docs": "^1.0.2", "prismjs": "^1.23.0", diff --git a/pages/[...path].tsx b/pages/[...path].tsx index 0976252..44810e9 100644 --- a/pages/[...path].tsx +++ b/pages/[...path].tsx @@ -18,7 +18,7 @@ export default function Folders() {
-
+
diff --git a/pages/index.tsx b/pages/index.tsx index 85eba46..e5b7d71 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -15,7 +15,7 @@ export default function Home() {
-
+
diff --git a/utils/tools.ts b/utils/tools.ts index 904e263..b97de21 100644 --- a/utils/tools.ts +++ b/utils/tools.ts @@ -1,6 +1,7 @@ import axios from 'axios' import sha256 from 'crypto-js/sha256' import useSWR, { cache, Key, useSWRInfinite } from 'swr' +import JSZip from 'jszip' import siteConfig from '../config/site.json' @@ -123,3 +124,35 @@ export const matchProtectedRoute = (route: string) => { } return authTokenPath } + +/** + * Download multiple files after compressing them into a zip + * @param files Files to be downloaded + * @param folder Optional folder name to hold files, otherwise flatten files in the zip + */ +export const downloadMultipleFiles = async (files: { name: string; url: string }[], folder?: string) => { + const zip = new JSZip() + const dir = folder ? zip.folder(folder)! : zip + + // Add selected file blobs to zip + files.forEach(({ name, url }) => { + dir.file( + name, + fetch(url).then(r => r.blob()) + ) + }) + + // Create zip file and prepare for download + const b = await dir.generateAsync({ type: 'blob' }) + const el = document.createElement('a') + el.style.display = 'none' + document.body.appendChild(el) + + // Download zip file + const bUrl = window.URL.createObjectURL(b) + el.href = bUrl + el.download = folder ? folder + '.zip' : 'download.zip' + el.click() + window.URL.revokeObjectURL(bUrl) + el.remove() +}