import { useState } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Plus, Trash2 } from "lucide-react"; import Modal from "./ui/Modal"; import { api, type DownloadJob } from "../lib/api"; import { inputCls } from "./ui/form"; import { Button } from "./ui/Button"; import { IconBtn } from "./downloadRow"; // Per-item user modals split out of DownloadCenter (self-contained, prop-driven): rename/metadata // editing and the failure-detail window. // Edit a download's display metadata: title, channel (name + link), and extra reference links. // All are display-only overrides — the on-disk file is never renamed. export function MetaEditModal({ job, onClose }: { job: DownloadJob; onClose: () => void }) { const { t } = useTranslation(); const qc = useQueryClient(); const [name, setName] = useState(job.display_name || job.asset?.title || ""); const [channel, setChannel] = useState(job.display_uploader || job.asset?.uploader || ""); const [channelUrl, setChannelUrl] = useState( job.display_uploader_url || job.asset?.uploader_url || "", ); // Editable extra-link rows; keep one blank row so there's always somewhere to type. const [links, setLinks] = useState(() => [...(job.extra_links ?? []), ""]); const setLink = (i: number, v: string) => setLinks((ls) => ls.map((l, j) => (j === i ? v : l))); const addLink = () => setLinks((ls) => [...ls, ""]); const removeLink = (i: number) => setLinks((ls) => ls.filter((_, j) => j !== i)); const save = useMutation({ mutationFn: () => api.updateDownloadMeta(job.id, { display_name: name.trim(), display_uploader: channel.trim(), display_uploader_url: channelUrl.trim(), extra_links: links.map((l) => l.trim()).filter(Boolean), }), onSuccess: () => { qc.invalidateQueries({ queryKey: qk.downloads() }); onClose(); }, }); return (
{t("downloads.edit.extraLinks")}
{links.map((l, i) => (
setLink(i, e.target.value)} placeholder="https://…" className={inputCls} /> removeLink(i)} title={t("downloads.edit.removeLink")} danger >
))}

{t("downloads.edit.hint")}

); } // Transient failures worth an in-modal Retry button; the rest are permanent (private/geo/DRM/ // sign-in/unsupported-host/…), where a retry can't help — the queue row still keeps its own retry // control regardless. `unknown` is deliberately NOT here: an unclassified failure is most often an // auth wall we couldn't parse (a login-walled Facebook post reads as a bare "no format"), so a // prominent Retry would mislead; force-retrying from the row stays available for genuine transients. const RETRYABLE_ERRORS = ["postprocess", "network"]; // A readable "why did this fail" window for a failed job: a category headline + plain explanation, // the raw yt-dlp/ffmpeg text behind a disclosure, and a Retry when the failure may be transient. export function ErrorModal({ job, onRetry, onClose, }: { job: DownloadJob; onRetry: () => void; onClose: () => void; }) { const { t } = useTranslation(); const code = job.error_code || "unknown"; const [showRaw, setShowRaw] = useState(false); const retryable = RETRYABLE_ERRORS.includes(code); return (
{t(`downloads.errors.fail.cat.${code}.short`)}

{t(`downloads.errors.fail.cat.${code}.body`)}

{job.error && (
{showRaw && (
                {job.error}
              
)}
)}
{retryable && ( )}
); }