소스 검색

feat: finish rpoxies editor

MystiPanda 11 달 전
부모
커밋
5a1edc5ffb
11개의 변경된 파일1524개의 추가작업 그리고 154개의 파일을 삭제
  1. 4 2
      package.json
  2. 120 40
      pnpm-lock.yaml
  3. 38 112
      src/components/profile/proxies-editor-viewer.tsx
  4. 3 0
      src/locales/en.json
  5. 1 0
      src/locales/fa.json
  6. 1 0
      src/locales/ru.json
  7. 3 0
      src/locales/zh.json
  8. 92 0
      src/services/types.d.ts
  9. 131 0
      src/utils/trojan-uri.peg
  10. 141 0
      src/utils/trojan-uri.ts
  11. 990 0
      src/utils/uri-parser.ts

+ 4 - 2
package.json

@@ -35,12 +35,14 @@
     "dayjs": "1.11.5",
     "foxact": "^0.2.35",
     "i18next": "^23.11.5",
+    "js-base64": "^3.7.7",
     "js-yaml": "^4.1.0",
     "lodash-es": "^4.17.21",
     "meta-json-schema": "1.18.6",
     "monaco-editor": "^0.49.0",
     "monaco-yaml": "^5.2.0",
     "nanoid": "^5.0.7",
+    "peggy": "^4.0.3",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-error-boundary": "^3.1.4",
@@ -48,7 +50,7 @@
     "react-i18next": "^13.5.0",
     "react-markdown": "^9.0.1",
     "react-monaco-editor": "^0.55.0",
-    "react-router-dom": "^6.24.0",
+    "react-router-dom": "^6.24.1",
     "react-transition-group": "^4.4.5",
     "react-virtuoso": "^4.7.11",
     "sockette": "^2.0.6",
@@ -79,7 +81,7 @@
     "sass": "^1.77.6",
     "terser": "^5.31.1",
     "typescript": "^5.5.3",
-    "vite": "^5.3.2",
+    "vite": "^5.3.3",
     "vite-plugin-monaco-editor": "^1.1.0",
     "vite-plugin-svgr": "^4.2.0"
   },

+ 120 - 40
pnpm-lock.yaml

@@ -58,6 +58,9 @@ importers:
       i18next:
         specifier: ^23.11.5
         version: 23.11.5
+      js-base64:
+        specifier: ^3.7.7
+        version: 3.7.7
       js-yaml:
         specifier: ^4.1.0
         version: 4.1.0
@@ -76,6 +79,9 @@ importers:
       nanoid:
         specifier: ^5.0.7
         version: 5.0.7
+      peggy:
+        specifier: ^4.0.3
+        version: 4.0.3
       react:
         specifier: ^18.3.1
         version: 18.3.1
@@ -98,8 +104,8 @@ importers:
         specifier: ^0.55.0
         version: 0.55.0(@types/react@18.3.3)(monaco-editor@0.49.0)(react@18.3.1)
       react-router-dom:
-        specifier: ^6.24.0
-        version: 6.24.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        specifier: ^6.24.1
+        version: 6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       react-transition-group:
         specifier: ^4.4.5
         version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -148,10 +154,10 @@ importers:
         version: 4.4.10
       "@vitejs/plugin-legacy":
         specifier: ^5.4.1
-        version: 5.4.1(terser@5.31.1)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
+        version: 5.4.1(terser@5.31.1)(vite@5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
       "@vitejs/plugin-react":
         specifier: ^4.3.1
-        version: 4.3.1(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
+        version: 4.3.1(vite@5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
       adm-zip:
         specifier: ^0.5.14
         version: 0.5.14
@@ -186,14 +192,14 @@ importers:
         specifier: ^5.5.3
         version: 5.5.3
       vite:
-        specifier: ^5.3.2
-        version: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
+        specifier: ^5.3.3
+        version: 5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
       vite-plugin-monaco-editor:
         specifier: ^1.1.0
         version: 1.1.0(monaco-editor@0.49.0)
       vite-plugin-svgr:
         specifier: ^4.2.0
-        version: 4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
+        version: 4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
 
 packages:
   "@actions/github@5.1.1":
@@ -1797,16 +1803,23 @@ packages:
         integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==,
       }
 
+  "@peggyjs/from-mem@1.3.0":
+    resolution:
+      {
+        integrity: sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw==,
+      }
+    engines: { node: ">=18" }
+
   "@popperjs/core@2.11.8":
     resolution:
       {
         integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==,
       }
 
-  "@remix-run/router@1.17.0":
+  "@remix-run/router@1.17.1":
     resolution:
       {
-        integrity: sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==,
+        integrity: sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==,
       }
     engines: { node: ">=14.0.0" }
 
@@ -2328,10 +2341,10 @@ packages:
     peerDependencies:
       vite: ^4.2.0 || ^5.0.0
 
-  acorn@8.12.0:
+  acorn@8.12.1:
     resolution:
       {
-        integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==,
+        integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==,
       }
     engines: { node: ">=0.4.0" }
     hasBin: true
@@ -2486,10 +2499,10 @@ packages:
       }
     engines: { node: ">=10" }
 
-  caniuse-lite@1.0.30001639:
+  caniuse-lite@1.0.30001640:
     resolution:
       {
-        integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==,
+        integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==,
       }
 
   ccount@2.0.1:
@@ -2581,6 +2594,13 @@ packages:
         integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==,
       }
 
+  commander@12.1.0:
+    resolution:
+      {
+        integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==,
+      }
+    engines: { node: ">=18" }
+
   commander@2.20.3:
     resolution:
       {
@@ -3128,6 +3148,12 @@ packages:
         integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==,
       }
 
+  js-base64@3.7.7:
+    resolution:
+      {
+        integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==,
+      }
+
   js-cookie@2.2.1:
     resolution:
       {
@@ -3244,6 +3270,13 @@ packages:
         integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==,
       }
 
+  lru-cache@6.0.0:
+    resolution:
+      {
+        integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==,
+      }
+    engines: { node: ">=10" }
+
   magic-string@0.30.10:
     resolution:
       {
@@ -3708,6 +3741,14 @@ packages:
       }
     engines: { node: ">=8" }
 
+  peggy@4.0.3:
+    resolution:
+      {
+        integrity: sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA==,
+      }
+    engines: { node: ">=18" }
+    hasBin: true
+
   picocolors@1.0.1:
     resolution:
       {
@@ -3863,20 +3904,20 @@ packages:
       }
     engines: { node: ">=0.10.0" }
 
-  react-router-dom@6.24.0:
+  react-router-dom@6.24.1:
     resolution:
       {
-        integrity: sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==,
+        integrity: sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==,
       }
     engines: { node: ">=14.0.0" }
     peerDependencies:
       react: ">=16.8"
       react-dom: ">=16.8"
 
-  react-router@6.24.0:
+  react-router@6.24.1:
     resolution:
       {
-        integrity: sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==,
+        integrity: sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==,
       }
     engines: { node: ">=14.0.0" }
     peerDependencies:
@@ -4028,6 +4069,14 @@ packages:
       }
     hasBin: true
 
+  semver@7.6.0:
+    resolution:
+      {
+        integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==,
+      }
+    engines: { node: ">=10" }
+    hasBin: true
+
   server-only@0.0.1:
     resolution:
       {
@@ -4066,6 +4115,13 @@ packages:
         integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==,
       }
 
+  source-map-generator@0.8.0:
+    resolution:
+      {
+        integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==,
+      }
+    engines: { node: ">= 10" }
+
   source-map-js@1.2.0:
     resolution:
       {
@@ -4328,10 +4384,10 @@ packages:
       }
     engines: { node: ">= 10.0.0" }
 
-  update-browserslist-db@1.0.16:
+  update-browserslist-db@1.1.0:
     resolution:
       {
-        integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==,
+        integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==,
       }
     hasBin: true
     peerDependencies:
