import { useState, useCallback, useMemo } from "react"; import { deviceMap, hardwareMap, portMap } from "../config"; import { generateDockerCompose } from "../generator"; import type { GeneratorInput } from "../generator"; /** * Main hook that holds all form state and generates the Docker Compose output. * Configuration is loaded synchronously from build-time generated .ts files. */ export function useConfigGenerator() { const [deviceId, setDeviceId] = useState("stable"); const [hardwareEnabled, setHardwareEnabled] = useState>(() => { const defaultDevice = deviceMap.get("stable"); const initial: Record = {}; if (defaultDevice) { for (const hwId of defaultDevice.autoHardware) { initial[hwId] = true; } } return initial; }); const [portEnabled, setPortEnabled] = useState>(() => { const initial: Record = {}; for (const p of portMap.values()) { initial[p.id] = p.defaultEnabled; } return initial; }); const [nvidiaGpuCount, setNvidiaGpuCount] = useState(""); const [nvidiaGpuDeviceId, setNvidiaGpuDeviceId] = useState(""); const [configPath, setConfigPath] = useState(""); const [mediaPath, setMediaPath] = useState(""); const [rtspPassword, setRtspPassword] = useState(""); const [timezone, setTimezone] = useState(""); const [shmSize, setShmSize] = useState("512mb"); const [shmSizeError, setShmSizeError] = useState(false); const [gpuDeviceIdError, setGpuDeviceIdError] = useState(false); const [configPathError, setConfigPathError] = useState(false); const [mediaPathError, setMediaPathError] = useState(false); const device = useMemo(() => deviceMap.get(deviceId)!, [deviceId]); const selectDevice = useCallback((id: string) => { const newDevice = deviceMap.get(id); if (!newDevice) return; setDeviceId(id); setHardwareEnabled(() => { const next: Record = {}; for (const hwId of newDevice.autoHardware) { next[hwId] = true; } return next; }); setNvidiaGpuCount(""); setNvidiaGpuDeviceId(""); setGpuDeviceIdError(false); }, []); const toggleHardware = useCallback((hwId: string) => { setHardwareEnabled((prev) => ({ ...prev, [hwId]: !prev[hwId] })); }, []); const togglePort = useCallback((portId: string) => { const port = portMap.get(portId); if (port?.locked) return; setPortEnabled((prev) => ({ ...prev, [portId]: !prev[portId] })); }, []); const isHardwareDisabled = useCallback( (hwId: string): boolean => { const hw = hardwareMap.get(hwId); if (!hw) return false; return hw.disabledWhen?.includes(deviceId) ?? false; }, [deviceId] ); const validateShmSize = useCallback((value: string): boolean => { if (!value) return true; return /^\d+(\.\d+)?[bkmgBKMG]{1,2}$/.test(value); }, []); const validatePath = useCallback((value: string): boolean => { if (!value) return true; return /^[a-zA-Z0-9_\-/./]+$/.test(value); }, []); const handleShmSizeChange = useCallback( (value: string) => { const filtered = value.replace(/[^0-9.bkmgBKMG]/g, ""); const valid = validateShmSize(filtered); setShmSize(filtered); setShmSizeError(!valid && filtered !== ""); }, [validateShmSize] ); const handleConfigPathChange = useCallback( (value: string) => { const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, ""); const valid = validatePath(filtered); setConfigPath(filtered); setConfigPathError(!valid && filtered !== ""); }, [validatePath] ); const handleMediaPathChange = useCallback( (value: string) => { const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, ""); const valid = validatePath(filtered); setMediaPath(filtered); setMediaPathError(!valid && filtered !== ""); }, [validatePath] ); const handleNvidiaGpuCountChange = useCallback((value: string) => { // Only allow digits setNvidiaGpuCount(value); if (value === "") { setNvidiaGpuDeviceId(""); setGpuDeviceIdError(false); } else { setGpuDeviceIdError(false); } }, []); const handleNvidiaGpuDeviceIdChange = useCallback((value: string) => { setNvidiaGpuDeviceId(value.trim()); setGpuDeviceIdError(false); }, []); const enabledPortLines = useMemo(() => { const lines: string[] = []; for (const [id, enabled] of Object.entries(portEnabled)) { if (!enabled) continue; const p = portMap.get(id); if (!p) continue; const proto = p.protocol && p.protocol !== "tcp" ? `/${p.protocol}` : ""; const comment = p.description ? ` # ${p.description}` : ""; lines.push(` - "${p.host}:${p.container}${proto}"${comment}`); } return lines; }, [portEnabled]); const selectedHardwareIds = useMemo(() => { return Object.entries(hardwareEnabled) .filter(([id, enabled]) => { if (!enabled) return false; const hw = hardwareMap.get(id); if (!hw) return false; if (hw.disabledWhen?.includes(deviceId)) return false; return true; }) .map(([id]) => id); }, [hardwareEnabled, deviceId]); const generatedYaml = useMemo(() => { const input: GeneratorInput = { device, selectedHardware: selectedHardwareIds, enabledPorts: enabledPortLines, configPath: configPath || "/path/to/your/config", mediaPath: mediaPath || "/path/to/your/storage", rtspPassword, timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC", shmSize: shmSize || "512mb", nvidiaGpuCount, nvidiaGpuDeviceId, }; return generateDockerCompose(input); }, [ device, selectedHardwareIds, enabledPortLines, configPath, mediaPath, rtspPassword, timezone, shmSize, nvidiaGpuCount, nvidiaGpuDeviceId, ]); const hasAnyHardware = selectedHardwareIds.length > 0 || !!device?.devices?.length; return { deviceId, device, hardwareEnabled, portEnabled, nvidiaGpuCount, nvidiaGpuDeviceId, configPath, mediaPath, rtspPassword, timezone, shmSize, shmSizeError, gpuDeviceIdError, configPathError, mediaPathError, hasAnyHardware, generatedYaml, selectDevice, toggleHardware, togglePort, handleShmSizeChange, handleConfigPathChange, handleMediaPathChange, handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange, setRtspPassword, setTimezone, isHardwareDisabled, }; }