import { useState } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Lock, Pencil, Plus, Trash2 } from "lucide-react"; import Modal from "./ui/Modal"; import { useConfirm } from "./confirmContext"; import { api, type DownloadProfile, type DownloadSpec } from "../lib/api"; import { Button } from "./ui/Button"; import SegmentedToggle from "./ui/SegmentedToggle"; import FilterSelect from "./ui/FilterSelect"; import { IconButton } from "./ui/IconButton"; import { CheckRow } from "./ui/NativeControls"; import { Input } from "./ui/Input"; import { SURFACE } from "./ui/surfaces"; import { SectionLabel } from "./ui/Headings"; const HEIGHTS = [null, 2160, 1440, 1080, 720, 480, 360]; const BLANK: DownloadSpec = { mode: "av", max_height: 720, container: "mp4", audio_format: "m4a", vcodec: null, embed_subs: false, embed_chapters: true, embed_thumbnail: true, sponsorblock: false, }; function Check({ label, checked, onChange, }: { label: string; checked: boolean; onChange: (v: boolean) => void; }) { return ( {label} ); } export default function ProfileEditor({ onClose }: { onClose: () => void }) { const { t } = useTranslation(); const qc = useQueryClient(); const confirm = useConfirm(); const profilesQ = useQuery({ queryKey: qk.downloadProfiles(), queryFn: api.downloadProfiles }); const profiles = profilesQ.data ?? []; const builtins = profiles.filter((p) => p.is_builtin); const mine = profiles.filter((p) => !p.is_builtin); const [editing, setEditing] = useState(null); const [name, setName] = useState(""); const [spec, setSpec] = useState(BLANK); const [formOpen, setFormOpen] = useState(false); const invalidate = () => qc.invalidateQueries({ queryKey: qk.downloadProfiles() }); const startNew = () => { setEditing(null); setName(""); setSpec(BLANK); setFormOpen(true); }; const startEdit = (p: DownloadProfile) => { setEditing(p); setName(p.name); setSpec({ ...BLANK, ...p.spec }); setFormOpen(true); }; const save = useMutation({ mutationFn: () => editing ? api.updateDownloadProfile(editing.id, { name, spec }) : api.createDownloadProfile({ name, spec }), onSuccess: () => { invalidate(); setFormOpen(false); }, meta: { failTitle: "errors.writeFailed.downloadProfileSave" }, }); const del = useMutation({ mutationFn: (id: number) => api.deleteDownloadProfile(id), onSuccess: invalidate, meta: { failTitle: "errors.writeFailed.downloadProfileDelete" }, }); const onDelete = async (p: DownloadProfile) => { if ( await confirm({ title: t("downloads.profiles.delete"), message: t("downloads.profiles.deleteConfirm", { name: p.name }), confirmLabel: t("downloads.profiles.delete"), danger: true, }) ) { del.mutate(p.id); } }; const patch = (p: Partial) => setSpec((s) => ({ ...s, ...p })); return ( {/* Existing formats */}
{t("downloads.profiles.builtin")} {builtins.map((p) => (
{p.name}
))} {mine.length > 0 && ( {t("downloads.profiles.yours")} )} {mine.map((p) => (
{p.name} startEdit(p)} label={t("downloads.actions.rename")}> onDelete(p)} label={t("downloads.profiles.delete")} >
))}
{!formOpen ? ( ) : (
setName(e.target.value)} placeholder={t("downloads.profiles.namePlaceholder")} />
{/* Mode on its own row: three worded choices need the width a half column lacks on a phone. Segmented for the short fixed sets, a menu for the seven heights (owner's pick). */}
patch({ mode })} options={[ { value: "av", label: t("downloads.profiles.modeAv") }, { value: "v", label: t("downloads.profiles.modeV") }, { value: "a", label: t("downloads.profiles.modeA") }, ]} />
{spec.mode !== "a" ? (
v !== undefined && patch({ max_height: v ? Number(v) : null })} options={HEIGHTS.map((h) => ({ value: String(h ?? ""), label: h ? `${h}p` : t("downloads.profiles.best"), }))} />
) : (
{/* Only browser-playable audio: AAC (m4a) + MP3. Opus is dropped — Safari plays it unreliably as a bare file, and the module's purpose is in-browser playback. */} patch({ audio_format })} options={[ { value: "m4a", label: "M4A" }, { value: "mp3", label: "MP3" }, ]} />
)} {/* Container/codec are no longer user-selectable: every download is clamped to the mp4/H.264/AAC compat profile (the only universally browser-playable combo) so a shared link plays everywhere, iOS/Safari included. Enforced in backend `normalize`. */}
{spec.mode !== "a" && ( patch({ embed_subs: v })} /> )} patch({ embed_chapters: v })} /> patch({ embed_thumbnail: v })} /> patch({ sponsorblock: v })} />
)}
); }