@@ -4373,10 +4429,10 @@ packages:
     peerDependencies:
       vite: ^2.6.0 || 3 || 4 || 5
 
-  vite@5.3.2:
+  vite@5.3.3:
     resolution:
       {
-        integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==,
+        integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==,
       }
     engines: { node: ^18.0.0 || >=20.0.0 }
     hasBin: true
@@ -5751,9 +5807,13 @@ snapshots:
     dependencies:
       "@octokit/openapi-types": 12.11.0
 
+  "@peggyjs/from-mem@1.3.0":
+    dependencies:
+      semver: 7.6.0
+
   "@popperjs/core@2.11.8": {}
 
-  "@remix-run/router@1.17.0": {}
+  "@remix-run/router@1.17.1": {}
 
   "@rollup/pluginutils@5.1.0(rollup@4.18.0)":
     dependencies:
@@ -6010,7 +6070,7 @@ snapshots:
 
   "@ungap/structured-clone@1.2.0": {}
 
-  "@vitejs/plugin-legacy@5.4.1(terser@5.31.1)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))":
+  "@vitejs/plugin-legacy@5.4.1(terser@5.31.1)(vite@5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))":
     dependencies:
       "@babel/core": 7.24.7
       "@babel/preset-env": 7.24.7(@babel/core@7.24.7)
@@ -6021,22 +6081,22 @@ snapshots:
       regenerator-runtime: 0.14.1
       systemjs: 6.15.1
       terser: 5.31.1
-      vite: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
+      vite: 5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
     transitivePeerDependencies:
       - supports-color
 
-  "@vitejs/plugin-react@4.3.1(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))":
+  "@vitejs/plugin-react@4.3.1(vite@5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))":
     dependencies:
       "@babel/core": 7.24.7
       "@babel/plugin-transform-react-jsx-self": 7.24.7(@babel/core@7.24.7)
       "@babel/plugin-transform-react-jsx-source": 7.24.7(@babel/core@7.24.7)
       "@types/babel__core": 7.20.5
       react-refresh: 0.14.2
-      vite: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
+      vite: 5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
     transitivePeerDependencies:
       - supports-color
 
-  acorn@8.12.0: {}
+  acorn@8.12.1: {}
 
   adm-zip@0.5.14: {}
 
@@ -6127,10 +6187,10 @@ snapshots:
 
   browserslist@4.23.1:
     dependencies:
-      caniuse-lite: 1.0.30001639
+      caniuse-lite: 1.0.30001640
       electron-to-chromium: 1.4.816
       node-releases: 2.0.14
-      update-browserslist-db: 1.0.16(browserslist@4.23.1)
+      update-browserslist-db: 1.1.0(browserslist@4.23.1)
 
   buffer-from@1.1.2: {}
 
@@ -6138,7 +6198,7 @@ snapshots:
 
   camelcase@6.3.0: {}
 
-  caniuse-lite@1.0.30001639: {}
+  caniuse-lite@1.0.30001640: {}
 
   ccount@2.0.1: {}
 
@@ -6186,6 +6246,8 @@ snapshots:
 
   comma-separated-tokens@2.0.3: {}
 
+  commander@12.1.0: {}
+
   commander@2.20.3: {}
 
   convert-source-map@1.9.0: {}
@@ -6492,6 +6554,8 @@ snapshots:
 
   isexe@2.0.0: {}
 
+  js-base64@3.7.7: {}
+
   js-cookie@2.2.1: {}
 
   js-tokens@4.0.0: {}
@@ -6542,6 +6606,10 @@ snapshots:
     dependencies:
       yallist: 3.1.1
 
+  lru-cache@6.0.0:
+    dependencies:
+      yallist: 4.0.0
+
   magic-string@0.30.10:
     dependencies:
       "@jridgewell/sourcemap-codec": 1.4.15
@@ -6914,6 +6982,12 @@ snapshots:
 
   path-type@4.0.0: {}
 
+  peggy@4.0.3:
+    dependencies:
+      "@peggyjs/from-mem": 1.3.0
+      commander: 12.1.0
+      source-map-generator: 0.8.0
+
   picocolors@1.0.1: {}
 
   picomatch@2.3.1: {}
@@ -7010,16 +7084,16 @@ snapshots:
 
   react-refresh@0.14.2: {}
 
-  react-router-dom@6.24.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+  react-router-dom@6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
     dependencies:
-      "@remix-run/router": 1.17.0
+      "@remix-run/router": 1.17.1
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
-      react-router: 6.24.0(react@18.3.1)
+      react-router: 6.24.1(react@18.3.1)
 
-  react-router@6.24.0(react@18.3.1):
+  react-router@6.24.1(react@18.3.1):
     dependencies:
-      "@remix-run/router": 1.17.0
+      "@remix-run/router": 1.17.1
       react: 18.3.1
 
   react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -7134,6 +7208,10 @@ snapshots:
 
   semver@6.3.1: {}
 
+  semver@7.6.0:
+    dependencies:
+      lru-cache: 6.0.0
+
   server-only@0.0.1: {}
 
   shebang-command@2.0.0:
@@ -7151,6 +7229,8 @@ snapshots:
 
   sockette@2.0.6: {}
 
+  source-map-generator@0.8.0: {}
+
   source-map-js@1.2.0: {}
 
   source-map-support@0.5.21:
@@ -7205,7 +7285,7 @@ snapshots:
   terser@5.31.1:
     dependencies:
       "@jridgewell/source-map": 0.3.6
-      acorn: 8.12.0
+      acorn: 8.12.1
       commander: 2.20.3
       source-map-support: 0.5.21
 
@@ -7288,7 +7368,7 @@ snapshots:
 
   universalify@2.0.1: {}
 
-  update-browserslist-db@1.0.16(browserslist@4.23.1):
+  update-browserslist-db@1.1.0(browserslist@4.23.1):
     dependencies:
       browserslist: 4.23.1
       escalade: 3.1.2
@@ -7313,18 +7393,18 @@ snapshots:
     dependencies:
       monaco-editor: 0.49.0
 
-  vite-plugin-svgr@4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)):
+  vite-plugin-svgr@4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)):
     dependencies:
       "@rollup/pluginutils": 5.1.0(rollup@4.18.0)
       "@svgr/core": 8.1.0(typescript@5.5.3)
       "@svgr/plugin-jsx": 8.1.0(@svgr/core@8.1.0(typescript@5.5.3))
-      vite: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
+      vite: 5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
     transitivePeerDependencies:
       - rollup
       - supports-color
       - typescript
 
-  vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1):
+  vite@5.3.3(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1):
     dependencies:
       esbuild: 0.21.5
       postcss: 8.4.39

+ 38 - 112
src/components/profile/proxies-editor-viewer.tsx

