| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- 'use client';
- import { useState } from 'react';
- import {
- buildSubscriptionUrls,
- buildShareLinks,
- buildBase64Subscription,
- buildJsonSubscription,
- type SubClient,
- type SubUrlInput,
- } from '@/lib/xray/subscription';
- import type { Network, Security } from '@/lib/xray/outbounds';
- import { ToolFrame } from './tool-frame';
- import { TextField, SelectField, CheckboxField } from './shared/fields';
- import { OutputBlock } from './shared/output-block';
- type ClientProtocol = 'vless' | 'vmess' | 'trojan' | 'ss';
- interface ClientRow {
- protocol: ClientProtocol;
- remark: string;
- address: string;
- port: string;
- credential: string; // id (vless/vmess) or password (trojan/ss)
- method: string; // ss
- network: Network;
- security: Security;
- sni: string;
- }
- const PROTOCOLS: readonly ClientProtocol[] = ['vless', 'vmess', 'trojan', 'ss'];
- const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
- const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
- const addBtn =
- 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
- const DEFAULT_CLIENTS: ClientRow[] = [
- {
- protocol: 'vless',
- remark: 'HK-01',
- address: 'a.example.com',
- port: '443',
- credential: '11111111-2222-3333-4444-555555555555',
- method: '',
- network: 'tcp',
- security: 'reality',
- sni: 'www.microsoft.com',
- },
- ];
- function toClient(r: ClientRow): SubClient {
- const isUuid = r.protocol === 'vless' || r.protocol === 'vmess';
- return {
- protocol: r.protocol,
- remark: r.remark,
- address: r.address,
- port: Number(r.port),
- id: isUuid ? r.credential : undefined,
- password: isUuid ? undefined : r.credential,
- method: r.protocol === 'ss' ? r.method : undefined,
- network: r.network,
- security: r.security,
- sni: r.sni || undefined,
- };
- }
- export function SubscriptionBuilder() {
- const [scheme, setScheme] = useState<'http' | 'https'>('https');
- const [host, setHost] = useState('sub.example.com');
- const [port, setPort] = useState('2096');
- const [subPath, setSubPath] = useState('/sub/');
- const [jsonPath, setJsonPath] = useState('/json/');
- const [subId, setSubId] = useState('user-1');
- const [behindProxy, setBehindProxy] = useState(false);
- const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
- function patch(i: number, p: Partial<ClientRow>) {
- setClients((prev) => prev.map((c, j) => (i === j ? { ...c, ...p } : c)));
- }
- const urlInput: SubUrlInput = { scheme, host, port: Number(port), subPath, jsonPath, subId, behindProxy };
- const urls = buildSubscriptionUrls(urlInput);
- const subClients = clients.filter((c) => c.address.trim()).map(toClient);
- function reset() {
- setScheme('https');
- setHost('sub.example.com');
- setPort('2096');
- setSubPath('/sub/');
- setJsonPath('/json/');
- setSubId('user-1');
- setBehindProxy(false);
- setClients(DEFAULT_CLIENTS);
- }
- return (
- <ToolFrame
- title="Subscription & sub-JSON builder"
- description="Build the subscription URLs and preview both body formats — the Base64 link list and the JSON (Xray-json) config."
- onReset={reset}
- >
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
- <SelectField
- label="Scheme"
- value={scheme}
- onChange={(v) => setScheme(v as 'http' | 'https')}
- options={['https', 'http']}
- />
- <TextField label="Host" value={host} onChange={setHost} />
- <TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
- <TextField label="Sub ID" value={subId} onChange={setSubId} />
- <TextField label="Sub path" value={subPath} onChange={setSubPath} />
- <TextField label="JSON path" value={jsonPath} onChange={setJsonPath} />
- <CheckboxField
- label="Behind a reverse proxy (omit the port)"
- checked={behindProxy}
- onChange={setBehindProxy}
- />
- </div>
- <div className="mt-4 grid grid-cols-1 gap-4">
- <OutputBlock label="Base64 subscription URL" value={urls.base64} qr />
- <OutputBlock label="JSON subscription URL" value={urls.json} />
- </div>
- <div className="mt-5 flex items-center justify-between">
- <h4 className="text-sm font-semibold">Clients in this subscription</h4>
- <button
- type="button"
- className={addBtn}
- onClick={() =>
- setClients((p) => [
- ...p,
- {
- protocol: 'vless',
- remark: '',
- address: '',
- port: '443',
- credential: '',
- method: '',
- network: 'tcp',
- security: 'reality',
- sni: '',
- },
- ])
- }
- >
- Add client
- </button>
- </div>
- <div className="mt-2 flex flex-col gap-3">
- {clients.map((c, i) => (
- <div key={i} className="rounded-xl border p-3">
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
- <SelectField
- label="Protocol"
- value={c.protocol}
- onChange={(v) => patch(i, { protocol: v as ClientProtocol })}
- options={PROTOCOLS}
- />
- <TextField label="Remark" value={c.remark} onChange={(v) => patch(i, { remark: v })} />
- <TextField label="Address" value={c.address} onChange={(v) => patch(i, { address: v })} />
- <TextField label="Port" value={c.port} onChange={(v) => patch(i, { port: v })} inputMode="numeric" />
- <TextField
- label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
- value={c.credential}
- onChange={(v) => patch(i, { credential: v })}
- />
- {c.protocol === 'ss' ? (
- <TextField label="Method" value={c.method} onChange={(v) => patch(i, { method: v })} />
- ) : null}
- <SelectField
- label="Transport"
- value={c.network}
- onChange={(v) => patch(i, { network: v as Network })}
- options={NETWORKS}
- />
- <SelectField
- label="Security"
- value={c.security}
- onChange={(v) => patch(i, { security: v as Security })}
- options={SECURITIES}
- />
- {c.security !== 'none' ? (
- <TextField label="SNI" value={c.sni} onChange={(v) => patch(i, { sni: v })} />
- ) : null}
- </div>
- <div className="mt-2 flex justify-end">
- <button
- type="button"
- className={addBtn}
- onClick={() => setClients((p) => p.filter((_, j) => j !== i))}
- >
- Remove
- </button>
- </div>
- </div>
- ))}
- </div>
- <div className="mt-4 grid grid-cols-1 gap-4">
- <OutputBlock label="Subscription links (decoded body)" value={buildShareLinks(subClients).join('\n')} />
- <OutputBlock label="Base64 body" value={buildBase64Subscription(subClients)} />
- <OutputBlock label="JSON subscription (preview)" value={buildJsonSubscription(subClients)} />
- </div>
- </ToolFrame>
- );
- }
|