176 lines
5.3 KiB
TypeScript
176 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, RefObject } from 'react';
|
|
import { pngToJpegWithExif, jpgToJpegWithExif } from '@/utility/exif-client';
|
|
import { clsx } from 'clsx/lite';
|
|
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
|
|
import { FiUploadCloud } from 'react-icons/fi';
|
|
import { MAX_IMAGE_SIZE } from '@/platforms/next-image';
|
|
import ProgressButton from './primitives/ProgressButton';
|
|
import { useAppState } from '@/app/AppState';
|
|
import { useAppText } from '@/i18n/state/client';
|
|
|
|
export default function ImageInput({
|
|
ref: inputRefExternal,
|
|
id = 'file',
|
|
onStart,
|
|
onBlobReady,
|
|
multiple = true,
|
|
shouldResize,
|
|
maxSize = MAX_IMAGE_SIZE,
|
|
quality = 0.9,
|
|
showButton,
|
|
disabled: disabledProp,
|
|
debug: _debug,
|
|
}: {
|
|
ref?: RefObject<HTMLInputElement | null>
|
|
id?: string
|
|
onStart?: () => void
|
|
onBlobReady?: (args: {
|
|
blob: Blob,
|
|
extension?: string,
|
|
hasMultipleUploads?: boolean,
|
|
isLastBlob?: boolean,
|
|
}) => Promise<any>
|
|
multiple?: boolean
|
|
shouldResize?: boolean
|
|
maxSize?: number
|
|
quality?: number
|
|
showButton?: boolean
|
|
disabled?: boolean
|
|
debug?: boolean
|
|
}) {
|
|
const inputRefInternal = useRef<HTMLInputElement>(null);
|
|
|
|
const inputRef = inputRefExternal ?? inputRefInternal;
|
|
|
|
const {
|
|
uploadState: {
|
|
isUploading,
|
|
filesLength,
|
|
fileUploadIndex,
|
|
},
|
|
setUploadState,
|
|
resetUploadState,
|
|
} = useAppState();
|
|
|
|
const appText = useAppText();
|
|
|
|
const disabled = disabledProp || isUploading;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 min-w-0">
|
|
<div className="flex items-center gap-2 sm:gap-4">
|
|
<label
|
|
htmlFor={id}
|
|
className={clsx(
|
|
'shrink-0 select-none text-main',
|
|
// Undo standard label styles since
|
|
// content is shown as button
|
|
'font-normal tracking-normal',
|
|
disabled && 'pointer-events-none cursor-not-allowed',
|
|
)}
|
|
>
|
|
{showButton &&
|
|
<ProgressButton
|
|
type="button"
|
|
isLoading={disabled}
|
|
progress={filesLength > 1
|
|
? (fileUploadIndex + 1) / filesLength * 0.95
|
|
: undefined}
|
|
icon={<FiUploadCloud
|
|
size={18}
|
|
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
|
/>}
|
|
aria-disabled={disabled}
|
|
onClick={() => inputRef.current?.click()}
|
|
hideText="never"
|
|
primary
|
|
>
|
|
{isUploading
|
|
? filesLength > 1
|
|
? appText.utility.paginateAction(
|
|
fileUploadIndex + 1,
|
|
filesLength,
|
|
appText.admin.uploading,
|
|
)
|
|
: appText.admin.uploading
|
|
: appText.admin.uploadPhotos}
|
|
</ProgressButton>}
|
|
<input
|
|
ref={inputRef}
|
|
id={id}
|
|
type="file"
|
|
className="hidden!"
|
|
accept={ACCEPTED_PHOTO_FILE_TYPES.join(',')}
|
|
disabled={disabled}
|
|
multiple={multiple}
|
|
onChange={async e => {
|
|
onStart?.();
|
|
const { files } = e.currentTarget;
|
|
if (files && files.length > 0) {
|
|
setUploadState?.({ filesLength: files.length });
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
setUploadState?.({
|
|
fileUploadIndex: i,
|
|
fileUploadName: file.name,
|
|
});
|
|
const inputExtension = file.name
|
|
.split('.')
|
|
.pop()?.toLowerCase();
|
|
|
|
const isInputPng = inputExtension === 'png';
|
|
|
|
const outputExtension = shouldResize
|
|
? 'jpeg'
|
|
: inputExtension;
|
|
|
|
const callbackArgs = {
|
|
extension: outputExtension,
|
|
hasMultipleUploads: files.length > 1,
|
|
isLastBlob: i === files.length - 1,
|
|
};
|
|
|
|
let blob: Blob | File = file;
|
|
|
|
if (shouldResize) {
|
|
if (isInputPng) {
|
|
// Use specialized PNG <> JPEG converter
|
|
// for EXIF preservation
|
|
blob = await pngToJpegWithExif(
|
|
file,
|
|
{ maxSize, quality },
|
|
).catch(() => file);
|
|
} else {
|
|
// Use specialized JPG <> JPEG converter
|
|
// for EXIF preservation
|
|
blob = await jpgToJpegWithExif(
|
|
file,
|
|
{ maxSize, quality },
|
|
).catch(() => file);
|
|
}
|
|
|
|
await onBlobReady?.({
|
|
...callbackArgs,
|
|
blob,
|
|
});
|
|
} else {
|
|
// No need to process
|
|
await onBlobReady?.({
|
|
...callbackArgs,
|
|
blob,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
resetUploadState?.();
|
|
}
|
|
}}
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|