import { useConfig } from '@/components/ConfigProvider'; import { bytes } from '@/lib/bytes'; import { uploadFiles } from '@/lib/client/upload/files'; import { uploadPartialFiles } from '@/lib/client/upload/partial'; import { useProgress } from '@/lib/client/upload/useProgress'; import { humanizeDuration } from '@/lib/relativeTime'; import { useUploadOptionsStore } from '@/lib/client/store/uploadOptions'; import { Button, Collapse, Grid, Group, Kbd, Paper, Progress, Text, Title, Tooltip, rem, useMantineTheme, } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useClipboard, useColorScheme } from '@mantine/hooks'; import { notifications, showNotification } from '@mantine/notifications'; import { IconDeviceSdCard, IconFiles, IconTrashFilled, IconUpload, IconX } from '@tabler/icons-react'; import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { useShallow } from 'zustand/shallow'; import UploadOptionsButton from '../UploadOptionsButton'; import DropzoneFile from './DropzoneFile'; const initialVisible = 24; export default function UploadFile({ title, folder }: { title?: string; folder?: string }) { const theme = useMantineTheme(); const colorScheme = useColorScheme(); const clipboard = useClipboard(); const config = useConfig(); const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Macintosh'); const [options, ephemeral, clearEphemeral] = useUploadOptionsStore( useShallow((state) => [state.options, state.ephemeral, state.clearEphemeral]), ); const [files, setFiles] = useState([]); const [visibleCount, setVisibleCount] = useState(initialVisible); const [progress, setProgress] = useProgress(); const [dropLoading, setLoading] = useState(false); const visibleFiles = files.slice(0, visibleCount); const hiddenFiles = Math.max(0, files.length - visibleFiles.length); const aggSize = useCallback(() => files.reduce((acc, file) => acc + file.size, 0), [files]); const handlePaste = useCallback((e: ClipboardEvent) => { if (!e.clipboardData) return; for (let i = 0; i !== e.clipboardData.items.length; ++i) { if (!e.clipboardData.items[i].type.startsWith('image')) return; const blob = e.clipboardData.items[i].getAsFile(); if (!blob) return; setFiles((prev) => [...prev, blob]); setVisibleCount(initialVisible); showNotification({ message: `Image ${blob.name} pasted from clipboard`, color: 'blue' }); } }, []); const upload = async () => { const toPartialFiles: File[] = files.filter( (file) => config.chunks.enabled && file.size >= bytes(config.chunks.max), ); if (toPartialFiles.length > 0) { uploadPartialFiles(toPartialFiles, { setFiles, setLoading, setProgress, clipboard, clearEphemeral, options, ephemeral, config, folder, }); } else { const size = aggSize(); if (size > bytes(config.files.maxFileSize)) { notifications.show({ title: 'Upload may fail', color: 'yellow', icon: , message: ( <> The upload may fail because the total size of the files (that are not being partially uploaded) you are trying to upload is {bytes(size)}, which is larger than the limit of{' '} {bytes(bytes(config.files.maxFileSize))} ), }); } await uploadFiles(files, { setFiles, setLoading, setProgress, clipboard, clearEphemeral, options, ephemeral, folder, }); } }; useEffect(() => { document.addEventListener('paste', handlePaste); return () => { document.removeEventListener('paste', handlePaste); }; }, [handlePaste]); useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (files.length > 0) { e.preventDefault(); } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [files.length]); if (!config) return null; return ( <> {title ?? 'Upload files'} {!folder && ( )} { setFiles((prev) => [...f, ...prev]); setVisibleCount(initialVisible); }} my='sm' loading={dropLoading} disabled={dropLoading} style={{ zIndex: 1 }} >
Drag images here or click to select files Or {isMac ? '⌘' : 'Ctrl'} + V to paste images from clipboard Attach as many files as you like, they will show up below to review before uploading. {bytes(bytes(config.files.maxFileSize))} limit per file
0 && progress.percent < 100}> {progress.percent > 0 && progress.percent < 100 && ( {Math.floor(progress.percent)}% )} 0 && progress.remaining > 0}> {bytes(progress.speed)}/s, {humanizeDuration(progress.remaining)} remaining Finalizing upload(s)... {visibleFiles.map((file, i) => ( setFiles((prev) => prev.filter((_, j) => i !== j))} /> ))} {hiddenFiles > 0 && ( {hiddenFiles} more file{hiddenFiles !== 1 && 's'} hidden{' '} )} ); }