feat(files): download from anywhere — a link box, a pasted link, the palette; the running downloads in a tray
- 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.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { ensureNavId, peekNavId } from "./history";
|
||||
import { createStore } from "./store";
|
||||
|
||||
// Downloading a link is an action you start anywhere: the Files page's link box, a link pasted onto
|
||||
// any page, the command palette. They all hand the link to ONE app-wide download dialog
|
||||
// (components/DownloadIntake), so none of them mounts a dialog of its own.
|
||||
|
||||
const pending = createStore<string | null>(null);
|
||||
|
||||
/** An http(s) link, alone — what a paste or the link box may download; anything else is not one. */
|
||||
export function asDownloadLink(text: string | null | undefined): string | null {
|
||||
const s = text?.trim() ?? "";
|
||||
return /^https?:\/\/\S+$/i.test(s) ? s : null;
|
||||
}
|
||||
|
||||
/** Whether a paste here is the page's to take: not typed into a field, and no dialog on top (the
|
||||
* reader is busy with that, and a link pasted at it is not a request to download). */
|
||||
export function pasteIsForThePage(target: EventTarget | null): boolean {
|
||||
const el = target instanceof Element ? target : null;
|
||||
if (el?.closest("input, textarea, select, [contenteditable]:not([contenteditable='false'])"))
|
||||
return false;
|
||||
return !document.querySelector("[aria-modal='true']");
|
||||
}
|
||||
|
||||
/** Ask to download `url`: the app's download dialog opens on it. */
|
||||
export const requestDownload = (url: string): void => pending.set(url);
|
||||
|
||||
/** The link the dialog is open on (null = closed). DownloadIntake only. */
|
||||
export const usePendingDownload = (): string | null => pending.useValue();
|
||||
|
||||
/** The dialog closed, downloading or not. */
|
||||
export const clearPendingDownload = (): void => pending.set(null);
|
||||
|
||||
// The palette's "Download from link…" takes the reader to the Files page's link box. A token for the
|
||||
// history entry the navigation made (as lib/homeIntent's): Files is a lazy module, often not mounted
|
||||
// when it is asked, and a request that outlived its entry must not grab the focus on a later visit.
|
||||
const linkBox = createStore<number | null>(null);
|
||||
|
||||
/** Ask the Files page to focus its link box on THIS entry — `setPage("files", requestLinkBox)`. */
|
||||
export const requestLinkBox = (): void => linkBox.set(ensureNavId());
|
||||
|
||||
/** The pending request (null = none) — the render subscription that wakes the link box. */
|
||||
export const useLinkBoxRequest = (): number | null => linkBox.useValue();
|
||||
|
||||
/** Take the request, clearing it; true only on the entry it was made for. */
|
||||
export const takeLinkBox = (): boolean => {
|
||||
const entry = linkBox.get();
|
||||
if (entry === null) return false;
|
||||
linkBox.set(null);
|
||||
return entry === peekNavId();
|
||||
};
|
||||
Reference in New Issue
Block a user