- Files toolbar link box (paste button, Enter); a lone http(s) link pasted onto any page opens the one app-wide download dialog, which now names the link; the palette's "Download from a link…" focuses the box on its history entry. - The download tray, bottom right on every page: what waits, runs, is paused or failed (reason + Retry, the queue's right-click / "…" menu), and what finished in this tab as "Done · Open" until closed; folds to a pill (per-account pref); closed, it stays shut — across a reload — until the work changes or something finishes. Back to top steps up over it, the page end keeps clear. - A finish is announced once: "Downloaded: …", whether a phone plays it, Copy link + Open (toasts and notices may carry a second action). - The inbox's ready notice opens the file in its folder: download_done carries job_id + folder_id. - GET /api/downloads is the queue only (in flight, failed, finished in the last 10 minutes), with each row's source key.
89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
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<string | null> {
|
|
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<HTMLInputElement>(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.
|
|
<span className={`flex min-w-0 flex-wrap items-center justify-end gap-2 ${className ?? ""}`}>
|
|
<Input
|
|
ref={field}
|
|
size="sm"
|
|
className="min-w-0 flex-[1_1_14rem]"
|
|
value={url}
|
|
onChange={(e) => 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")}
|
|
/>
|
|
<IconButton
|
|
label={t("downloads.page.paste")}
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => void paste(true)}
|
|
>
|
|
<ClipboardPaste className="w-4 h-4" />
|
|
</IconButton>
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
icon={<Download className="w-4 h-4" />}
|
|
onClick={submit}
|
|
className="shrink-0"
|
|
>
|
|
{t("files.link.download")}
|
|
</Button>
|
|
</span>
|
|
);
|
|
}
|