mirror of
https://github.com/stashapp/stash.git
synced 2026-03-09 06:32:39 +01:00
New: Add From Clipboard to Set Image (#6637)
* add from clipboard to UI * only trigger when input not focused
This commit is contained in:
parent
f7da37400b
commit
fbf91b2526
3 changed files with 93 additions and 33 deletions
|
|
@ -10,8 +10,10 @@ import {
|
|||
import { useIntl } from "react-intl";
|
||||
import { ModalComponent } from "./Modal";
|
||||
import { Icon } from "./Icon";
|
||||
import { faFile, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClipboard, faFile, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
|
||||
interface IImageInput {
|
||||
isEditing: boolean;
|
||||
|
|
@ -39,6 +41,7 @@ export const ImageInput: React.FC<IImageInput> = PatchComponent(
|
|||
const [isShowDialog, setIsShowDialog] = useState(false);
|
||||
const [url, setURL] = useState("");
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
if (!isEditing) return <div />;
|
||||
|
||||
|
|
@ -58,6 +61,28 @@ export const ImageInput: React.FC<IImageInput> = PatchComponent(
|
|||
);
|
||||
}
|
||||
|
||||
async function onPasteClipboard() {
|
||||
try {
|
||||
const data = await ImageUtils.readClipboardImage();
|
||||
if (data && onImageURL) {
|
||||
onImageURL(data);
|
||||
Toast.success(
|
||||
intl.formatMessage({ id: "toast.clipboard_image_pasted" })
|
||||
);
|
||||
} else {
|
||||
Toast.error(intl.formatMessage({ id: "toast.clipboard_no_image" }));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === "NotAllowedError") {
|
||||
Toast.error(
|
||||
intl.formatMessage({ id: "toast.clipboard_access_denied" })
|
||||
);
|
||||
} else {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showDialog() {
|
||||
setURL("");
|
||||
setIsShowDialog(true);
|
||||
|
|
@ -127,6 +152,16 @@ export const ImageInput: React.FC<IImageInput> = PatchComponent(
|
|||
<span>{intl.formatMessage({ id: "actions.from_url" })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
{window.isSecureContext && (
|
||||
<div>
|
||||
<Button className="minimal" onClick={onPasteClipboard}>
|
||||
<Icon icon={faClipboard} className="fa-fw" />
|
||||
<span>
|
||||
{intl.formatMessage({ id: "actions.from_clipboard" })}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
"export_all": "Export all…",
|
||||
"find": "Find",
|
||||
"finish": "Finish",
|
||||
"from_clipboard": "From clipboard",
|
||||
"from_file": "From file…",
|
||||
"from_url": "From URL…",
|
||||
"full_export": "Full export",
|
||||
|
|
@ -1636,6 +1637,9 @@
|
|||
"toast": {
|
||||
"added_entity": "Added {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||
"added_generation_job_to_queue": "Added generation job to queue",
|
||||
"clipboard_access_denied": "Clipboard access denied. Check your browser permissions",
|
||||
"clipboard_image_pasted": "Image pasted from clipboard",
|
||||
"clipboard_no_image": "No image found in clipboard",
|
||||
"created_entity": "Created {entity}",
|
||||
"default_filter_set": "Default filter set",
|
||||
"delete_past_tense": "Deleted {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||
|
|
|
|||
|
|
@ -1,25 +1,18 @@
|
|||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
const blobToDataURL = (blob: Blob): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
const readImage = (file: File, onLoadEnd: (imageData: string) => void) => {
|
||||
const reader: FileReader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
// only proceed if no error encountered
|
||||
if (!reader.error) {
|
||||
onLoadEnd(reader.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const pasteImage = (
|
||||
event: ClipboardEvent,
|
||||
onLoadEnd: (imageData: string) => void
|
||||
) => {
|
||||
const files = event?.clipboardData?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
const file = files[0];
|
||||
readImage(file, onLoadEnd);
|
||||
// only proceed if no error encountered
|
||||
blobToDataURL(file)
|
||||
.then(onLoadEnd)
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const onImageChange = (
|
||||
|
|
@ -30,6 +23,46 @@ const onImageChange = (
|
|||
if (file) readImage(file, onLoadEnd);
|
||||
};
|
||||
|
||||
const imageToDataURL = async (url: string) => {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
return blobToDataURL(blob);
|
||||
};
|
||||
|
||||
// uses event.clipboardData which works in all contexts including insecure HTTP
|
||||
const pasteImage = (
|
||||
event: ClipboardEvent,
|
||||
onLoadEnd: (imageData: string) => void
|
||||
) => {
|
||||
const files = event?.clipboardData?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
if (document.activeElement instanceof HTMLInputElement) {
|
||||
// don't interfere with pasting text into inputs
|
||||
return;
|
||||
}
|
||||
|
||||
const file = Array.from(files).find((f) => f.type.startsWith("image/"));
|
||||
if (file) readImage(file, onLoadEnd);
|
||||
};
|
||||
|
||||
// uses Clipboard API which requires secure context (HTTPS or localhost)
|
||||
const readClipboardImage = async (): Promise<string | null> => {
|
||||
if (!window.isSecureContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = await navigator.clipboard.read();
|
||||
for (const item of items) {
|
||||
const imageType = item.types.find((t) => t.startsWith("image/"));
|
||||
if (imageType) {
|
||||
const blob = await item.getType(imageType);
|
||||
return blobToDataURL(blob);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const usePasteImage = (
|
||||
onLoadEnd: (imageData: string) => void,
|
||||
isActive: boolean = true
|
||||
|
|
@ -53,23 +86,11 @@ const usePasteImage = (
|
|||
return false;
|
||||
};
|
||||
|
||||
const imageToDataURL = async (url: string) => {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
const ImageUtils = {
|
||||
onImageChange,
|
||||
usePasteImage,
|
||||
imageToDataURL,
|
||||
readClipboardImage,
|
||||
};
|
||||
|
||||
export default ImageUtils;
|
||||
|
|
|
|||
Loading…
Reference in a new issue