'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, 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 { cn } from '@workspace/ui/lib/utils'; import { useTranslations } from 'next-intl'; import { useEffect, useMemo, useState } from 'react'; import { useFieldArray, useForm, useWatch } from 'react-hook-form'; import { toast } from 'sonner'; import { formScheme, getProtocolDefaultConfig, protocols as PROTOCOLS } from './form-scheme'; interface ServerFormProps { onSubmit: (data: T) => Promise | boolean; initialValues?: T | any; loading?: boolean; trigger: string; title: string; } function titleCase(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); } function normalizeValues(raw: any) { const map = new Map(); const given = Array.isArray(raw?.protocols) ? raw.protocols : []; for (const it of given) map.set(it?.protocol, it); const normalized = { name: raw?.name ?? '', server_addr: raw?.server_addr ?? '', country: raw?.country ?? '', city: raw?.city ?? '', protocols: PROTOCOLS.map((p) => { const incoming = map.get(p); const def = getProtocolDefaultConfig(p as any); if (incoming) { return { protocol: p, enabled: !!incoming.enabled, config: { ...def, ...(incoming.config ?? {}) }, }; } return { protocol: p, enabled: false, config: def }; }), }; return normalized; } export default function ServerForm({ onSubmit, initialValues, loading, trigger, title, }: Readonly>) { const t = useTranslations('servers'); const [open, setOpen] = useState(false); const defaultValues = useMemo( () => normalizeValues({ name: '', server_addr: '', country: '', city: '', protocols: [], }), [], ); const form = useForm({ resolver: zodResolver(formScheme), defaultValues, }); const { control } = form; useFieldArray({ control, name: 'protocols' }); const [activeProto, setActiveProto] = useState(PROTOCOLS[0]); const activeIndex = useMemo(() => PROTOCOLS.findIndex((p) => p === activeProto), [activeProto]); useEffect(() => { if (initialValues) { const normalized = normalizeValues(initialValues); form.reset(normalized); const enabledFirst = normalized.protocols.find((p: any) => p.enabled)?.protocol; setActiveProto((enabledFirst as any) || PROTOCOLS[0]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialValues]); async function handleSubmit(data: { [x: string]: any }) { const ok = await onSubmit(data as unknown as T); if (ok) setOpen(false); } function ProtocolEditor({ idx, proto }: { idx: number; proto: string }) { const transport = useWatch({ control, name: `protocols.${idx}.config.transport` as const }); const security = useWatch({ control, name: `protocols.${idx}.config.security` as const }); const method = useWatch({ control, name: `protocols.${idx}.config.method` as const }); const enabled = useWatch({ control, name: `protocols.${idx}.enabled` as const }); return (
(
{t('enabled')} { field.onChange(checked); if (checked) { form.setValue( `protocols.${idx}.config` as const, getProtocolDefaultConfig(proto as any), ); if (['trojan', 'hysteria2'].includes(proto)) { form.setValue(`protocols.${idx}.config.security` as const, 'tls'); } } }} />
)} /> {enabled && ( <> {['shadowsocks'].includes(proto) && (
( {t('encryption_method')} )} /> ( {t('port')} field.onChange(v)} /> )} /> {[ '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305', ].includes(method as any) && ( ( {t('server_key')} field.onChange(v)} /> )} /> )}
)} {['vmess', 'vless', 'trojan', 'hysteria2', 'tuic', 'anytls'].includes(proto) && (
( {t('port')} field.onChange(v)} /> )} /> {proto === 'vless' && ( ( {t('flow')} )} /> )} {proto === 'hysteria2' && ( <> ( {t('obfs_password')} field.onChange(v)} /> )} /> ( {t('hop_ports')} field.onChange(v)} /> )} /> ( {t('hop_interval')} field.onChange(v)} suffix={t('unitSecondsShort')} /> )} /> )} {proto === 'tuic' && ( <> ( {t('udp_relay_mode')} )} /> ( {t('congestion_controller')} )} />
( {t('disable_sni')}
field.onChange(checked)} />
)} /> ( {t('reduce_rtt')}
field.onChange(checked)} />
)} />
)}
)} {['vmess', 'vless', 'trojan'].includes(proto) && ( {t('transport_title')} ( )} /> {transport !== 'tcp' && ( {['websocket', 'http2', 'httpupgrade'].includes(transport as any) && ( <> ( {t('path')} field.onChange(v)} /> )} /> ( {t('host')} field.onChange(v)} /> )} /> )} {transport === 'grpc' && ( ( {t('service_name')} field.onChange(v)} /> )} /> )} )} )} {['vmess', 'vless', 'trojan', 'anytls', 'tuic', 'hysteria2'].includes(proto) && ( {t('security_title')} {['vmess', 'vless', 'trojan'].includes(proto) && ( ( )} /> )} {(['anytls', 'tuic', 'hysteria2'].includes(proto) || (['vmess', 'vless', 'trojan'].includes(proto) && security !== 'none')) && ( ( {t('security_sni')} field.onChange(v)} /> )} /> {proto === '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)} /> )} /> )} {proto === 'vless' && ( ( {t('security_fingerprint')} )} /> )} ( {t('security_allow_insecure')}
field.onChange(checked)} />
)} />
)}
)} )}
); } return ( {title}
( {t('name')} field.onChange(v)} /> )} /> ( {t('server_addr')} field.onChange(v)} /> )} /> ( {t('country')} field.onChange(v)} /> )} /> ( {t('city')} field.onChange(v)} /> )} />
setActiveProto(v as any)} className='w-full pt-3' > {PROTOCOLS.map((p) => (
{titleCase(p)}
))}
); }