mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-07 05:55:27 +03:00
196 lines
6.4 KiB
TypeScript
196 lines
6.4 KiB
TypeScript
|
|
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<Record<string, boolean>>(() => {
|
||
|
|
const defaultDevice = deviceMap.get("stable");
|
||
|
|
const initial: Record<string, boolean> = {};
|
||
|
|
if (defaultDevice) {
|
||
|
|
for (const hwId of defaultDevice.autoHardware) {
|
||
|
|
initial[hwId] = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return initial;
|
||
|
|
});
|
||
|
|
|
||
|
|
const [portEnabled, setPortEnabled] = useState<Record<string, boolean>>(() => {
|
||
|
|
const initial: Record<string, boolean> = {};
|
||
|
|
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<string, boolean> = {};
|
||
|
|
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,
|
||
|
|
};
|
||
|
|
}
|