@@ -16,7 +16,6 @@ import {
   sortableKeyboardCoordinates,
 } from "@dnd-kit/sortable";
 import {
-  Autocomplete,
   Box,
   Button,
   Dialog,
@@ -31,13 +30,13 @@ import {
 } from "@mui/material";
 import { ProxyItem } from "@/components/profile/proxy-item";
 import { readProfileFile, saveProfileFile } from "@/services/cmds";
-import { Notice, Switch } from "@/components/base";
+import { Notice } from "@/components/base";
 import getSystem from "@/utils/get-system";
 import { BaseSearchBox } from "../base/base-search-box";
 import { Virtuoso } from "react-virtuoso";
 import MonacoEditor from "react-monaco-editor";
 import { useThemeMode } from "@/services/states";
-import { Controller, useForm } from "react-hook-form";
+import parseUri from "@/utils/uri-parser";
 
 interface Props {
   profileUid: string;
@@ -47,8 +46,6 @@ interface Props {
   onSave?: (prev?: string, curr?: string) => void;
 }
 
-const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
-
 export const ProxiesEditorViewer = (props: Props) => {
   const { profileUid, property, open, onClose, onSave } = props;
   const { t } = useTranslation();
@@ -57,13 +54,7 @@ export const ProxiesEditorViewer = (props: Props) => {
   const [currData, setCurrData] = useState("");
   const [visualization, setVisualization] = useState(true);
   const [match, setMatch] = useState(() => (_: string) => true);
-
-  const { control, watch, register, ...formIns } = useForm<IProxyConfig>({
-    defaultValues: {
-      type: "ss",
-      name: "",
-    },
-  });
+  const [proxyUri, setProxyUri] = useState<string>("");
 
   const [proxyList, setProxyList] = useState<IProxyConfig[]>([]);
   const [prependSeq, setPrependSeq] = useState<IProxyConfig[]>([]);
@@ -231,104 +222,36 @@ export const ProxiesEditorViewer = (props: Props) => {
                   overflowY: "auto",
                 }}
               >
-                <Controller
-                  name="type"
-                  control={control}
-                  render={({ field }) => (
-                    <Item>
-                      <ListItemText primary={t("Proxy Type")} />
-                      <Autocomplete
-                        size="small"
-                        sx={{ minWidth: "240px" }}
-                        options={[
-                          "ss",
-                          "ssr",
-                          "direct",
-                          "dns",
-                          "snell",
-                          "http",
-                          "trojan",
-                          "hysteria",
-                          "hysteria2",
-                          "tuic",
-                          "wireguard",
-                          "ssh",
-                          "socks5",
-                          "vmess",
-                          "vless",
-                        ]}
-                        value={field.value}
-                        onChange={(_, value) => value && field.onChange(value)}
-                        renderInput={(params) => <TextField {...params} />}
-                      />
-                    </Item>
-                  )}
-                />
-                <Controller
-                  name="name"
-                  control={control}
-                  render={({ field }) => (
-                    <Item>
-                      <ListItemText primary={t("Proxy Name")} />
-                      <TextField
-                        autoComplete="off"
-                        size="small"
-                        sx={{ minWidth: "240px" }}
-                        {...field}
-                        required={true}
-                      />
-                    </Item>
-                  )}
-                />
-                <Controller
-                  name="server"
-                  control={control}
-                  render={({ field }) => (
-                    <Item>
-                      <ListItemText primary={t("Proxy Server")} />
-                      <TextField
-                        autoComplete="off"
-                        size="small"
-                        sx={{ minWidth: "240px" }}
-                        {...field}
-                      />
-                    </Item>
-                  )}
-                />
-                <Controller
-                  name="port"
-                  control={control}
-                  render={({ field }) => (
-                    <Item>
-                      <ListItemText primary={t("Proxy Port")} />
-                      <TextField
-                        autoComplete="off"
-                        type="number"
-                        size="small"
-                        sx={{ minWidth: "240px" }}
-                        onChange={(e) => {
-                          field.onChange(parseInt(e.target.value));
-                        }}
-                      />
-                    </Item>
-                  )}
-                />
+                <Item>
+                  <TextField
+                    autoComplete="off"
+                    placeholder={t("Use newlines for multiple uri")}
+                    fullWidth
+                    minRows={8}
+                    multiline
+                    size="small"
+                    onChange={(e) => setProxyUri(e.target.value)}
+                  />
+                </Item>
               </Box>
               <Item>
                 <Button
                   fullWidth
                   variant="contained"
                   onClick={() => {
-                    try {
-                      for (const item of prependSeq) {
-                        if (item.name === formIns.getValues().name) {
-                          throw new Error(t("Proxy Name Already Exists"));
+                    let proxies = [] as IProxyConfig[];
+                    proxyUri
+                      .trim()
+                      .split("\n")
+                      .forEach((uri) => {
+                        try {
+                          let proxy = parseUri(uri.trim());
+                          proxies.push(proxy);
+                        } catch (err: any) {
+                          Notice.error(err.message || err.toString());
                         }
-                      }
-                      setPrependSeq([...prependSeq, formIns.getValues()]);
-                    } catch (err: any) {
-                      Notice.error(err.message || err.toString());
-                    }
+                      });
+                    setPrependSeq([...prependSeq, ...proxies]);
                   }}
                 >
                   {t("Prepend Proxy")}
@@ -339,16 +262,19 @@ export const ProxiesEditorViewer = (props: Props) => {
                   fullWidth
                   variant="contained"
                   onClick={() => {
-                    try {
-                      for (const item of appendSeq) {
-                        if (item.name === formIns.getValues().name) {
-                          throw new Error(t("Proxy Name Already Exists"));
+                    let proxies = [] as IProxyConfig[];
+                    proxyUri
+                      .trim()
+                      .split("\n")
+                      .forEach((uri) => {
+                        try {
+                          let proxy = parseUri(uri.trim());
+                          proxies.push(proxy);
+                        } catch (err: any) {
+                          Notice.error(err.message || err.toString());
                         }
-                      }
-                      setAppendSeq([...appendSeq, formIns.getValues()]);
-                    } catch (err: any) {
-                      Notice.error(err.message || err.toString());
-                    }
+                      });
+                    setAppendSeq([...appendSeq, ...proxies]);
                   }}
                 >
                   {t("Append Proxy")}

+ 3 - 0
src/locales/en.json

@@ -54,6 +54,7 @@
   "Create Profile": "Create Profile",
   "Edit Profile": "Edit Profile",
   "Edit Proxies": "Edit Proxies",
+  "Use newlines for multiple uri": "Use newlines for multiple uri",
   "Edit Rules": "Edit Rules",
   "Rule Type": "Rule Type",
   "Rule Content": "Rule Content",
@@ -63,6 +64,8 @@
   "Append Rule": "Append Rule",
   "Prepend Group": "Prepend Group",
   "Append Group": "Append Group",
+  "Prepend Proxy": "Prepend Proxy",
+  "Append Proxy": "Append Proxy",
   "Rule Condition Required": "Rule Condition Required",
   "Invalid Rule": "Invalid Rule",
   "Advanced": "Advanced",

+ 1 - 0
src/locales/fa.json

@@ -54,6 +54,7 @@
   "Create Profile": "ایجاد پروفایل",
   "Edit Profile": "ویرایش پروفایل",
   "Edit Proxies": "ویرایش پروکسی‌ها",
+  "Use newlines for multiple uri": "استفاده از خطوط جدید برای چندین آدرس",
   "Edit Rules": "ویرایش قوانین",
   "Rule Type": "نوع قانون",
   "Rule Content": "محتوای قانون",

+ 1 - 0
src/locales/ru.json

@@ -54,6 +54,7 @@
   "Create Profile": "Создать профиль",
   "Edit Profile": "Изменить профиль",
   "Edit Proxies": "Редактировать прокси",
+  "Use newlines for multiple uri": "Используйте новые строки для нескольких URI",
   "Edit Rules": "Редактировать правила",
   "Rule Type": "Тип правила",
   "Rule Content": "Содержимое правила",

+ 3 - 0
src/locales/zh.json

@@ -54,6 +54,7 @@
   "Create Profile": "新建配置",
   "Edit Profile": "编辑配置",
   "Edit Proxies": "编辑节点",
+  "Use newlines for multiple uri": "多条URI请使用换行分隔",
   "Edit Rules": "编辑规则",
   "Rule Type": "规则类型",
   "Rule Content": "规则内容",
@@ -63,6 +64,8 @@
   "Append Rule": "添加后置规则",
   "Prepend Group": "添加前置代理组",
   "Append Group": "添加后置代理组",
+  "Prepend Proxy": "添加前置代理节点",
+  "Append Proxy": "添加后置代理节点",
   "Rule Condition Required": "规则条件缺失",
   "Invalid Rule": "无效规则",
   "Advanced": "高级",

+ 92 - 0
src/services/types.d.ts

@@ -255,6 +255,98 @@ interface IProxyConfig {
   tfo?: boolean;
   mptcp?: boolean;
   "dialer-proxy"?: string;
+  plugin?: "obfs" | "v2ray-plugin" | "shadow-tls" | "restls";
+  "plugin-opts"?: {
+    mode?: string;
+    host?: string;
+    path?: string;
+    tls?: string;
+  };
+  cipher?: string;
+  password?: string;
+  "udp-over-tcp"?: boolean;
+  protocol?: string;
+  obfs?: string;
+  "protocol-param"?: string;
+  "obfs-param"?: string;
+  uuid?: string;
+  tls?: boolean;
+  "skip-cert-verify"?: boolean;
+  network?: "ws" | "http" | "h2" | "grpc";
+  "ws-opts"?: {
+    path?: string;
+    headers?: {};
+  };
+  alterId?: number;
+  sni?: string;
+  "http-opts"?: {};
+  "grpc-opts"?: {};
+  "ws-opts"?: {};
+  "h2-opts"?: {};
+  "reality-opts"?: {
+    "public-key"?: string;
+    "short-id"?: string;
+  };
+  flow?: "xtls-rprx-vision";
+  "client-fingerprint"?:
+    | "chrome"
+    | "firefox"
+    | "safari"
+    | "iOS"
+    | "android"
+    | "edge"
+    | "360"
+    | "qq"
+    | "random";
+  alpn?: string[];
+  ws?: {
+    headers?: {
+      Host?: string;
+    };
+    "ws-service-name"?: string;
+    path?: string;
+  };
+  http?: {
+    headers?: {
+      Host?: string;
+    };
+    "http-service-name"?: string;
+    path?: string;
+  };
+  grpc?: {};
+  ports?: string;
+  "obfs-password"?: string;
+  "tls-fingerprint"?: string;
+  "auth-str"?: string;
+  up?: string;
+  down?: string;
+  "fast-open"?: boolean;
+  fingerprint?: string;
+  "disable-mtu-discovery"?: boolean;
+  ca?: string;
+  "ca-str"?: string;
+  "recv-window-conn"?: number;
+  "recv-window"?: number;
+  token?: string;
+  ip?: string;
+  "heartbeat-interval"?: number;
+  "disable-sni"?: boolean;
+  "reduce-rtt"?: boolean;
+  "request-timeout"?: number;
+  "udp-relay-mode"?: string;
+  "congestion-controller"?: string;
+  "max-udp-relay-packet-size"?: number;
+  "max-open-streams"?: number;
+  "private-key"?: string;
+  "public-key"?: string;
+  ipv6?: string;
+  reserved?: number[];
+  mtu?: number;
+  "remote-dns-resolve"?: boolean;
+  "allowed-ips"?: string[];
+  dns?: string[];
+  "pre-shared-key"?: string;
+  username?: string;
 }
 
 interface IVergeConfig {

+ 131 - 0
src/utils/trojan-uri.peg

@@ -0,0 +1,131 @@
+// global initializer
+{{
+  function $set(obj, path, value) {
+    if (Object(obj) !== obj) return obj;
+    if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
+    path
+      .slice(0, -1)
+      .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
+      path[path.length - 1]
+    ] = value;
+    return obj;
+  }
+
+  function toBool(str) {
+    if (typeof str === 'undefined' || str === null) return undefined;
+    return /(TRUE)|1/i.test(str);
+  }
+}}
+
+{
+  const proxy = {};
+  const obfs = {};
+  const $ = {};
+  const params = {};
+}
+
+start = (trojan) {
+  return proxy
+}
+
+trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
+  proxy.type = "trojan";
+  proxy.password = password;
+  proxy.server = server;
+  proxy.port = port;
+  proxy.name = name;
+
+  // name may be empty
+  if (!proxy.name) {
+    proxy.name = server + ":" + port;
+  }
+};
+
+password = match:$[^@]+ {
+  return decodeURIComponent(match);
+};
+
+server = ip/domain;
+
+domain = match:[0-9a-zA-z-_.]+ { 
+  const domain = match.join(""); 
+  if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
+    return domain;
+  }
+}
+
+ip = & {
+  const start = peg$currPos;
+  let end;
+  let j = start;
+  while (j < input.length) {
+    if (input[j] === ",") break;
+    if (input[j] === ":") end = j;
+    j++;
+  }
+  peg$currPos = end || j;
+  $.ip = input.substring(start, end).trim();
+  return true;
+} { return $.ip; }
+
+port = digits:[0-9]+ { 
+  const port = parseInt(digits.join(""), 10); 
+  if (port >= 0 && port <= 65535) {
+    return port;
+  } else {
+    throw new Error("Invalid port: " + port);
+  }
+}
+
+params = "?" head:param tail:("&"@param)* {
+  proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
+  proxy.sni = params["sni"] || params["peer"];
+
+  if (toBool(params["ws"])) {
+    proxy.network = "ws";
+    $set(proxy, "ws-opts.path", params["wspath"]);
+  }
+  
+  if (params["type"]) {
+    let httpupgrade
+    proxy.network = params["type"]
+    if(proxy.network === 'httpupgrade') {
+      proxy.network = 'ws'
+      httpupgrade = true
+    }
+    if (['grpc'].includes(proxy.network)) {
+        proxy[proxy.network + '-opts'] = {
+            'grpc-service-name': params["serviceName"],
+            '_grpc-type': params["mode"],
+        };
+    } else {
+      if (params["path"]) {
+        $set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));  
+      }
+      if (params["host"]) {
+        $set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"])); 
+      }
+      if (httpupgrade) {
+        $set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true); 
+        $set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true); 
+      }
+    }
+  }
+
+  proxy.udp = toBool(params["udp"]);
+  proxy.tfo = toBool(params["tfo"]);
+}
+
+param = kv/single;
+
+kv = key:$[a-z]i+ "=" value:$[^&#]i* {
+  params[key] = value;
+}
+
+single = key:$[a-z]i+ {
+  params[key] = true;
+};
+
+name = "#" + match:$.* {
+  return decodeURIComponent(match);
+}

