From 8bef969908cfd490deab5400bcd2968b2575b546 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 10 Nov 2023 12:14:07 -0600 Subject: [PATCH] Accept multiple files when uploading --- src/components/ImageInput.tsx | 127 +++++++++++++++++++++------------- src/photo/PhotoUpload.tsx | 28 +++++--- src/site/paths.ts | 4 +- 3 files changed, 98 insertions(+), 61 deletions(-) diff --git a/src/components/ImageInput.tsx b/src/components/ImageInput.tsx index 3241786b..cd59cb62 100644 --- a/src/components/ImageInput.tsx +++ b/src/components/ImageInput.tsx @@ -19,7 +19,12 @@ export default function ImageInput({ debug, }: { onStart?: () => void - onBlobReady?: (blob: Blob, extension?: string) => void + onBlobReady?: (args: { + blob: Blob, + extension?: string, + hasMultipleUploads?: boolean, + isLastBlob?: boolean, + }) => Promise maxSize?: number quality?: number loading?: boolean @@ -27,7 +32,7 @@ export default function ImageInput({ }) { const ref = useRef(null); - const [fileName, setFileName] = useState(); + const [statusText, setStatusText] = useState(); const [image, setImage] = useState(); return ( @@ -63,62 +68,84 @@ export default function ImageInput({ className="!hidden" accept={ACCEPTED_PHOTO_FILE_TYPES.join(',')} disabled={loading} + multiple onChange={async e => { onStart?.(); - const file = e.currentTarget.files?.[0]; - setFileName(file?.name); - const extension = file?.name.split('.').pop()?.toLowerCase(); - const canvas = ref.current; - if (file) { - if (!(maxSize && canvas)) { - // No need to process - onBlobReady?.(file); - } else { - // Process images that need resizing - const image = await blobToImage(file); - setImage(image); - const { naturalWidth, naturalHeight } = image; - const ratio = naturalWidth / naturalHeight; - - const width = - Math.round(ratio >= 1 ? maxSize : maxSize * ratio); - const height = - Math.round(ratio >= 1 ? maxSize / ratio : maxSize); - - canvas.width = width; - canvas.height = height; - - // Specify wide gamut to avoid data loss while resizing - const ctx = canvas.getContext( - '2d', - { colorSpace: 'display-p3' }, - ); - - ctx?.drawImage( - image, - 0, - 0, - canvas.width, - canvas.height, - ); - canvas.toBlob( - async blob => { - if (blob) { - const blobWithExif = await CopyExif(file, blob); - onBlobReady?.(blobWithExif, extension); - } - }, - 'image/jpeg', - quality, - ); + const { files } = e.currentTarget; + if (files && files.length > 0) { + for (let i = 0; i < files?.length; i++) { + const file = files[i]; + if (file) { + const callbackArgs = { + extension: file.name.split('.').pop()?.toLowerCase(), + hasMultipleUploads: files.length > 1, + isLastBlob: i === files.length - 1, + }; + if (files.length > 1) { + setStatusText( + `Uploading ${i + 1} of ${files.length}: ${file.name}` + ); + } else { + setStatusText(`Uploading ${file.name}`); + } + const canvas = ref.current; + if (!(maxSize && canvas)) { + // No need to process + await onBlobReady?.({ + ...callbackArgs, + blob: file, + }); + } else { + // Process images that need resizing + const image = await blobToImage(file); + setImage(image); + const { naturalWidth, naturalHeight } = image; + const ratio = naturalWidth / naturalHeight; + + const width = + Math.round(ratio >= 1 ? maxSize : maxSize * ratio); + const height = + Math.round(ratio >= 1 ? maxSize / ratio : maxSize); + + canvas.width = width; + canvas.height = height; + + // Specify wide gamut to avoid data loss while resizing + const ctx = canvas.getContext( + '2d', + { colorSpace: 'display-p3' }, + ); + + ctx?.drawImage( + image, + 0, + 0, + canvas.width, + canvas.height, + ); + canvas.toBlob( + async blob => { + if (blob) { + const blobWithExif = await CopyExif(file, blob); + await onBlobReady?.({ + ...callbackArgs, + blob: blobWithExif, + }); + } + }, + 'image/jpeg', + quality, + ); + } + } } } }} /> - {fileName && + {statusText &&
- {fileName} + {statusText}
} { + onBlobReady={async ({ + blob, + extension, + hasMultipleUploads, + isLastBlob, + }) => { if (debug) { setDebugDownload({ href: URL.createObjectURL(blob), @@ -47,16 +52,23 @@ export default function PhotoUpload({ setIsUploading(false); setUploadError(''); } else { - uploadPhotoFromClient( + return uploadPhotoFromClient( blob, extension, ) .then(({ url }) => { - // Refresh page to update upload list, - // relevant only when a photo isn't added - router.refresh(); - // Redirect to photo detail page - router.push(pathForAdminUploadUrl(url)); + if (isLastBlob) { + // Refresh page to update upload list, + // relevant to upload count in nav + router.refresh(); + if (hasMultipleUploads) { + // Redirect to view multiple uploads + router.push(PATH_ADMIN_UPLOADS); + } else { + // Redirect to photo detail page + router.push(pathForAdminUploadUrl(url)); + } + } }) .catch(error => { setIsUploading(false); diff --git a/src/site/paths.ts b/src/site/paths.ts index 1a213a51..3c627991 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -31,8 +31,7 @@ const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/:camera`; export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`; export const PATH_ADMIN_UPLOADS = `${PATH_ADMIN}/uploads`; export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`; -export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`; -export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOAD}/blob`; +export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOADS}/blob`; export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`; // Modifiers @@ -45,7 +44,6 @@ export const PATHS_ADMIN = [ PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS, PATH_ADMIN_TAGS, - PATH_ADMIN_UPLOAD, PATH_ADMIN_UPLOAD_BLOB, PATH_ADMIN_CONFIGURATION, ];