'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@workspace/ui/components/button'; import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@workspace/ui/components/form'; import { ScrollArea } from '@workspace/ui/components/scroll-area'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@workspace/ui/components/select'; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from '@workspace/ui/components/sheet'; import { Switch } from '@workspace/ui/components/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { Icon } from '@workspace/ui/custom-components/icon'; import { useTranslations } from 'next-intl'; import { useEffect, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { toast } from 'sonner'; import { FINGERPRINTS, FLOWS, formSchema, getLabel, getProtocolDefaultConfig, LABELS, protocols as PROTOCOLS, SECURITY, ServerFormValues, SS_CIPHERS, TRANSPORTS, TUIC_CONGESTION, TUIC_UDP_RELAY_MODES, } from './form-schema'; function titleCase(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); } export default function ServerForm(props: { trigger: string; title: string; loading?: boolean; initialValues?: Partial; onSubmit: (values: ServerFormValues) => Promise | boolean; }) { const { trigger, title, loading, initialValues, onSubmit } = props; const t = useTranslations('servers'); const [open, setOpen] = useState(false); const [activeType, setActiveType] = useState<(typeof PROTOCOLS)[number]>('shadowsocks'); const [protocolsEnabled, setProtocolsEnabled] = useState([]); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: '', address: '', country: '', city: '', ratio: 1, protocols: [], ...initialValues, }, }); const { control } = form; const protocolsValues = useWatch({ control, name: 'protocols' }); useEffect(() => { if (initialValues) { const enabledProtocols = PROTOCOLS.filter((type) => { const protocol = initialValues.protocols?.find((p) => p.type === type); return protocol && protocol.port && Number(protocol.port) > 0; }); setProtocolsEnabled(enabledProtocols); form.reset({ name: '', address: '', country: '', city: '', ratio: 1, ...initialValues, protocols: PROTOCOLS.map((type) => { const existingProtocol = initialValues.protocols?.find((p) => p.type === type); return existingProtocol || getProtocolDefaultConfig(type); }), }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialValues]); async function handleSubmit(values: Record) { const filtered = (values?.protocols || []) .filter((p: any, index: number) => { const port = Number(p?.port); const protocolType = PROTOCOLS[index]; return ( protocolType && protocolsEnabled.includes(protocolType) && Number.isFinite(port) && port > 0 && port <= 65535 ); }) .map((p: any) => ({ ...p, port: Number(p.port) })); if (filtered.length === 0) { toast.error(t('validation_failed')); return; } const result = { name: values.name, country: values.country, city: values.city, ratio: Number(values.ratio || 1), address: values.address, protocols: filtered, }; const ok = await onSubmit(result); if (ok) { form.reset(); setOpen(false); } } return ( {title}
( {t('name')} field.onChange(v)} /> )} /> ( {t('country')} field.onChange(v)} /> )} /> ( {t('city')} field.onChange(v)} /> )} />
( {t('address')} field.onChange(v)} /> )} /> ( {t('traffic_ratio')} field.onChange(v)} /> )} />
setActiveType(v as any)}> {PROTOCOLS.map((type) => ( {titleCase(type)} ))} {PROTOCOLS.map((type) => { const i = Math.max( 0, PROTOCOLS.findIndex((t) => t === type), ); const current = Array.isArray(protocolsValues) ? protocolsValues[i] || {} : {}; const transport = ((current as any)?.transport as string | undefined) ?? 'tcp'; const security = (current as any)?.security as string | undefined; const cipher = (current as any)?.cipher as string | undefined; const isEnabled = protocolsEnabled.includes(type); return (
{t('enabled')} { if (checked) { setProtocolsEnabled([...protocolsEnabled, type]); } else { setProtocolsEnabled(protocolsEnabled.filter((p) => p !== type)); } }} />
{isEnabled && (
( {t('port')} field.onChange(v)} /> )} /> {type === 'shadowsocks' && ( <> ( {t('encryption_method')} )} /> {[ '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305', ].includes((cipher || '').toString()) && ( ( {t('server_key')} field.onChange(v)} /> )} /> )} )} {type === 'vless' && ( ( {t('flow')} )} /> )} {type === 'hysteria2' && ( <> ( {t('obfs_password')} field.onChange(v)} /> )} /> ( {t('hop_ports')} field.onChange(v)} /> )} /> ( {t('hop_interval')} field.onChange(v)} /> )} /> )} {type === 'tuic' && ( <> ( {t('udp_relay_mode')} )} /> ( {t('congestion_controller')} )} />
( {t('disable_sni')}
field.onChange(checked) } />
)} /> ( {t('reduce_rtt')}
field.onChange(checked) } />
)} />
)}
{['vmess', 'vless', 'trojan'].includes(type) && ( {t('transport_title')} ( )} /> {transport !== 'tcp' && ( {['websocket', 'http2', 'httpupgrade'].includes( transport as string, ) && ( <> ( HOST { form.setValue(field.name, value); }} /> )} /> ( {t('path')} field.onChange(v)} /> )} /> )} {transport === 'grpc' && ( ( {t('service_name')} field.onChange(v)} /> )} /> )} )} )} {['vmess', 'vless', 'trojan', 'anytls', 'tuic', 'hysteria2'].includes( type, ) && ( {t('security_title')} {['vmess', 'vless', 'trojan'].includes(type) && ( ( )} /> )} {(['anytls', 'tuic', 'hysteria2'].includes(type) || (['vmess', 'vless', 'trojan'].includes(type) && security !== 'none')) && ( ( {t('security_sni')} field.onChange(v)} /> )} /> {type === 'vless' && security === 'reality' && ( <> ( {t('security_server_address')} field.onChange(v)} /> )} /> ( {t('security_server_port')} field.onChange(v)} /> )} /> ( {t('security_private_key')} field.onChange(v)} /> )} /> ( {t('security_public_key')} field.onChange(v)} /> )} /> ( {t('security_short_id')} field.onChange(v)} /> )} /> )} ( {t('security_fingerprint')} )} /> ( {t('security_allow_insecure')}
field.onChange(checked) } />
)} />
)}
)}
)}
); })}
); }