Files
siftlode/frontend/src/features/downloads/useDownloadActions.tsx
T

116 lines
4.4 KiB
TypeScript

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { Ban, Pause, Play, RefreshCw } from "lucide-react";
import { api, type DownloadJob, type DownloadJobKind } from "../../lib/api";
import { notifyActionError } from "../../lib/actionErrors";
import { invalidateDownloads } from "../../lib/downloads";
import { useConfirm } from "../../components/ui/confirmContext";
import { useCopyItems } from "../../components/ui/copyItems";
import { jobTitle } from "./downloadTitle";
import { type ActionMenuItem } from "../../components/ui/ActionMenu";
import { menuGroups } from "../../components/ui/menuGroups";
import { msg } from "../../lib/notifications";
// A queued download's ONE action list: the row's "…", its right-click, long-press and Shift+F10. The
// row keeps its primary action (pause / resume / retry) and its state (the error and its details) in
// sight; the copies and Cancel live here. A finished download is a library file: its actions are
// the Files page's (useFileActions).
export type JobOp = "pause" | "resume" | "cancel";
// The Cancel confirmation of a queued job; an upload's names what it deletes.
const CANCEL_DOWNLOAD = {
title: "downloads.confirm.cancelTitle",
body: "downloads.confirm.cancelBody",
action: "downloads.confirm.cancelAction",
};
const CANCEL_UPLOAD = {
title: "downloads.confirm.cancelUploadTitle",
body: "downloads.confirm.cancelUploadBody",
action: "downloads.confirm.cancelUploadAction",
};
const CANCEL_EDIT = {
title: "downloads.confirm.cancelEditTitle",
body: "downloads.confirm.cancelEditBody",
action: "downloads.confirm.cancelEditAction",
};
// Cancelling an upload deletes the file it uploaded (there is nothing to fetch again), so its
// confirmation says so — "stop the download" would read as safe to undo. A clip is being cut, not
// downloaded.
const CANCEL_COPY: Record<DownloadJobKind, typeof CANCEL_DOWNLOAD> = {
download: CANCEL_DOWNLOAD,
upload: CANCEL_UPLOAD,
edit: CANCEL_EDIT,
clip: CANCEL_EDIT, // both are a clip being cut: the same words
};
export function useDownloadActions() {
const qc = useQueryClient();
const { t } = useTranslation();
const confirm = useConfirm();
const copy = useCopyItems();
const act = useMutation({
mutationFn: ({ id, op }: { id: number; op: JobOp }) =>
op === "pause"
? api.pauseDownload(id)
: op === "resume"
? api.resumeDownload(id)
: api.cancelDownload(id),
onSuccess: () => invalidateDownloads(qc),
onError: (e, v) => notifyActionError(e, msg(`errors.writeFailed.job.${v.op}`)),
});
const confirmThen = async (title: string, body: string, action: string, fn: () => void) => {
if (await confirm({ title, message: body, confirmLabel: action, danger: true })) fn();
};
const icon = (I: typeof Play) => <I className="w-4 h-4" />;
const copies = (job: DownloadJob) => copy(jobTitle(job), job.source_url);
function queueItems(job: DownloadJob): ActionMenuItem[] {
const running = job.status === "queued" || job.status === "running";
return menuGroups(
[
running
? {
id: "pause",
label: t("downloads.actions.pause"),
icon: icon(Pause),
onSelect: () => act.mutate({ id: job.id, op: "pause" }),
}
: {
id: "resume",
label: t(
job.status === "error" ? "downloads.actions.retry" : "downloads.actions.resume",
),
icon: icon(job.status === "error" ? RefreshCw : Play),
onSelect: () => act.mutate({ id: job.id, op: "resume" }),
},
],
copies(job),
[
{
id: "cancel",
label: t("downloads.actions.cancel"),
icon: icon(Ban),
danger: true,
onSelect: () => {
const copy = CANCEL_COPY[job.job_kind ?? "download"];
void confirmThen(t(copy.title), t(copy.body), t(copy.action), () =>
act.mutate({ id: job.id, op: "cancel" }),
);
},
},
],
);
}
/** The job's menu rows. */
const items = (job: DownloadJob): ActionMenuItem[] => queueItems(job);
/** The queue's visible primary action and the error dialog's Retry. */
const run = (job: DownloadJob, op: JobOp) => act.mutate({ id: job.id, op });
return { items, run };
}