Accept multiple files when uploading

This commit is contained in:
Sam Becker 2023-11-10 12:14:07 -06:00
parent f200d1d754
commit 8bef969908
3 changed files with 98 additions and 61 deletions

View File

@ -19,7 +19,12 @@ export default function ImageInput({
debug, debug,
}: { }: {
onStart?: () => void onStart?: () => void
onBlobReady?: (blob: Blob, extension?: string) => void onBlobReady?: (args: {
blob: Blob,
extension?: string,
hasMultipleUploads?: boolean,
isLastBlob?: boolean,
}) => Promise<any>
maxSize?: number maxSize?: number
quality?: number quality?: number
loading?: boolean loading?: boolean
@ -27,7 +32,7 @@ export default function ImageInput({
}) { }) {
const ref = useRef<HTMLCanvasElement>(null); const ref = useRef<HTMLCanvasElement>(null);
const [fileName, setFileName] = useState<string>(); const [statusText, setStatusText] = useState<string>();
const [image, setImage] = useState<HTMLImageElement>(); const [image, setImage] = useState<HTMLImageElement>();
return ( return (
@ -63,62 +68,84 @@ export default function ImageInput({
className="!hidden" className="!hidden"
accept={ACCEPTED_PHOTO_FILE_TYPES.join(',')} accept={ACCEPTED_PHOTO_FILE_TYPES.join(',')}
disabled={loading} disabled={loading}
multiple
onChange={async e => { onChange={async e => {
onStart?.(); onStart?.();
const file = e.currentTarget.files?.[0]; const { files } = e.currentTarget;
setFileName(file?.name); if (files && files.length > 0) {
const extension = file?.name.split('.').pop()?.toLowerCase(); for (let i = 0; i < files?.length; i++) {
const canvas = ref.current; const file = files[i];
if (file) { if (file) {
if (!(maxSize && canvas)) { const callbackArgs = {
// No need to process extension: file.name.split('.').pop()?.toLowerCase(),
onBlobReady?.(file); hasMultipleUploads: files.length > 1,
} else { isLastBlob: i === files.length - 1,
// Process images that need resizing };
const image = await blobToImage(file); if (files.length > 1) {
setImage(image); setStatusText(
const { naturalWidth, naturalHeight } = image; `Uploading ${i + 1} of ${files.length}: ${file.name}`
const ratio = naturalWidth / naturalHeight; );
} else {
const width = setStatusText(`Uploading ${file.name}`);
Math.round(ratio >= 1 ? maxSize : maxSize * ratio); }
const height = const canvas = ref.current;
Math.round(ratio >= 1 ? maxSize / ratio : maxSize); if (!(maxSize && canvas)) {
// No need to process
canvas.width = width; await onBlobReady?.({
canvas.height = height; ...callbackArgs,
blob: file,
// Specify wide gamut to avoid data loss while resizing });
const ctx = canvas.getContext( } else {
'2d', // Process images that need resizing
{ colorSpace: 'display-p3' }, const image = await blobToImage(file);
); setImage(image);
const { naturalWidth, naturalHeight } = image;
ctx?.drawImage( const ratio = naturalWidth / naturalHeight;
image,
0, const width =
0, Math.round(ratio >= 1 ? maxSize : maxSize * ratio);
canvas.width, const height =
canvas.height, Math.round(ratio >= 1 ? maxSize / ratio : maxSize);
);
canvas.toBlob( canvas.width = width;
async blob => { canvas.height = height;
if (blob) {
const blobWithExif = await CopyExif(file, blob); // Specify wide gamut to avoid data loss while resizing
onBlobReady?.(blobWithExif, extension); const ctx = canvas.getContext(
} '2d',
}, { colorSpace: 'display-p3' },
'image/jpeg', );
quality,
); 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,
);
}
}
} }
} }
}} }}
/> />
</label> </label>
{fileName && {statusText &&
<div className="max-w-full truncate text-ellipsis"> <div className="max-w-full truncate text-ellipsis">
{fileName} {statusText}
</div>} </div>}
</div> </div>
<canvas <canvas

View File

@ -3,7 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { uploadPhotoFromClient } from '@/services/blob'; import { uploadPhotoFromClient } from '@/services/blob';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { pathForAdminUploadUrl } from '@/site/paths'; import { PATH_ADMIN_UPLOADS, pathForAdminUploadUrl } from '@/site/paths';
import ImageInput from '../components/ImageInput'; import ImageInput from '../components/ImageInput';
import { MAX_IMAGE_SIZE } from '@/utility/image'; import { MAX_IMAGE_SIZE } from '@/utility/image';
import { cc } from '@/utility/css'; import { cc } from '@/utility/css';
@ -38,7 +38,12 @@ export default function PhotoUpload({
setIsUploading(true); setIsUploading(true);
setUploadError(''); setUploadError('');
}} }}
onBlobReady={(blob, extension) => { onBlobReady={async ({
blob,
extension,
hasMultipleUploads,
isLastBlob,
}) => {
if (debug) { if (debug) {
setDebugDownload({ setDebugDownload({
href: URL.createObjectURL(blob), href: URL.createObjectURL(blob),
@ -47,16 +52,23 @@ export default function PhotoUpload({
setIsUploading(false); setIsUploading(false);
setUploadError(''); setUploadError('');
} else { } else {
uploadPhotoFromClient( return uploadPhotoFromClient(
blob, blob,
extension, extension,
) )
.then(({ url }) => { .then(({ url }) => {
// Refresh page to update upload list, if (isLastBlob) {
// relevant only when a photo isn't added // Refresh page to update upload list,
router.refresh(); // relevant to upload count in nav
// Redirect to photo detail page router.refresh();
router.push(pathForAdminUploadUrl(url)); if (hasMultipleUploads) {
// Redirect to view multiple uploads
router.push(PATH_ADMIN_UPLOADS);
} else {
// Redirect to photo detail page
router.push(pathForAdminUploadUrl(url));
}
}
}) })
.catch(error => { .catch(error => {
setIsUploading(false); setIsUploading(false);

View File

@ -31,8 +31,7 @@ const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/:camera`;
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`; export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
export const PATH_ADMIN_UPLOADS = `${PATH_ADMIN}/uploads`; export const PATH_ADMIN_UPLOADS = `${PATH_ADMIN}/uploads`;
export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`; export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`; export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOADS}/blob`;
export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOAD}/blob`;
export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`; export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`;
// Modifiers // Modifiers
@ -45,7 +44,6 @@ export const PATHS_ADMIN = [
PATH_ADMIN_PHOTOS, PATH_ADMIN_PHOTOS,
PATH_ADMIN_UPLOADS, PATH_ADMIN_UPLOADS,
PATH_ADMIN_TAGS, PATH_ADMIN_TAGS,
PATH_ADMIN_UPLOAD,
PATH_ADMIN_UPLOAD_BLOB, PATH_ADMIN_UPLOAD_BLOB,
PATH_ADMIN_CONFIGURATION, PATH_ADMIN_CONFIGURATION,
]; ];