import React, {useState} from "react"; import {fetchVideoDownloadDefaults, getDownloaders, postDownload, putDownload} from "../api"; import { APIButton, DirectorySearch, InfoHeader, InfoPopup, mergeDeep, RequiredAsterisk, SimpleAccordion, Toggle, useLocalStorage, validURL, validURLs, } from "./Common"; import {Button, Group, Header, Icon, Message, MultiSelect, NumberInput, Select, Stack, TextInput, Textarea} from "./ui"; import {Link} from "react-router"; import {TagsSelector} from "../Tags"; import { commaSeparatedValidator, useForm, } from "../hooks/useForm"; import { channelFrequencyOptions, days30Option, defaultAudioFormatOption, defaultVideoFormatOption, defaultVideoResolutionOptions, downloadAudioFormatOptions, Downloaders, downloadFormatOptions, downloadOrderOptions, downloadResolutionOptions, extendedFrequencyOptions, frequencyOptions, weeklyOption } from "./Vars"; /* * Download.js was built around the old
family (Form.Input, * FormDropdown, TextArea) via `hooks/useForm.js`'s JSX helpers (InputForm, * NumberInputForm, ToggleForm, UrlInput, UrlsTextarea). `hooks/useForm.js` is * off-limits for this migration and its JSX helpers still render through * `./Theme`. Rather than use those helpers, the field * components below call `useForm`'s plain data getters (getInputProps, * getSelectionProps, getCustomProps) directly and render our own markup with * the token-driven `./ui` library. See the migration report for what this * leaves out of scope (hooks/useForm.js itself is unmigrated). */ function FieldLabel({htmlFor, label, required, helpContent, helpPosition = 'top'}) { if (!label) return null; return } /** Text field backed by `form.getInputProps`. TextInput passes a DOM event, which is exactly * what `handleInputEvent` (inside useForm) expects, so the props can be spread through. */ function TextField({ form, name, path, type = 'text', validator, required = false, placeholder = '', label, helpContent, helpPosition, disabled = false, }) { const [inputProps] = form.getInputProps({name, path, validator, type, required}); return
} /** Number field. Mantine's NumberInput hands back the value itself, not an event, so it is * wrapped into the event shape `handleInputEvent` expects. */ function NumberField({ form, name, path, validator, required = false, placeholder = '', label, helpContent, helpPosition, min = 1, max = 9999999999, }) { validator = validator || ((value) => { value = typeof value === 'number' ? value : parseFloat(value); if (value < 0) { return 'Number must be positive'; } }); const [inputProps] = form.getInputProps({name, path, validator, type: 'number', required}); const dataPath = inputProps['data-path']; return
inputProps.onChange({target: {value, type: 'number', dataset: {path: dataPath}}})} disabled={inputProps.disabled} error={inputProps.error} />
} /** A single-select field. `options` are {key, text, value}; Mantine's Select * requires string data values, so values are stringified for display and mapped back on change. */ function SelectField({form, name, path, label, options, required = false, placeholder, disabled = false, afterChange}) { const [inputProps] = form.getSelectionProps({name, path, required, afterChange}); const data = options.map(o => ({value: String(o.value), label: o.text})); const stringValue = (inputProps.value === null || inputProps.value === undefined) ? null : String(inputProps.value); const handleChange = (val) => { if (val === null || val === undefined) { inputProps.onChange(null, {value: null}); return; } const match = options.find(o => String(o.value) === val); inputProps.onChange(null, {value: match ? match.value : val}); }; return