Add ability to paste in image dropzone (#20310)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

Primarily used in the face library, now users can use ctrl/meta-v to paste images from the clipboard in an image entry field
This commit is contained in:
Josh Hawkins 2025-10-01 12:49:26 -05:00 committed by GitHub
parent 1f061a8e73
commit 8307fe31aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 50 additions and 6 deletions

View File

@ -66,7 +66,7 @@
"selectImage": "Please select an image file." "selectImage": "Please select an image file."
}, },
"dropActive": "Drop the image here…", "dropActive": "Drop the image here…",
"dropInstructions": "Drag and drop an image here, or click to select", "dropInstructions": "Drag and drop or paste an image here, or click to select",
"maxSize": "Max size: {{size}}MB" "maxSize": "Max size: {{size}}MB"
}, },
"nofaces": "No faces available", "nofaces": "No faces available",

View File

@ -8,7 +8,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -30,6 +30,23 @@ export default function ImageEntry({
}: ImageEntryProps) { }: ImageEntryProps) {
const { t } = useTranslation(["views/faceLibrary"]); const { t } = useTranslation(["views/faceLibrary"]);
const [preview, setPreview] = useState<string | null>(null); const [preview, setPreview] = useState<string | null>(null);
const dropzoneRef = useRef<HTMLDivElement>(null);
// Auto focus the dropzone
useEffect(() => {
if (dropzoneRef.current && !preview) {
dropzoneRef.current.focus();
}
}, [preview]);
// Clean up preview URL on unmount or preview change
useEffect(() => {
return () => {
if (preview) {
URL.revokeObjectURL(preview);
}
};
}, [preview]);
const formSchema = z.object({ const formSchema = z.object({
file: z file: z
@ -52,9 +69,6 @@ export default function ImageEntry({
// Create preview // Create preview
const objectUrl = URL.createObjectURL(file); const objectUrl = URL.createObjectURL(file);
setPreview(objectUrl); setPreview(objectUrl);
// Clean up preview URL when component unmounts
return () => URL.revokeObjectURL(objectUrl);
} }
}, },
[form], [form],
@ -68,6 +82,31 @@ export default function ImageEntry({
multiple: false, multiple: false,
}); });
const handlePaste = useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => {
event.preventDefault();
const clipboardItems = Array.from(event.clipboardData.items);
for (const item of clipboardItems) {
if (item.type.startsWith("image/")) {
const blob = item.getAsFile();
if (blob && blob.size <= maxSize) {
const mimeType = blob.type.split("/")[1];
const extension = `.${mimeType}`;
if (accept["image/*"].includes(extension)) {
const fileName = blob.name || `pasted-image.${mimeType}`;
const file = new File([blob], fileName, { type: blob.type });
form.setValue("file", file, { shouldValidate: true });
const objectUrl = URL.createObjectURL(file);
setPreview(objectUrl);
return; // Take the first valid image
}
}
}
}
},
[form, maxSize, accept],
);
const onSubmit = useCallback( const onSubmit = useCallback(
(data: z.infer<typeof formSchema>) => { (data: z.infer<typeof formSchema>) => {
if (!data.file) return; if (!data.file) return;
@ -90,7 +129,12 @@ export default function ImageEntry({
render={() => ( render={() => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<div className="w-full"> <div
className="w-full"
onPaste={handlePaste}
tabIndex={0}
ref={dropzoneRef}
>
{!preview ? ( {!preview ? (
<div <div
{...getRootProps()} {...getRootProps()}