mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-14 12:21:10 -05:00
859 lines
35 KiB
TypeScript
859 lines
35 KiB
TypeScript
'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<T> {
|
|
onSubmit: (data: T) => Promise<boolean> | 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<string, any>();
|
|
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<T extends { [x: string]: any }>({
|
|
onSubmit,
|
|
initialValues,
|
|
loading,
|
|
trigger,
|
|
title,
|
|
}: Readonly<ServerFormProps<T>>) {
|
|
const t = useTranslations('servers');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const defaultValues = useMemo(
|
|
() =>
|
|
normalizeValues({
|
|
name: '',
|
|
server_addr: '',
|
|
country: '',
|
|
city: '',
|
|
protocols: [],
|
|
}),
|
|
[],
|
|
);
|
|
|
|
const form = useForm<any>({
|
|
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 (
|
|
<div className='grid gap-4 p-3'>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.enabled` as const}
|
|
render={({ field }) => (
|
|
<div className='flex items-center justify-between gap-2'>
|
|
<FormLabel className='m-0'>{t('enabled')}</FormLabel>
|
|
<Switch
|
|
checked={!!field.value}
|
|
onCheckedChange={(checked) => {
|
|
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');
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
|
|
{enabled && (
|
|
<>
|
|
{['shadowsocks'].includes(proto) && (
|
|
<div className='grid grid-cols-2 gap-2'>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.method` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('encryption_method')}</FormLabel>
|
|
<FormControl>
|
|
<Select
|
|
value={field.value}
|
|
onValueChange={(value) => field.onChange(value)}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('select_encryption_method')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value='aes-128-gcm'>aes-128-gcm</SelectItem>
|
|
<SelectItem value='aes-192-gcm'>aes-192-gcm</SelectItem>
|
|
<SelectItem value='aes-256-gcm'>aes-256-gcm</SelectItem>
|
|
<SelectItem value='chacha20-ietf-poly1305'>
|
|
chacha20-ietf-poly1305
|
|
</SelectItem>
|
|
<SelectItem value='2022-blake3-aes-128-gcm'>
|
|
2022-blake3-aes-128-gcm
|
|
</SelectItem>
|
|
<SelectItem value='2022-blake3-aes-256-gcm'>
|
|
2022-blake3-aes-256-gcm
|
|
</SelectItem>
|
|
<SelectItem value='2022-blake3-chacha20-poly1305'>
|
|
2022-blake3-chacha20-poly1305
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.port` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('port')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
type='number'
|
|
step={1}
|
|
min={1}
|
|
max={65535}
|
|
placeholder={t('port_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{[
|
|
'2022-blake3-aes-128-gcm',
|
|
'2022-blake3-aes-256-gcm',
|
|
'2022-blake3-chacha20-poly1305',
|
|
].includes(method as any) && (
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.server_key` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('server_key')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput {...field} onValueChange={(v) => field.onChange(v)} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{['vmess', 'vless', 'trojan', 'hysteria2', 'tuic', 'anytls'].includes(proto) && (
|
|
<div
|
|
className={cn('flex gap-4 *:flex-1', {
|
|
'grid grid-cols-2': ['hysteria2', 'tuic'].includes(proto),
|
|
})}
|
|
>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.port` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('port')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
type='number'
|
|
step={1}
|
|
min={1}
|
|
max={65535}
|
|
placeholder={t('port_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{proto === 'vless' && (
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.flow` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('flow')}</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={(v) => field.onChange(v)}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('please_select')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value='none'>NONE</SelectItem>
|
|
<SelectItem value='xtls-rprx-vision'>XTLS-RPRX-Vision</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{proto === 'hysteria2' && (
|
|
<>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.obfs_password` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('obfs_password')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
placeholder={t('obfs_password_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.hop_ports` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('hop_ports')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
placeholder={t('hop_ports_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.hop_interval` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('hop_interval')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
type='number'
|
|
onValueChange={(v) => field.onChange(v)}
|
|
suffix={t('unitSecondsShort')}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{proto === 'tuic' && (
|
|
<>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.udp_relay_mode` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('udp_relay_mode')}</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={(v) => field.onChange(v)}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('please_select')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value='native'>Native</SelectItem>
|
|
<SelectItem value='quic'>QUIC</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.congestion_controller` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('congestion_controller')}</FormLabel>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={(v) => field.onChange(v)}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('please_select')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value='bbr'>BBR</SelectItem>
|
|
<SelectItem value='cubic'>Cubic</SelectItem>
|
|
<SelectItem value='reno'>Reno</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<div className='flex gap-2'>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.disable_sni` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('disable_sni')}</FormLabel>
|
|
<FormControl>
|
|
<div className='pt-2'>
|
|
<Switch
|
|
checked={!!field.value}
|
|
onCheckedChange={(checked) => field.onChange(checked)}
|
|
/>
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.reduce_rtt` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('reduce_rtt')}</FormLabel>
|
|
<FormControl>
|
|
<div className='pt-2'>
|
|
<Switch
|
|
checked={!!field.value}
|
|
onCheckedChange={(checked) => field.onChange(checked)}
|
|
/>
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{['vmess', 'vless', 'trojan'].includes(proto) && (
|
|
<Card>
|
|
<CardHeader className='flex flex-row items-center justify-between p-3'>
|
|
<CardTitle>{t('transport_title')}</CardTitle>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.transport` as const}
|
|
render={({ field }) => (
|
|
<FormItem className='!mt-0 min-w-32'>
|
|
<FormControl>
|
|
<Select value={field.value} onValueChange={(v) => field.onChange(v)}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('please_select')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value='tcp'>TCP</SelectItem>
|
|
<SelectItem value='websocket'>WebSocket</SelectItem>
|
|
{proto === 'vless' && <SelectItem value='http2'>HTTP/2</SelectItem>}
|
|
<SelectItem value='grpc'>gRPC</SelectItem>
|
|
{['vmess', 'vless'].includes(proto) && (
|
|
<SelectItem value='httpupgrade'>HTTP Upgrade</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</CardHeader>
|
|
{transport !== 'tcp' && (
|
|
<CardContent className='flex gap-4 p-3'>
|
|
{['websocket', 'http2', 'httpupgrade'].includes(transport as any) && (
|
|
<>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.transport_config.path` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('path')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.transport_config.host` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('host')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
{transport === 'grpc' && (
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.transport_config.service_name` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('service_name')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput {...field} onValueChange={(v) => field.onChange(v)} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
)}
|
|
|
|
{['vmess', 'vless', 'trojan', 'anytls', 'tuic', 'hysteria2'].includes(proto) && (
|
|
<Card>
|
|
<CardHeader className='flex flex-row items-center justify-between p-3'>
|
|
<CardTitle>{t('security_title')}</CardTitle>
|
|
{['vmess', 'vless', 'trojan'].includes(proto) && (
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.security` as const}
|
|
render={({ field }) => (
|
|
<FormItem className='!mt-0 min-w-32'>
|
|
<Select value={field.value} onValueChange={(v) => field.onChange(v)}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('please_select')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{['vmess', 'vless'].includes(proto) && (
|
|
<SelectItem value='none'>NONE</SelectItem>
|
|
)}
|
|
<SelectItem value='tls'>TLS</SelectItem>
|
|
{proto === 'vless' && (
|
|
<SelectItem value='reality'>Reality</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
</CardHeader>
|
|
|
|
{(['anytls', 'tuic', 'hysteria2'].includes(proto) ||
|
|
(['vmess', 'vless', 'trojan'].includes(proto) && security !== 'none')) && (
|
|
<CardContent className='grid grid-cols-2 gap-4 p-3'>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.security_config.sni` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_sni')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput {...field} onValueChange={(v) => field.onChange(v)} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{proto === 'vless' && security === 'reality' && (
|
|
<>
|
|
<FormField
|
|
control={control}
|
|
name={
|
|
`protocols.${idx}.config.security_config.reality_server_addr` as const
|
|
}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_server_address')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
placeholder={t('security_server_address_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={
|
|
`protocols.${idx}.config.security_config.reality_server_port` as const
|
|
}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_server_port')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
type='number'
|
|
min={1}
|
|
max={65535}
|
|
placeholder={t('security_server_port_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={
|
|
`protocols.${idx}.config.security_config.reality_private_key` as const
|
|
}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_private_key')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
placeholder={t('security_private_key_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={
|
|
`protocols.${idx}.config.security_config.reality_public_key` as const
|
|
}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_public_key')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
placeholder={t('security_public_key_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.security_config.reality_short_id` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_short_id')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput
|
|
{...field}
|
|
placeholder={t('security_short_id_placeholder')}
|
|
onValueChange={(v) => field.onChange(v)}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{proto === 'vless' && (
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.security_config.fingerprint` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_fingerprint')}</FormLabel>
|
|
<Select value={field.value} onValueChange={(v) => field.onChange(v)}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('please_select')} />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value='chrome'>Chrome</SelectItem>
|
|
<SelectItem value='firefox'>Firefox</SelectItem>
|
|
<SelectItem value='safari'>Safari</SelectItem>
|
|
<SelectItem value='ios'>iOS</SelectItem>
|
|
<SelectItem value='android'>Android</SelectItem>
|
|
<SelectItem value='edge'>Edge</SelectItem>
|
|
<SelectItem value='360'>360</SelectItem>
|
|
<SelectItem value='qq'>QQ</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<FormField
|
|
control={control}
|
|
name={`protocols.${idx}.config.security_config.allow_insecure` as const}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('security_allow_insecure')}</FormLabel>
|
|
<FormControl>
|
|
<div className='pt-2'>
|
|
<Switch
|
|
checked={!!field.value}
|
|
onCheckedChange={(checked) => field.onChange(checked)}
|
|
/>
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
<SheetTrigger asChild>
|
|
<Button
|
|
onClick={() => {
|
|
if (!initialValues) {
|
|
form.reset(defaultValues);
|
|
setActiveProto(PROTOCOLS[0]);
|
|
}
|
|
setOpen(true);
|
|
}}
|
|
>
|
|
{trigger}
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent className='w-[580px] max-w-full md:max-w-screen-md'>
|
|
<SheetHeader>
|
|
<SheetTitle>{title}</SheetTitle>
|
|
</SheetHeader>
|
|
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
|
|
<Form {...form}>
|
|
<form className='grid grid-cols-1 gap-2 px-6 pt-4'>
|
|
<div className='grid grid-cols-2 gap-2'>
|
|
<FormField
|
|
control={control}
|
|
name='name'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('name')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput {...field} onValueChange={(v) => field.onChange(v)} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name='server_addr'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('server_addr')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput {...field} onValueChange={(v) => field.onChange(v)} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name='country'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('country')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput {...field} onValueChange={(v) => field.onChange(v)} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={control}
|
|
name='city'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('city')}</FormLabel>
|
|
<FormControl>
|
|
<EnhancedInput {...field} onValueChange={(v) => field.onChange(v)} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<Tabs
|
|
value={activeProto}
|
|
onValueChange={(v) => setActiveProto(v as any)}
|
|
className='w-full pt-3'
|
|
>
|
|
<TabsList className='h-full w-full flex-wrap md:flex-nowrap'>
|
|
{PROTOCOLS.map((p) => (
|
|
<TabsTrigger value={p} key={p} className='relative'>
|
|
<div className='flex items-center gap-2'>{titleCase(p)}</div>
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
</Tabs>
|
|
<ProtocolEditor idx={activeIndex} proto={activeProto as string} />
|
|
</form>
|
|
</Form>
|
|
</ScrollArea>
|
|
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
|
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
|
|
{t('cancel')}
|
|
</Button>
|
|
<Button
|
|
disabled={loading}
|
|
onClick={form.handleSubmit(handleSubmit, () => {
|
|
toast.error(t('validation_failed'));
|
|
return false;
|
|
})}
|
|
>
|
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />} {t('confirm')}
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|