multiple file download with progress bar and percentage

This commit is contained in:
spencerwooo
2021-12-17 23:16:18 +08:00
parent 93376e071e
commit e20a2f52a5
4 changed files with 61 additions and 27 deletions
+17 -15
View File
@@ -14,10 +14,13 @@ import { getExtension, getFileIcon, hasKey } from '../utils/getFileIcon'
import { extensions, preview } from '../utils/getPreviewType' import { extensions, preview } from '../utils/getPreviewType'
import { useProtectedSWRInfinite } from '../utils/fetchWithSWR' import { useProtectedSWRInfinite } from '../utils/fetchWithSWR'
import { getBaseUrl } from '../utils/getBaseUrl' import { getBaseUrl } from '../utils/getBaseUrl'
import { downloadMultipleFiles, downloadTreelikeMultipleFiles, traverseFolder } from '../utils/downloadMultipleFiles' import {
DownloadingToast,
downloadMultipleFiles,
downloadTreelikeMultipleFiles,
traverseFolder,
} from './MultiFileDownloader'
import { VideoPreview } from './previews/VideoPreview'
import { AudioPreview } from './previews/AudioPreview'
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'
@@ -25,6 +28,8 @@ import TextPreview from './previews/TextPreview'
import MarkdownPreview from './previews/MarkdownPreview' import MarkdownPreview from './previews/MarkdownPreview'
import CodePreview from './previews/CodePreview' import CodePreview from './previews/CodePreview'
import OfficePreview from './previews/OfficePreview' import OfficePreview from './previews/OfficePreview'
import AudioPreview from './previews/AudioPreview'
import VideoPreview from './previews/VideoPreview'
import DownloadBtn from './DownloadBtn' import DownloadBtn from './DownloadBtn'
// Disabling SSR for some previews (image gallery view, and PDF view) // Disabling SSR for some previews (image gallery view, and PDF view)
@@ -285,17 +290,16 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
el.remove() el.remove()
} else if (files.length > 1) { } else if (files.length > 1) {
setTotalGenerating(true) setTotalGenerating(true)
const toastId = toast.loading('Downloading selected files. Refresh to cancel, this may take some time...')
downloadMultipleFiles({ toastId, files, folder }) const toastId = toast.loading(DownloadingToast(router))
downloadMultipleFiles({ toastId, router, files, folder })
.then(() => { .then(() => {
setTotalGenerating(false) setTotalGenerating(false)
toast.dismiss(toastId) toast.success('Finished downloading selected files.', { id: toastId })
toast.success('Finished downloading selected files.')
}) })
.catch(() => { .catch(() => {
setTotalGenerating(false) setTotalGenerating(false)
toast.dismiss(toastId) toast.error('Failed to download selected files.', { id: toastId })
toast.error('Failed to download selected files.')
}) })
} }
} }
@@ -314,18 +318,16 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
})() })()
setFolderGenerating({ ...folderGenerating, [id]: true }) setFolderGenerating({ ...folderGenerating, [id]: true })
const toastId = toast.loading('Downloading folder. Refresh to cancel, this may take some time...') const toastId = toast.loading(DownloadingToast(router))
downloadTreelikeMultipleFiles({ toastId, files, basePath: path, folder: name }) downloadTreelikeMultipleFiles({ toastId, router, files, basePath: path, folder: name })
.then(() => { .then(() => {
setFolderGenerating({ ...folderGenerating, [id]: false }) setFolderGenerating({ ...folderGenerating, [id]: false })
toast.dismiss(toastId) toast.success('Finished downloading folder.', { id: toastId })
toast.success('Finished downloading folder.')
}) })
.catch(() => { .catch(() => {
setFolderGenerating({ ...folderGenerating, [id]: false }) setFolderGenerating({ ...folderGenerating, [id]: false })
toast.dismiss(toastId) toast.error('Failed to download folder.', { id: toastId })
toast.error('Failed to download folder.')
}) })
} }
@@ -1,8 +1,37 @@
import { NextRouter } from 'next/router'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import JSZip from 'jszip' import JSZip from 'jszip'
import { fetcher } from './fetchWithSWR' import { fetcher } from '../utils/fetchWithSWR'
import { getStoredToken } from './protectedRouteHandler' import { getStoredToken } from '../utils/protectedRouteHandler'
/**
* Generates a loading toast with file download progress support
* @param router Next router instance, used for reloading the page
* @param progress Current downloading and compression progress (returned by jszip metadata)
* @returns Toast component with loading progress
*/
export function DownloadingToast(router: NextRouter, progress?: string) {
return (
<div className="flex items-center space-x-2">
<div className="w-56">
<span>Downloading {progress ? `${progress}%` : 'selected files...'}</span>
<div className="relative mt-2">
<div className="overflow-hidden h-1 flex rounded bg-gray-100">
<div style={{ width: `${progress}%` }} className="text-white bg-gray-500 transition-all duration-100"></div>
</div>
</div>
</div>
<button
className="p-2 rounded bg-red-500 hover:bg-red-400 text-white focus:outline-none focus:ring focus:ring-red-300"
onClick={() => router.reload()}
>
Cancel
</button>
</div>
)
}
// Blob download helper // Blob download helper
export function downloadBlob({ blob, name }: { blob: Blob; name: string }) { export function downloadBlob({ blob, name }: { blob: Blob; name: string }) {
@@ -28,10 +57,12 @@ export function downloadBlob({ blob, name }: { blob: Blob; name: string }) {
*/ */
export async function downloadMultipleFiles({ export async function downloadMultipleFiles({
toastId, toastId,
router,
files, files,
folder, folder,
}: { }: {
toastId: string toastId: string
router: NextRouter
files: { name: string; url: string }[] files: { name: string; url: string }[]
folder?: string folder?: string
}): Promise<void> { }): Promise<void> {
@@ -50,9 +81,7 @@ export async function downloadMultipleFiles({
// Create zip file and download it // Create zip file and download it
const b = await zip.generateAsync({ type: 'blob' }, metadata => { const b = await zip.generateAsync({ type: 'blob' }, metadata => {
toast.loading(`Downloading ${metadata.percent.toFixed(0)}%. Refresh to cancel...`, { toast.loading(DownloadingToast(router, metadata.percent.toFixed(0)), { id: toastId })
id: toastId,
})
}) })
downloadBlob({ blob: b, name: folder ? folder + '.zip' : 'download.zip' }) downloadBlob({ blob: b, name: folder ? folder + '.zip' : 'download.zip' })
} }
@@ -68,14 +97,15 @@ export async function downloadMultipleFiles({
* @param basePath Root dir path of files to be downloaded * @param basePath Root dir path of files to be downloaded
* @param folder Optional folder name to hold files, otherwise flatten files in the zip * @param folder Optional folder name to hold files, otherwise flatten files in the zip
*/ */
export async function downloadTreelikeMultipleFiles({ export async function downloadTreelikeMultipleFiles({
toastId, toastId,
router,
files, files,
basePath, basePath,
folder, folder,
}: { }: {
toastId: string toastId: string
router: NextRouter
files: AsyncGenerator<{ files: AsyncGenerator<{
name: string name: string
url?: string url?: string
@@ -117,9 +147,7 @@ export async function downloadTreelikeMultipleFiles({
// Create zip file and download it // Create zip file and download it
const b = await zip.generateAsync({ type: 'blob' }, metadata => { const b = await zip.generateAsync({ type: 'blob' }, metadata => {
toast.loading(`Downloading ${metadata.percent.toFixed(0)}%. Refresh to cancel...`, { toast.loading(DownloadingToast(router, metadata.percent.toFixed(0)), { id: toastId })
id: toastId,
})
}) })
downloadBlob({ blob: b, name: folder ? folder + '.zip' : 'download.zip' }) downloadBlob({ blob: b, name: folder ? folder + '.zip' : 'download.zip' })
} }
+3 -1
View File
@@ -11,7 +11,7 @@ enum PlayerState {
Paused, Paused,
} }
export const AudioPreview: FunctionComponent<{ file: any }> = ({ file }) => { const AudioPreview: FunctionComponent<{ file: any }> = ({ file }) => {
const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading) const [playerStatus, setPlayerStatus] = useState(PlayerState.Loading)
return ( return (
@@ -79,3 +79,5 @@ export const AudioPreview: FunctionComponent<{ file: any }> = ({ file }) => {
</> </>
) )
} }
export default AudioPreview
+4 -2
View File
@@ -6,9 +6,9 @@ import { useClipboard } from 'use-clipboard-copy'
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 { getBaseUrl } from "../../utils/getBaseUrl" import { getBaseUrl } from '../../utils/getBaseUrl'
export const VideoPreview: FunctionComponent<{ file: any }> = ({ file }) => { const VideoPreview: FunctionComponent<{ file: any }> = ({ file }) => {
const { asPath } = useRouter() const { asPath } = useRouter()
const clipboard = useClipboard() const clipboard = useClipboard()
@@ -81,3 +81,5 @@ export const VideoPreview: FunctionComponent<{ file: any }> = ({ file }) => {
</> </>
) )
} }
export default VideoPreview