mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
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
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:
parent
1f061a8e73
commit
8307fe31aa
@ -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",
|
||||||
|
|||||||
@ -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()}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user