+ 141 - 0
src/utils/trojan-uri.ts

@@ -0,0 +1,141 @@
+import * as peggy from "peggy";
+const grammars = String.raw`
+// global initializer
+{{
+  function $set(obj, path, value) {
+    if (Object(obj) !== obj) return obj;
+    if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
+    path
+      .slice(0, -1)
+      .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
+      path[path.length - 1]
+    ] = value;
+    return obj;
+  }
+
+  function toBool(str) {
+    if (typeof str === 'undefined' || str === null) return undefined;
+    return /(TRUE)|1/i.test(str);
+  }
+}}
+
+{
+  const proxy = {};
+  const obfs = {};
+  const $ = {};
+  const params = {};
+}
+
+start = (trojan) {
+  return proxy
+}
+
+trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
+  proxy.type = "trojan";
+  proxy.password = password;
+  proxy.server = server;
+  proxy.port = port;
+  proxy.name = name;
+
+  // name may be empty
+  if (!proxy.name) {
+    proxy.name = server + ":" + port;
+  }
+};
+
+password = match:$[^@]+ {
+  return decodeURIComponent(match);
+};
+
+server = ip/domain;
+
+domain = match:[0-9a-zA-z-_.]+ { 
+  const domain = match.join(""); 
+  if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
+    return domain;
+  }
+}
+
+ip = & {
+  const start = peg$currPos;
+  let end;
+  let j = start;
+  while (j < input.length) {
+    if (input[j] === ",") break;
+    if (input[j] === ":") end = j;
+    j++;
+  }
+  peg$currPos = end || j;
+  $.ip = input.substring(start, end).trim();
+  return true;
+} { return $.ip; }
+
+port = digits:[0-9]+ { 
+  const port = parseInt(digits.join(""), 10); 
+  if (port >= 0 && port <= 65535) {
+    return port;
+  } else {
+    throw new Error("Invalid port: " + port);
+  }
+}
+
+params = "?" head:param tail:("&"@param)* {
+  proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
+  proxy.sni = params["sni"] || params["peer"];
+
+  if (toBool(params["ws"])) {
+    proxy.network = "ws";
+    $set(proxy, "ws-opts.path", params["wspath"]);
+  }
+  
+  if (params["type"]) {
+    let httpupgrade
+    proxy.network = params["type"]
+    if(proxy.network === 'httpupgrade') {
+      proxy.network = 'ws'
+      httpupgrade = true
+    }
+    if (['grpc'].includes(proxy.network)) {
+        proxy[proxy.network + '-opts'] = {
+            'grpc-service-name': params["serviceName"],
+            '_grpc-type': params["mode"],
+        };
+    } else {
+      if (params["path"]) {
+        $set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));  
+      }
+      if (params["host"]) {
+        $set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"])); 
+      }
+      if (httpupgrade) {
+        $set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true); 
+        $set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true); 
+      }
+    }
+  }
+
+  proxy.udp = toBool(params["udp"]);
+  proxy.tfo = toBool(params["tfo"]);
+}
+
+param = kv/single;
+
+kv = key:$[a-z]i+ "=" value:$[^&#]i* {
+  params[key] = value;
+}
+
+single = key:$[a-z]i+ {
+  params[key] = true;
+};
+
+name = "#" + match:$.* {
+  return decodeURIComponent(match);
+}
+`;
+let parser: any;
+export default function getParser() {
+  if (!parser) {
+    parser = peggy.generate(grammars);
+  }
+  return parser;
+}

