"use client"; import React, { useState, useEffect, FormEvent } from 'react'; import { Inbound, Protocol, ClientSetting } from '@/types/inbound'; import { useRouter } from 'next/navigation'; import ProtocolClientSettings from './ProtocolClientSettings'; import StreamSettingsForm from './stream_settings/StreamSettingsForm'; // Import new StreamSettingsForm const availableProtocols: Protocol[] = ["vmess", "vless", "trojan", "shadowsocks", "dokodemo-door", "socks", "http"]; const availableShadowsocksCiphers: string[] = [ "aes-256-gcm", "aes-128-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "none" ]; const inputStyles = "mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"; interface InboundFormProps { initialData?: Partial; isEditMode?: boolean; formLoading?: boolean; onSubmitForm: (inboundData: Partial) => Promise; } const InboundForm: React.FC = ({ initialData, isEditMode = false, formLoading, onSubmitForm }) => { const router = useRouter(); const [remark, setRemark] = useState(''); const [listen, setListen] = useState(''); const [port, setPort] = useState(''); const [protocol, setProtocol] = useState(''); const [enable, setEnable] = useState(true); const [expiryTime, setExpiryTime] = useState(0); const [total, setTotal] = useState(0); const [clientList, setClientList] = useState([]); const [ssMethod, setSsMethod] = useState(availableShadowsocksCiphers[0]); const [ssPassword, setSsPassword] = useState(''); const [settingsJson, setSettingsJson] = useState('{}'); const [streamSettingsJson, setStreamSettingsJson] = useState('{}'); const [sniffingJson, setSniffingJson] = useState('{}'); const [formError, setFormError] = useState(null); useEffect(() => { if (initialData) { const currentProtocol = (initialData.protocol || '') as Protocol; setRemark(initialData.remark || ''); setListen(initialData.listen || ''); setPort(initialData.port || ''); setProtocol(currentProtocol); setEnable(initialData.enable !== undefined ? initialData.enable : true); setExpiryTime(initialData.expiryTime || 0); setTotal(initialData.total && initialData.total > 0 ? initialData.total / (1024 * 1024 * 1024) : 0); setStreamSettingsJson(initialData.streamSettings || '{}'); setSniffingJson(initialData.sniffing || '{}'); const initialSettings = initialData.settings || '{}'; setSettingsJson(initialSettings); // Initialize settingsJson with all settings if ((currentProtocol === 'vmess' || currentProtocol === 'vless' || currentProtocol === 'trojan')) { try { const parsedSettings = JSON.parse(initialSettings); setClientList(parsedSettings.clients || []); } catch (e) { console.error(`Error parsing settings for ${currentProtocol}:`, e); setClientList([]); } } else if (currentProtocol === 'shadowsocks') { try { const parsedSettings = JSON.parse(initialSettings); setSsMethod(parsedSettings.method || availableShadowsocksCiphers[0]); setSsPassword(parsedSettings.password || ''); } catch (e) { console.error("Error parsing Shadowsocks settings:", e); } } else { // For other protocols, specific UI states are not used for settings setClientList([]); setSsMethod(availableShadowsocksCiphers[0]); setSsPassword(''); } } else { // Reset all fields for a new form setRemark(''); setListen(''); setPort(''); setProtocol(''); setEnable(true); setExpiryTime(0); setTotal(0); setClientList([]); setSsMethod(availableShadowsocksCiphers[0]); setSsPassword(''); setSettingsJson('{}'); setStreamSettingsJson('{}'); setSniffingJson('{}'); } }, [initialData]); // This useEffect updates settingsJson based on UI changes for client lists or SS settings useEffect(() => { if (protocol === 'vmess' || protocol === 'vless' || protocol === 'trojan' || protocol === 'shadowsocks') { let baseSettings: Record = {}; try { baseSettings = JSON.parse(settingsJson) || {}; // Preserve other settings } catch { /* If settingsJson is invalid, it will be overwritten */ } const newSettingsObject = { ...baseSettings }; // Changed to const if (protocol === 'vmess' || protocol === 'vless' || protocol === 'trojan') { delete newSettingsObject.method; delete newSettingsObject.password; newSettingsObject.clients = clientList; } else if (protocol === 'shadowsocks') { delete newSettingsObject.clients; newSettingsObject.method = ssMethod; newSettingsObject.password = ssPassword; } try { const finalJson = JSON.stringify(newSettingsObject, null, 2); if (finalJson !== settingsJson) { // Only update if there's an actual change setSettingsJson(finalJson); } } catch (e) { console.error("Error stringifying settings:", e); } } // No 'else' needed: for other protocols, settingsJson is managed by its textarea directly. // eslint-disable-next-line react-hooks/exhaustive-deps }, [clientList, ssMethod, ssPassword, protocol]); // settingsJson is NOT a dependency here. const handleProtocolChange = (newProtocol: Protocol) => { setProtocol(newProtocol); setFormError(null); // When protocol changes, re-initialize specific UI states from current settingsJson let currentSettings: Record = {}; // Changed any to unknown try { currentSettings = JSON.parse(settingsJson || '{}'); } catch {} if (newProtocol === 'vmess' || newProtocol === 'vless' || newProtocol === 'trojan') { if (Array.isArray(currentSettings.clients)) { setClientList(currentSettings.clients as ClientSetting[]); } else { setClientList([]); } setSsMethod(availableShadowsocksCiphers[0]); setSsPassword(''); } else if (newProtocol === 'shadowsocks') { setClientList([]); if (typeof currentSettings.method === 'string') { setSsMethod(currentSettings.method); } else { setSsMethod(availableShadowsocksCiphers[0]); } if (typeof currentSettings.password === 'string') { setSsPassword(currentSettings.password); } else { setSsPassword(''); } } else { // For "other" protocols, clear all specific UI states setClientList([]); setSsMethod(availableShadowsocksCiphers[0]); setSsPassword(''); // settingsJson remains as is, for manual editing } }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setFormError(null); if (!protocol) { setFormError("Protocol is required."); return; } if (port === '' || Number(port) <= 0 || Number(port) > 65535) { setFormError("Valid Port (1-65535) is required."); return; } // settingsJson should already be up-to-date from the useEffect hook // for vmess/vless/trojan/shadowsocks. // For other protocols, it's taken directly from its textarea. if (protocol === 'shadowsocks' && !ssPassword) { setFormError("Password is required for Shadowsocks."); return; } // Validate all JSON fields before submitting for (const [fieldName, jsonStr] of Object.entries({ settings: settingsJson, streamSettings: streamSettingsJson, sniffing: sniffingJson })) { try { JSON.parse(jsonStr); } catch (err) { setFormError(`Invalid JSON in ${fieldName === 'settings' ? 'protocol settings' : fieldName}: ${(err as Error).message}`); return; } } const inboundData: Partial = { remark, listen, port: Number(port), protocol, enable, expiryTime: Number(expiryTime), total: Number(total) * 1024 * 1024 * 1024, settings: settingsJson, streamSettings: streamSettingsJson, sniffing: sniffingJson, }; if (isEditMode && initialData?.id) { inboundData.id = initialData.id; inboundData.up = initialData.up; inboundData.down = initialData.down; } await onSubmitForm(inboundData); }; return (
{formError &&
{formError}
}
Basic Settings
setRemark(e.target.value)} className={inputStyles} />
setListen(e.target.value)} placeholder="Default: 0.0.0.0" className={inputStyles} />
setPort(e.target.value === '' ? '' : Number(e.target.value))} required min="1" max="65535" className={inputStyles} />
setTotal(e.target.value === '' ? '' : Number(e.target.value))} min="0" placeholder="0 for unlimited" className={inputStyles} />
setExpiryTime(e.target.value === '' ? '' : Number(e.target.value))} min="0" placeholder="0 for never" className={inputStyles} />
setEnable(e.target.checked)} className="h-4 w-4 text-primary-600 border-gray-300 dark:border-gray-500 rounded focus:ring-primary-500 bg-white dark:bg-gray-700" />
{protocol && (
{protocol} Settings {(protocol === 'vmess' || protocol === 'vless' || protocol === 'trojan') ? ( ) : protocol === 'shadowsocks' ? (
setSsPassword(e.target.value)} required className={inputStyles} />
) : (