import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ClipboardPaste, Download } from "lucide-react"; import { asDownloadLink, requestDownload, takeLinkBox, useLinkBoxRequest, } from "../lib/downloadIntake"; import { Button } from "./ui/Button"; import { IconButton } from "./ui/IconButton"; import { Input } from "./ui/Input"; /** The clipboard's text, or null where the browser refuses. Outside the component: the React * Compiler cannot lower an optional call inside a try yet, and skipped the whole page for it. */ async function clipboardText(): Promise { try { return (await navigator.clipboard?.readText()) ?? null; } catch { return null; // permission denied or unsupported — the paste button still works on a click } } // "Paste a link and I download it": the field, the clipboard button and Download. The link goes to // the app's one download dialog (lib/downloadIntake), where the profile is picked. export function LinkBox({ className }: { className?: string }) { const { t } = useTranslation(); const [url, setUrl] = useState(""); const field = useRef(null); const submit = () => { const link = url.trim(); if (!link) return; requestDownload(link); setUrl(""); }; // Pull a link off the clipboard. Reading it needs a secure context and a gesture or a permission // (iOS Safari and Brave are strict), so it is best-effort: the button is a gesture and reliable, the // field's focus tries silently. Only an http(s) link is taken, and never over what was typed. const paste = async (force: boolean) => { if (!force && url.trim()) return; const link = asDownloadLink(await clipboardText()); if (link) setUrl(link); }; // The palette's "Download from link…" (lib/downloadIntake): the request wakes this effect, the // take decides — it clears the token, so a later pass or another entry's visit does nothing. const request = useLinkBoxRequest(); useEffect(() => { if (request !== null && takeLinkBox()) field.current?.focus(); }, [request]); return ( // Narrow, the field takes its own line and the two buttons go under it, right: beside them it // was left a few letters wide on a phone. setUrl(e.target.value)} onFocus={() => void paste(false)} onKeyDown={(e) => e.key === "Enter" && submit()} placeholder={t("files.link.placeholder")} aria-label={t("files.link.label")} /> void paste(true)} > ); }