+ 990 - 0
src/utils/uri-parser.ts

@@ -0,0 +1,990 @@
+import getTrojanURIParser from "@/utils/trojan-uri";
+
+export default function parseUri(uri: string): IProxyConfig {
+  const head = uri.split("://")[0];
+  switch (head) {
+    case "ss":
+      return URI_SS(uri);
+    case "ssr":
+      return URI_SSR(uri);
+    case "vmess":
+      return URI_VMESS(uri);
+    case "vless":
+      return URI_VLESS(uri);
+    case "trojan":
+      return URI_Trojan(uri);
+    case "hysteria2":
+      return URI_Hysteria2(uri);
+    case "hy2":
+      return URI_Hysteria2(uri);
+    case "hysteria":
+      return URI_Hysteria(uri);
+    case "hy":
+      return URI_Hysteria(uri);
+    case "tuic":
+      return URI_TUIC(uri);
+    case "wireguard":
+      return URI_Wireguard(uri);
+    case "wg":
+      return URI_Wireguard(uri);
+    case "http":
+      return URI_HTTP(uri);
+    case "socks5":
+      return URI_SOCKS(uri);
+    default:
+      throw Error(`Unknown uri type: ${head}`);
+  }
+}
+
+function getIfNotBlank(
+  value: string | undefined,
+  dft?: string
+): string | undefined {
+  return value && value.trim() !== "" ? value : dft;
+}
+
+function getIfPresent(value: any, dft?: any): any {
+  return value ? value : dft;
+}
+
+function isPresent(value: any): boolean {
+  return value !== null && value !== undefined;
+}
+
+function isNotBlank(name: string) {
+  return name.trim().length !== 0;
+}
+
+function isIPv4(address: string): boolean {
+  // Check if the address is IPv4
+  const ipv4Regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
+  return ipv4Regex.test(address);
+}
+
+function isIPv6(address: string): boolean {
+  // Check if the address is IPv6
+  const ipv6Regex =
+    /^((?=.*(::))(?!.*\3.+)(::)?)([0-9A-Fa-f]{1,4}(\3|:\b)|\3){7}[0-9A-Fa-f]{1,4}$/;
+  return ipv6Regex.test(address);
+}
+
+function decodeBase64OrOriginal(str: string): string {
+  try {
+    return atob(str);
+  } catch {
+    return str;
+  }
+}
+
+function URI_SS(line: string): IProxyConfig {
+  // parse url
+  let content = line.split("ss://")[1];
+
+  const proxy: IProxyConfig = {
+    name: decodeURIComponent(line.split("#")[1]),
+    type: "ss",
+    server: "",
+    port: 0,
+  };
+  content = content.split("#")[0]; // strip proxy name
+  // handle IPV4 and IPV6
+  let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
+  let userInfoStr = decodeBase64OrOriginal(content.split("@")[0]);
+  let query = "";
+  if (!serverAndPortArray) {
+    if (content.includes("?")) {
+      const parsed = content.match(/^(.*)(\?.*)$/);
+      content = parsed?.[1] ?? "";
+      query = parsed?.[2] ?? "";
+    }
+    content = decodeBase64OrOriginal(content);
+    if (query) {
+      if (/(&|\?)v2ray-plugin=/.test(query)) {
+        const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
+        let v2rayPlugin = parsed![2];
+        if (v2rayPlugin) {
+          proxy.plugin = "v2ray-plugin";
+          proxy["plugin-opts"] = JSON.parse(
+            decodeBase64OrOriginal(v2rayPlugin)
+          );
+        }
+      }
+      content = `${content}${query}`;
+    }
+    userInfoStr = content.split("@")[0];
+    serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
+  }
+  const serverAndPort = serverAndPortArray?.[1];
+  const portIdx = serverAndPort?.lastIndexOf(":") ?? 0;
+  proxy.server = serverAndPort?.substring(0, portIdx) ?? "";
+  proxy.port = parseInt(
+    `${serverAndPort?.substring(portIdx + 1)}`.match(/\d+/)?.[0] ?? ""
+  );
+  console.log(userInfoStr);
+  const userInfo = userInfoStr.match(/(^.*?):(.*$)/);
+  console.log(userInfo);
+  proxy.cipher = userInfo?.[1];
+  proxy.password = userInfo?.[2];
+
+  // handle obfs
+  const idx = content.indexOf("?plugin=");
+  if (idx !== -1) {
+    const pluginInfo = (
+      "plugin=" + decodeURIComponent(content.split("?plugin=")[1].split("&")[0])
+    ).split(";");
+    const params: Record<string, any> = {};
+    for (const item of pluginInfo) {
+      const [key, val] = item.split("=");
+      if (key) params[key] = val || true; // some options like "tls" will not have value
+    }
+    switch (params.plugin) {
+      case "obfs-local":
+      case "simple-obfs":
+        proxy.plugin = "obfs";
+        proxy["plugin-opts"] = {
+          mode: params.obfs,
+          host: getIfNotBlank(params["obfs-host"]),
+        };
+        break;
+      case "v2ray-plugin":
+        proxy.plugin = "v2ray-plugin";
+        proxy["plugin-opts"] = {
+          mode: "websocket",
+          host: getIfNotBlank(params["obfs-host"]),
+          path: getIfNotBlank(params.path),
+          tls: getIfPresent(params.tls),
+        };
+        break;
+      default:
+        throw new Error(`Unsupported plugin option: ${params.plugin}`);
+    }
+  }
+  if (/(&|\?)uot=(1|true)/i.test(query)) {
+    proxy["udp-over-tcp"] = true;
+  }
+  if (/(&|\?)tfo=(1|true)/i.test(query)) {
+    proxy.tfo = true;
+  }
+  return proxy;
+}
+
+function URI_SSR(line: string): IProxyConfig {
+  line = decodeBase64OrOriginal(line.split("ssr://")[1]);
+
+  // handle IPV6 & IPV4 format
+  let splitIdx = line.indexOf(":origin");
+  if (splitIdx === -1) {
+    splitIdx = line.indexOf(":auth_");
+  }
+  const serverAndPort = line.substring(0, splitIdx);
+  const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
+  const port = parseInt(
+    serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1)
+  );
+
+  let params = line
+    .substring(splitIdx + 1)
+    .split("/?")[0]
+    .split(":");
+  let proxy: IProxyConfig = {
+    name: "",
+    type: "ssr",
+    server,
+    port,
+    protocol: params[0],
+    cipher: params[1],
+    obfs: params[2],
+    password: decodeBase64OrOriginal(params[3]),
+  };
+
+  // get other params
+  const other_params: Record<string, string> = {};
+  const paramsArray = line.split("/?")[1]?.split("&") || [];
+  for (const item of paramsArray) {
+    const [key, val] = item.split("=");
+    if (val?.trim().length > 0) {
+      other_params[key] = val.trim();
+    }
+  }
+
+  proxy = {
+    ...proxy,
+    name: other_params.remarks
+      ? decodeBase64OrOriginal(other_params.remarks)
+      : proxy.server,
+    "protocol-param": getIfNotBlank(
+      decodeBase64OrOriginal(other_params.protoparam || "").replace(/\s/g, "")
+    ),
+    "obfs-param": getIfNotBlank(
+      decodeBase64OrOriginal(other_params.obfsparam || "").replace(/\s/g, "")
+    ),
+  };
+  return proxy;
+}
+
+function URI_VMESS(line: string): IProxyConfig {
+  line = line.split("vmess://")[1];
+  let content = decodeBase64OrOriginal(line);
+  console.log(content);
+  if (/=\s*vmess/.test(content)) {
+    // Quantumult VMess URI format
+    const partitions = content.split(",").map((p) => p.trim());
+    console.log(partitions);
+    const params: Record<string, string> = {};
+    for (const part of partitions) {
+      if (part.indexOf("=") !== -1) {
+        const [key, val] = part.split("=");
+        params[key.trim()] = val.trim();
+      }
+    }
+
+    const proxy: IProxyConfig = {
+      name: partitions[0].split("=")[0].trim(),
+      type: "vmess",
+      server: partitions[1],
+      port: parseInt(partitions[2], 10),
+      cipher: getIfNotBlank(partitions[3], "auto"),
+      uuid: partitions[4].match(/^"(.*)"$/)?.[1] || "",
+      tls: params.obfs === "wss",
+      udp: getIfPresent(params["udp-relay"]),
+      tfo: getIfPresent(params["fast-open"]),
+      "skip-cert-verify": isPresent(params["tls-verification"])
+        ? !params["tls-verification"]
+        : undefined,
+    };
+
+    if (isPresent(params.obfs)) {
+      if (params.obfs === "ws" || params.obfs === "wss") {
+        proxy.network = "ws";
+        proxy["ws-opts"] = {
+          path:
+            (getIfNotBlank(params["obfs-path"]) || '"/"').match(
+              /^"(.*)"$/
+            )?.[1] || "/",
+          headers: {
+            Host:
+              params["obfs-header"]?.match(/Host:\s*([a-zA-Z0-9-.]*)/)?.[1] ||
+              "",
+          },
+        };
+      } else {
+        throw new Error(`Unsupported obfs: ${params.obfs}`);
+      }
+    }
+
+    return proxy;
+  } else {
+    let params: Record<string, any> = {};
+
+    try {
+      // V2rayN URI format
+      params = JSON.parse(content);
+    } catch (e) {
+      // Shadowrocket URI format
+      const match = /(^[^?]+?)\/?\?(.*)$/.exec(line);
+      if (match) {
+        let [_, base64Line, qs] = match;
+        content = decodeBase64OrOriginal(base64Line);
+
+        for (const addon of qs.split("&")) {
+          const [key, valueRaw] = addon.split("=");
+          const value = decodeURIComponent(valueRaw);
+          if (value.indexOf(",") === -1) {
+            params[key] = value;
+          } else {
+            params[key] = value.split(",");
+          }
+        }
+
+        const contentMatch = /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
+
+        if (contentMatch) {
+          let [__, cipher, uuid, server, port] = contentMatch;
+
+          params.scy = cipher;
+          params.id = uuid;
+          params.port = port;
+          params.add = server;
+        }
+      }
+    }
+    console.log(params);
+    const server = params.add;
+    const port = parseInt(getIfPresent(params.port), 10);
+    const proxy: IProxyConfig = {
+      name:
+        params.ps ??
+        params.remarks ??
+        params.remark ??
+        `VMess ${server}:${port}`,
+      type: "vmess",
+      server,
+      port,
+      cipher: getIfPresent(params.scy, "auto"),
+      uuid: params.id,
+      alterId: parseInt(getIfPresent(params.aid ?? params.alterId, 0), 10),
+      tls: ["tls", true, 1, "1"].includes(params.tls),
+      "skip-cert-verify": isPresent(params.verify_cert)
+        ? !params.verify_cert
+        : undefined,
+    };
+
+    if (proxy.tls && params.sni) {
+      proxy.sni = params.sni;
+    }
+
+    let httpupgrade = false;
+    if (params.net === "ws" || params.obfs === "websocket") {
+      proxy.network = "ws";
+    } else if (
+      ["http"].includes(params.net) ||
+      ["http"].includes(params.obfs) ||
+      ["http"].includes(params.type)
+    ) {
+      proxy.network = "http";
+    } else if (["grpc"].includes(params.net)) {
+      proxy.network = "grpc";
+    } else if (params.net === "httpupgrade") {
+      proxy.network = "ws";
+      httpupgrade = true;
+    } else if (params.net === "h2" || proxy.network === "h2") {
+      proxy.network = "h2";
+    }
+
+    if (proxy.network) {
+      let transportHost = params.host ?? params.obfsParam;
+      try {
+        const parsedObfs = JSON.parse(transportHost);
+        const parsedHost = parsedObfs?.Host;
+        if (parsedHost) {
+          transportHost = parsedHost;
+        }
+      } catch (e) {
+        // ignore JSON parse errors
+      }
+
+      let transportPath = params.path;
+      if (proxy.network === "http") {
+        if (transportHost) {
+          transportHost = Array.isArray(transportHost)
+            ? transportHost[0]
+            : transportHost;
+        }
+        if (transportPath) {
+          transportPath = Array.isArray(transportPath)
+            ? transportPath[0]
+            : transportPath;
+        } else {
+          transportPath = "/";
+        }
+      }
+
+      if (transportPath || transportHost) {
+        if (["grpc"].includes(proxy.network)) {
+          proxy[`grpc-opts`] = {
+            "grpc-service-name": getIfNotBlank(transportPath),
+            "_grpc-type": getIfNotBlank(params.type),
+          };
+        } else {
+          const opts: Record<string, any> = {
+            path: getIfNotBlank(transportPath),
+            headers: { Host: getIfNotBlank(transportHost) },
+          };
+          if (httpupgrade) {
+            opts["v2ray-http-upgrade"] = true;
+            opts["v2ray-http-upgrade-fast-open"] = true;
+          }
+          proxy[`${proxy.network}-opts`] = opts;
+        }
+      } else {
+        delete proxy.network;
+      }
+
+      if (proxy.tls && !proxy.sni && transportHost) {
+        proxy.sni = transportHost;
+      }
+    }
+
+    return proxy;
+  }
+}
+
+function URI_VLESS(line: string): IProxyConfig {
+  line = line.split("vless://")[1];
+  let isShadowrocket;
+  let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
+  if (!parsed) {
+    let [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line)!;
+    line = `${atob(base64)}${other}`;
+    parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
+    isShadowrocket = true;
+  }
+  let [__, uuid, server, portStr, ___, addons = "", name] = parsed;
+  if (isShadowrocket) {
+    uuid = uuid.replace(/^.*?:/g, "");
+  }
+
+  const port = parseInt(portStr, 10);
+  uuid = decodeURIComponent(uuid);
+  name = decodeURIComponent(name);
+
+  const proxy: IProxyConfig = {
+    type: "vless",
+    name,
+    server,
+    port,
+    uuid,
+  };
+
+  const params: Record<string, string> = {};
+  for (const addon of addons.split("&")) {
+    const [key, valueRaw] = addon.split("=");
+    const value = decodeURIComponent(valueRaw);
+    params[key] = value;
+  }
+
+  proxy.name =
+    name ?? params.remarks ?? params.remark ?? `VLESS ${server}:${port}`;
+
+  proxy.tls = (params.security && params.security !== "none") || undefined;
+  if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
+    proxy.tls = true;
+    params.security = params.security ?? "reality";
+  }
+  proxy.sni = params.sni || params.peer;
+  proxy.flow = params.flow ? "xtls-rprx-vision" : undefined;
+
+  proxy["client-fingerprint"] = params.fp as
+    | "chrome"
+    | "firefox"
+    | "safari"
+    | "iOS"
+    | "android"
+    | "edge"
+    | "360"
+    | "qq"
+    | "random";
+  proxy.alpn = params.alpn ? params.alpn.split(",") : undefined;
+  proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.allowInsecure);
+
+  if (["reality"].includes(params.security)) {
+    const opts: IProxyConfig["reality-opts"] = {};
+    if (params.pbk) {
+      opts["public-key"] = params.pbk;
+    }
+    if (params.sid) {
+      opts["short-id"] = params.sid;
+    }
+    if (Object.keys(opts).length > 0) {
+      proxy["reality-opts"] = opts;
+    }
+  }
+
+  let httpupgrade = false;
+  proxy.ws = {
+    headers: undefined,
+    "ws-service-name": undefined,
+    path: undefined,
+  };
+  proxy.http = {
+    headers: undefined,
+    "http-service-name": undefined,
+    path: undefined,
+  };
+  proxy.grpc = { "_grpc-type": undefined };
+  proxy.network = params.type as "ws" | "http" | "h2" | "grpc";
+  if (params.headerType === "http") {
+    proxy.network = "http";
+  } else {
+    proxy.network = "ws";
+    httpupgrade = true;
+  }
+  if (!proxy.network && isShadowrocket && params.obfs) {
+    proxy.network = params.obfs as "ws" | "http" | "h2" | "grpc";
+  }
+  if (["websocket"].includes(proxy.network)) {
+    proxy.network = "ws";
+  }
+  if (proxy.network && !["tcp", "none"].includes(proxy.network)) {
+    const opts: Record<string, any> = {};
+    const host = params.host ?? params.obfsParam;
+    if (host) {
+      if (params.obfsParam) {
+        try {
+          const parsed = JSON.parse(host);
+          opts.headers = parsed;
+        } catch (e) {
+          opts.headers = { Host: host };
+        }
+      } else {
+        opts.headers = { Host: host };
+      }
+    }
+    if (params.serviceName) {
+      opts[`${proxy.network}-service-name`] = params.serviceName;
+    } else if (isShadowrocket && params.path) {
+      if (!["ws", "http", "h2"].includes(proxy.network)) {
+        opts[`${proxy.network}-service-name`] = params.path;
+        delete params.path;
+      }
+    }
+    if (params.path) {
+      opts.path = params.path;
+    }
+    if (["grpc"].includes(proxy.network)) {
+      opts["_grpc-type"] = params.mode || "gun";
+    }
+    if (httpupgrade) {
+      opts["v2ray-http-upgrade"] = true;
+      opts["v2ray-http-upgrade-fast-open"] = true;
+    }
+    if (Object.keys(opts).length > 0) {
+      proxy[`${proxy.network}-opts`] = opts;
+    }
+  }
+
+  if (proxy.tls && !proxy.sni) {
+    if (proxy.network === "ws") {
+      proxy.sni = proxy.ws?.headers?.Host;
+    } else if (proxy.network === "http") {
+      let httpHost = proxy.http?.headers?.Host;
+      proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
+    }
+  }
+
+  return proxy;
+}
+
+function URI_Trojan(line: string): IProxyConfig {
+  let [newLine, name] = line.split(/#(.+)/, 2);
+  const parser = getTrojanURIParser();
+  const proxy = parser.parse(newLine);
+  if (isNotBlank(name)) {
+    try {
+      proxy.name = decodeURIComponent(name);
+    } catch (e) {
+      throw Error("Can not get proxy name");
+    }
+  }
+  return proxy;
+}
+
+function URI_Hysteria2(line: string): IProxyConfig {
+  line = line.split(/(hysteria2|hy2):\/\//)[2];
+  // eslint-disable-next-line no-unused-vars
+  let [__, password, server, ___, port, ____, addons = "", name] =
+    /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
+  let portNum = parseInt(`${port}`, 10);
+  if (isNaN(portNum)) {
+    portNum = 443;
+  }
+  password = decodeURIComponent(password);
+  if (name != null) {
+    name = decodeURIComponent(name);
+  }
+  name = name ?? `Hysteria2 ${server}:${port}`;
+
+  const proxy: IProxyConfig = {
+    type: "hysteria2",
+    name,
+    server,
+    port: portNum,
+    password,
+  };
+
+  const params: Record<string, string> = {};
+  for (const addon of addons.split("&")) {
+    const [key, valueRaw] = addon.split("=");
+    let value = valueRaw;
+    value = decodeURIComponent(valueRaw);
+    params[key] = value;
+  }
+
+  proxy.sni = params.sni;
+  if (!proxy.sni && params.peer) {
+    proxy.sni = params.peer;
+  }
+  if (params.obfs && params.obfs !== "none") {
+    proxy.obfs = params.obfs;
+  }
+
+  proxy.ports = params.mport;
+  proxy["obfs-password"] = params["obfs-password"];
+  proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.insecure);
+  proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
+  proxy["tls-fingerprint"] = params.pinSHA256;
+
+  return proxy;
+}
+
+function URI_Hysteria(line: string): IProxyConfig {
+  line = line.split(/(hysteria|hy):\/\//)[2];
+  let [__, server, ___, port, ____, addons = "", name] =
+    /^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
+  let portNum = parseInt(`${port}`, 10);
+  if (isNaN(portNum)) {
+    portNum = 443;
+  }
+  if (name != null) {
+    name = decodeURIComponent(name);
+  }
+  name = name ?? `Hysteria ${server}:${port}`;
+
+  const proxy: IProxyConfig = {
+    type: "hysteria",
+    name,
+    server,
+    port: portNum,
+  };
+  const params: Record<string, string> = {};
+
+  for (const addon of addons.split("&")) {
+    let [key, value] = addon.split("=");
+    key = key.replace(/_/, "-");
+    value = decodeURIComponent(value);
+    switch (key) {
+      case "alpn":
+        proxy["alpn"] = value ? value.split(",") : undefined;
+        break;
+      case "insecure":
+        proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
+        break;
+      case "auth":
+        proxy["auth-str"] = value;
+        break;
+      case "mport":
+        proxy["ports"] = value;
+        break;
+      case "obfsParam":
+        proxy["obfs"] = value;
+        break;
+      case "upmbps":
+        proxy["up"] = value;
+        break;
+      case "downmbps":
+        proxy["down"] = value;
+        break;
+      case "obfs":
+        proxy["obfs"] = value || "";
+        break;
+      case "fast-open":
+        proxy["fast-open"] = /(TRUE)|1/i.test(value);
+        break;
+      case "peer":
+        proxy["fast-open"] = /(TRUE)|1/i.test(value);
+        break;
+      case "recv-window-conn":
+        proxy["recv-window-conn"] = parseInt(value);
+        break;
+      case "recv-window":
+        proxy["recv-window"] = parseInt(value);
+        break;
+      case "ca":
+        proxy["ca"] = value;
+        break;
+      case "ca-str":
+        proxy["ca-str"] = value;
+        break;
+      case "disable-mtu-discovery":
+        proxy["disable-mtu-discovery"] = /(TRUE)|1/i.test(value);
+        break;
+      case "fingerprint":
+        proxy["fingerprint"] = value;
+        break;
+      case "protocol":
+        proxy["protocol"] = value;
+      case "sni":
+        proxy["sni"] = value;
+      default:
+        break;
+    }
+  }
+
+  if (!proxy.sni && params.peer) {
+    proxy.sni = params.peer;
+  }
+  if (!proxy["fast-open"] && params["fast-open"]) {
+    proxy["fast-open"] = true;
+  }
+  if (!proxy.protocol) {
+    proxy.protocol = "udp";
+  }
+
+  return proxy;
+}
+
+function URI_TUIC(line: string): IProxyConfig {
+  line = line.split(/tuic:\/\//)[1];
+
+  let [__, uuid, password, server, ___, port, ____, addons = "", name] =
+    /^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
+
+  let portNum = parseInt(`${port}`, 10);
+  if (isNaN(portNum)) {
+    portNum = 443;
+  }
+  password = decodeURIComponent(password);
+  if (name != null) {
+    name = decodeURIComponent(name);
+  }
+  name = name ?? `TUIC ${server}:${port}`;
+
+  const proxy: IProxyConfig = {
+    type: "tuic",
+    name,
+    server,
+    port: portNum,
+    password,
+    uuid,
+  };
+
+  for (const addon of addons.split("&")) {
+    let [key, value] = addon.split("=");
+    key = key.replace(/_/, "-");
+    value = decodeURIComponent(value);
+    switch (key) {
+      case "token":
+        proxy["token"] = value;
+        break;
+      case "ip":
+        proxy["ip"] = value;
+        break;
+      case "heartbeat-interval":
+        proxy["heartbeat-interval"] = parseInt(value);
+        break;
+      case "alpn":
+        proxy["alpn"] = value ? value.split(",") : undefined;
+        break;
+      case "disable-sni":
+        proxy["disable-sni"] = /(TRUE)|1/i.test(value);
+        break;
+      case "reduce-rtt":
+        proxy["reduce-rtt"] = /(TRUE)|1/i.test(value);
+        break;
+      case "request-timeout":
+        proxy["request-timeout"] = parseInt(value);
+        break;
+      case "udp-relay-mode":
+        proxy["udp-relay-mode"] = value;
+        break;
+      case "congestion-controller":
+        proxy["congestion-controller"] = value;
+        break;
+      case "max-udp-relay-packet-size":
+        proxy["max-udp-relay-packet-size"] = parseInt(value);
+        break;
+      case "fast-open":
+        proxy["fast-open"] = /(TRUE)|1/i.test(value);
+        break;
+      case "skip-cert-verify":
+        proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
+        break;
+      case "max-open-streams":
+        proxy["max-open-streams"] = parseInt(value);
+        break;
+      case "sni":
+        proxy["sni"] = value;
+        break;
+      case "allow-insecure":
+        proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
+        break;
+    }
+  }
+
+  return proxy;
+}
+
+function URI_Wireguard(line: string): IProxyConfig {
+  line = line.split(/(wireguard|wg):\/\//)[2];
+  let [__, ___, privateKey, server, ____, port, _____, addons = "", name] =
+    /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
+
+  let portNum = parseInt(`${port}`, 10);
+  if (isNaN(portNum)) {
+    portNum = 443;
+  }
+  privateKey = decodeURIComponent(privateKey);
+  if (name != null) {
+    name = decodeURIComponent(name);
+  }
+  name = name ?? `WireGuard ${server}:${port}`;
+  const proxy: IProxyConfig = {
+    type: "wireguard",
+    name,
+    server,
+    port: portNum,
+    "private-key": privateKey,
+    udp: true,
+  };
+  for (const addon of addons.split("&")) {
+    let [key, value] = addon.split("=");
+    key = key.replace(/_/, "-");
+    value = decodeURIComponent(value);
+    switch (key) {
+      case "address":
+      case "ip":
+        value.split(",").map((i) => {
+          const ip = i
+            .trim()
+            .replace(/\/\d+$/, "")
+            .replace(/^\[/, "")
+            .replace(/\]$/, "");
+          if (isIPv4(ip)) {
+            proxy.ip = ip;
+          } else if (isIPv6(ip)) {
+            proxy.ipv6 = ip;
+          }
+        });
+        break;
+      case "publickey":
+        proxy["public-key"] = value;
+        break;
+      case "allowed-ips":
+        proxy["allowed-ips"] = value.split(",");
+        break;
+      case "pre-shared-key":
+        proxy["pre-shared-key"] = value;
+        break;
+      case "reserved":
+        const parsed = value
+          .split(",")
+          .map((i) => parseInt(i.trim(), 10))
+          .filter((i) => Number.isInteger(i));
+        if (parsed.length === 3) {
+          proxy["reserved"] = parsed;
+        }
+        break;
+      case "udp":
+        proxy["udp"] = /(TRUE)|1/i.test(value);
+        break;
+      case "mtu":
+        proxy.mtu = parseInt(value.trim(), 10);
+        break;
+      case "dialer-proxy":
+        proxy["dialer-proxy"] = value;
+        break;
+      case "remote-dns-resolve":
+        proxy["remote-dns-resolve"] = /(TRUE)|1/i.test(value);
+        break;
+      case "dns":
+        proxy.dns = value.split(",");
+        break;
+      default:
+        break;
+    }
+  }
+
+  return proxy;
+}
+
+function URI_HTTP(line: string): IProxyConfig {
+  line = line.split(/(http|https):\/\//)[2];
+  let [__, ___, auth, server, ____, port, _____, addons = "", name] =
+    /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
+
+  let portNum = parseInt(`${port}`, 10);
+  if (isNaN(portNum)) {
+    portNum = 443;
+  }
+  if (auth) {
+    auth = decodeURIComponent(auth);
+  }
+  if (name != null) {
+    name = decodeURIComponent(name);
+  }
+  name = name ?? `HTTP ${server}:${portNum}`;
+  const proxy: IProxyConfig = {
+    type: "http",
+    name,
+    server,
+    port: portNum,
+  };
+  if (auth) {
+    const [username, password] = auth.split(":");
+    proxy.username = username;
+    proxy.password = password;
+  }
+
+  for (const addon of addons.split("&")) {
+    let [key, value] = addon.split("=");
+    key = key.replace(/_/, "-");
+    value = decodeURIComponent(value);
+    switch (key) {
+      case "tls":
+        proxy.tls = /(TRUE)|1/i.test(value);
+        break;
+      case "fingerprint":
+        proxy["fingerprint"] = value;
+        break;
+      case "skip-cert-verify":
+        proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
+        break;
+      case "udp":
+        proxy["udp"] = /(TRUE)|1/i.test(value);
+        break;
+      case "ip-version":
+        proxy["ip-version"] = value;
+        break;
+      default:
+        break;
+    }
+  }
+
+  return proxy;
+}
+
+function URI_SOCKS(line: string): IProxyConfig {
+  line = line.split(/socks5:\/\//)[1];
+  let [__, ___, auth, server, ____, port, _____, addons = "", name] =
+    /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
+
+  let portNum = parseInt(`${port}`, 10);
+  if (isNaN(portNum)) {
+    portNum = 443;
+  }
+  if (auth) {
+    auth = decodeURIComponent(auth);
+  }
+  if (name != null) {
+    name = decodeURIComponent(name);
+  }
+  name = name ?? `SOCKS5 ${server}:${portNum}`;
+  const proxy: IProxyConfig = {
+    type: "socks5",
+    name,
+    server,
+    port: portNum,
+  };
+  if (auth) {
+    const [username, password] = auth.split(":");
+    proxy.username = username;
+    proxy.password = password;
+  }
+
+  for (const addon of addons.split("&")) {
+    let [key, value] = addon.split("=");
+    key = key.replace(/_/, "-");
+    value = decodeURIComponent(value);
+    switch (key) {
+      case "tls":
+        proxy.tls = /(TRUE)|1/i.test(value);
+        break;
+      case "fingerprint":
+        proxy["fingerprint"] = value;
+        break;
+      case "skip-cert-verify":
+        proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
+        break;
+      case "udp":
+        proxy["udp"] = /(TRUE)|1/i.test(value);
+        break;
+      case "ip-version":
+        proxy["ip-version"] = value;
+        break;
+      default:
+        break;
+    }
+  }
+
+  return proxy;
+}