Compare commits

..
53 changed files with 3030 additions and 1148 deletions
Generated
+123 -187
View File
@@ -150,7 +150,7 @@ dependencies = [
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot 0.12.5",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
@@ -159,9 +159,9 @@ dependencies = [
[[package]]
name = "arc-swap"
version = "1.9.3"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [
"rustversion",
]
@@ -1117,7 +1117,7 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation",
"open",
"parking_lot 0.13.0",
"parking_lot",
"percent-encoding",
"regex",
"reqwest 0.13.5",
@@ -1175,7 +1175,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"criterion",
"parking_lot 0.13.0",
"parking_lot",
"tokio",
]
@@ -1198,7 +1198,7 @@ dependencies = [
"anyhow",
"flexi_logger",
"log",
"parking_lot 0.13.0",
"parking_lot",
"tracing-estuary",
]
@@ -1255,8 +1255,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66"
dependencies = [
"crossbeam-utils",
"lock_api 0.4.14",
"parking_lot_core 0.9.12",
"lock_api",
"parking_lot_core",
"polonius-the-crab",
]
@@ -1333,7 +1333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1767,18 +1767,8 @@ version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core 0.23.0",
"darling_macro 0.23.0",
]
[[package]]
name = "darling"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [
"darling_core 0.24.1",
"darling_macro 0.24.1",
"darling_core",
"darling_macro",
]
[[package]]
@@ -1794,41 +1784,17 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "darling_core"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 3.0.5",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core 0.23.0",
"darling_core",
"quote",
"syn 2.0.119",
]
[[package]]
name = "darling_macro"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
"darling_core 0.24.1",
"quote",
"syn 3.0.5",
]
[[package]]
name = "dashmap"
version = "6.2.1"
@@ -1838,9 +1804,9 @@ dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api 0.4.14",
"lock_api",
"once_cell",
"parking_lot_core 0.9.12",
"parking_lot_core",
]
[[package]]
@@ -1918,9 +1884,9 @@ dependencies = [
[[package]]
name = "devtools-core"
version = "0.4.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e85926bda763dd434fcfbcfb24af4b2c1a8c3e3c34bcd5d844c48f655cca51"
checksum = "c53d416e473377276435d49d731a75d253b96ee4339831cd85653702b94d8298"
dependencies = [
"bytes",
"devtools-wire-format",
@@ -2048,7 +2014,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -2307,7 +2273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -2823,7 +2789,7 @@ dependencies = [
"gobject-sys 0.21.5",
"libc",
"system-deps 7.0.8",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -3377,7 +3343,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.62.2",
"windows-core 0.61.2",
]
[[package]]
@@ -3391,12 +3357,12 @@ dependencies = [
[[package]]
name = "ico"
version = "0.6.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [
"byteorder",
"png",
"png 0.17.16",
]
[[package]]
@@ -3582,7 +3548,7 @@ dependencies = [
"byteorder-lite",
"moxcms",
"num-traits",
"png",
"png 0.18.1",
"tiff",
]
@@ -3891,7 +3857,7 @@ dependencies = [
"http",
"httparse",
"libc",
"parking_lot 0.12.5",
"parking_lot",
"path-tree",
"pin-project-lite",
"rand 0.10.2",
@@ -4051,20 +4017,11 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lock_api"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65d91b22da179613dcf4d6ec8349563f52a363f6849e03b00dd0a185ec4a59af"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.35"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-slab"
@@ -4074,9 +4031,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rust2"
version = "0.24.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490750560fa7851fbbf5d3ab1a8750f09350ed361ed8aed85b1e1e40587812ee"
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
dependencies = [
"sha2 0.11.1",
]
@@ -4254,10 +4211,10 @@ dependencies = [
"objc2-core-foundation",
"objc2-foundation",
"once_cell",
"png",
"png 0.18.1",
"serde",
"thiserror 2.0.20",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -4424,7 +4381,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -4806,7 +4763,7 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [
"parking_lot_core 0.9.12",
"parking_lot_core",
]
[[package]]
@@ -4829,9 +4786,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
version = "5.4.5"
version = "5.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
dependencies = [
"dunce",
"is-wsl",
@@ -4901,7 +4858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4965,18 +4922,8 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api 0.4.14",
"parking_lot_core 0.9.12",
]
[[package]]
name = "parking_lot"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9cf937ca4e1d0b7578784559063422fc39ae7c5a00f02471f2614d5486fd226"
dependencies = [
"lock_api 0.5.0",
"parking_lot_core 0.10.0",
"lock_api",
"parking_lot_core",
]
[[package]]
@@ -4987,19 +4934,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.18",
"smallvec",
"windows-link 0.2.1",
]
[[package]]
name = "parking_lot_core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cfff060068cc18aa7815010fcaf80c68c7fb9e85fc7f5c0f9bf142c21d19826"
dependencies = [
"libc",
"redox_syscall 0.9.4",
"redox_syscall",
"smallvec",
"windows-link 0.2.1",
]
@@ -5290,6 +5225,19 @@ dependencies = [
"plotters-backend",
]
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
]
[[package]]
name = "png"
version = "0.18.1"
@@ -5575,7 +5523,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -5719,15 +5667,6 @@ dependencies = [
"bitflags 2.13.2",
]
[[package]]
name = "redox_syscall"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
dependencies = [
"bitflags 2.13.2",
]
[[package]]
name = "redox_users"
version = "0.4.6"
@@ -5925,9 +5864,9 @@ dependencies = [
[[package]]
name = "rfd"
version = "0.17.2"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
@@ -5935,18 +5874,16 @@ dependencies = [
"gobject-sys 0.18.0",
"gtk-sys",
"js-sys",
"libc",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"percent-encoding",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -6090,7 +6027,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -6103,7 +6040,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -6162,7 +6099,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -6540,7 +6477,7 @@ version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
dependencies = [
"darling 0.23.0",
"darling",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -6782,7 +6719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -6800,7 +6737,7 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
"redox_syscall 0.5.18",
"redox_syscall",
"tracing",
"wasm-bindgen",
"web-sys",
@@ -6835,9 +6772,9 @@ dependencies = [
[[package]]
name = "specta"
version = "2.0.0-rc.26"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed155e448f42fc4be7a5cbc12bd43f6c549981f7c32fcba429ff6bc57cc4f30c"
checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
dependencies = [
"paste",
"rustc_version 0.4.1",
@@ -6847,9 +6784,9 @@ dependencies = [
[[package]]
name = "specta-macros"
version = "2.0.0-rc.26"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b34b2516e16a9ddd3dc4e73cd6454cfe21fe209f8cb5a6f37346fe3774d759b3"
checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
dependencies = [
"Inflector",
"proc-macro2",
@@ -6859,9 +6796,9 @@ dependencies = [
[[package]]
name = "specta-serde"
version = "0.0.13"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4983177ed0eb28235080261953de5f9ebae8a2d107851b5852420bd2ab246245"
checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
dependencies = [
"specta",
"specta-macros",
@@ -6869,18 +6806,18 @@ dependencies = [
[[package]]
name = "specta-typescript"
version = "0.0.13"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ecbe75dc1d27b36141b50b02b7c73bed00945710cee6edffa86d296a159a6f9"
checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
dependencies = [
"specta",
]
[[package]]
name = "specta-util"
version = "0.0.13"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bf531215ea70fba8264edecf98f12600462cf359f2038602a47aeda5ecd5b8"
checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
dependencies = [
"specta",
]
@@ -6910,7 +6847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [
"new_debug_unreachable",
"parking_lot 0.12.5",
"parking_lot",
"phf_shared 0.13.1",
"precomputed-hash",
]
@@ -7046,9 +6983,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.40.1"
version = "0.39.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8"
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
dependencies = [
"libc",
"memchr",
@@ -7157,7 +7094,7 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation",
"objc2-ui-kit",
"parking_lot 0.12.5",
"parking_lot",
"percent-encoding",
"raw-window-handle",
"tao-macros",
@@ -7211,9 +7148,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tauri"
version = "2.12.5"
version = "2.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072"
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
dependencies = [
"anyhow",
"bytes",
@@ -7266,9 +7203,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.7.4"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614"
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
dependencies = [
"anyhow",
"cargo_toml",
@@ -7289,16 +7226,16 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.7.4"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316"
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
dependencies = [
"base64 0.23.1",
"brotli",
"ico",
"json-patch",
"plist",
"png",
"png 0.18.1",
"proc-macro2",
"quote",
"semver 1.0.28",
@@ -7316,9 +7253,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.7.4"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1"
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -7364,7 +7301,7 @@ version = "0.1.0"
dependencies = [
"deelevate",
"libc",
"parking_lot 0.13.0",
"parking_lot",
"serde",
"specta",
"sysinfo",
@@ -7389,9 +7326,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.6.3"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab551d9bec27bb79d1c7fe5ed38c5cfd1cebb86efa745acc65f5853f345f6e56"
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
dependencies = [
"dunce",
"plist",
@@ -7410,9 +7347,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-devtools"
version = "2.2.3"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299a62101a16c14b914ebfaf529b4b64ad9ca534a0ece1c21fc9308d1e3ba9db"
checksum = "1acb4d1b8490ee79166b72c3f8c989e066650a11aa94dfd0d71fb05c61cdbed0"
dependencies = [
"async-stream",
"bytes",
@@ -7437,9 +7374,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.8.4"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5"
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
dependencies = [
"log",
"raw-window-handle",
@@ -7543,13 +7480,13 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.8.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf"
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
dependencies = [
"log",
"notify-rust",
"rand 0.10.2",
"rand 0.9.5",
"serde",
"serde_json",
"serde_repr",
@@ -7595,9 +7532,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.6.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7375b25f72f70f6168f27f1ba3a413e30eaa0a2098cff53a4046ebfe9e2f142d"
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
dependencies = [
"encoding_rs",
"libc",
@@ -7652,9 +7589,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-wdio-webdriver"
version = "1.5.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a775b0b59f6f12b38d1f28667311a89cd07f532f540e27d65c6eadfba1156d8"
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
dependencies = [
"async-trait",
"axum",
@@ -7725,9 +7662,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.12.3"
version = "2.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f"
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
dependencies = [
"gtk",
"http",
@@ -7735,6 +7672,7 @@ dependencies = [
"log",
"objc2",
"objc2-app-kit",
"once_cell",
"percent-encoding",
"raw-window-handle",
"softbuffer",
@@ -7751,12 +7689,11 @@ dependencies = [
[[package]]
name = "tauri-specta"
version = "2.0.0-rc.26"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe71fc209fa4c49f959644f1ae2d70f72d5675bcb3d3ad8518122fa7b330994e"
checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
dependencies = [
"heck 0.5.0",
"paste",
"serde",
"serde_json",
"specta",
@@ -7770,15 +7707,15 @@ dependencies = [
[[package]]
name = "tauri-specta-macros"
version = "2.0.0-rc.26"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf5689a1dbf2611e2351b79ba042ca089d0a3ce45274695e79084cd732a5aec"
checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
dependencies = [
"darling 0.24.1",
"darling",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 3.0.5",
"syn 2.0.119",
]
[[package]]
@@ -7848,10 +7785,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -8104,9 +8041,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.54.1"
version = "1.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
dependencies = [
"bytes",
"libc",
@@ -8462,7 +8399,7 @@ dependencies = [
"flexi_logger",
"log",
"nu-ansi-term",
"parking_lot 0.12.5",
"parking_lot",
"tokio",
"tracing",
"tracing-log",
@@ -8514,10 +8451,10 @@ dependencies = [
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
"png",
"png 0.18.1",
"serde",
"thiserror 2.0.20",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -8626,7 +8563,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -9072,9 +9009,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.11"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
@@ -9168,7 +9105,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -9189,7 +9126,7 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
"windows-version",
]
@@ -9764,7 +9701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [
"cfg-if",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -10096,9 +10033,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.3.0"
version = "9.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaeb7f16d2605b49aef586996ded29e65642fab7259a3077e537351df6d80426"
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
dependencies = [
"aes",
"bzip2",
@@ -10109,7 +10046,6 @@ dependencies = [
"getrandom 0.4.3",
"hmac",
"indexmap 2.14.0",
"libc",
"lzma-rust2",
"memchr",
"nix",
+4 -4
View File
@@ -61,20 +61,20 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1.44"
tauri = { version = "2.12.5" }
tauri = { version = "2.12.4" }
tauri-plugin-clipboard-manager = "2.5.0"
parking_lot = "0.13.0"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104"
chrono = "0.4.45"
criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.54.1", features = [
tokio = { version = "1.53.3", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
flexi_logger = "0.31.10"
log = "0.4.35"
log = "0.4.34"
regex = "1.13.1"
reqwest = { version = "0.13.5", features = [
"json",
-2
View File
@@ -25,7 +25,6 @@
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复检测更新后又发布新版时,应用内更新失败
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
</details>
@@ -38,6 +37,5 @@
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
</details>
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
clash-verge-logging = { workspace = true }
chrono = { workspace = true }
log = { workspace = true }
@@ -5,14 +5,14 @@ edition = "2024"
rust-version = "1.91"
[dependencies]
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] }
# sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.40.1", features = ["network", "system"] }
sysinfo = { version = "0.39.6", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.191"
@@ -27,9 +27,9 @@ pub struct SysInfo {
impl Default for SysInfo {
#[inline]
fn default() -> Self {
let system_name = System::name().unwrap_or_else(|_| "Null".into());
let system_version = System::long_os_version().unwrap_or_else(|_| "Null".into());
let system_kernel_version = System::kernel_version().unwrap_or_else(|_| "Null".into());
let system_name = System::name().unwrap_or_else(|| "Null".into());
let system_version = System::long_os_version().unwrap_or_else(|| "Null".into());
let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into());
let system_arch = System::cpu_arch();
Self {
system_name,
@@ -130,14 +130,9 @@ pub fn current_gid() -> u32 {
#[inline]
pub fn list_network_interfaces() -> Vec<String> {
let Ok(mut networks) = Networks::new() else {
return Vec::new();
};
let mut networks = Networks::new();
networks.refresh(false);
networks
.keys()
.map(|name| name.to_string_lossy().into_owned())
.collect()
networks.keys().map(|name| name.to_owned()).collect()
}
#[inline]
+34
View File
@@ -256,6 +256,40 @@ export default defineConfig([
},
},
},
{
files: ['scripts/react/*.{mjs,mts,ts,tsx}'],
languageOptions: {
globals: { ...globals.browser, ...globals.node },
parserOptions: {
projectService: false,
project: './scripts/react/tsconfig.json',
},
},
settings: {
'import-x/resolver-next': [
createTypeScriptImportResolver({
project: './scripts/react/tsconfig.json',
}),
],
},
},
{
files: ['scripts/react/*.{mjs,mts}'],
languageOptions: {
parserOptions: {
projectService: false,
project: './scripts/react/tsconfig.node.json',
},
},
},
{
files: ['scripts/react/{boundaries,material,scene}.tsx'],
// The isolated fixtures use full reloads and preserve aliased Hook names.
rules: {
'react-refresh/only-export-components': 'off',
'@eslint-react/no-unnecessary-use-prefix': 'off',
},
},
{
files: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}'],
rules: ipcBoundaryRules(),
+10 -1
View File
@@ -1,6 +1,15 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["src/polyfills/*.js!", "scripts/check-contract-changes.mjs!"],
"entry": [
"src/polyfills/*.js!",
"scripts/check-contract-changes.mjs!",
"scripts/react/scene.tsx!",
"scripts/react/profiler.ts!",
"scripts/react/boundaries.tsx!",
"scripts/react/material.tsx!",
"scripts/react/yaml.ts!",
"scripts/react/wdyr.ts!"
],
"project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
"include": [
"files",
+12 -5
View File
@@ -40,7 +40,11 @@
"perf:build": "node scripts/perf/build.mjs",
"perf:run": "node scripts/perf/run.mjs",
"perf:compare": "node scripts/perf/compare.mjs",
"perf:test": "node --test scripts/perf/report.test.mjs"
"perf:test": "node --test scripts/perf/report.test.mjs",
"dev:profile": "cross-env CVR_REACT_DIAGNOSTICS=profiler node scripts/dev.mjs dev",
"react:ab": "node scripts/react/ab.mjs",
"dev:wdyr": "cross-env CVR_REACT_DIAGNOSTICS=wdyr node scripts/dev.mjs dev",
"audit:react": "node scripts/react/doctor.mjs"
},
"dependencies": {
"@dnd-kit/collision": "^0.5.0",
@@ -56,7 +60,7 @@
"@tanstack/react-virtual": "^3.14.13",
"@tauri-apps/api": "2.12.4",
"@tauri-apps/plugin-clipboard-manager": "2.5.0",
"@tauri-apps/plugin-dialog": "2.8.4",
"@tauri-apps/plugin-dialog": "2.8.3",
"@tauri-apps/plugin-fs": "2.7.1",
"@tauri-apps/plugin-http": "2.8.3",
"@tauri-apps/plugin-opener": "2.7.2",
@@ -94,7 +98,7 @@
"@biomejs/biome": "^2.5.14",
"@eslint-react/eslint-plugin": "^5.20.5",
"@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.4",
"@tauri-apps/cli": "2.12.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.0.0",
"@types/react": "19.3.0",
@@ -102,7 +106,7 @@
"@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.2.3",
"@vitejs/plugin-react": "^6.1.1",
"@wdio/tauri-service": "1.5.1",
"@wdio/tauri-service": "1.5.0",
"adm-zip": "^0.6.1",
"axios": "^1.20.0",
"cli-color": "^2.0.4",
@@ -134,7 +138,10 @@
"vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.1",
"webdriverio": "10.1.0",
"ws": "8.22.1"
"ws": "8.22.1",
"playwright": "1.64.1",
"@welldone-software/why-did-you-render": "10.0.1",
"react-doctor": "0.9.21"
},
"lint-staged": {
"*.{ts,tsx,js,mjs}": [
+1104 -166
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -113,27 +113,6 @@
"ghcr.io/devcontainers/features/rust"
],
"groupName": "Rust toolchain"
},
{
"description": "Update the Specta contract toolchain together",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta", "tauri-specta", "specta-typescript"],
"groupName": "specta",
"groupSlug": "specta",
"prCreation": "immediate",
"automerge": false
},
{
"description": "Keep Specta aligned with the supported tauri-specta release",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta", "tauri-specta"],
"allowedVersions": "/^2\\.0\\.0-rc\\.26$/"
},
{
"description": "Keep the TypeScript exporter compatible with Specta rc.26",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta-typescript"],
"allowedVersions": "/^0\\.0\\.13$/"
}
],
"customManagers": [
+47
View File
@@ -0,0 +1,47 @@
# React diagnostics
`pnpm dev:profile` runs the usual development launcher with a root React
Profiler. In WebView DevTools, `window.__CVR_REACT_PROFILER__.read()` returns
copies of the latest 1,000 standard Profiler callback records; `.clear()` drops
them. Records contain timing and commit metadata, never business props. Normal
development and production builds do not load the collector.
`pnpm dev:wdyr` instead initializes why-did-you-render before loading the app
and uses its development JSX runtime. It reports avoidable renders for
`ProxyItem`, `ProxyItemMini` and `ProxyTunCard` in the console. The modes are
mutually exclusive; `CVR_REACT_DIAGNOSTICS` accepts only `profiler` or `wdyr` on
the development server. Each mode uses its own Vite cache. Production builds
ignore this variable and include neither diagnostic entry nor runtime.
`pnpm audit:react` scans supported source files under `src`. Pass file paths to
limit the scan, for example `pnpm audit:react src/hooks/use-proxy-delay-state.ts`;
add `--json` for the original structured report. Diagnostics are advisory.
Execution errors and incomplete scans fail the command. Telemetry, remote scores
and supply-chain scanning are disabled; the command does not fix code, erase
historical diagnostics or run in CI.
Install the browser explicitly with `pnpm exec playwright install chromium`.
Then run `pnpm react:ab --base <ref> --head <ref>`. Both Git trees run against
one installed dependency set and the same fixed DNS, delay-hook and proxy-tab
fixtures. The command never installs a browser or changes either checkout.
It retains source hashes, dependency identity, browser/environment information,
behavior assertions, render observations and profiles under `/tmp/cvr-react-ab-*`.
Each version and mode has separate Vite caches and output. Three pairs alternate
order in ordinary development, Strict Mode and production. A behavior failure
returns a nonzero exit status. DNS stale serialization and same-value renders
are observations so a known baseline can be compared with its fix. Append
`--assert dns` to require the head version to keep the latest serialized YAML in sync with
continuous field edits; this is the focused DNS regression assertion.
`--assert delay` requires same-value delay updates to retain their reference and memoized consumer,
while each changed field (including clearing elapsed time) propagates.
`--assert tabs` requires zero tab renders during unrelated parent updates. Use
`--assert dns --assert delay --assert tabs` for a cumulative candidate.
The fixtures execute the actual selected components and hook. IPC, data,
editor and UI rendering boundaries are replaced, and outgoing network requests
are rejected. DNS serialization is observed at `js-yaml.dump`; tab execution is
observed at the Material Paper boundary. These are component results, not native
Tauri integration, real DNS/proxy/TUN writes, or whole-application CPU/RSS gains.
Production React does not emit Profiler callbacks. Timing is diagnostic and has
no hard acceptance threshold; measure native release CPU/RSS separately and
calibrate A/A before claiming a performance gain. Keep raw reports out of Git.
+596
View File
@@ -0,0 +1,596 @@
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
cp,
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
symlink,
writeFile,
} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseArgs } from 'node:util'
import react from '@vitejs/plugin-react'
import { chromium } from 'playwright'
import { build, createServer, preview } from 'vite'
const { values } = parseArgs({
options: {
base: { type: 'string' },
head: { type: 'string' },
assert: { type: 'string', multiple: true },
},
})
if (!values.base || !values.head)
throw new Error('Usage: pnpm react:ab --base <ref> --head <ref>')
const assertions = new Set(values.assert ?? [])
for (const name of assertions) {
if (!['dns', 'delay', 'tabs'].includes(name))
throw new Error(`Unknown regression assertion: ${name}`)
}
const harness = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(harness, '../..')
const output = await mkdtemp('/tmp/cvr-react-ab-')
const dependencies = path.join(root, 'node_modules')
const frozenHarness = path.join(output, 'harness')
await cp(harness, frozenHarness, {
recursive: true,
filter: (file) => !path.basename(file).startsWith('tsconfig'),
})
const files = [
'src/components/setting/mods/dns-viewer.tsx',
'src/hooks/use-proxy-delay-state.ts',
'src/components/home/proxy-tun-card.tsx',
]
const hash = (content) => createHash('sha256').update(content).digest('hex')
function git(...args) {
const result = spawnSync('git', args, {
cwd: root,
maxBuffer: 128 * 1024 * 1024,
})
if (result.status !== 0) throw new Error(result.stderr.toString())
return result.stdout
}
const manifest = {
assertions: [...assertions],
environment: {
platform: process.platform,
architecture: process.arch,
node: process.version,
os: os.release(),
lock: hash(await readFile(path.join(root, 'pnpm-lock.yaml'))),
},
harness: Object.fromEntries(
await Promise.all(
(await readdir(frozenHarness)).map(async (file) => [
file,
hash(await readFile(path.join(frozenHarness, file))),
]),
),
),
installedLock: hash(
await readFile(path.join(dependencies, '.pnpm/lock.yaml')),
),
sources: {},
runs: [],
}
let browser
try {
// Playwright intentionally fails here if its explicitly installed browser is missing.
browser = await chromium.launch({ headless: true })
manifest.environment.browser = browser.version()
for (const [variant, ref] of Object.entries({
base: values.base,
head: values.head,
})) {
const tree = git('rev-parse', '--verify', `${ref}^{tree}`).toString().trim()
const source = path.join(output, variant)
await mkdir(source)
const archive = git('archive', tree, 'src')
const extracted = spawnSync('tar', ['-x', '-C', source], { input: archive })
if (extracted.status !== 0) throw new Error(extracted.stderr.toString())
await cp(frozenHarness, path.join(source, 'fixture'), { recursive: true })
await symlink(dependencies, path.join(source, 'node_modules'), 'dir')
manifest.sources[variant] = {
ref,
tree,
files: Object.fromEntries(
await Promise.all(
files.map(async (file) => [
file,
hash(await readFile(path.join(source, file))),
]),
),
),
}
}
for (const mode of ['development', 'strict', 'production']) {
const servers = {}
try {
for (const variant of ['base', 'head']) {
const source = path.join(output, variant)
const fixture = path.join(source, 'fixture')
const boundary = path.join(fixture, 'boundaries.tsx')
const aliases = [
'@/hooks/use-clash',
'@/hooks/use-verge',
'@/hooks/use-profiles',
'@/hooks/use-system-state',
'@/hooks/use-system-proxy-state',
'@/services/states',
'@/services/cmds',
'@/services/mutate',
'@/services/notice-service',
'@/components/base',
'@/components/shared/proxy-control-switches',
'react-i18next',
].map((find) => ({ find, replacement: boundary }))
const config = {
configFile: false,
root: fixture,
cacheDir: path.join(output, 'cache', variant, mode),
plugins: [react()],
resolve: {
alias: [
...aliases,
{
find: '@/services/delay',
replacement: path.join(fixture, 'delay.ts'),
},
{
find: /^@mui\/material$/,
replacement: path.join(fixture, 'material.tsx'),
},
{
find: 'fixture-material',
replacement: path.join(dependencies, '@mui/material/index.mjs'),
},
{ find: 'js-yaml', replacement: path.join(fixture, 'yaml.ts') },
{
find: 'fixture-yaml',
replacement: path.join(
dependencies,
'js-yaml/dist/js-yaml.mjs',
),
},
{ find: '@', replacement: path.join(source, 'src') },
{
find: /^react(?=\/|$)/,
replacement: path.join(dependencies, 'react'),
},
{
find: /^react-dom(?=\/|$)/,
replacement: path.join(dependencies, 'react-dom'),
},
],
dedupe: ['react', 'react-dom'],
},
define: { OS_PLATFORM: JSON.stringify(process.platform) },
server: {
host: '127.0.0.1',
port: 0,
hmr: false,
fs: {
allow: [
output,
await realpath(output),
await realpath(dependencies),
],
},
},
build: {
outDir: path.join(output, 'dist', variant, mode),
emptyOutDir: true,
},
preview: { host: '127.0.0.1', port: 0 },
}
if (mode === 'production') {
await build(config)
servers[variant] = await preview(config)
} else {
servers[variant] = await createServer(config)
await servers[variant].listen()
}
}
for (const [pair, order] of [
['base', 'head'],
['head', 'base'],
['base', 'head'],
].entries()) {
for (const variant of order) {
const label = `${mode}-${pair + 1}-${variant}`
const context = await browser.newContext()
const errors = []
const address = servers[variant].httpServer.address()
const origin = `http://127.0.0.1:${address.port}`
await context.route('**/*', (route) => {
if (new URL(route.request().url()).origin === origin)
return route.continue()
errors.push(`Unexpected network: ${route.request().url()}`)
return route.abort()
})
const page = await context.newPage()
page.on('pageerror', (error) => errors.push(String(error)))
const result = { label, variant, mode, pair: pair + 1, errors }
try {
await page.goto(`${origin}/${mode === 'strict' ? '?strict' : ''}`)
await page.waitForSelector('#delay', { timeout: 60000 })
await page.waitForFunction(
() => window.__REACT_AB__.read().listeners.length === 1,
)
result.tabs = await tabs(page)
if (variant === 'head' && assertions.has('tabs')) {
assert.equal(
result.tabs.unrelated,
0,
'Unrelated parent updates must not rerender the memoized tabs',
)
}
result.dns = await dns(page)
if (variant === 'head' && assertions.has('dns')) {
for (const edit of result.dns.edits) {
assert.equal(
edit.serialized.at(-1),
edit.value,
'Latest DNS serialization must match the edited field',
)
}
}
result.delay = await delay(page)
if (variant === 'head' && assertions.has('delay')) {
for (const [index, observation] of result.delay.entries()) {
assert.equal(
observation.sameReference,
index === 1,
'Preserve the reference only when all three delay fields match',
)
}
assert.equal(
result.delay[1].childRenders,
result.delay[0].childRenders,
'Equal delay state must not rerender the memoized consumer',
)
}
result.profile = await page.evaluate(() => {
const before = window.__CVR_REACT_PROFILER__.read()
window.__CVR_REACT_PROFILER__.clear()
return { before, after: window.__CVR_REACT_PROFILER__.read() }
})
assert.equal(result.profile.after.length, 0)
result.collector = await page.evaluate(() =>
window.__REACT_AB__.checkCollector(),
)
assert.deepEqual(result.collector, {
length: 1000,
first: '5',
last: '1004',
copied: true,
cleared: true,
})
if (mode !== 'production')
assert(
result.profile.before.length > 0 &&
result.profile.before.length <= 1000,
)
assert.deepEqual(errors, [])
assert.deepEqual(
await page.evaluate(() =>
window.__REACT_AB__.fixture.notices.filter(
(notice) => notice.error,
),
),
[],
)
result.ok = true
} catch (error) {
result.snapshot = await page
.evaluate(() => window.__REACT_AB__?.read())
.catch(() => null)
result.ok = false
result.error = String(error)
throw error
} finally {
manifest.runs.push(result)
await writeFile(
path.join(output, `${label}.json`),
JSON.stringify(result, null, 2),
)
await context.close()
console.log(`${label}: ${result.ok ? 'pass' : 'FAIL'}`)
}
}
}
} finally {
for (const server of Object.values(servers)) {
if (server.close) await server.close()
else
await new Promise((resolve, reject) =>
server.httpServer.close((error) =>
error ? reject(error) : resolve(),
),
)
}
}
}
} finally {
await browser?.close()
await writeFile(
path.join(output, 'report.json'),
JSON.stringify(manifest, null, 2),
)
console.log(`Evidence: ${output}`)
}
async function settle(page) {
await page.waitForTimeout(100)
}
async function tabs(page) {
const buttons = page.locator('#tabs .MuiPaper-root')
const readTabs = () =>
page.evaluate(() =>
Object.fromEntries(
window.__REACT_AB__.fixture.tabRenders.map((tab) => [
tab.label.split('.').at(-1),
tab,
]),
),
)
assert.equal(await buttons.count(), 2)
assert.equal((await readTabs()).systemProxy.active, true)
assert.equal((await readTabs()).tunMode.active, false)
const before = await page.evaluate(
() => window.__REACT_AB__.fixture.tabRenders.length,
)
for (let index = 0; index < 5; index++) {
await page.evaluate(() => window.__REACT_AB__.update())
await settle(page)
}
const unrelated =
(await page.evaluate(() => window.__REACT_AB__.fixture.tabRenders.length)) -
before
await buttons.nth(1).click()
await settle(page)
assert.equal((await readTabs()).systemProxy.active, false)
assert.equal((await readTabs()).tunMode.active, true)
assert.equal(
await page.evaluate(() =>
localStorage.getItem('clash-verge-proxy-active-tab'),
),
'tun',
)
assert(
(await page.locator('#fixture-controls').textContent()).endsWith(
'toggles.tunMode',
),
)
await page.evaluate(() => window.__REACT_AB__.update({ mounted: false }))
await settle(page)
assert(
(await page.locator('#fixture-controls').textContent()).endsWith(
'toggles.tunMode',
),
)
await page.evaluate(() => window.__REACT_AB__.update({ mounted: true }))
await settle(page)
assert.equal((await readTabs()).tunMode.active, true)
assert.equal((await readTabs()).systemProxy.active, false)
await buttons.nth(0).click()
await settle(page)
assert.equal((await readTabs()).systemProxy.active, true)
assert.equal((await readTabs()).tunMode.active, false)
assert.equal(
await page.evaluate(() =>
localStorage.getItem('clash-verge-proxy-active-tab'),
),
'system',
)
await page.evaluate(() => {
window.__REACT_AB__.fixture.indicator = true
window.__REACT_AB__.fixture.verge.enable_tun_mode = true
window.__REACT_AB__.update()
})
await settle(page)
assert(Object.values(await readTabs()).every((tab) => tab.indicator))
await page.evaluate(() => {
window.__REACT_AB__.fixture.language = 'zh'
window.__REACT_AB__.update()
})
await settle(page)
const updated = Object.values(await readTabs())
assert(updated.every((tab) => tab.label.startsWith('zh:')))
assert(
(await page.locator('#tabs').textContent()).includes(
'zh:home.components.proxyTun.status.systemProxyEnabled',
),
)
const beforeColor = await buttons
.nth(0)
.evaluate((element) => getComputedStyle(element).backgroundColor)
await page.evaluate(() => {
window.__REACT_AB__.fixture.theme = 'dark'
window.__REACT_AB__.update()
})
await settle(page)
const color = await buttons
.nth(0)
.evaluate((element) => getComputedStyle(element).backgroundColor)
assert.notEqual(color, beforeColor)
return { unrelated, updated, beforeColor, color }
}
async function dns(page) {
await page.evaluate(() => window.__REACT_AB__.open())
const listen = page.locator('#dns input[placeholder=":53"]')
await page.waitForFunction(
() =>
document.querySelector('#dns input[placeholder=":53"]')?.value ===
':5353',
)
const edits = []
for (const value of [':54', ':55', ':56']) {
await page.evaluate(() => {
window.__REACT_AB__.fixture.serialized.length = 0
})
await listen.fill(value)
await settle(page)
assert.equal(await listen.inputValue(), value)
edits.push({
value,
serialized: await page.evaluate(() =>
window.__REACT_AB__.fixture.serialized.map(
(config) => config.dns.listen,
),
),
})
}
await page.evaluate(() => {
window.__REACT_AB__.fixture.serialized.length = 0
})
await listen.fill(':')
await listen.pressSequentially('567', { delay: 0 })
await settle(page)
assert.equal(await listen.inputValue(), ':567')
edits.push({
value: ':567',
rapid: true,
serialized: await page.evaluate(() =>
window.__REACT_AB__.fixture.serialized.map((config) => config.dns.listen),
),
})
await page.locator('#save-dns').click()
await page.waitForFunction(
() => window.__REACT_AB__.fixture.saved.length === 1,
)
assert.equal(
await page.evaluate(() => window.__REACT_AB__.fixture.saved[0].dns.listen),
':567',
)
await page.evaluate(() => window.__REACT_AB__.open())
await page.waitForSelector('#dns')
await page
.getByRole('button', { name: /shared.actions.resetToDefault$/ })
.click()
await settle(page)
assert.equal(await listen.inputValue(), ':53')
await page
.getByRole('button', { name: /shared.editorModes.advanced$/ })
.click()
await settle(page)
const editor = page.locator('#fixture-monaco')
assert.match(await editor.inputValue(), /listen: ["']?:53/)
await editor.fill(
'dns:\n enable: true\n listen: ":8053"\nhosts:\n example.test: 127.0.0.1\n',
)
await page
.getByRole('button', { name: /shared.editorModes.visualization$/ })
.click()
await settle(page)
assert.equal(await listen.inputValue(), ':8053')
await page
.getByRole('button', { name: /shared.editorModes.advanced$/ })
.click()
await settle(page)
await page.locator('#save-dns').click()
await page.waitForFunction(
() => window.__REACT_AB__.fixture.saved.length === 2,
)
const saved = await page.evaluate(() => window.__REACT_AB__.fixture.saved)
assert.equal(saved[1].dns.listen, ':8053')
assert.equal(saved[1].hosts['example.test'], '127.0.0.1')
return { edits, saved }
}
async function delay(page) {
const updates = [
{ delay: 10, elapsed: 12, updatedAt: 100 },
{ delay: 10, elapsed: 12, updatedAt: 100 },
{ delay: 11, elapsed: 12, updatedAt: 100 },
{ delay: 11, elapsed: 13, updatedAt: 100 },
{ delay: 11, elapsed: 13, updatedAt: 101 },
{ delay: 11, updatedAt: 101 },
{ delay: -1, updatedAt: 102 },
{ delay: 0, updatedAt: 103 },
{ delay: 1000000, elapsed: 20, updatedAt: 104 },
]
const observations = []
for (const update of updates) {
await page.evaluate((value) => {
window.__REACT_AB__.remember()
window.__REACT_AB__.delay.emit('Node 1', 'Group 1', value)
}, update)
await settle(page)
const result = await page.evaluate(() => window.__REACT_AB__.read())
assert.deepEqual(result.delay, update)
observations.push({
update,
sameReference: result.sameReference,
childRenders: result.childRenders,
})
}
await page.evaluate(() => window.__REACT_AB__.request())
await settle(page)
assert.equal(
await page.evaluate(() => window.__REACT_AB__.read().delay.delay),
-2,
)
await page.evaluate(() =>
window.__REACT_AB__.delay.finish({ delay: 0, updatedAt: 200 }),
)
await settle(page)
assert.equal(
await page.evaluate(() => window.__REACT_AB__.read().delay.delay),
0,
)
await page.evaluate(() => window.__REACT_AB__.request())
await settle(page)
assert.equal(
await page.evaluate(() => window.__REACT_AB__.read().delay.delay),
-2,
)
await page.evaluate(() =>
window.__REACT_AB__.delay.finish({
delay: 1000000,
elapsed: 20,
updatedAt: 201,
}),
)
await settle(page)
assert.deepEqual(
await page.evaluate(() => window.__REACT_AB__.read().delay),
{ delay: 1000000, elapsed: 20, updatedAt: 201 },
)
await page.evaluate(() => window.__REACT_AB__.update({ name: 'Node 2' }))
await settle(page)
let state = await page.evaluate(() => window.__REACT_AB__.read())
assert.deepEqual(state.listeners, ['Group 1:Node 2'])
assert.deepEqual(state.delay, { delay: -1, updatedAt: 0 })
await page.evaluate(() => window.__REACT_AB__.update({ group: 'Group 2' }))
await settle(page)
state = await page.evaluate(() => window.__REACT_AB__.read())
assert.deepEqual(state.listeners, ['Group 2:Node 2'])
for (const name of ['DIRECT', 'Unknown']) {
await page.evaluate((name) => window.__REACT_AB__.update({ name }), name)
await settle(page)
const preset = await page.evaluate(() => window.__REACT_AB__.read())
assert.deepEqual(preset.listeners, [])
assert.deepEqual(preset.delay, { delay: -1, updatedAt: 0 })
}
await page.evaluate(() => window.__REACT_AB__.update({ name: 'Node 2' }))
await settle(page)
assert.deepEqual(
await page.evaluate(() => window.__REACT_AB__.read().listeners),
['Group 2:Node 2'],
)
await page.evaluate(() => window.__REACT_AB__.update({ mounted: false }))
await settle(page)
assert.deepEqual(
await page.evaluate(() => window.__REACT_AB__.read().listeners),
[],
)
return observations
}
+68
View File
@@ -0,0 +1,68 @@
import { Switch as MuiSwitch } from '@mui/material'
import { fixture } from './state'
const translations = {
en: (key: string) => `en:${key}`,
zh: (key: string) => `zh:${key}`,
}
export const useTranslation = () => ({
t: translations[fixture.language as 'en' | 'zh'],
})
export const useVerge = () => ({ verge: fixture.verge })
export const useProfiles = () => ({ current: { uid: 'fixture' } })
export const useClash = () => ({
mutateClash: () => {
throw new Error('Unexpected mutateClash')
},
})
export const useSystemState = () => ({ isTunModeAvailable: fixture.available })
export const useSystemProxyState = () => ({ indicator: fixture.indicator })
export const useThemeMode = () => fixture.theme
export const getDnsConfigContent = async () => {
fixture.commands.push('getDnsConfigContent')
return fixture.initialDns
}
export const saveDnsConfig = async (config: any) => {
fixture.commands.push('saveDnsConfig')
fixture.saved.push(structuredClone(config))
}
export const validateDnsConfig = async () => {
fixture.commands.push('validateDnsConfig')
return { status: 'valid' }
}
export const applyDnsConfig = () => {
throw new Error('Unexpected applyDnsConfig')
}
export const mutate = (fn: () => unknown) => fn()
export const showNotice = {
error: (...args: any[]) => fixture.notices.push({ error: args }),
success: (...args: any[]) => fixture.notices.push({ success: args }),
}
export const Switch = MuiSwitch
export function BaseDialog({ open, title, children, onClose, onOk }: any) {
return open ? (
<section id="dns">
{title}
<button id="close-dns" onClick={onClose}>
close
</button>
<button id="save-dns" onClick={onOk}>
save
</button>
{children}
</section>
) : null
}
export function MonacoEditor({ value, onChange }: any) {
return (
<textarea
id="fixture-monaco"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
)
}
export default function ProxyControlSwitches({ label }: any) {
return <output id="fixture-controls">{label}</output>
}
+36
View File
@@ -0,0 +1,36 @@
type Update = { delay: number; elapsed?: number; updatedAt: number }
const listeners = new Map<string, (update: Update) => void>()
const cache = new Map<string, Update>()
let finish: ((update: Update) => void) | undefined
const key = (name: string, group: string) => `${group}:${name}`
export const delayFixture = {
listeners,
emit(name: string, group: string, update: Update) {
cache.set(key(name, group), update)
listeners.get(key(name, group))?.(update)
},
finish(update: Update) {
if (!finish) throw new Error('No pending delay request')
finish(update)
finish = undefined
},
}
export default {
setListener(name: string, group: string, listener: (update: Update) => void) {
listeners.set(key(name, group), listener)
},
removeListener(name: string, group: string) {
listeners.delete(key(name, group))
},
getDelayUpdate(name: string, group: string) {
return cache.get(key(name, group))
},
getDelayFix() {
return -1
},
checkDelay() {
return new Promise<Update>((resolve) => {
finish = resolve
})
},
}
+77
View File
@@ -0,0 +1,77 @@
import { spawnSync } from 'node:child_process'
import { statSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseArgs } from 'node:util'
import { globSync } from 'glob'
const { values, positionals } = parseArgs({
options: { json: { type: 'boolean' } },
allowPositionals: true,
})
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
const files = positionals.length
? positionals
: globSync('src/**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,html}', {
cwd: root,
nodir: true,
}).sort()
if (files.length === 0) throw new Error('No source files found')
for (const file of files) {
if (
!/\.(?:[jt]sx?|[cm][jt]s|html)$/.test(file) ||
!statSync(path.resolve(root, file)).isFile()
)
throw new Error(`Expected a source file: ${file}`)
}
const executable = fileURLToPath(
new URL('../bin/react-doctor.js', import.meta.resolve('react-doctor')),
)
const result = spawnSync(
process.execPath,
[
executable,
...files,
'--json',
'--no-telemetry',
'--no-supply-chain',
'--blocking',
'none',
'--yes',
],
{
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
maxBuffer: 64 * 1024 * 1024,
},
)
if (result.error) throw result.error
if (result.signal)
throw new Error(`React Doctor terminated by ${result.signal}`)
const report = JSON.parse(result.stdout)
if (values.json) {
process.stdout.write(result.stdout)
} else {
for (const diagnostic of report.diagnostics) {
console.log(
`${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} ${diagnostic.plugin}/${diagnostic.rule}\n ${diagnostic.message}`,
)
}
console.log(
`${report.summary.totalDiagnosticCount} advisory diagnostics across ${report.projects.reduce((count, project) => count + project.analyzedFileCount, 0)} analyzed files`,
)
}
// In 0.9.21, --blocking none also masks linter execution failures.
const incomplete = report.projects.filter((project) => !project.complete)
if (report.error) console.error(report.error.message)
for (const project of incomplete) {
console.error(
'Incomplete React Doctor scan:',
project.directory,
project.skippedCheckReasons,
)
}
process.exitCode =
result.status || (!report.ok || incomplete.length > 0 ? 1 : 0)
+5
View File
@@ -0,0 +1,5 @@
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><title>React component A/B</title></head>
<body><div id="root"></div><script type="module" src="./scene.tsx"></script></body>
</html>
+16
View File
@@ -0,0 +1,16 @@
import { Paper as MuiPaper } from 'fixture-material'
export * from 'fixture-material'
import { fixture } from './state'
export function Paper(props: any) {
const { onClick, elevation, children, sx } = props
if (onClick && sx?.maxWidth === 160) {
fixture.tabRenders.push({
active: elevation === 2,
indicator: Boolean(children[2]),
label: children[1].props.children,
})
}
return <MuiPaper {...props} />
}
+19
View File
@@ -0,0 +1,19 @@
import type { ProfilerOnRenderCallback } from 'react'
export type ProfileRecord = Parameters<ProfilerOnRenderCallback>
const records: ProfileRecord[] = []
export const recordRender: ProfilerOnRenderCallback = (...record) => {
if (records.length === 1000) records.shift()
records.push(record)
}
export const profiler = {
read: () => records.map((record) => [...record]),
clear: () => {
records.length = 0
},
}
Object.assign(window, { __CVR_REACT_PROFILER__: profiler })
+124
View File
@@ -0,0 +1,124 @@
import { ThemeProvider, createTheme } from '@mui/material'
import {
Profiler,
StrictMode,
memo,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { createRoot } from 'react-dom/client'
import { ProxyTunCard } from '@/components/home/proxy-tun-card'
import { DnsViewer } from '@/components/setting/mods/dns-viewer'
import { useProxyDelayState } from '@/hooks/use-proxy-delay-state'
import { delayFixture } from './delay'
import { profiler, recordRender } from './profiler'
import { fixture } from './state'
const member = (name: string): any =>
name === 'Unknown'
? {
kind: 'unresolved',
ref: { kind: 'unresolved', name, reason: 'missing' },
}
: {
kind: 'node',
ref: { kind: 'node', recordId: name, name },
node: { name, history: [], source: { kind: 'core', proxyName: name } },
}
let update: (patch?: any) => void
let open: () => void
let delay: ReturnType<typeof useProxyDelayState>
let previous: unknown
let childRenders = 0
const DelayValue = memo(({ state }: any) => {
childRenders++
return <output id="delay">{JSON.stringify(state)}</output>
})
function DelayProbe({ name, group }: any) {
const resolved = useMemo(() => member(name), [name])
const current = useProxyDelayState(resolved, group)
useEffect(() => {
delay = current
})
return <DelayValue state={current.delayState} />
}
function Scene() {
const [state, setState] = useState({
revision: 0,
name: 'Node 1',
group: 'Group 1',
mounted: true,
})
const ref = useRef<any>(null)
useEffect(() => {
update = (patch = {}) =>
setState((old) => ({ ...old, ...patch, revision: old.revision + 1 }))
open = () => ref.current.open()
}, [])
const mode = fixture.theme
const theme = useMemo(() => createTheme({ palette: { mode } }), [mode])
return (
<ThemeProvider theme={theme}>
<main data-revision={state.revision}>
<section id="tabs" key={state.mounted ? 'mounted' : 'reset'}>
<ProxyTunCard />
</section>
<DnsViewer ref={ref} />
{state.mounted && <DelayProbe name={state.name} group={state.group} />}
</main>
</ThemeProvider>
)
}
const strict = new URLSearchParams(location.search).has('strict')
const app = (
<Profiler id="fixtures" onRender={recordRender}>
<Scene />
</Profiler>
)
createRoot(document.getElementById('root')!).render(
strict ? <StrictMode>{app}</StrictMode> : app,
)
Object.assign(window, {
__REACT_AB__: {
update: (patch?: any) => update(patch),
open: () => open(),
fixture,
delay: delayFixture,
request: () => {
void delay.onDelay()
},
remember: () => {
previous = delay.delayState
},
checkCollector: () => {
profiler.clear()
for (let index = 0; index < 1005; index++)
recordRender(String(index), 'update', 1, 2, 3, 4)
const records = profiler.read()
records[0][0] = 'changed copy'
const preserved = profiler.read()
const result = {
length: preserved.length,
first: preserved[0][0],
last: preserved.at(-1)![0],
copied: preserved[0][0] === '5',
cleared: false,
}
profiler.clear()
result.cleared = profiler.read().length === 0
return result
},
read: () => ({
fixture,
profile: profiler.read(),
childRenders,
sameReference: previous === delay.delayState,
delay: delay.delayState,
listeners: [...delayFixture.listeners.keys()],
}),
},
})
+13
View File
@@ -0,0 +1,13 @@
export const fixture = {
language: 'en',
theme: 'light' as 'light' | 'dark',
verge: { enable_tun_mode: false },
indicator: false,
available: true,
initialDns: 'dns:\n enable: true\n listen: ":5353"\n',
saved: [] as any[],
serialized: [] as any[],
tabRenders: [] as any[],
notices: [] as any[],
commands: [] as string[],
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": {
"@/*": ["../../src/*"],
"@root/*": ["../../*"],
"fixture-material": ["../../node_modules/@mui/material"],
"fixture-yaml": ["../../node_modules/js-yaml"]
}
},
"include": ["./*.ts", "./*.tsx", "../../src"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowJs": true,
"types": ["node"]
},
"include": ["./*.mjs", "./*.mts"]
}
+61
View File
@@ -0,0 +1,61 @@
import path from 'node:path'
import { normalizePath, type Plugin } from 'vite'
export function reactDiagnostics(mode: string | undefined): Plugin[] {
if (!mode) return []
return [
{
name: 'react-diagnostics',
enforce: 'pre',
apply: 'serve',
config() {
if (mode !== 'profiler' && mode !== 'wdyr')
throw new Error(`Unknown CVR_REACT_DIAGNOSTICS mode: ${mode}`)
return {
cacheDir: path.resolve('node_modules/.vite', mode),
...(mode === 'wdyr'
? {
oxc: {
jsx: {
importSource: '@welldone-software/why-did-you-render',
},
},
optimizeDeps: {
include: [
'@welldone-software/why-did-you-render',
'@welldone-software/why-did-you-render/jsx-dev-runtime',
],
},
}
: {}),
}
},
transformIndexHtml(html) {
if (mode !== 'wdyr') return html
const entry = 'src="./main.tsx"'
if (!html.includes(entry))
throw new Error('WDYR entry no longer matches index.html')
return html.replace(
entry,
`src="/@fs/${normalizePath(path.resolve('scripts/react/wdyr.ts'))}"`,
)
},
transform(code, id) {
if (mode !== 'profiler') return
if (id !== normalizePath(path.resolve('src/main.tsx'))) return
const open = '<React.StrictMode>'
const close = '</React.StrictMode>'
if (!code.includes(open) || !code.includes(close)) {
throw new Error('React profiling entry no longer matches main.tsx')
}
return {
code: `import { Profiler } from 'react'\nimport { recordRender } from '../scripts/react/profiler'\n${code}`
.replace(open, `<Profiler id="app" onRender={recordRender}>${open}`)
.replace(close, `${close}</Profiler>`),
map: null,
}
},
},
]
}
+10
View File
@@ -0,0 +1,10 @@
import whyDidYouRender from '@welldone-software/why-did-you-render'
import React from 'react'
whyDidYouRender(React, {
include: [/^(ProxyItem|ProxyItemMini|ProxyTunCard)$/],
trackAllPureComponents: false,
trackHooks: true,
})
await import('../../src/main')
+9
View File
@@ -0,0 +1,9 @@
import * as yaml from 'fixture-yaml'
export * from 'fixture-yaml'
import { fixture } from './state'
export function dump(value: any, options: any) {
fixture.serialized.push(structuredClone(value))
return yaml.dump(value, options)
}
+15 -15
View File
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.7.4", features = [] }
tauri-build = { version = "2.7.3", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
@@ -58,8 +58,8 @@ smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
open = "5.4.5"
rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] }
open = "5.4.4"
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
dirs = "7.0.0"
dunce = "1.0.5"
nanoid = "0.5"
@@ -72,14 +72,14 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
"guard",
] }
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.6.0"
tauri-plugin-shell = "2.5.0"
tauri-plugin-opener = "2.7.2"
tauri-plugin-dialog = "2.8.4"
tauri-plugin-dialog = "2.8.3"
tauri-plugin-fs = "2.7.1"
tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.3"
tauri-plugin-deep-link = "2.6.2"
tauri-plugin-window-state = "2.5.0"
zip = "9.2.2"
zip = "9.1.3"
flate2 = "1.1.10"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
@@ -89,27 +89,27 @@ getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.8.0"
tauri-plugin-notification = "2.7.0"
tokio-stream = "0.1.19"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.3", optional = true }
tauri-plugin-devtools = { version = "2.2.2", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
"client",
] }
arc-swap = "1.9.3"
arc-swap = "1.9.2"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.11"
webpki-roots = "1.0.9"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] }
specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.26", features = ["derive", "typescript"] }
specta-typescript = "=0.0.13"
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies]
libc = "0.2.191"
@@ -119,7 +119,7 @@ libc = "0.2.191"
clash-verge-signal = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.5.1", optional = true }
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
objc2 = "0.6.5"
objc2-foundation = { version = "0.3.2", features = [
"NSString",
-53
View File
@@ -614,53 +614,6 @@ async fn select_node_with_timeout(group_name: &String, node: &String) -> Result<
.with_context(|| format!("failed to select node [{node}] for group [{group_name}]"))
}
// mihomo lists these first in GLOBAL, so an unpicked GLOBAL starts on DIRECT.
const GLOBAL_BUILTINS: [&str; 2] = ["DIRECT", "REJECT"];
/// GLOBAL's first visible group, else its first node, while GLOBAL is still on a built-in.
fn global_exit_target(proxies: &Proxies) -> Option<&str> {
let global = proxies.proxies.get("GLOBAL")?;
if !global.now.as_deref().is_some_and(|now| GLOBAL_BUILTINS.contains(&now)) {
return None;
}
let members = global.all.as_deref()?;
let is_visible_group = |name: &str| {
proxies.proxies.get(name).is_some_and(|proxy| {
proxy.hidden != Some(true)
&& matches!(
proxy.proxy_type,
ProxyType::Selector | ProxyType::URLTest | ProxyType::Fallback | ProxyType::LoadBalance
)
})
};
members
.iter()
.find(|name| is_visible_group(name))
.or_else(|| members.iter().find(|name| !GLOBAL_BUILTINS.contains(&name.as_str())))
.map(|name| name.as_str())
}
/// Moves GLOBAL off a built-in unless the profile records any GLOBAL choice.
pub(crate) async fn leave_default_global_exit() -> Result<()> {
let profiles = Config::profiles().await.data_arc();
let has_record = profiles
.current
.as_ref()
.and_then(|current| profiles.get_item(current).ok())
.and_then(|item| item.selected.as_deref())
.is_some_and(|selected| selected.iter().any(|item| item.name.as_deref() == Some("GLOBAL")));
if has_record {
return Ok(());
}
let snapshot = fetch_proxies_with_timeout().await?;
let Some(target) = global_exit_target(&snapshot) else {
return Ok(());
};
select_node_with_timeout(&String::from("GLOBAL"), &String::from(target)).await?;
logging!(info, Type::Config, "moved GLOBAL off its built-in default to {target}");
Ok(())
}
fn remaining_activations(
activations: &[(String, String)],
completed: &HashMap<String, String>,
@@ -1066,12 +1019,6 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
.clone()
.unwrap_or_default();
if Config::clash().await.latest_arc().get_mode().as_deref() == Some("global")
&& let Err(err) = leave_default_global_exit().await
{
logging!(warn, Type::Config, "failed to move GLOBAL off its default: {err:#}");
}
if selected.is_empty() {
if is_activation_current(generation) {
announce(Refresh::Clash);
-6
View File
@@ -66,12 +66,6 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
return Err(err.to_string().into());
}
if mode.as_str() == "global"
&& let Err(err) = crate::config::profiles::leave_default_global_exit().await
{
logging!(warn, Type::Core, "failed to move GLOBAL off its default: {err:#}");
}
let clash = Config::clash().await;
let save_result = {
let _config_write = Config::lock_config_write().await;
@@ -464,6 +464,21 @@ export const ConnectionTable = (props: Props) => {
]
}, [t])
useEffect(() => {
setColumnOrder((prevValue) => {
const baseFields = baseColumns.map((col) => col.field)
const prev = Array.isArray(prevValue) ? prevValue : []
const reconciled = reconcileColumnOrder(prev, baseFields)
if (
reconciled.length === prev.length &&
reconciled.every((field, i) => field === prev[i])
) {
return prevValue
}
return reconciled
})
}, [baseColumns, setColumnOrder])
const orderedColumns = useMemo(() => {
const baseFields = baseColumns.map((column) => column.field)
const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields)
+73 -64
View File
@@ -1,3 +1,4 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import {
AccessTimeRounded,
ArrowDropDown,
@@ -30,15 +31,7 @@ import {
useTheme,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import {
useCallback,
useEffect,
useEffectEvent,
useId,
useMemo,
useRef,
useState,
} from 'react'
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
@@ -577,22 +570,20 @@ export const CurrentProxyCard = () => {
return profileValue
}
return profileKey !== baseKey ? localStorage.getItem(baseKey) : null
if (profileKey !== baseKey) {
const legacyValue = localStorage.getItem(baseKey)
if (legacyValue != null) {
localStorage.removeItem(baseKey)
localStorage.setItem(profileKey, legacyValue)
return legacyValue
}
}
return null
},
[getProfileStorageKey],
)
// Hand the pre-profile-scoping group key to the first profile that shows the card.
useEffect(() => {
const profileKey = getProfileStorageKey(STORAGE_KEY_GROUP)
if (profileKey === STORAGE_KEY_GROUP) return
if (localStorage.getItem(profileKey) != null) return
const legacyValue = localStorage.getItem(STORAGE_KEY_GROUP)
if (legacyValue == null) return
localStorage.removeItem(STORAGE_KEY_GROUP)
localStorage.setItem(profileKey, legacyValue)
}, [getProfileStorageKey])
const writeProfileScopedItem = useCallback(
(baseKey: string, value: string) => {
if (typeof window === 'undefined') return
@@ -624,17 +615,25 @@ export const CurrentProxyCard = () => {
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0
})
const [lastGroupName, setLastGroupName] = useState('')
const [selectedGroupName, setSelectedGroupName] = useState('')
const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
const [openSelectMode, setOpenSelectMode] = useState(mode)
if (openSelectMode !== mode) {
setOpenSelectMode(mode)
useEffect(() => {
setOpenSelect(null)
}
}, [mode])
const delayButtonRef = useRef<HTMLButtonElement>(null)
const delays = useGroupDelays(selectedGroupName || null)
const autoCheckInProgressRef = useRef(false)
const latestTimeoutRef = useRef<number>(
verge?.default_latency_timeout || 10000,
)
const latestProxyMemberRef = useRef<ResolvedProxyMember | null>(null)
useEffect(() => {
latestTimeoutRef.current = verge?.default_latency_timeout || 10000
}, [verge?.default_latency_timeout])
const selectableGroups = useMemo(() => {
if (!proxyView) return []
@@ -645,39 +644,6 @@ export const CurrentProxyCard = () => {
)
}, [proxyView])
const selectedGroupName = useMemo(() => {
if (isDirectMode) return 'DIRECT'
if (!proxyView) return lastGroupName
if (isGlobalMode) return proxyView.global?.name ?? 'GLOBAL'
const hasGroup = (name: string | null) =>
!!name && selectableGroups.some((group) => group.name === name)
if (hasGroup(lastGroupName)) return lastGroupName
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
if (hasGroup(savedGroup)) return savedGroup!
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
const primaryGroup =
selectableGroups.find((group) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) ?? selectableGroups[0]
return primaryGroup?.name ?? ''
}, [
isDirectMode,
isGlobalMode,
lastGroupName,
proxyView,
readProfileScopedItem,
selectableGroups,
])
// Keep the shown group, auto picks included, until it leaves the profile.
if (proxyView && lastGroupName !== selectedGroupName) {
setLastGroupName(selectedGroupName)
}
const delays = useGroupDelays(selectedGroupName || null)
const selectedGroup = useMemo<ProxyGroupView | null>(() => {
if (!proxyView || isDirectMode) return null
if (isGlobalMode) return proxyView.global
@@ -714,6 +680,46 @@ export const CurrentProxyCard = () => {
[optionsForGroup, selectedGroup],
)
useEffect(() => {
if (!proxyView) return
if (isDirectMode) {
setSelectedGroupName('DIRECT')
return
}
if (isGlobalMode) {
setSelectedGroupName(proxyView.global?.name ?? 'GLOBAL')
return
}
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
const primaryGroup =
selectableGroups.find((group) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) ?? selectableGroups[0]
const nextGroup = selectableGroups.some(
({ name }) => name === selectedGroupName,
)
? selectedGroupName
: selectableGroups.some(({ name }) => name === savedGroup)
? savedGroup!
: (primaryGroup?.name ?? '')
if (nextGroup !== selectedGroupName) {
setSelectedGroupName(nextGroup)
if (nextGroup) writeProfileScopedItem(STORAGE_KEY_GROUP, nextGroup)
}
}, [
isDirectMode,
isGlobalMode,
proxyView,
readProfileScopedItem,
selectableGroups,
selectedGroupName,
writeProfileScopedItem,
])
const currentOption = useMemo(() => {
if (!proxyView) return undefined
if (isDirectMode) {
@@ -737,10 +743,12 @@ export const CurrentProxyCard = () => {
: undefined
}, [isDirectMode, proxyView, selectedGroup])
latestProxyMemberRef.current = currentOption?.member ?? null
const handleGroupChange = useCallback(
(newGroupName: string) => {
if (isGlobalMode || isDirectMode) return
setLastGroupName(newGroupName)
setSelectedGroupName(newGroupName)
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
},
[isDirectMode, isGlobalMode, writeProfileScopedItem],
@@ -785,7 +793,7 @@ export const CurrentProxyCard = () => {
color: 'text.secondary',
}
const checkCurrentProxyDelay = useEffectEvent(async () => {
const checkCurrentProxyDelay = useCallback(async () => {
if (autoCheckInProgressRef.current) return
if (isDirectMode) return
@@ -794,7 +802,7 @@ export const CurrentProxyCard = () => {
if (!groupName || !proxyName) return
const proxyMember = currentOption?.member
const proxyMember = latestProxyMemberRef.current
if (!proxyMember || !isInteractableMember(proxyMember)) {
debugLog(
`[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`,
@@ -804,7 +812,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = true
const timeout = verge?.default_latency_timeout || 10000
const timeout = latestTimeoutRef.current || 10000
try {
debugLog(
@@ -820,7 +828,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = false
refreshProxy()
}
})
}, [isDirectMode, refreshProxy, selectedGroupName, selectedProxyName])
useEffect(() => {
if (isDirectMode) return
@@ -850,6 +858,7 @@ export const CurrentProxyCard = () => {
if (intervalTimer) clearTimeout(intervalTimer)
}
}, [
checkCurrentProxyDelay,
autoDelayIntervalMs,
isDirectMode,
selectedGroupName,
@@ -4,7 +4,6 @@ import {
memo,
useCallback,
useEffect,
useEffectEvent,
useImperativeHandle,
useMemo,
useReducer,
@@ -121,6 +120,7 @@ export const EnhancedCanvasTrafficGraph = memo(
const drawFrameRef = useRef<number | undefined>(undefined)
const hoverFrameRef = useRef<number | undefined>(undefined)
const mouseMoveFrameRef = useRef<number | undefined>(undefined)
const scheduleDrawGraphRef = useRef<() => void>(() => {})
const pendingMousePositionRef = useRef<{
clientX: number
clientY: number
@@ -189,11 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
}
}, [displayData])
const handleFocusStateChange = useEffectEvent((focused: boolean) => {
const handleFocusStateChange = useCallback((focused: boolean) => {
isWindowFocusedRef.current = focused
scheduleDrawGraph()
})
scheduleDrawGraphRef.current()
}, [])
useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
@@ -217,7 +217,7 @@ export const EnhancedCanvasTrafficGraph = memo(
window.removeEventListener('blur', handleBlur)
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [])
}, [handleFocusStateChange])
const calculateY = useCallback(
(
@@ -906,21 +906,25 @@ export const EnhancedCanvasTrafficGraph = memo(
scheduleDrawGraph()
}, [scheduleDrawGraph])
const redrawGraph = useEffectEvent(() => scheduleDrawGraph())
useEffect(() => {
scheduleDrawGraphRef.current = scheduleDrawGraph
}, [scheduleDrawGraph])
useEffect(() => {
const canvas = canvasRef.current
if (!canvas || typeof window === 'undefined') return
if (typeof ResizeObserver === 'undefined') {
const handleResize = () => redrawGraph()
const handleResize = () => scheduleDrawGraphRef.current()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}
const resizeObserver = new ResizeObserver(() => redrawGraph())
const resizeObserver = new ResizeObserver(() =>
scheduleDrawGraphRef.current(),
)
resizeObserver.observe(canvas)
return () => {
+7 -12
View File
@@ -14,7 +14,7 @@ import {
useTheme,
Fade,
} from '@mui/material'
import { useState, useMemo, useCallback, memo, FC } from 'react'
import { useState, useMemo, memo, FC } from 'react'
import { useTranslation } from 'react-i18next'
import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
@@ -152,15 +152,10 @@ export const ProxyTunCard: FC = () => {
showNotice.error(err)
}
const handleSystemTabClick = useCallback(() => {
setActiveTab('system')
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'system')
}, [])
const handleTunTabClick = useCallback(() => {
setActiveTab('tun')
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'tun')
}, [])
const handleTabChange = (tab: string) => {
setActiveTab(tab)
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab)
}
const tabDescription = useMemo(() => {
if (activeTab === 'system') {
@@ -196,14 +191,14 @@ export const ProxyTunCard: FC = () => {
>
<TabButton
isActive={activeTab === 'system'}
onClick={handleSystemTabClick}
onClick={() => handleTabChange('system')}
icon={ComputerRounded}
label={t('settings.sections.system.toggles.systemProxy')}
hasIndicator={systemProxyIndicator}
/>
<TabButton
isActive={activeTab === 'tun'}
onClick={handleTunTabClick}
onClick={() => handleTabChange('tun')}
icon={TroubleshootRounded}
label={t('settings.sections.system.toggles.tunMode')}
hasIndicator={enable_tun_mode! && isTunModeAvailable}
+20 -18
View File
@@ -8,7 +8,7 @@ import {
} from '@mui/icons-material'
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
@@ -22,23 +22,10 @@ import {
import { useVerge } from '@/hooks/use-verge'
import { getSystemInfo } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { version as appVersion } from '@root/package.json'
import { EnhancedCard } from './enhanced-card'
const getOsInfo = async () => {
const info = await getSystemInfo()
const sysName = info.system_name
let sysVersion = info.system_version
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
sysVersion = sysVersion.substring(sysName.length).trim()
}
return `${sysName} ${sysVersion}`
}
export const SystemInfoCard = () => {
const { t } = useTranslation()
const { verge, patchVerge } = useVerge()
@@ -49,16 +36,31 @@ export const SystemInfoCard = () => {
const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true)
const { data: osInfo = '' } = useQuery({
queryKey: ['getSystemInfo'],
queryFn: getOsInfo,
})
const [osInfo, setOsInfo] = useState('')
const lastCheckUpdateText = useMemo(
() => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'),
[lastCheckUpdate],
)
useEffect(() => {
getSystemInfo()
.then((info) => {
const sysName = info.system_name
let sysVersion = info.system_version
if (
sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim()
}
setOsInfo(`${sysName} ${sysVersion}`)
})
.catch(console.error)
}, [])
useEffect(() => {
if (!verge?.auto_check_update) return
if (readLastCheckTime() !== null) return
+7 -5
View File
@@ -21,7 +21,6 @@ import {
memo,
useCallback,
useEffect,
useEffectEvent,
useReducer,
useRef,
useState,
@@ -114,6 +113,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const setProfileLoading = useSetProfileLoading()
const [showNextUpdate, setShowNextUpdate] = useState(false)
const showNextUpdateRef = useRef(false)
const [nextUpdateTime, setNextUpdateTime] = useState('')
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
@@ -203,6 +203,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
setShowNextUpdate(!showNextUpdate)
}
useEffect(() => {
showNextUpdateRef.current = showNextUpdate
}, [showNextUpdate])
useEffect(() => {
if (showNextUpdate) {
fetchNextUpdateTime()
@@ -214,10 +218,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
updated,
])
const isNextUpdateShown = useEffectEvent(() => showNextUpdate)
useEffect(() => {
if (timerUpdateRevision === 0 || !isNextUpdateShown()) return
if (timerUpdateRevision === 0 || !showNextUpdateRef.current) return
if (refreshTimeoutRef.current !== undefined) {
clearTimeout(refreshTimeoutRef.current)
@@ -234,7 +236,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
}, [fetchNextUpdateTime, timerUpdateRevision])
useEffect(() => {
if (completedUpdateRevision === 0 || !isNextUpdateShown()) return
if (completedUpdateRevision === 0 || !showNextUpdateRef.current) return
fetchNextUpdateTime()
}, [completedUpdateRevision, fetchNextUpdateTime])
+14 -19
View File
@@ -10,7 +10,7 @@ import {
} from '@mui/material'
import { useLockFn } from 'ahooks'
import type { Ref } from 'react'
import { useImperativeHandle, useRef, useState } from 'react'
import { useEffect, useImperativeHandle, useRef, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
@@ -77,6 +77,17 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
},
}))
const selfProxy = watch('option.self_proxy')
const withProxy = watch('option.with_proxy')
useEffect(() => {
if (selfProxy) setValue('option.with_proxy', false)
}, [selfProxy, setValue])
useEffect(() => {
if (withProxy) setValue('option.self_proxy', false)
}, [setValue, withProxy])
const handleOk = useLockFn(
handleSubmit(async (form) => {
setLoading(true)
@@ -408,15 +419,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel>
{t('profiles.modals.profileForm.fields.useSystemProxy')}
</InputLabel>
<Switch
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.self_proxy', false)
}}
color="primary"
/>
<Switch checked={field.value!} {...field} color="primary" />
</StyledBox>
)}
/>
@@ -429,15 +432,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel>
{t('profiles.modals.profileForm.fields.useClashProxy')}
</InputLabel>
<Switch
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.with_proxy', false)
}}
color="primary"
/>
<Switch checked={field.value!} {...field} color="primary" />
</StyledBox>
)}
/>
-30
View File
@@ -1,5 +1,3 @@
import * as yaml from 'js-yaml'
import {
getRecord,
rebindNode,
@@ -36,31 +34,3 @@ export const rebindProxyChainItems = (
delay: record?.history.at(-1)?.delay,
}
})
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
export const parseChainConfigItems = (configData: string): ProxyChainItem[] => {
try {
// JSON is valid YAML, so one parser covers both persisted formats.
const parsedConfig = yaml.load(configData) as ParsedChainConfig | null
const timestamp = Date.now()
return (
parsedConfig?.proxies?.map((proxy, index) => ({
id: `${proxy.name}_${timestamp}_${index}`,
name: proxy.name,
type: proxy.type,
delay: undefined,
})) || []
)
} catch (error) {
console.error('Failed to process chain config data:', error)
return []
}
}
+43
View File
@@ -25,6 +25,7 @@ import {
keyframes,
useTheme,
} from '@mui/material'
import * as yaml from 'js-yaml'
import {
type Ref,
useCallback,
@@ -59,9 +60,18 @@ const chainPointerSensor = PointerSensor.configure({
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
interface ProxyChainProps {
proxyChain: ProxyChainItem[]
onUpdateChain: (chain: ProxyChainItem[]) => void
chainConfigData?: string | null
onMarkUnsavedChanges?: () => void
mode?: string
selectedGroup?: string | null
@@ -88,6 +98,21 @@ const arrowFadeIn = keyframes`
}
`
const toChainItems = (
parsedConfig: ParsedChainConfig | null | undefined,
): ProxyChainItem[] => {
const timestamp = Date.now()
return (
parsedConfig?.proxies?.map((proxy, index) => ({
id: `${proxy.name}_${timestamp}_${index}`,
name: proxy.name,
type: proxy.type,
delay: undefined,
})) || []
)
}
interface ChainCardProps {
proxy: ProxyChainItem
index: number
@@ -303,6 +328,7 @@ const SortableProxyChainItem = ({
export const ProxyChain = ({
proxyChain,
onUpdateChain,
chainConfigData,
onMarkUnsavedChanges,
mode,
selectedGroup,
@@ -554,6 +580,23 @@ export const ProxyChain = ({
recordSelection,
])
// 处理链式代理配置数据
useEffect(() => {
if (chainConfigData) {
try {
// JSON is valid YAML, so one parser covers both persisted formats.
const parsedConfig = yaml.load(chainConfigData) as ParsedChainConfig
const chainItems = toChainItems(parsedConfig)
if (chainItems.length > 0) {
onUpdateChain(chainItems)
}
} catch (error) {
console.error('Failed to process chain config data:', error)
}
}
}, [chainConfigData, onUpdateChain])
return (
<Paper
elevation={1}
+3 -12
View File
@@ -76,7 +76,7 @@ interface GroupSelectMenuProps {
interface ProxyGroupsChainProps {
mode: string
chainConfigItems?: ProxyChainItem[] | null
chainConfigData?: string | null
onExitChainMode: () => void
availableGroups: any[]
activeSelectedGroup: string | null
@@ -305,7 +305,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
const { t } = useTranslation()
const {
mode,
chainConfigItems,
chainConfigData,
onExitChainMode,
availableGroups,
activeSelectedGroup,
@@ -338,16 +338,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
return []
})
// Each fetched runtime chain replaces the stored one once.
const configItems = chainConfigItems ?? null
const [appliedConfigItems, setAppliedConfigItems] = useState<
ProxyChainItem[] | null
>(null)
if (configItems !== appliedConfigItems) {
setAppliedConfigItems(configItems)
if (configItems?.length) setProxyChain(configItems)
}
const candidateNodes = useMemo(
() =>
renderList.flatMap((item) => {
@@ -530,6 +520,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
<ProxyChain
proxyChain={currentProxyChain}
onUpdateChain={setProxyChain}
chainConfigData={chainConfigData}
mode={mode}
selectedGroup={activeSelectedGroup}
onExitChainMode={onExitChainMode}
+8 -34
View File
@@ -32,7 +32,6 @@ import {
} from '@/types/proxy-view'
import { debugLog } from '@/utils/debug'
import type { ProxyChainItem } from './proxy-chain-model'
import { ProxyEmptyState } from './proxy-empty-state'
import {
resolveEmptyListReason,
@@ -69,7 +68,7 @@ function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
interface Props {
mode: string
isChainMode?: boolean
chainConfigItems?: ProxyChainItem[] | null
chainConfigData?: string | null
onExitChainMode: () => void
}
@@ -184,7 +183,6 @@ function useProxyRenderState(
renderList,
onProxies,
onHeadState,
setHeadState: setListHeadState,
handleCheckAll,
saveScrollPosition,
getScrollPosition,
@@ -193,10 +191,10 @@ function useProxyRenderState(
function ChainProxyGroups(props: {
mode: string
chainConfigItems?: ProxyChainItem[] | null
chainConfigData?: string | null
onExitChainMode: () => void
}) {
const { mode, chainConfigItems, onExitChainMode } = props
const { mode, chainConfigData, onExitChainMode } = props
const { proxyView } = useProxiesData()
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
@@ -350,7 +348,7 @@ function ChainProxyGroups(props: {
<Suspense fallback={<BaseLoading />}>
<ProxyGroupsChain
mode={mode}
chainConfigItems={chainConfigItems}
chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode}
availableGroups={availableGroups}
activeSelectedGroup={activeSelectedGroup}
@@ -379,17 +377,12 @@ function NormalProxyGroups(props: { mode: string }) {
renderList,
onProxies,
onHeadState,
setHeadState,
handleCheckAll,
getScrollPosition,
saveScrollPosition,
} = useProxyRenderState(mode, false, null)
const emptyList = useEmptyRenderList()
const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout()
const { proxyView } = useProxiesData()
const globalGroupName = proxyView?.global?.name
// The revealed group sits below GLOBAL's rows, so a click must bring it into view.
const pendingRevealRef = useRef<string | null>(null)
const renderFirstRef = useRef(true)
// Do not persist intermediate positions produced while restoring virtual scroll.
const isRestoringRef = useRef(false)
@@ -484,17 +477,9 @@ function NormalProxyGroups(props: { mode: string }) {
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
if (!isInteractableMember(member)) return
if (group.name === globalGroupName) {
pendingRevealRef.current =
member.kind === 'group' ? member.ref.name : null
// The selection refetches the view, so skip onHeadState's refetch.
if (member.kind === 'group') {
setHeadState(member.ref.name, { open: true })
}
}
select(group.name, member.ref.name, group.now!, group.fixed!)
},
[globalGroupName, select, setHeadState],
[select],
)
const handleLocation = useStableCallback((group: ProxyGroupView) => {
@@ -540,17 +525,6 @@ function NormalProxyGroups(props: { mode: string }) {
return Array.from(new Set(names))
}, [renderList])
useEffect(() => {
if (mode !== 'global') {
pendingRevealRef.current = null
return
}
const groupName = pendingRevealRef.current
if (!groupName || !proxyGroupNames.includes(groupName)) return
pendingRevealRef.current = null
handleGroupLocationByName(groupName)
}, [handleGroupLocationByName, mode, proxyGroupNames])
const handleGroupToggle = useCallback(
async (group: ProxyGroupView) => {
const index = renderList.findIndex(
@@ -634,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
/>
</DragDropProvider>
{mode === 'rule' && (
{(mode === 'rule' || mode === 'global') && (
<ProxyGroupNavigator
proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName}
@@ -647,7 +621,7 @@ function NormalProxyGroups(props: { mode: string }) {
}
export const ProxyGroups = (props: Props) => {
const { mode, isChainMode = false, chainConfigItems, onExitChainMode } = props
const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props
const { profiles, isLoading: isProfilesLoading } = useProfiles()
const { isProxyViewPending } = useProxiesData()
const { isRunningModePending } = useSystemData()
@@ -671,7 +645,7 @@ export const ProxyGroups = (props: Props) => {
return isChainMode ? (
<ChainProxyGroups
mode={mode}
chainConfigItems={chainConfigItems}
chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode}
/>
) : (
+11 -12
View File
@@ -6,7 +6,6 @@ import { useVerge } from '@/hooks/use-verge'
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
import delayManager, { type DelaySnapshot } from '@/services/delay'
import {
findCurrentGroupMember,
isInteractableMember,
resolveMember,
selectGlobalChainNodes,
@@ -210,10 +209,8 @@ export const useRenderList = (
const renderGroups = useMemo(() => {
if (!proxyView) return []
if (mode === 'rule') return proxyView.groups
const { global } = proxyView
if (!global) return []
const current = findCurrentGroupMember(proxyView, global)?.member
return current?.kind === 'group' ? [global, current.group] : [global]
const global = proxyView.global ? [proxyView.global] : []
return mode === 'global' ? [...global, ...proxyView.groups] : global
}, [mode, proxyView])
// Every group this list draws, so a test settling in any of them re-sorts that group.
@@ -263,10 +260,10 @@ export const useRenderList = (
}))
}
const useRule = mode === 'rule' || mode === 'global'
const cache = groupCacheRef.current
const retList = renderGroups.flatMap((group) => {
const isFlat = group === proxyView.global
const headState = headStates[group.name] || DEFAULT_STATE
const cached = cache.get(group.name)
if (
@@ -282,9 +279,7 @@ export const useRenderList = (
}
const ret: IRenderItem[] = [
isFlat
? { type: 1, key: `head-${group.name}`, group, headState }
: {
{
type: 0,
key: group.name,
group,
@@ -294,7 +289,7 @@ export const useRenderList = (
},
]
if (headState.open || isFlat) {
if (headState.open || !useRule) {
const occurrences = filterSort(
resolveOccurrences(proxyView, group),
group.name,
@@ -307,6 +302,9 @@ export const useRenderList = (
useRegularExpression: headState.filterUseRegularExpression,
},
)
if (!useRule) {
ret.push({ type: 1, key: `head-${group.name}`, group, headState })
}
if (occurrences.length === 0) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState })
} else if (col > 1) {
@@ -345,8 +343,9 @@ export const useRenderList = (
return ret
})
const filtered =
mode === 'rule' ? retList.filter((item) => !item.group.hidden) : retList
const filtered = !useRule
? retList.slice(1)
: retList.filter((item) => !item.group.hidden)
if (
prevListRef.current.length === filtered.length &&
filtered.every((item, index) => item === prevListRef.current[index])
@@ -146,12 +146,7 @@ export const BackupHistoryViewer = ({
setRows([])
return
}
// Read the status here: as a dependency, the write below re-runs the fetch.
if (
!isLocal &&
getWebdavStatus(webdavSignature) === 'failed' &&
!options?.force
) {
if (!isLocal && webdavStatus === 'failed' && !options?.force) {
setRows([])
return
}
@@ -185,7 +180,7 @@ export const BackupHistoryViewer = ({
setLoading(false)
}
},
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature],
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus],
)
useEffect(() => {
+143 -87
View File
@@ -190,7 +190,23 @@ const DEFAULT_DNS_CONFIG = {
},
}
type DnsFormValues = {
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const { mutateClash } = useClash()
const { verge } = useVerge()
const { current: currentProfile } = useProfiles()
const dnsEnabled = currentProfile
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
verge?.enable_dns_settings ??
false)
: false
const themeMode = useThemeMode()
const [open, setOpen] = useState(false)
const [visualization, setVisualization] = useState(true)
const skipYamlSyncRef = useRef(false)
const editorRef = useRef<MonacoEditorInstance | null>(null)
const [values, setValues] = useState<{
enable: boolean
listen: string
enhancedMode: 'fake-ip' | 'redir-host'
@@ -215,9 +231,7 @@ type DnsFormValues = {
fallbackDomain: string
nameserverPolicy: string
hosts: string // hosts设置,独立于dns
}
const DEFAULT_FORM_VALUES: DnsFormValues = {
}>({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
@@ -246,70 +260,7 @@ const DEFAULT_FORM_VALUES: DnsFormValues = {
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
}
function buildDnsConfig(values: DnsFormValues) {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
return dnsConfig
}
function buildDnsYaml(values: DnsFormValues) {
const config: Record<string, any> = {}
const dnsConfig = buildDnsConfig(values)
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
return yaml.dump(config, { forceQuotes: true })
}
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation()
const { mutateClash } = useClash()
const { verge } = useVerge()
const { current: currentProfile } = useProfiles()
const dnsEnabled = currentProfile
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
verge?.enable_dns_settings ??
false)
: false
const themeMode = useThemeMode()
const [open, setOpen] = useState(false)
const [visualization, setVisualization] = useState(true)
const editorRef = useRef<MonacoEditorInstance | null>(null)
const [values, setValues] = useState<DnsFormValues>(DEFAULT_FORM_VALUES)
})
// Prevent YAML-to-form sync before configuration loads.
const [yamlContent, setYamlContent] = useReducer(
@@ -318,7 +269,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
)
const updateValuesFromConfig = useCallback((config: any) => {
if (!config) return undefined
if (!config) return
const dnsConfig = config.dns || {}
const hostsConfig = config.hosts || {}
@@ -338,7 +289,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? fakeIpFilterMode
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
const next: DnsFormValues = {
setValues({
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode,
@@ -390,27 +341,127 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
nameserverPolicy:
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
hosts: formatHosts(hostsConfig) || '',
}
setValues(next)
return next
})
}, [])
const generateDnsConfig = useCallback(() => {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
return dnsConfig
}, [values])
const updateYamlFromValues = useCallback(() => {
const config: Record<string, any> = {}
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
setYamlContent(yaml.dump(config, { forceQuotes: true }))
}, [generateDnsConfig, values.hosts])
const resetToDefaults = useCallback(() => {
setValues(DEFAULT_FORM_VALUES)
setYamlContent(buildDnsYaml(DEFAULT_FORM_VALUES))
}, [])
setValues({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver:
DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
updateYamlFromValues()
}, [updateYamlFromValues])
const updateValuesFromYaml = useCallback(() => {
try {
const parsedYaml = yaml.load(yamlContent) as any
if (!parsedYaml || parsedYaml.isInitValue) return
skipYamlSyncRef.current = true
updateValuesFromConfig(parsedYaml)
} catch {
showNotice.error('settings.modals.dns.errors.invalidYaml')
}
}, [yamlContent, updateValuesFromConfig])
useEffect(() => {
if (skipYamlSyncRef.current) {
skipYamlSyncRef.current = false
return
}
updateYamlFromValues()
}, [updateYamlFromValues])
const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml)
const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues)
useEffect(() => {
latestUpdateValuesFromYamlRef.current = updateValuesFromYaml
latestUpdateYamlFromValuesRef.current = updateYamlFromValues
}, [updateValuesFromYaml, updateYamlFromValues])
useEffect(() => {
if (visualization) {
latestUpdateValuesFromYamlRef.current()
} else {
latestUpdateYamlFromValuesRef.current()
}
}, [visualization])
useEffect(() => {
return () => {
editorRef.current?.dispose()
@@ -425,8 +476,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (dnsConfig !== null) {
const config = yaml.load(dnsConfig) as any
const loaded = updateValuesFromConfig(config)
setYamlContent(loaded ? buildDnsYaml(loaded) : dnsConfig)
updateValuesFromConfig(config)
setYamlContent(dnsConfig)
} else {
resetToDefaults()
}
@@ -455,7 +506,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (visualization) {
config = {}
const dnsConfig = buildDnsConfig(values)
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
@@ -535,10 +586,20 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? event.target.checked
: event.target.value
setValues((prev) => ({
setValues((prev) => {
const newValues = {
...prev,
[field]: value,
}))
}
if (visualization) {
setTimeout(() => {
updateYamlFromValues()
}, 0)
}
return newValues
})
}
return (
@@ -568,12 +629,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="contained"
size="small"
onClick={() => {
if (visualization) {
setYamlContent(buildDnsYaml(values))
} else {
updateValuesFromYaml()
}
setVisualization(!visualization)
setVisualization((prev) => !prev)
}}
>
{visualization
@@ -13,7 +13,13 @@ import {
Typography,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
@@ -35,7 +41,6 @@ import {
getSystemHostname,
} from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { debugLog } from '@/utils/debug'
import getSystem from '@/utils/get-system'
@@ -123,10 +128,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
} = verge ?? {}
const [value, setValue] = useState(() => buildInitialValues(null))
const { data: embeddedServerPort } = useQuery({
queryKey: ['getEmbeddedServerPort'],
queryFn: getEmbeddedServerPort,
})
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
null,
)
useEffect(() => {
getEmbeddedServerPort().then(setEmbeddedServerPort).catch(console.error)
}, [])
const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows])
+20 -9
View File
@@ -1,3 +1,4 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import { Delete, ExpandLess, ExpandMore } from '@mui/icons-material'
import {
Button,
@@ -13,6 +14,7 @@ import {
} from '@mui/material'
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef,
@@ -155,20 +157,29 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const selectedProxyToken = selectedProxyOption
? proxyOptionToken(selectedProxyOption)
: ''
// A group or member missing from the proxy view reads as the default.
const effectiveGroup = proxyGroups.some(({ name }) => name === values.group)
? values.group
: ''
const effectiveProxy = selectedProxyOption ? values.proxy : null
const selectedGroupExists =
!values.group || proxyGroups.some(({ name }) => name === values.group)
const latestProxyStateRef = useRef({
binding: effectiveProxy,
binding: values.proxy,
options: proxyOptions,
})
latestProxyStateRef.current = {
binding: effectiveProxy,
binding: values.proxy,
options: proxyOptions,
}
useEffect(() => {
if (selectedGroupExists && (!values.proxy || selectedProxyOption)) return
setValues((current) => {
if (current.group !== values.group || current.proxy !== values.proxy) {
return current
}
return selectedGroupExists
? { ...current, proxy: null }
: { ...current, group: '', proxy: null }
})
}, [selectedGroupExists, selectedProxyOption, values.group, values.proxy])
const handleSave = async () => {
try {
await patchClash({ tunnels: draftTunnels })
@@ -479,7 +490,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
<Select
size="small"
sx={{ width: 200, '> div': { py: '7.5px' } }}
value={effectiveGroup}
value={values.group}
displayEmpty
onChange={(e) => {
const nextGroup = e.target.value as string
@@ -558,7 +569,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
: null,
}))
}}
disabled={!effectiveGroup} // 没选组就禁用
disabled={!values.group} // 没选组就禁用
>
<MenuItem value="">
{t('settings.sections.clash.form.fields.tunnels.default')}
+4 -18
View File
@@ -7,10 +7,6 @@ import { useTranslation } from 'react-i18next'
import { BaseDialog } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import type {
IVergeTestItem_Deserialize,
IVergeTestItem_Serialize,
} from '@/services/bindings'
import { showNotice } from '@/services/notice-service'
interface Props {
@@ -31,7 +27,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const [loading, setLoading] = useState(false)
const { verge, patchVerge } = useVerge()
const testList = verge?.test_list ?? []
const { control, ...formIns } = useForm<IVergeTestItem_Deserialize>({
const { control, ...formIns } = useForm<IVergeTestItem>({
defaultValues: {
name: '',
icon: '',
@@ -41,16 +37,11 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const patchTestList = async (
uid: string,
patch: Partial<IVergeTestItem_Deserialize>,
patch: Partial<IVergeTestItem>,
) => {
const newList = testList.map((x) => {
if (x.uid === uid) {
return {
uid: x.uid,
name: patch.name === undefined ? x.name : patch.name,
icon: patch.icon === undefined ? x.icon : patch.icon,
url: patch.url === undefined ? x.url : patch.url,
}
return { ...x, ...patch }
}
return x
})
@@ -86,12 +77,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
if (openType === 'new') {
const uid = nanoid()
const item: IVergeTestItem_Serialize = {
uid,
name: form.name ?? null,
icon: form.icon ?? null,
url: form.url ?? null,
}
const item = { ...form, uid }
const newList = [...testList, item]
await patchVerge({ test_list: newList })
onChange(uid)
+1 -6
View File
@@ -17,12 +17,7 @@ const PRESET_PROXY_NAMES = [
'COMPATIBLE',
]
const identity = (previous: DelayUpdate, next: DelayUpdate): DelayUpdate =>
previous.delay === next.delay &&
previous.elapsed === next.elapsed &&
previous.updatedAt === next.updatedAt
? previous
: next
const identity = (_: DelayUpdate, next: DelayUpdate): DelayUpdate => next
const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 }
@@ -1,7 +1,7 @@
import { arrayMove } from '@dnd-kit/helpers'
import type { DragEndEvent } from '@dnd-kit/react'
import { isSortable } from '@dnd-kit/react/sortable'
import { useCallback, useMemo, useReducer, useState } from 'react'
import { useCallback, useEffect, useMemo, useReducer } from 'react'
type MenuOrderAction = { type: 'sync'; payload: string[] }
@@ -79,11 +79,9 @@ export const useNavMenuOrder = <T extends { path: string }>({
configMenuOrder,
)
const [syncedConfigOrder, setSyncedConfigOrder] = useState(configMenuOrder)
if (!areOrdersEqual(syncedConfigOrder, configMenuOrder)) {
setSyncedConfigOrder(configMenuOrder)
useEffect(() => {
dispatchMenuOrder({ type: 'sync', payload: configMenuOrder })
}
}, [configMenuOrder])
const isDefaultOrder = useMemo(
() => areOrdersEqual(menuOrder, defaultOrder),
+18 -17
View File
@@ -1,15 +1,11 @@
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
import { Box, Button, ButtonGroup, Typography } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, BasePage, TooltipIcon } from '@/components/base'
import { ProviderButton } from '@/components/proxy/provider-button'
import {
parseChainConfigItems,
type ProxyChainItem,
} from '@/components/proxy/proxy-chain-model'
import { ProxyGroups } from '@/components/proxy/proxy-groups'
import {
useAppRefreshers,
@@ -44,13 +40,18 @@ const ProxyPage = () => {
})
const [chainConfirmOpen, setChainConfirmOpen] = useState(false)
const [chainConfigItems, setChainConfigItems] = useState<
ProxyChainItem[] | null
>(null)
const [chainConfigData, dispatchChainConfigData] = useReducer(
(_: string | null, action: string | null) => action,
null as string | null,
)
const { clashConfig } = useClashConfigData()
const { refreshClashConfig } = useAppRefreshers()
const updateChainConfigData = useCallback((value: string | null) => {
dispatchChainConfigData(value)
}, [])
const normalizedMode = clashConfig?.mode?.toLowerCase()
const curMode = isMode(normalizedMode) ? normalizedMode : undefined
const chainWarning = t('proxies.page.chain.warning')
@@ -70,7 +71,6 @@ const ProxyPage = () => {
const applyChainMode = (enabled: boolean) => {
setIsChainMode(enabled)
if (!enabled) setChainConfigItems(null)
localStorage.setItem('proxy-chain-mode-enabled', enabled.toString())
}
@@ -99,7 +99,10 @@ const ProxyPage = () => {
// 当开启链式代理模式时,获取配置数据
useEffect(() => {
if (!isChainMode) return
if (!isChainMode) {
updateChainConfigData(null)
return
}
let cancelled = false
@@ -110,21 +113,19 @@ const ProxyPage = () => {
if (!exitNode) {
console.error('No proxy chain exit node found in localStorage')
if (!cancelled) {
setChainConfigItems([])
updateChainConfigData('')
}
return
}
const configData = await getRuntimeProxyChainConfig(exitNode)
if (!cancelled) {
setChainConfigItems(
configData ? parseChainConfigItems(configData) : [],
)
updateChainConfigData(configData || '')
}
} catch (error) {
console.error('Failed to get runtime proxy chain config:', error)
if (!cancelled) {
setChainConfigItems([])
updateChainConfigData('')
}
}
}
@@ -134,7 +135,7 @@ const ProxyPage = () => {
return () => {
cancelled = true
}
}, [isChainMode])
}, [isChainMode, updateChainConfigData])
useEffect(() => {
if (normalizedMode && !isMode(normalizedMode)) {
@@ -203,7 +204,7 @@ const ProxyPage = () => {
<ProxyGroups
mode={curMode ?? 'rule'}
isChainMode={isChainMode}
chainConfigItems={chainConfigItems}
chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode}
/>
<BaseDialog
+149 -255
View File
@@ -23,10 +23,7 @@ export const commands = {
__TAURI_INVOKE<string[]>('get_network_interfaces'),
getSystemHostname: () => __TAURI_INVOKE<string>('get_system_hostname'),
restartApp: () => __TAURI_INVOKE<null>('restart_app'),
installUpdate: (
version: string,
onEvent: Channel<DownloadEvent_Deserialize>,
) =>
installUpdate: (version: string, onEvent: Channel<DownloadEvent>) =>
__TAURI_INVOKE<InstallUpdateOutcome>('install_update', {
version,
onEvent,
@@ -37,12 +34,11 @@ export const commands = {
upgradeClashCore: (force: boolean) =>
__TAURI_INVOKE<CoreUpgradeReport>('upgrade_clash_core', { force }),
/** Returns one coherent core/service snapshot instead of independently refreshed state. */
getRuntimeState: () =>
__TAURI_INVOKE<RunStateView_Serialize>('get_runtime_state'),
getRuntimeState: () => __TAURI_INVOKE<RunStateView>('get_runtime_state'),
getPendingFailures: () =>
__TAURI_INVOKE<PendingFailure[]>('get_pending_failures'),
getSidecarFailure: () =>
__TAURI_INVOKE<SidecarFailureSnapshot_Serialize>('get_sidecar_failure'),
__TAURI_INVOKE<SidecarFailureSnapshot>('get_sidecar_failure'),
entryLightweightMode: () => __TAURI_INVOKE<null>('entry_lightweight_mode'),
installService: () =>
__TAURI_INVOKE<ServiceInstallOutcome>('install_service'),
@@ -52,7 +48,7 @@ export const commands = {
repairService: () => __TAURI_INVOKE<ServiceInstallOutcome>('repair_service'),
continueWithSidecar: () => __TAURI_INVOKE<null>('continue_with_sidecar'),
syncRuntimeProviders: () => __TAURI_INVOKE<void>('sync_runtime_providers'),
getClashInfo: () => __TAURI_INVOKE<ClashInfo_Serialize>('get_clash_info'),
getClashInfo: () => __TAURI_INVOKE<ClashInfo>('get_clash_info'),
patchClashConfig: (payload: YamlMapping) =>
__TAURI_INVOKE<null>('patch_clash_config', { payload }),
patchClashMode: (payload: string) =>
@@ -60,11 +56,16 @@ export const commands = {
/** Reads the saved mode without depending on strict mihomo `/configs` deserialization. */
getClashMode: () => __TAURI_INVOKE<string | null>('get_clash_mode'),
changeClashCore: (clashCore: string) =>
__TAURI_INVOKE<CommandFailure_Serialize | null>('change_clash_core', {
clashCore,
}),
__TAURI_INVOKE<{
/** Stable frontend identifier, when classified. */
code?: string | null
/** Human-readable diagnostic detail. */
detail: string
/** Requested operation when known by the command. */
operation?: FailedOperation | null
} | null>('change_clash_core', { clashCore }),
getRuntimeConfig: () =>
__TAURI_INVOKE<YamlMapping | null>('get_runtime_config'),
__TAURI_INVOKE<{ [key in string]: JsonValue } | null>('get_runtime_config'),
getProxyView: () => __TAURI_INVOKE<ProxyViewV1_Serialize>('get_proxy_view'),
getRuntimeYaml: () => __TAURI_INVOKE<string>('get_runtime_yaml'),
getRuntimeLogs: () => __TAURI_INVOKE<RuntimeLogs>('get_runtime_logs'),
@@ -72,7 +73,7 @@ export const commands = {
__TAURI_INVOKE<string>('get_runtime_proxy_chain_config', {
proxyChainExitNode,
}),
updateProxyChainConfigInRuntime: (proxyChainConfig: YamlValue | null) =>
updateProxyChainConfigInRuntime: (proxyChainConfig: JsonValue | null) =>
__TAURI_INVOKE<ValidationOutcome>('update_proxy_chain_config_in_runtime', {
proxyChainConfig,
}),
@@ -102,11 +103,18 @@ export const commands = {
takeDnsOverrideNotice: () =>
__TAURI_INVOKE<boolean>('take_dns_override_notice'),
takeServiceFallbackNotice: () =>
__TAURI_INVOKE<ServiceFallbackNotice | null>(
'take_service_fallback_notice',
),
__TAURI_INVOKE<
| { kind: 'unavailable' }
| { kind: 'coreRejected'; reason: string }
| { kind: 'notAutoStarted' }
| null
>('take_service_fallback_notice'),
getCoreStartupError: () =>
__TAURI_INVOKE<CoreFailure | null>('get_core_startup_error'),
__TAURI_INVOKE<
| { kind: 'startFailed'; detail: string }
| { kind: 'serviceCoreStopped'; detail: string }
| null
>('get_core_startup_error'),
takeServiceRepairNotice: () =>
__TAURI_INVOKE<boolean>('take_service_repair_notice'),
takeServiceOwnerNotice: () =>
@@ -132,12 +140,10 @@ export const commands = {
openDevtools: () => __TAURI_INVOKE<void>('open_devtools'),
exitApp: () => __TAURI_INVOKE<void>('exit_app'),
getNetworkInterfacesInfo: () =>
__TAURI_INVOKE<NetworkInterfaceView_Serialize[]>(
'get_network_interfaces_info',
),
__TAURI_INVOKE<NetworkInterfaceView[]>('get_network_interfaces_info'),
getProfiles: () => __TAURI_INVOKE<IProfiles_Serialize>('get_profiles'),
enhanceProfiles: () => __TAURI_INVOKE<ValidationOutcome>('enhance_profiles'),
patchProfilesConfig: (profiles: ProfileSelection_Deserialize) =>
patchProfilesConfig: (profiles: ProfileSelection) =>
__TAURI_INVOKE<ValidationOutcome>('patch_profiles_config', { profiles }),
viewProfile: (index: string) =>
__TAURI_INVOKE<null>('view_profile', { index }),
@@ -145,12 +151,52 @@ export const commands = {
__TAURI_INVOKE<null>('patch_profile', { index, profile }),
createProfile: (item: ProfileCreate_Deserialize, fileData: string | null) =>
__TAURI_INVOKE<null>('create_profile', { item, fileData }),
importProfile: (url: string, option: PrfOption_Deserialize | null) =>
__TAURI_INVOKE<null>('import_profile', { url, option }),
importProfile: (
url: string,
option: {
user_agent?: string | null
with_proxy?: boolean | null
self_proxy?: boolean | null
update_interval?: number | null
/**
* HTTP request timeout in seconds
* default is 60 seconds
*/
timeout_seconds?: number | null
/** default is `false` */
danger_accept_invalid_certs?: boolean | null
allow_auto_update?: boolean | null
merge?: string | null
script?: string | null
rules?: string | null
proxies?: string | null
groups?: string | null
} | null,
) => __TAURI_INVOKE<null>('import_profile', { url, option }),
reorderProfile: (activeId: string, overId: string) =>
__TAURI_INVOKE<null>('reorder_profile', { activeId, overId }),
updateProfile: (index: string, option: PrfOption_Deserialize | null) =>
__TAURI_INVOKE<null>('update_profile', { index, option }),
updateProfile: (
index: string,
option: {
user_agent?: string | null
with_proxy?: boolean | null
self_proxy?: boolean | null
update_interval?: number | null
/**
* HTTP request timeout in seconds
* default is 60 seconds
*/
timeout_seconds?: number | null
/** default is `false` */
danger_accept_invalid_certs?: boolean | null
allow_auto_update?: boolean | null
merge?: string | null
script?: string | null
rules?: string | null
proxies?: string | null
groups?: string | null
} | null,
) => __TAURI_INVOKE<null>('update_profile', { index, option }),
deleteProfile: (index: string) =>
__TAURI_INVOKE<null>('delete_profile', { index }),
readProfileFile: (index: string) =>
@@ -178,14 +224,11 @@ export const commands = {
__TAURI_INVOKE<null>('delete_webdav_backup', { filename }),
restoreWebdavBackup: (filename: string) =>
__TAURI_INVOKE<null>('restore_webdav_backup', { filename }),
getUnlockItems: () =>
__TAURI_INVOKE<UnlockItem_Serialize[]>('get_unlock_items'),
checkMediaUnlock: (onComplete: Channel<UnlockItem_Deserialize>) =>
__TAURI_INVOKE<UnlockItem_Serialize[]>('check_media_unlock', {
onComplete,
}),
getUnlockItems: () => __TAURI_INVOKE<UnlockItem[]>('get_unlock_items'),
checkMediaUnlock: (onComplete: Channel<UnlockItem>) =>
__TAURI_INVOKE<UnlockItem[]>('check_media_unlock', { onComplete }),
checkMediaUnlockItem: (name: string) =>
__TAURI_INVOKE<UnlockItem_Serialize>('check_media_unlock_item', { name }),
__TAURI_INVOKE<UnlockItem>('check_media_unlock_item', { name }),
}
/* Types */
@@ -194,20 +237,7 @@ export type AutoProxy = {
url: string
}
export type ClashInfo = ClashInfo_Serialize | ClashInfo_Deserialize
export type ClashInfo_Deserialize = {
/** clash core port */
mixed_port: number
socks_port: number
port: number
/** same as `external-controller` */
server: string
/** clash secret */
secret?: string | null
}
export type ClashInfo_Serialize = {
export type ClashInfo = {
/** clash core port */
mixed_port: number
socks_port: number
@@ -226,21 +256,21 @@ export type CommandFailure =
/** Structured command failure returned to the frontend. */
export type CommandFailure_Deserialize = {
/** Stable frontend identifier, when classified. */
code?: string | null
code: string | null
/** Human-readable diagnostic detail. */
detail: string
/** Requested operation when known by the command. */
operation?: FailedOperation | null
operation: FailedOperation | null
}
/** Structured command failure returned to the frontend. */
export type CommandFailure_Serialize = {
/** Stable frontend identifier, when classified. */
code?: string
code?: string | null
/** Human-readable diagnostic detail. */
detail: string
/** Requested operation when known by the command. */
operation?: FailedOperation
operation?: FailedOperation | null
}
/** Why the Core is not running, kept until a Core starts again. */
@@ -263,26 +293,7 @@ export type DnsOverrideOutcome =
| { status: 'confirmation_required'; source: string }
/** Same wire shape as the updater plugin's JS `DownloadEvent`. */
export type DownloadEvent = DownloadEvent_Serialize | DownloadEvent_Deserialize
/** Same wire shape as the updater plugin's JS `DownloadEvent`. */
export type DownloadEvent_Deserialize =
| {
event: 'Started'
data: {
contentLength?: number | null
}
}
| {
event: 'Progress'
data: {
chunkLength: number
}
}
| { event: 'Finished' }
/** Same wire shape as the updater plugin's JS `DownloadEvent`. */
export type DownloadEvent_Serialize =
export type DownloadEvent =
| {
event: 'Started'
data: {
@@ -322,27 +333,14 @@ export type IProfiles_Serialize = {
/** ### `verge.yaml` schema */
export type IVerge = IVerge_Serialize | IVerge_Deserialize
export type IVergeTestItem =
| IVergeTestItem_Serialize
| IVergeTestItem_Deserialize
export type IVergeTestItem_Deserialize = {
export type IVergeTestItem = {
uid?: string | null
name?: string | null
icon?: string | null
url?: string | null
}
export type IVergeTestItem_Serialize = {
uid: string | null
name: string | null
icon: string | null
url: string | null
}
export type IVergeTheme = IVergeTheme_Serialize | IVergeTheme_Deserialize
export type IVergeTheme_Deserialize = {
export type IVergeTheme = {
primary_color?: string | null
secondary_color?: string | null
primary_text?: string | null
@@ -355,19 +353,6 @@ export type IVergeTheme_Deserialize = {
css_injection?: string | null
}
export type IVergeTheme_Serialize = {
primary_color: string | null
secondary_color: string | null
primary_text: string | null
secondary_text: string | null
info_color: string | null
error_color: string | null
warning_color: string | null
success_color: string | null
font_family: string | null
css_injection: string | null
}
/** ### `verge.yaml` schema */
export type IVerge_Deserialize = {
/** silent | error | warn | info | debug | trace */
@@ -410,7 +395,7 @@ export type IVerge_Deserialize = {
proxy_auto_config?: boolean | null
pac_file_content?: string | null
proxy_host?: string | null
theme_setting?: IVergeTheme_Deserialize | null
theme_setting?: IVergeTheme | null
web_ui_list?: string[] | null
clash_core?: string | null
/** format: {func},{key} */
@@ -426,7 +411,7 @@ export type IVerge_Deserialize = {
auto_delay_detection_interval_minutes?: number | null
enable_builtin_enhanced?: boolean | null
proxy_layout_column?: number | null
test_list?: IVergeTestItem_Deserialize[] | null
test_list?: IVergeTestItem[] | null
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
auto_log_clean?: number | null
enable_auto_backup_schedule?: boolean | null
@@ -475,13 +460,13 @@ export type IVerge_Serialize = {
traffic_graph: boolean | null
/** show memory info (only for Mihomo) */
enable_memory_usage: boolean | null
enable_group_icon?: boolean
enable_group_icon?: boolean | null
pause_render_traffic_stats_on_blur: boolean | null
common_tray_icon?: boolean
menu_icon?: string
menu_order?: string[]
proxy_group_tools_position?: string
notice_position?: string
common_tray_icon?: boolean | null
menu_icon?: string | null
menu_order?: string[] | null
proxy_group_tools_position?: string | null
notice_position?: string | null
collapse_navbar: boolean | null
sysproxy_tray_icon: boolean | null
tun_tray_icon: boolean | null
@@ -500,11 +485,11 @@ export type IVerge_Serialize = {
proxy_auto_config: boolean | null
pac_file_content: string | null
proxy_host: string | null
theme_setting: IVergeTheme_Serialize | null
theme_setting: IVergeTheme | null
web_ui_list: string[] | null
clash_core?: string
clash_core?: string | null
/** format: {func},{key} */
hotkeys?: string[]
hotkeys?: string[] | null
enable_global_hotkey: boolean | null
home_cards: JsonValue | null
auto_close_connection: boolean | null
@@ -516,7 +501,7 @@ export type IVerge_Serialize = {
auto_delay_detection_interval_minutes: number | null
enable_builtin_enhanced: boolean | null
proxy_layout_column: number | null
test_list: IVergeTestItem_Serialize[] | null
test_list: IVergeTestItem[] | null
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
auto_log_clean: number | null
enable_auto_backup_schedule: boolean | null
@@ -528,10 +513,10 @@ export type IVerge_Serialize = {
verge_socks_enabled: boolean | null
verge_port: number | null
verge_http_enabled: boolean | null
webdav_url?: string
webdav_username?: string
webdav_password?: string
tray_proxy_groups_display_mode?: string
webdav_url?: string | null
webdav_username?: string | null
webdav_password?: string | null
tray_proxy_groups_display_mode?: string | null
tray_inline_outbound_modes: boolean | null
enable_auto_light_weight_mode: boolean | null
/** 自动进入轻量模式的延迟(分钟) */
@@ -593,17 +578,7 @@ export type LocalBackupFile = {
content_length: number
}
export type NetworkInterfaceView =
| NetworkInterfaceView_Serialize
| NetworkInterfaceView_Deserialize
export type NetworkInterfaceView_Deserialize = {
name: string
addr: InterfaceAddress[]
mac_addr?: string | null
}
export type NetworkInterfaceView_Serialize = {
export type NetworkInterfaceView = {
name: string
addr: InterfaceAddress[]
mac_addr: string | null
@@ -662,20 +637,7 @@ export type PendingFailure = {
sequence: number
}
export type PlatformVergeFields =
| PlatformVergeFields_Serialize
| PlatformVergeFields_Deserialize
export type PlatformVergeFields_Deserialize = {
tray_icon?: string | null
verge_redir_port?: number | null
verge_redir_enabled?: boolean | null
verge_tproxy_port?: number | null
verge_tproxy_enabled?: boolean | null
enable_tray_speed?: boolean | null
}
export type PlatformVergeFields_Serialize = {
export type PlatformVergeFields = {
tray_icon?: string | null
verge_redir_port?: number | null
verge_redir_enabled?: boolean | null
@@ -701,7 +663,7 @@ export type PrfItem_Deserialize = {
file?: string | null
desc?: string | null
url?: string | null
selected?: PrfSelected_Deserialize[] | null
selected?: PrfSelected[] | null
extra?: PrfExtra | null
updated?: number | null
option?: PrfOption_Deserialize | null
@@ -714,13 +676,13 @@ export type PrfItem_Serialize = {
type: string | null
name: string | null
file: string | null
desc?: string
url?: string
selected?: PrfSelected_Serialize[]
extra?: PrfExtra
desc?: string | null
url?: string | null
selected?: PrfSelected[] | null
extra?: PrfExtra | null
updated: number | null
option?: PrfOption_Serialize
home?: string
option?: PrfOption_Serialize | null
home?: string | null
}
export type PrfOption = PrfOption_Serialize | PrfOption_Deserialize
@@ -746,18 +708,18 @@ export type PrfOption_Deserialize = {
}
export type PrfOption_Serialize = {
user_agent?: string
with_proxy?: boolean
self_proxy?: boolean
update_interval?: number
user_agent?: string | null
with_proxy?: boolean | null
self_proxy?: boolean | null
update_interval?: number | null
/**
* HTTP request timeout in seconds
* default is 60 seconds
*/
timeout_seconds?: number
timeout_seconds?: number | null
/** default is `false` */
danger_accept_invalid_certs?: boolean
allow_auto_update?: boolean
danger_accept_invalid_certs?: boolean | null
allow_auto_update?: boolean | null
merge: string | null
script: string | null
rules: string | null
@@ -765,18 +727,11 @@ export type PrfOption_Serialize = {
groups: string | null
}
export type PrfSelected = PrfSelected_Serialize | PrfSelected_Deserialize
export type PrfSelected_Deserialize = {
export type PrfSelected = {
name?: string | null
now?: string | null
}
export type PrfSelected_Serialize = {
name: string | null
now: string | null
}
export type ProfileCreate = ProfileCreate_Serialize | ProfileCreate_Deserialize
export type ProfileCreate_Deserialize = {
@@ -806,7 +761,7 @@ export type ProfileDnsSettings_Deserialize = {
export type ProfileDnsSettings_Serialize = {
enabled: boolean
confirmation?: string
confirmation?: string | null
}
export type ProfilePatch = ProfilePatch_Serialize | ProfilePatch_Deserialize
@@ -817,7 +772,7 @@ export type ProfilePatch_Deserialize = {
desc?: string | null
file?: string | null
url?: string | null
selected?: PrfSelected_Deserialize[] | null
selected?: PrfSelected[] | null
extra?: PrfExtra | null
updated?: number | null
option?: PrfOption_Deserialize | null
@@ -829,24 +784,16 @@ export type ProfilePatch_Serialize = {
desc: string | null
file: string | null
url: string | null
selected: PrfSelected_Serialize[] | null
selected: PrfSelected[] | null
extra: PrfExtra | null
updated: number | null
option: PrfOption_Serialize | null
}
export type ProfileSelection =
| ProfileSelection_Serialize
| ProfileSelection_Deserialize
export type ProfileSelection_Deserialize = {
export type ProfileSelection = {
current?: string | null
}
export type ProfileSelection_Serialize = {
current: string | null
}
export type ProfileUpdatePayload = {
uid: string
}
@@ -866,11 +813,11 @@ export type ProxyGroupView =
export type ProxyGroupView_Deserialize = {
name: string
type: string
now?: string | null
fixed?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
now: string | null
fixed: string | null
hidden: boolean | null
icon: string | null
testUrl: string | null
history: DelayHistory[]
members: ProxyMemberRef[]
} & ProxyCapabilities
@@ -878,11 +825,11 @@ export type ProxyGroupView_Deserialize = {
export type ProxyGroupView_Serialize = {
name: string
type: string
now?: string
fixed?: string
hidden?: boolean
icon?: string
testUrl?: string
now?: string | null
fixed?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
history: DelayHistory[]
members: ProxyMemberRef[]
} & ProxyCapabilities
@@ -908,10 +855,10 @@ export type ProxyNodeView_Deserialize = {
name: string
type: string
history: DelayHistory[]
id?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
id: string | null
hidden: boolean | null
icon: string | null
testUrl: string | null
source: ProxyNodeSource
} & ProxyCapabilities
@@ -920,10 +867,10 @@ export type ProxyNodeView_Serialize = {
name: string
type: string
history: DelayHistory[]
id?: string
hidden?: boolean
icon?: string
testUrl?: string
id?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
source: ProxyNodeSource
} & ProxyCapabilities
@@ -944,16 +891,16 @@ export type ProxyProviderView =
export type ProxyProviderView_Deserialize = {
name: string
vehicleType: ProxyProviderVehicleType
updatedAt?: string | null
subscriptionInfo?: ProxySubscriptionInfo | null
updatedAt: string | null
subscriptionInfo: ProxySubscriptionInfo | null
proxyRecordIds: string[]
}
export type ProxyProviderView_Serialize = {
name: string
vehicleType: ProxyProviderVehicleType
updatedAt?: string
subscriptionInfo?: ProxySubscriptionInfo
updatedAt?: string | null
subscriptionInfo?: ProxySubscriptionInfo | null
proxyRecordIds: string[]
}
@@ -974,8 +921,8 @@ export type ProxyViewV1_Deserialize = {
schemaVersion: number
orderSource: ProxyViewOrderSource
providerState: ProxyViewProviderState
global?: ProxyGroupView_Deserialize | null
direct?: string | null
global: ProxyGroupView_Deserialize | null
direct: string | null
groups: ProxyGroupView_Deserialize[]
records: { [key in string]: ProxyNodeView_Deserialize }
standalone: string[]
@@ -994,22 +941,7 @@ export type ProxyViewV1_Serialize = {
providers: ProxyProviderView_Serialize[]
}
export type RunStateView = RunStateView_Serialize | RunStateView_Deserialize
export type RunStateView_Deserialize = {
mode: RunningMode
service: ServiceHealthKind
serviceUnavailableReason?: string | null
pendingAction?: PendingAction | null
sidecarAllowed: boolean
isAdmin: boolean
opInFlight: boolean
serviceUsable: boolean
tunCapable: boolean
serviceNeedsAttention: boolean
}
export type RunStateView_Serialize = {
export type RunStateView = {
mode: RunningMode
service: ServiceHealthKind
serviceUnavailableReason: string | null
@@ -1048,18 +980,7 @@ export type ServiceInstallOutcome =
| { status: 'sidecar'; reason: string }
/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */
export type SidecarFailureSnapshot =
| SidecarFailureSnapshot_Serialize
| SidecarFailureSnapshot_Deserialize
/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */
export type SidecarFailureSnapshot_Deserialize = {
revision: number
detail?: string | null
}
/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */
export type SidecarFailureSnapshot_Serialize = {
export type SidecarFailureSnapshot = {
revision: number
detail: string | null
}
@@ -1080,16 +1001,7 @@ export type ToggleableProxyPort = {
port: number
}
export type UnlockItem = UnlockItem_Serialize | UnlockItem_Deserialize
export type UnlockItem_Deserialize = {
name: string
status: string
region?: string | null
check_time?: string | null
}
export type UnlockItem_Serialize = {
export type UnlockItem = {
name: string
status: string
region: string | null
@@ -1115,11 +1027,7 @@ export type ValidationOutcome =
export type ValidationSkipReason = 'exiting' | 'debounced'
export type VergeEventPayloads =
| VergeEventPayloads_Serialize
| VergeEventPayloads_Deserialize
export type VergeEventPayloads_Deserialize = {
export type VergeEventPayloads = {
'verge://refresh-clash-config': null
'verge://refresh-verge-config': null
'verge://refresh-profiles': null
@@ -1129,21 +1037,7 @@ export type VergeEventPayloads_Deserialize = {
'verge://timer-updated': string
'profile-update-started': ProfileUpdatePayload
'profile-update-completed': ProfileUpdatePayload
'verge://run-state-changed': RunStateView_Deserialize
'verge://pending-failures-changed': null
}
export type VergeEventPayloads_Serialize = {
'verge://refresh-clash-config': null
'verge://refresh-verge-config': null
'verge://refresh-profiles': null
'verge://refresh-proxy-config': null
'verge://notice-message': [NoticeStatus, string]
'profile-changed': string
'verge://timer-updated': string
'profile-update-started': ProfileUpdatePayload
'profile-update-completed': ProfileUpdatePayload
'verge://run-state-changed': RunStateView_Serialize
'verge://run-state-changed': RunStateView
'verge://pending-failures-changed': null
}
+1 -1
View File
@@ -19,7 +19,7 @@ import type {
export type {
RunningMode,
RunStateView_Serialize as RunState,
RunStateView as RunState,
FailedOperation,
PendingFailure,
SidecarFailureSnapshot,
+1 -4
View File
@@ -1,4 +1 @@
export type {
NoticeStatus,
VergeEventPayloads_Serialize as VergeEventPayloads,
} from './bindings'
export type { NoticeStatus, VergeEventPayloads } from './bindings'
+4
View File
@@ -5,12 +5,16 @@ import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import svgr from 'vite-plugin-svgr'
import { reactDiagnostics } from './scripts/react/vite.mts'
export default defineConfig({
root: 'src',
cacheDir: path.resolve('node_modules/.vite/default'),
server: { port: 3000 },
plugins: [
svgr(),
react(),
...reactDiagnostics(process.env.CVR_REACT_DIAGNOSTICS),
legacy({
modernTargets: ['edge>=109', 'safari>=14'],
renderLegacyChunks: false,