Compare commits

..
68 changed files with 3255 additions and 1376 deletions
Generated
+151 -188
View File
@@ -150,7 +150,7 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation", "objc2-foundation",
"parking_lot 0.12.5", "parking_lot",
"percent-encoding", "percent-encoding",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"wl-clipboard-rs", "wl-clipboard-rs",
@@ -159,9 +159,9 @@ dependencies = [
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.9.3" version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [ dependencies = [
"rustversion", "rustversion",
] ]
@@ -1089,6 +1089,7 @@ dependencies = [
"async-trait", "async-trait",
"backon", "backon",
"base64 0.23.1", "base64 0.23.1",
"bitflags 2.13.2",
"boa_engine", "boa_engine",
"bytes", "bytes",
"chrono", "chrono",
@@ -1099,6 +1100,7 @@ dependencies = [
"clash-verge-media-unlock", "clash-verge-media-unlock",
"clash-verge-signal", "clash-verge-signal",
"clash_verge_service_ipc", "clash_verge_service_ipc",
"criterion",
"dark-light", "dark-light",
"deelevate", "deelevate",
"dirs 7.0.0", "dirs 7.0.0",
@@ -1115,8 +1117,9 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation",
"open", "open",
"parking_lot 0.13.0", "parking_lot",
"percent-encoding", "percent-encoding",
"regex",
"reqwest 0.13.5", "reqwest 0.13.5",
"reqwest_dav", "reqwest_dav",
"rfd", "rfd",
@@ -1146,6 +1149,7 @@ dependencies = [
"tauri-plugin-mihomo", "tauri-plugin-mihomo",
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-wdio-webdriver", "tauri-plugin-wdio-webdriver",
@@ -1153,6 +1157,7 @@ dependencies = [
"tauri-specta", "tauri-specta",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-stream",
"tokio-util", "tokio-util",
"tracing", "tracing",
"warp", "warp",
@@ -1170,7 +1175,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"criterion", "criterion",
"parking_lot 0.13.0", "parking_lot",
"tokio", "tokio",
] ]
@@ -1193,7 +1198,7 @@ dependencies = [
"anyhow", "anyhow",
"flexi_logger", "flexi_logger",
"log", "log",
"parking_lot 0.13.0", "parking_lot",
"tracing-estuary", "tracing-estuary",
] ]
@@ -1250,8 +1255,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66" checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
"lock_api 0.4.14", "lock_api",
"parking_lot_core 0.9.12", "parking_lot_core",
"polonius-the-crab", "polonius-the-crab",
] ]
@@ -1328,7 +1333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1762,18 +1767,8 @@ version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [ dependencies = [
"darling_core 0.23.0", "darling_core",
"darling_macro 0.23.0", "darling_macro",
]
[[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",
] ]
[[package]] [[package]]
@@ -1789,41 +1784,17 @@ dependencies = [
"syn 2.0.119", "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]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.23.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [ dependencies = [
"darling_core 0.23.0", "darling_core",
"quote", "quote",
"syn 2.0.119", "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]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.2.1" version = "6.2.1"
@@ -1833,9 +1804,9 @@ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
"hashbrown 0.14.5", "hashbrown 0.14.5",
"lock_api 0.4.14", "lock_api",
"once_cell", "once_cell",
"parking_lot_core 0.9.12", "parking_lot_core",
] ]
[[package]] [[package]]
@@ -1913,9 +1884,9 @@ dependencies = [
[[package]] [[package]]
name = "devtools-core" name = "devtools-core"
version = "0.4.2" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e85926bda763dd434fcfbcfb24af4b2c1a8c3e3c34bcd5d844c48f655cca51" checksum = "c53d416e473377276435d49d731a75d253b96ee4339831cd85653702b94d8298"
dependencies = [ dependencies = [
"bytes", "bytes",
"devtools-wire-format", "devtools-wire-format",
@@ -2043,7 +2014,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.2", "redox_users 0.5.2",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -2302,7 +2273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -2818,7 +2789,7 @@ dependencies = [
"gobject-sys 0.21.5", "gobject-sys 0.21.5",
"libc", "libc",
"system-deps 7.0.8", "system-deps 7.0.8",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -3372,7 +3343,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.62.2", "windows-core 0.61.2",
] ]
[[package]] [[package]]
@@ -3386,12 +3357,12 @@ dependencies = [
[[package]] [[package]]
name = "ico" name = "ico"
version = "0.6.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"png", "png 0.17.16",
] ]
[[package]] [[package]]
@@ -3577,7 +3548,7 @@ dependencies = [
"byteorder-lite", "byteorder-lite",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png", "png 0.18.1",
"tiff", "tiff",
] ]
@@ -3886,7 +3857,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"libc", "libc",
"parking_lot 0.12.5", "parking_lot",
"path-tree", "path-tree",
"pin-project-lite", "pin-project-lite",
"rand 0.10.2", "rand 0.10.2",
@@ -4046,20 +4017,11 @@ dependencies = [
"scopeguard", "scopeguard",
] ]
[[package]]
name = "lock_api"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65d91b22da179613dcf4d6ec8349563f52a363f6849e03b00dd0a185ec4a59af"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.35" version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]] [[package]]
name = "lru-slab" name = "lru-slab"
@@ -4069,9 +4031,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "lzma-rust2" name = "lzma-rust2"
version = "0.24.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490750560fa7851fbbf5d3ab1a8750f09350ed361ed8aed85b1e1e40587812ee" checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
dependencies = [ dependencies = [
"sha2 0.11.1", "sha2 0.11.1",
] ]
@@ -4249,10 +4211,10 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"once_cell", "once_cell",
"png", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4419,7 +4381,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4689,6 +4651,17 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-open-directory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d"
dependencies = [
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-osa-kit" name = "objc2-osa-kit"
version = "0.3.2" version = "0.3.2"
@@ -4790,7 +4763,7 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [ dependencies = [
"parking_lot_core 0.9.12", "parking_lot_core",
] ]
[[package]] [[package]]
@@ -4813,9 +4786,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "open" name = "open"
version = "5.4.5" version = "5.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95" checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
dependencies = [ dependencies = [
"dunce", "dunce",
"is-wsl", "is-wsl",
@@ -4885,7 +4858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -4949,18 +4922,8 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [ dependencies = [
"lock_api 0.4.14", "lock_api",
"parking_lot_core 0.9.12", "parking_lot_core",
]
[[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",
] ]
[[package]] [[package]]
@@ -4971,19 +4934,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.18", "redox_syscall",
"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",
"smallvec", "smallvec",
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
@@ -5274,6 +5225,19 @@ dependencies = [
"plotters-backend", "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]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -5559,7 +5523,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -5703,15 +5667,6 @@ dependencies = [
"bitflags 2.13.2", "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]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.6" version = "0.4.6"
@@ -5909,9 +5864,9 @@ dependencies = [
[[package]] [[package]]
name = "rfd" name = "rfd"
version = "0.17.2" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [ dependencies = [
"block2", "block2",
"dispatch2", "dispatch2",
@@ -5919,18 +5874,16 @@ dependencies = [
"gobject-sys 0.18.0", "gobject-sys 0.18.0",
"gtk-sys", "gtk-sys",
"js-sys", "js-sys",
"libc",
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"percent-encoding",
"raw-window-handle", "raw-window-handle",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -6074,7 +6027,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6087,7 +6040,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.12.1", "linux-raw-sys 0.12.1",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6146,7 +6099,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6524,7 +6477,7 @@ version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
dependencies = [ dependencies = [
"darling 0.23.0", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 2.0.119",
@@ -6766,7 +6719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -6784,7 +6737,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"redox_syscall 0.5.18", "redox_syscall",
"tracing", "tracing",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@@ -6819,9 +6772,9 @@ dependencies = [
[[package]] [[package]]
name = "specta" name = "specta"
version = "2.0.0-rc.26" version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed155e448f42fc4be7a5cbc12bd43f6c549981f7c32fcba429ff6bc57cc4f30c" checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
dependencies = [ dependencies = [
"paste", "paste",
"rustc_version 0.4.1", "rustc_version 0.4.1",
@@ -6831,9 +6784,9 @@ dependencies = [
[[package]] [[package]]
name = "specta-macros" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b34b2516e16a9ddd3dc4e73cd6454cfe21fe209f8cb5a6f37346fe3774d759b3" checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"proc-macro2", "proc-macro2",
@@ -6843,9 +6796,9 @@ dependencies = [
[[package]] [[package]]
name = "specta-serde" name = "specta-serde"
version = "0.0.13" version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4983177ed0eb28235080261953de5f9ebae8a2d107851b5852420bd2ab246245" checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
dependencies = [ dependencies = [
"specta", "specta",
"specta-macros", "specta-macros",
@@ -6853,18 +6806,18 @@ dependencies = [
[[package]] [[package]]
name = "specta-typescript" name = "specta-typescript"
version = "0.0.13" version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ecbe75dc1d27b36141b50b02b7c73bed00945710cee6edffa86d296a159a6f9" checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
dependencies = [ dependencies = [
"specta", "specta",
] ]
[[package]] [[package]]
name = "specta-util" name = "specta-util"
version = "0.0.13" version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bf531215ea70fba8264edecf98f12600462cf359f2038602a47aeda5ecd5b8" checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
dependencies = [ dependencies = [
"specta", "specta",
] ]
@@ -6894,7 +6847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [ dependencies = [
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot 0.12.5", "parking_lot",
"phf_shared 0.13.1", "phf_shared 0.13.1",
"precomputed-hash", "precomputed-hash",
] ]
@@ -7030,15 +6983,16 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.40.1" version = "0.39.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8" checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
dependencies = [ dependencies = [
"libc", "libc",
"memchr", "memchr",
"ntapi", "ntapi",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-kit", "objc2-io-kit",
"objc2-open-directory",
"windows 0.62.2", "windows 0.62.2",
] ]
@@ -7140,7 +7094,7 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation",
"objc2-ui-kit", "objc2-ui-kit",
"parking_lot 0.12.5", "parking_lot",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"tao-macros", "tao-macros",
@@ -7194,9 +7148,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.12.5" version = "2.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072" checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -7249,9 +7203,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614" checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -7272,16 +7226,16 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316" checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.23.1",
"brotli", "brotli",
"ico", "ico",
"json-patch", "json-patch",
"plist", "plist",
"png", "png 0.18.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
"semver 1.0.28", "semver 1.0.28",
@@ -7299,9 +7253,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.7.4" version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1" checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -7347,7 +7301,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"deelevate", "deelevate",
"libc", "libc",
"parking_lot 0.13.0", "parking_lot",
"serde", "serde",
"specta", "specta",
"sysinfo", "sysinfo",
@@ -7372,9 +7326,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.6.3" version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab551d9bec27bb79d1c7fe5ed38c5cfd1cebb86efa745acc65f5853f345f6e56" checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
dependencies = [ dependencies = [
"dunce", "dunce",
"plist", "plist",
@@ -7393,9 +7347,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-devtools" name = "tauri-plugin-devtools"
version = "2.2.3" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299a62101a16c14b914ebfaf529b4b64ad9ca534a0ece1c21fc9308d1e3ba9db" checksum = "1acb4d1b8490ee79166b72c3f8c989e066650a11aa94dfd0d71fb05c61cdbed0"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"bytes", "bytes",
@@ -7420,9 +7374,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.8.4" version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5" checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -7526,13 +7480,13 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.8.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf" checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
dependencies = [ dependencies = [
"log", "log",
"notify-rust", "notify-rust",
"rand 0.10.2", "rand 0.9.5",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -7567,10 +7521,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-process"
version = "2.6.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7375b25f72f70f6168f27f1ba3a413e30eaa0a2098cff53a4046ebfe9e2f142d" checksum = "74ae3d8549ae564c0397377373e262f340908eb23a5b9fbe03f2a00007dc0038"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"libc", "libc",
@@ -7625,9 +7589,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-wdio-webdriver" name = "tauri-plugin-wdio-webdriver"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a775b0b59f6f12b38d1f28667311a89cd07f532f540e27d65c6eadfba1156d8" checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -7698,9 +7662,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.12.3" version = "2.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@@ -7708,6 +7672,7 @@ dependencies = [
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
"once_cell",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"softbuffer", "softbuffer",
@@ -7724,12 +7689,11 @@ dependencies = [
[[package]] [[package]]
name = "tauri-specta" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe71fc209fa4c49f959644f1ae2d70f72d5675bcb3d3ad8518122fa7b330994e" checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"paste",
"serde", "serde",
"serde_json", "serde_json",
"specta", "specta",
@@ -7743,15 +7707,15 @@ dependencies = [
[[package]] [[package]]
name = "tauri-specta-macros" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf5689a1dbf2611e2351b79ba042ca089d0a3ce45274695e79084cd732a5aec" checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
dependencies = [ dependencies = [
"darling 0.24.1", "darling",
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.5", "syn 2.0.119",
] ]
[[package]] [[package]]
@@ -7821,10 +7785,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.3", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix 1.1.4", "rustix 1.1.4",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -8077,9 +8041,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.54.1" version = "1.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -8435,7 +8399,7 @@ dependencies = [
"flexi_logger", "flexi_logger",
"log", "log",
"nu-ansi-term", "nu-ansi-term",
"parking_lot 0.12.5", "parking_lot",
"tokio", "tokio",
"tracing", "tracing",
"tracing-log", "tracing-log",
@@ -8487,10 +8451,10 @@ dependencies = [
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation", "objc2-foundation",
"once_cell", "once_cell",
"png", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -8599,7 +8563,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"tempfile", "tempfile",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -9045,9 +9009,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.11" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
@@ -9141,7 +9105,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -9162,7 +9126,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"windows-sys 0.61.2", "windows-sys 0.60.2",
"windows-version", "windows-version",
] ]
@@ -9737,7 +9701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -10069,9 +10033,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "9.3.0" version = "9.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaeb7f16d2605b49aef586996ded29e65642fab7259a3077e537351df6d80426" checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
dependencies = [ dependencies = [
"aes", "aes",
"bzip2", "bzip2",
@@ -10082,7 +10046,6 @@ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"hmac", "hmac",
"indexmap 2.14.0", "indexmap 2.14.0",
"libc",
"lzma-rust2", "lzma-rust2",
"memchr", "memchr",
"nix", "nix",
+6 -4
View File
@@ -61,20 +61,21 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" } tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1.44" tracing = "0.1.44"
tauri = { version = "2.12.5" } tauri = { version = "2.12.4" }
tauri-plugin-clipboard-manager = "2.5.0" 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" anyhow = "1.0.104"
chrono = "0.4.45" chrono = "0.4.45"
criterion = { version = "0.8.2", features = ["async_tokio"] } criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.54.1", features = [ tokio = { version = "1.53.3", features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
"time", "time",
"sync", "sync",
] } ] }
flexi_logger = "0.31.10" flexi_logger = "0.31.10"
log = "0.4.35" log = "0.4.34"
regex = "1.13.1"
reqwest = { version = "0.13.5", features = [ reqwest = { version = "0.13.5", features = [
"json", "json",
"cookies", "cookies",
@@ -89,6 +90,7 @@ smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" } serde = { version = "1.0.229" }
serde_json = { version = "1.0.154" } serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" } serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
socket2 = "0.6.5" socket2 = "0.6.5"
# *** For Windows platform only *** # *** For Windows platform only ***
-11
View File
@@ -25,12 +25,6 @@
<summary><strong> 🐞 修复问题 </strong></summary> <summary><strong> 🐞 修复问题 </strong></summary>
- 修复检测更新后又发布新版时,应用内更新失败 - 修复检测更新后又发布新版时,应用内更新失败
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
**🍎 macOS**
- 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
</details> </details>
@@ -43,10 +37,5 @@
<details> <details>
<summary><strong> 🚀 优化改进 </strong></summary> <summary><strong> 🚀 优化改进 </strong></summary>
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
**🍎 macOS**
- 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址
</details> </details>
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [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 } clash-verge-logging = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
log = { workspace = true } log = { workspace = true }
@@ -5,14 +5,14 @@ edition = "2024"
rust-version = "1.91" rust-version = "1.91"
[dependencies] [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 = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true } tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
# sysinfo 0.38.2 conflicts with dark-light # sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623 # see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.40.1", default-features = false, features = ["network", "system"] } sysinfo = { version = "0.39.6", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
libc = "0.2.191" libc = "0.2.191"
@@ -27,9 +27,9 @@ pub struct SysInfo {
impl Default for SysInfo { impl Default for SysInfo {
#[inline] #[inline]
fn default() -> Self { fn default() -> Self {
let system_name = System::name().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_version = System::long_os_version().unwrap_or_else(|| "Null".into());
let system_kernel_version = System::kernel_version().unwrap_or_else(|_| "Null".into()); let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into());
let system_arch = System::cpu_arch(); let system_arch = System::cpu_arch();
Self { Self {
system_name, system_name,
@@ -130,14 +130,9 @@ pub fn current_gid() -> u32 {
#[inline] #[inline]
pub fn list_network_interfaces() -> Vec<String> { pub fn list_network_interfaces() -> Vec<String> {
let Ok(mut networks) = Networks::new() else { let mut networks = Networks::new();
return Vec::new();
};
networks.refresh(false); networks.refresh(false);
networks networks.keys().map(|name| name.to_owned()).collect()
.keys()
.map(|name| name.to_string_lossy().into_owned())
.collect()
} }
#[inline] #[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}'], files: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}'],
rules: ipcBoundaryRules(), rules: ipcBoundaryRules(),
+10 -1
View File
@@ -1,6 +1,15 @@
{ {
"$schema": "https://unpkg.com/knip@6/schema.json", "$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}!"], "project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
"include": [ "include": [
"files", "files",
+13 -5
View File
@@ -40,7 +40,11 @@
"perf:build": "node scripts/perf/build.mjs", "perf:build": "node scripts/perf/build.mjs",
"perf:run": "node scripts/perf/run.mjs", "perf:run": "node scripts/perf/run.mjs",
"perf:compare": "node scripts/perf/compare.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": { "dependencies": {
"@dnd-kit/collision": "^0.5.0", "@dnd-kit/collision": "^0.5.0",
@@ -56,7 +60,7 @@
"@tanstack/react-virtual": "^3.14.13", "@tanstack/react-virtual": "^3.14.13",
"@tauri-apps/api": "2.12.4", "@tauri-apps/api": "2.12.4",
"@tauri-apps/plugin-clipboard-manager": "2.5.0", "@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-fs": "2.7.1",
"@tauri-apps/plugin-http": "2.8.3", "@tauri-apps/plugin-http": "2.8.3",
"@tauri-apps/plugin-opener": "2.7.2", "@tauri-apps/plugin-opener": "2.7.2",
@@ -94,7 +98,7 @@
"@biomejs/biome": "^2.5.14", "@biomejs/biome": "^2.5.14",
"@eslint-react/eslint-plugin": "^5.20.5", "@eslint-react/eslint-plugin": "^5.20.5",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.4", "@tauri-apps/cli": "2.12.3",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^26.0.0", "@types/node": "^26.0.0",
"@types/react": "19.3.0", "@types/react": "19.3.0",
@@ -102,7 +106,7 @@
"@types/validator": "^13.15.10", "@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.2.3", "@vitejs/plugin-legacy": "^8.2.3",
"@vitejs/plugin-react": "^6.1.1", "@vitejs/plugin-react": "^6.1.1",
"@wdio/tauri-service": "1.5.1", "@wdio/tauri-service": "1.5.0",
"adm-zip": "^0.6.1", "adm-zip": "^0.6.1",
"axios": "^1.20.0", "axios": "^1.20.0",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
@@ -117,6 +121,7 @@
"eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-react-refresh": "^0.5.7",
"eslint-plugin-sukka": "^10.2.0", "eslint-plugin-sukka": "^10.2.0",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"glob": "^14.0.0",
"globals": "^17.12.0", "globals": "^17.12.0",
"https-proxy-agent": "^9.1.0", "https-proxy-agent": "^9.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
@@ -133,7 +138,10 @@
"vite-plugin-svgr": "^5.2.0", "vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.1", "vitest": "^5.0.1",
"webdriverio": "10.1.0", "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": { "lint-staged": {
"*.{ts,tsx,js,mjs}": [ "*.{ts,tsx,js,mjs}": [
+1164 -190
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -113,27 +113,6 @@
"ghcr.io/devcontainers/features/rust" "ghcr.io/devcontainers/features/rust"
], ],
"groupName": "Rust toolchain" "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": [ "customManagers": [
+2 -1
View File
@@ -6,6 +6,7 @@ import path from 'node:path'
import zlib from 'node:zlib' import zlib from 'node:zlib'
import AdmZip from 'adm-zip' import AdmZip from 'adm-zip'
import { glob } from 'glob'
import { HttpsProxyAgent } from 'https-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent'
import { extract } from 'tar' import { extract } from 'tar'
@@ -518,7 +519,7 @@ const resolveServicePermission = async () => {
let hasChanges = false let hasChanges = false
for (const f of serviceExecutables) { for (const f of serviceExecutables) {
const files = fs.globSync(path.join(SERVICE_DIR, f)) const files = glob.sync(path.join(SERVICE_DIR, f))
for (const filePath of files) { for (const filePath of files) {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath) const currentHash = await calculateFileHash(filePath)
+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)
}
+17 -50
View File
@@ -32,9 +32,7 @@ function is_valid_ip() {
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent. # Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
function sc_value() { function sc_value() {
local dictionary printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p"
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
sed -n "s/^ *$2 : //p" <<<"$dictionary"
} }
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer. # Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
@@ -52,62 +50,31 @@ function dns_of() {
echo "$dns" echo "$dns"
} }
[ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1 [ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
dns_servers=() ! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
while read -r server; do
! is_valid_ip "$server" && echo "$server is not a valid IP address." >&2 && exit 1
dns_servers+=("$server")
done <<<"$1"
# Backups are keyed by service ID, which survives renaming the service and renumbering its interface. # Backups are keyed by service ID, which survives renaming the service and renumbering its interface.
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) || exit 1 service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName) || exit 1 hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName)
[ -z "$hardware_port" ] && echo "No primary network service; waiting for the network." >&2 && exit 75 [ -z "$hardware_port" ] && echo "No primary network service." && exit 1
original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; } original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; }
backup=".original_dns/$service_id" backup=".original_dns/$service_id"
mkdir -p .original_dns || exit 1 mkdir -p .original_dns || exit 1
# Key old single-slot backups before import so a later network switch cannot reassign them. # Older releases kept a single backup for whichever service was primary.
for state_dir in "$PWD" "${2:-$PWD}"; do if [ -f .original_dns.txt ]; then
if [ -f "$state_dir/.original_dns.txt" ]; then
mkdir -p "$state_dir/.original_dns" || exit 1
if [ -f "$state_dir/.original_dns/$service_id" ]; then
rm -f "$state_dir/.original_dns.txt" || exit 1
else
mv "$state_dir/.original_dns.txt" "$state_dir/.original_dns/$service_id" || exit 1
fi
fi
done
if [ ! -f "$backup" ] && [ -n "$2" ] && [ -f "$2/$backup" ]; then
cp "$2/$backup" ".original_dns/.$service_id" || exit 1
mv ".original_dns/.$service_id" "$backup" || exit 1
fi
backup_header='# clash-verge-dns-managed'
function save_backup() {
printf '%s\n%s\n' "$backup_header" "$1" >".original_dns/.$service_id" &&
mv ".original_dns/.$service_id" "$backup"
}
saved_dns=
legacy_backup=false
if [ -f "$backup" ]; then if [ -f "$backup" ]; then
saved_dns=$(cat "$backup") || exit 1 rm -f .original_dns.txt || exit 1
case "$saved_dns" in else
"$backup_header"$'\n'*) saved_dns=${saved_dns#*$'\n'} ;; mv .original_dns.txt "$backup" || exit 1
*) legacy_backup=true ;;
esac
fi fi
fi
# The placeholder is ours, not the user's: keep the backup taken before it was applied.
if [ "$original_dns" != "$1" ]; then if [ "$original_dns" != "$1" ]; then
# Only an unmarked historical backup owns the old single-address placeholder. echo "$original_dns" >"$backup" || exit 1
if [ "$original_dns" != "114.114.114.114" ] || [ "$legacy_backup" != true ]; then networksetup -setdnsservers "$hardware_port" "$1"
save_backup "$original_dns" || exit 1
legacy_backup=false
fi
networksetup -setdnsservers "$hardware_port" "${dns_servers[@]}"
[ "$(dns_of "$hardware_port")" = "$1" ] || { echo "Failed to set DNS for $hardware_port." >&2; exit 1; } [ "$(dns_of "$hardware_port")" = "$1" ] || { echo "Failed to set DNS for $hardware_port." >&2; exit 1; }
fi elif [ ! -f "$backup" ]; then
# Keep legacy ownership until the new list is verified, including a retry after a failed marker write. echo "empty" >"$backup" || exit 1
if [ "$legacy_backup" = true ]; then
save_backup "$saved_dns" || exit 1
fi fi
+10 -39
View File
@@ -1,11 +1,9 @@
#!/bin/bash #!/bin/bash
[ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1 [ $# -lt 1 ] && echo "Usage: $0 <placeholder IP address>" && exit 1
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent. # Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
function sc_value() { function sc_value() {
local dictionary printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p"
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
sed -n "s/^ *$2 : //p" <<<"$dictionary"
} }
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer. # Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
@@ -23,11 +21,7 @@ function dns_of() {
echo "$dns" echo "$dns"
} }
backup_header='# clash-verge-dns-managed' result=0
completed_backups=()
function restore_dns() {
local result=0
# Older releases kept a single backup for whichever service was primary. # Older releases kept a single backup for whichever service was primary.
if [ -f .original_dns.txt ]; then if [ -f .original_dns.txt ]; then
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
@@ -42,12 +36,7 @@ function restore_dns() {
for backup in .original_dns/*; do for backup in .original_dns/*; do
[ -f "$backup" ] || continue [ -f "$backup" ] || continue
# Current backups take precedence even after their DNS has already been restored. hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName)
if [ "$PWD" != "$state_dir" ] && [ -f "$state_dir/$backup" ]; then
completed_backups+=("$PWD/$backup")
continue
fi
hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName) || { result=1; continue; }
# Kept for a service that is gone: there is nothing to write it back to. # Kept for a service that is gone: there is nothing to write it back to.
[ -n "$hardware_port" ] || continue [ -n "$hardware_port" ] || continue
if ! current_dns=$(dns_of "$hardware_port"); then if ! current_dns=$(dns_of "$hardware_port"); then
@@ -55,14 +44,11 @@ function restore_dns() {
result=1 result=1
continue continue
fi fi
# A service no longer on the placeholder was changed by someone else, and that change stands.
if [ "$current_dns" = "$1" ]; then
original_dns=$(cat "$backup") || { result=1; continue; } original_dns=$(cat "$backup") || { result=1; continue; }
legacy_backup=false # Older releases could back up the placeholder itself.
case "$original_dns" in [ "$original_dns" = "$1" ] && original_dns=empty
"$backup_header"$'\n'*) original_dns=${original_dns#*$'\n'} ;;
*) legacy_backup=true ;;
esac
# A service no longer on its recorded placeholder was changed by someone else.
if [ "$current_dns" = "$1" ] || { [ "$legacy_backup" = true ] && [ "$current_dns" = "114.114.114.114" ]; }; then
networksetup -setdnsservers "$hardware_port" $original_dns networksetup -setdnsservers "$hardware_port" $original_dns
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then
echo "Failed to restore DNS for $hardware_port." >&2 echo "Failed to restore DNS for $hardware_port." >&2
@@ -70,22 +56,7 @@ function restore_dns() {
continue continue
fi fi
fi fi
completed_backups+=("$PWD/$backup") rm -f "$backup" || result=1
done done
return $result rmdir .original_dns 2>/dev/null
}
result=0
state_dir=$PWD
restore_dns "$1" || result=1
if [ -n "$2" ] && [ "$2" != "$PWD" ]; then
{ cd "$2" && restore_dns "$1"; } || result=1
fi
# Remove superseded legacy backups first; a cleanup failure must leave current authority intact.
if [ "$result" -eq 0 ]; then
for ((index=${#completed_backups[@]}-1; index>=0; index--)); do
rm -f "${completed_backups[index]}" || { result=1; break; }
done
fi
rmdir "$state_dir/.original_dns" "${2:-$state_dir}/.original_dns" 2>/dev/null
exit $result exit $result
+20 -15
View File
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev" identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.7.4", features = [] } tauri-build = { version = "2.7.3", features = [] }
[dependencies] [dependencies]
clash-verge-draft = { workspace = true } clash-verge-draft = { workspace = true }
@@ -55,10 +55,11 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_yaml_ng = { workspace = true } serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] } smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true } socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] } warp = { version = "0.4.3", features = ["server"] }
open = "5.4.5" open = "5.4.4"
rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] } rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
dirs = "7.0.0" dirs = "7.0.0"
dunce = "1.0.5" dunce = "1.0.5"
nanoid = "0.5" nanoid = "0.5"
@@ -66,17 +67,19 @@ chrono = { workspace = true }
boa_engine = "0.22.0" boa_engine = "0.22.0"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
reqwest = { workspace = true } reqwest = { workspace = true }
regex = { workspace = true }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [
"guard", "guard",
] } ] }
network-interface = { version = "2.0.5", features = ["serde"] } 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-opener = "2.7.2"
tauri-plugin-dialog = "2.8.4" tauri-plugin-dialog = "2.8.3"
tauri-plugin-fs = "2.7.1" tauri-plugin-fs = "2.7.1"
tauri-plugin-deep-link = "2.6.3" tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.2"
tauri-plugin-window-state = "2.5.0" tauri-plugin-window-state = "2.5.0"
zip = "9.2.2" zip = "9.1.3"
flate2 = "1.1.10" flate2 = "1.1.10"
reqwest_dav = "0.3.3" reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" } aes-gcm = { version = "0.11.1" }
@@ -86,26 +89,27 @@ getrandom = "0.4.3"
futures = "0.3.34" futures = "0.3.34"
gethostname = "1.1.0" gethostname = "1.1.0"
scopeguard = "1.2.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"] } backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3" tauri-plugin-http = "2.8.3"
tracing = { workspace = true } 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" } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
async-trait = "0.1.92" 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 = [ 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", "client",
] } ] }
arc-swap = "1.9.3" arc-swap = "1.9.2"
tokio-rustls = "0.26.6" tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] } rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.11" webpki-roots = "1.0.9"
dark-light = "3.0.0" dark-light = "3.0.0"
bytes = "1.12.1" bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] } tokio-util = { version = "0.7.20", features = ["time"] }
specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] } specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.26", features = ["derive", "typescript"] } tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
specta-typescript = "=0.0.13" specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.191" libc = "0.2.191"
@@ -115,7 +119,7 @@ libc = "0.2.191"
clash-verge-signal = { workspace = true } clash-verge-signal = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies] [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 = "0.6.5"
objc2-foundation = { version = "0.3.2", features = [ objc2-foundation = { version = "0.3.2", features = [
"NSString", "NSString",
@@ -152,6 +156,7 @@ tauri-plugin-global-shortcut = "2.4.1"
tauri-plugin-updater = "2.14.1" tauri-plugin-updater = "2.14.1"
[dev-dependencies] [dev-dependencies]
criterion = { workspace = true }
tokio = { workspace = true, features = ["test-util"] } tokio = { workspace = true, features = ["test-util"] }
warp = { version = "0.4.3", features = ["test"] } warp = { version = "0.4.3", features = ["test"] }
+1
View File
@@ -73,6 +73,7 @@
"global-shortcut:allow-register-all", "global-shortcut:allow-register-all",
"global-shortcut:allow-unregister", "global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all", "global-shortcut:allow-unregister-all",
"process:allow-exit",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"dialog:default" "dialog:default"
-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}]")) .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( fn remaining_activations(
activations: &[(String, String)], activations: &[(String, String)],
completed: &HashMap<String, String>, completed: &HashMap<String, String>,
@@ -1066,12 +1019,6 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
.clone() .clone()
.unwrap_or_default(); .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 selected.is_empty() {
if is_activation_current(generation) { if is_activation_current(generation) {
announce(Refresh::Clash); announce(Refresh::Clash);
+6 -18
View File
@@ -559,13 +559,12 @@ impl CoreManager {
})?; })?;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
crate::utils::resolve::dns::sync_public_dns().await; crate::utils::resolve::dns::sync_public_dns().await;
// Only automatic restores can wait for the watcher; explicit requests must report failure. // At login the app usually beats the network. Leave the write to the network watcher
// rather than fail — only if the watcher is actually live; a user toggling while
// offline still fails fast.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let defer_until_network = crate::core::network_watch::is_armed() if crate::core::network_watch::is_armed()
&& crate::core::notification::what_was_asked() && !crate::utils::resolve::is_resolve_done()
== crate::core::notification::FailedOperation::SystemProxyRestore;
#[cfg(target_os = "macos")]
if defer_until_network
&& Config::verge() && Config::verge()
.await .await
.latest_arc() .latest_arc()
@@ -580,18 +579,7 @@ impl CoreManager {
); );
return Ok(()); return Ok(());
} }
if let Err(error) = proxy_control::apply().await { proxy_control::apply().await?;
#[cfg(target_os = "macos")]
if defer_until_network && proxy_control::is_missing_network_service_failure(&error) {
logging!(
debug,
Type::Core,
"network service disappeared; the system proxy waits for its return"
);
return Ok(());
}
return Err(error);
}
if !expectation.is_valid( if !expectation.is_valid(
self.get_running_mode().as_ref(), self.get_running_mode().as_ref(),
self.current_core_readiness_generation(), self.current_core_readiness_generation(),
-1
View File
@@ -49,7 +49,6 @@ async fn reapply() {
.enable_system_proxy .enable_system_proxy
.unwrap_or_default() .unwrap_or_default()
{ {
crate::utils::resolve::dns::sync_public_dns().await;
return; return;
} }
if !proxy_control::has_network_service().await { if !proxy_control::has_network_service().await {
+4 -24
View File
@@ -180,14 +180,6 @@ pub const fn is_missing_network_service(_error: &sysproxy::Error) -> bool {
false false
} }
pub fn is_missing_network_service_failure(error: &anyhow::Error) -> bool {
!SystemProxyStateUnknown::is_in(error)
&& SysproxyFailure::from_chain(error).is_none()
&& error
.downcast_ref::<sysproxy::Error>()
.is_some_and(is_missing_network_service)
}
/// Distinguish a misplaced sidecar from a missing privileged service. /// Distinguish a misplaced sidecar from a missing privileged service.
async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error { async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error {
if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) { if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) {
@@ -367,13 +359,6 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()), ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()),
// Direct fallback satisfies a disable request. // Direct fallback satisfies a disable request.
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()), ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
// Older helpers encode a failed service lookup as fallback even when no OS write occurred.
#[cfg(target_os = "macos")]
ProxyApplyOutcome::DirectFallback { message }
if message == sysproxy::Error::NoActiveNetworkService.to_string() =>
{
Err(sysproxy::Error::NoActiveNetworkService.into())
}
ProxyApplyOutcome::DirectFallback { message } => { ProxyApplyOutcome::DirectFallback { message } => {
Err(SysproxyFailure::DirectFallback { detail: message }.into()) Err(SysproxyFailure::DirectFallback { detail: message }.into())
} }
@@ -624,10 +609,6 @@ pub async fn refresh_guard() -> Result<()> {
logging!(debug, Type::Core, "proxy guard superseded (generation {generation})"); logging!(debug, Type::Core, "proxy guard superseded (generation {generation})");
break; break;
} }
Err(error) if is_missing_network_service_failure(&error) => {
consecutive_failures = 0;
logging!(debug, Type::Core, "system proxy guard waits for a network service");
}
Err(error) => { Err(error) => {
consecutive_failures += 1; consecutive_failures += 1;
logging!( logging!(
@@ -1129,11 +1110,10 @@ mod tests {
.err() .err()
.unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail")); .unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail"));
assert!(super::is_missing_network_service_failure(&error)); assert_eq!(
assert!(SysproxyFailure::from_chain(&error).is_none()); SysproxyFailure::from_chain(&error).map(SysproxyFailure::code),
assert!(!super::is_missing_network_service_failure( Some("SYSPROXY_DIRECT_FALLBACK")
&error.context(SystemProxyStateUnknown) );
));
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+1 -1
View File
@@ -1970,7 +1970,7 @@ impl ServiceManager {
async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> { async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> {
run_operation_and_then(&RUN_STATE, operation, || async { run_operation_and_then(&RUN_STATE, operation, || async {
if let Err(error) = Tray::global().update_part().await { if let Err(error) = Tray::global().update_menu().await {
logging!( logging!(
warn, warn,
Type::Service, Type::Service,
+2 -2
View File
@@ -7,7 +7,6 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use clash_verge_logging::{Type, logging, logging_error}; use clash_verge_logging::{Type, logging, logging_error};
use futures::StreamExt as _;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use smartstring::alias::String; use smartstring::alias::String;
use std::{ use std::{
@@ -22,6 +21,7 @@ use tokio::{
sync::mpsc, sync::mpsc,
time::{sleep, timeout}, time::{sleep, timeout},
}; };
use tokio_stream::StreamExt as _;
use tokio_util::time::{DelayQueue, delay_queue::Key}; use tokio_util::time::{DelayQueue, delay_queue::Key};
enum TimerCommand { enum TimerCommand {
@@ -501,7 +501,6 @@ impl Timer {
mod tests { mod tests {
use super::{TaskSchedule, Timer, run_timer_profile_update_transition}; use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
use crate::config::{PrfItem, PrfOption}; use crate::config::{PrfItem, PrfOption};
use futures::StreamExt as _;
use parking_lot::Mutex; use parking_lot::Mutex;
use smartstring::alias::String; use smartstring::alias::String;
use std::collections::HashMap; use std::collections::HashMap;
@@ -514,6 +513,7 @@ mod tests {
time::Duration, time::Duration,
}; };
use tokio::sync::Barrier; use tokio::sync::Barrier;
use tokio_stream::StreamExt as _;
use tokio_util::time::DelayQueue; use tokio_util::time::DelayQueue;
struct CancellationProbe { struct CancellationProbe {
-6
View File
@@ -66,12 +66,6 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
return Err(err.to_string().into()); 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 clash = Config::clash().await;
let save_result = { let save_result = {
let _config_write = Config::lock_config_write().await; let _config_write = Config::lock_config_write().await;
+1
View File
@@ -54,6 +54,7 @@ mod app_init {
.build(), .build(),
) )
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build()) .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
+54 -24
View File
@@ -4,14 +4,16 @@ use serde_yaml_ng::{Mapping, Value};
use std::{ use std::{
io::Read as _, io::Read as _,
os::unix::process::CommandExt as _, os::unix::process::CommandExt as _,
path::PathBuf, path::{Path, PathBuf},
process::{Command, Stdio}, process::{Command, Stdio},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tokio::sync::Mutex; use tokio::sync::Mutex;
const DNS_STATE_FILE: &str = ".original_dns.txt";
const DNS_BACKUP_DIR: &str = ".original_dns";
// Any address routed into TUN works, since dns-hijack answers it; the value only matters if left behind. // Any address routed into TUN works, since dns-hijack answers it; the value only matters if left behind.
const PLACEHOLDER_DNS: &str = "114.114.114.114\n223.5.5.5\n1.1.1.1"; const PLACEHOLDER_DNS: &str = "114.114.114.114";
// Scripts run under lifecycle_lock, and networksetup waits for other preference writers without a limit. // Scripts run under lifecycle_lock, and networksetup waits for other preference writers without a limit.
pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5); pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -24,6 +26,14 @@ fn dns_state_dir() -> anyhow::Result<PathBuf> {
Ok(dir) Ok(dir)
} }
fn restore_dns_state_dir(resource_dir: &Path, state_dir: PathBuf) -> PathBuf {
if resource_dir.join(DNS_STATE_FILE).exists() || resource_dir.join(DNS_BACKUP_DIR).exists() {
resource_dir.to_path_buf()
} else {
state_dir
}
}
// Without the placeholder, a LAN resolver is reached outside TUN and fake-ip is bypassed. // Without the placeholder, a LAN resolver is reached outside TUN and fake-ip is bypassed.
fn needs_public_dns(config: &Mapping) -> bool { fn needs_public_dns(config: &Mapping) -> bool {
let section = |name: &str| config.get(name).and_then(Value::as_mapping); let section = |name: &str| config.get(name).and_then(Value::as_mapping);
@@ -69,19 +79,25 @@ pub async fn restore_public_dns() -> anyhow::Result<()> {
async fn set_public_dns() { async fn set_public_dns() {
logging!(debug, Type::Config, "try to set system dns"); logging!(debug, Type::Config, "try to set system dns");
let result = run_dns_script("set_dns.sh", true).await; let result = async {
match result { let script = crate::utils::dirs::app_resources_dir()?.join("set_dns.sh");
Ok(DnsScriptOutcome::Complete) => logging!(info, Type::Config, "set system dns successfully"), run_dns_script(&script, dns_state_dir()?).await
Ok(DnsScriptOutcome::Deferred) => {
logging!(debug, Type::Config, "system DNS waits for a primary network service")
} }
.await;
match result {
Ok(()) => logging!(info, Type::Config, "set system dns successfully"),
Err(err) => logging!(error, Type::Config, "set system dns failed: {err:#}"), Err(err) => logging!(error, Type::Config, "set system dns failed: {err:#}"),
} }
} }
async fn unset_public_dns() -> anyhow::Result<()> { async fn unset_public_dns() -> anyhow::Result<()> {
logging!(debug, Type::Config, "try to unset system dns"); logging!(debug, Type::Config, "try to unset system dns");
let result = run_dns_script("unset_dns.sh", false).await.map(|_| ()); let result = async {
let resource_dir = crate::utils::dirs::app_resources_dir()?;
let state_dir = restore_dns_state_dir(&resource_dir, dns_state_dir()?);
run_dns_script(&resource_dir.join("unset_dns.sh"), state_dir).await
}
.await;
match &result { match &result {
Ok(()) => logging!(info, Type::Config, "unset system dns successfully"), Ok(()) => logging!(info, Type::Config, "unset system dns successfully"),
Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"), Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"),
@@ -89,21 +105,12 @@ async fn unset_public_dns() -> anyhow::Result<()> {
result result
} }
enum DnsScriptOutcome {
Complete,
Deferred,
}
/// On timeout the script's whole process group is killed, so no networksetup it started writes late. /// On timeout the script's whole process group is killed, so no networksetup it started writes late.
async fn run_dns_script(script_name: &str, allow_deferred: bool) -> anyhow::Result<DnsScriptOutcome> { async fn run_dns_script(script: &Path, dir: PathBuf) -> anyhow::Result<()> {
let resource_dir = crate::utils::dirs::app_resources_dir()?;
let script = resource_dir.join(script_name);
let dir = dns_state_dir()?;
ensure!(script.exists(), "DNS script not found: {}", script.display()); ensure!(script.exists(), "DNS script not found: {}", script.display());
let mut child = Command::new("bash") let mut child = Command::new("bash")
.arg(&script) .arg(script)
.arg(PLACEHOLDER_DNS) .arg(PLACEHOLDER_DNS)
.arg(resource_dir)
.current_dir(dir) .current_dir(dir)
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@@ -127,18 +134,41 @@ async fn run_dns_script(script_name: &str, allow_deferred: bool) -> anyhow::Resu
if let Some(mut pipe) = child.stderr.take() { if let Some(mut pipe) = child.stderr.take() {
pipe.read_to_string(&mut stderr)?; pipe.read_to_string(&mut stderr)?;
} }
// EX_TEMPFAIL is reserved by set_dns.sh for a network service that is not ready.
if allow_deferred && status.code() == Some(75) {
return Ok(DnsScriptOutcome::Deferred);
}
ensure!( ensure!(
status.success(), status.success(),
"exit code {}: {}", "exit code {}: {}",
status.code().unwrap_or(-1), status.code().unwrap_or(-1),
stderr.trim() stderr.trim()
); );
Ok(DnsScriptOutcome::Complete) Ok(())
}) })
.await .await
.context("DNS script task failed")? .context("DNS script task failed")?
} }
#[cfg(test)]
mod tests {
#[test]
#[allow(clippy::expect_used)]
fn restore_dns_state_dir_defaults_to_app_data_but_honors_legacy_file() {
use super::restore_dns_state_dir;
let root = std::env::temp_dir().join(format!("clash-verge-dns-{}", nanoid::nanoid!()));
let resource_dir = root.join("resources");
let state_dir = root.join("app-data");
std::fs::create_dir_all(&resource_dir).expect("create test resource directory");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), state_dir);
std::fs::write(resource_dir.join(".original_dns.txt"), "empty").expect("create legacy DNS state");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), resource_dir);
// A migrated legacy backup whose restore failed must still be retried there.
std::fs::remove_file(resource_dir.join(".original_dns.txt")).expect("remove legacy DNS state");
std::fs::create_dir(resource_dir.join(".original_dns")).expect("create migrated DNS state");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir), resource_dir);
std::fs::remove_dir_all(root).expect("remove test directory");
}
}
+2 -2
View File
@@ -33,9 +33,9 @@
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json" "https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
], ],
"windows": { "windows": {
"installMode": "passive" "installMode": "passive"
+1 -2
View File
@@ -26,9 +26,8 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
+1 -2
View File
@@ -26,9 +26,8 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
+1 -2
View File
@@ -26,9 +26,8 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
@@ -464,6 +464,21 @@ export const ConnectionTable = (props: Props) => {
] ]
}, [t]) }, [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 orderedColumns = useMemo(() => {
const baseFields = baseColumns.map((column) => column.field) const baseFields = baseColumns.map((column) => column.field)
const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields) const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields)
+73 -64
View File
@@ -1,3 +1,4 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import { import {
AccessTimeRounded, AccessTimeRounded,
ArrowDropDown, ArrowDropDown,
@@ -30,15 +31,7 @@ import {
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
useCallback,
useEffect,
useEffectEvent,
useId,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
@@ -577,22 +570,20 @@ export const CurrentProxyCard = () => {
return profileValue 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], [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( const writeProfileScopedItem = useCallback(
(baseKey: string, value: string) => { (baseKey: string, value: string) => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@@ -624,17 +615,25 @@ export const CurrentProxyCard = () => {
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0 return savedSortType ? (Number(savedSortType) as ProxySortType) : 0
}) })
const [lastGroupName, setLastGroupName] = useState('') const [selectedGroupName, setSelectedGroupName] = useState('')
const [openSelect, setOpenSelect] = useState<OpenSelect>(null) const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
const [openSelectMode, setOpenSelectMode] = useState(mode)
if (openSelectMode !== mode) { useEffect(() => {
setOpenSelectMode(mode)
setOpenSelect(null) setOpenSelect(null)
} }, [mode])
const delayButtonRef = useRef<HTMLButtonElement>(null) const delayButtonRef = useRef<HTMLButtonElement>(null)
const delays = useGroupDelays(selectedGroupName || null)
const autoCheckInProgressRef = useRef(false) 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(() => { const selectableGroups = useMemo(() => {
if (!proxyView) return [] if (!proxyView) return []
@@ -645,39 +644,6 @@ export const CurrentProxyCard = () => {
) )
}, [proxyView]) }, [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>(() => { const selectedGroup = useMemo<ProxyGroupView | null>(() => {
if (!proxyView || isDirectMode) return null if (!proxyView || isDirectMode) return null
if (isGlobalMode) return proxyView.global if (isGlobalMode) return proxyView.global
@@ -714,6 +680,46 @@ export const CurrentProxyCard = () => {
[optionsForGroup, selectedGroup], [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(() => { const currentOption = useMemo(() => {
if (!proxyView) return undefined if (!proxyView) return undefined
if (isDirectMode) { if (isDirectMode) {
@@ -737,10 +743,12 @@ export const CurrentProxyCard = () => {
: undefined : undefined
}, [isDirectMode, proxyView, selectedGroup]) }, [isDirectMode, proxyView, selectedGroup])
latestProxyMemberRef.current = currentOption?.member ?? null
const handleGroupChange = useCallback( const handleGroupChange = useCallback(
(newGroupName: string) => { (newGroupName: string) => {
if (isGlobalMode || isDirectMode) return if (isGlobalMode || isDirectMode) return
setLastGroupName(newGroupName) setSelectedGroupName(newGroupName)
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName) writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
}, },
[isDirectMode, isGlobalMode, writeProfileScopedItem], [isDirectMode, isGlobalMode, writeProfileScopedItem],
@@ -785,7 +793,7 @@ export const CurrentProxyCard = () => {
color: 'text.secondary', color: 'text.secondary',
} }
const checkCurrentProxyDelay = useEffectEvent(async () => { const checkCurrentProxyDelay = useCallback(async () => {
if (autoCheckInProgressRef.current) return if (autoCheckInProgressRef.current) return
if (isDirectMode) return if (isDirectMode) return
@@ -794,7 +802,7 @@ export const CurrentProxyCard = () => {
if (!groupName || !proxyName) return if (!groupName || !proxyName) return
const proxyMember = currentOption?.member const proxyMember = latestProxyMemberRef.current
if (!proxyMember || !isInteractableMember(proxyMember)) { if (!proxyMember || !isInteractableMember(proxyMember)) {
debugLog( debugLog(
`[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`, `[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`,
@@ -804,7 +812,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = true autoCheckInProgressRef.current = true
const timeout = verge?.default_latency_timeout || 10000 const timeout = latestTimeoutRef.current || 10000
try { try {
debugLog( debugLog(
@@ -820,7 +828,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = false autoCheckInProgressRef.current = false
refreshProxy() refreshProxy()
} }
}) }, [isDirectMode, refreshProxy, selectedGroupName, selectedProxyName])
useEffect(() => { useEffect(() => {
if (isDirectMode) return if (isDirectMode) return
@@ -850,6 +858,7 @@ export const CurrentProxyCard = () => {
if (intervalTimer) clearTimeout(intervalTimer) if (intervalTimer) clearTimeout(intervalTimer)
} }
}, [ }, [
checkCurrentProxyDelay,
autoDelayIntervalMs, autoDelayIntervalMs,
isDirectMode, isDirectMode,
selectedGroupName, selectedGroupName,
@@ -4,7 +4,6 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useEffectEvent,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useReducer, useReducer,
@@ -121,6 +120,7 @@ export const EnhancedCanvasTrafficGraph = memo(
const drawFrameRef = useRef<number | undefined>(undefined) const drawFrameRef = useRef<number | undefined>(undefined)
const hoverFrameRef = useRef<number | undefined>(undefined) const hoverFrameRef = useRef<number | undefined>(undefined)
const mouseMoveFrameRef = useRef<number | undefined>(undefined) const mouseMoveFrameRef = useRef<number | undefined>(undefined)
const scheduleDrawGraphRef = useRef<() => void>(() => {})
const pendingMousePositionRef = useRef<{ const pendingMousePositionRef = useRef<{
clientX: number clientX: number
clientY: number clientY: number
@@ -189,11 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
} }
}, [displayData]) }, [displayData])
const handleFocusStateChange = useEffectEvent((focused: boolean) => { const handleFocusStateChange = useCallback((focused: boolean) => {
isWindowFocusedRef.current = focused isWindowFocusedRef.current = focused
scheduleDrawGraph() scheduleDrawGraphRef.current()
}) }, [])
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') { if (typeof window === 'undefined' || typeof document === 'undefined') {
@@ -217,7 +217,7 @@ export const EnhancedCanvasTrafficGraph = memo(
window.removeEventListener('blur', handleBlur) window.removeEventListener('blur', handleBlur)
document.removeEventListener('visibilitychange', handleVisibilityChange) document.removeEventListener('visibilitychange', handleVisibilityChange)
} }
}, []) }, [handleFocusStateChange])
const calculateY = useCallback( const calculateY = useCallback(
( (
@@ -906,21 +906,25 @@ export const EnhancedCanvasTrafficGraph = memo(
scheduleDrawGraph() scheduleDrawGraph()
}, [scheduleDrawGraph]) }, [scheduleDrawGraph])
const redrawGraph = useEffectEvent(() => scheduleDrawGraph()) useEffect(() => {
scheduleDrawGraphRef.current = scheduleDrawGraph
}, [scheduleDrawGraph])
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current const canvas = canvasRef.current
if (!canvas || typeof window === 'undefined') return if (!canvas || typeof window === 'undefined') return
if (typeof ResizeObserver === 'undefined') { if (typeof ResizeObserver === 'undefined') {
const handleResize = () => redrawGraph() const handleResize = () => scheduleDrawGraphRef.current()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => { return () => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
} }
const resizeObserver = new ResizeObserver(() => redrawGraph()) const resizeObserver = new ResizeObserver(() =>
scheduleDrawGraphRef.current(),
)
resizeObserver.observe(canvas) resizeObserver.observe(canvas)
return () => { return () => {
+7 -12
View File
@@ -14,7 +14,7 @@ import {
useTheme, useTheme,
Fade, Fade,
} from '@mui/material' } from '@mui/material'
import { useState, useMemo, useCallback, memo, FC } from 'react' import { useState, useMemo, memo, FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ProxyControlSwitches from '@/components/shared/proxy-control-switches' import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
@@ -152,15 +152,10 @@ export const ProxyTunCard: FC = () => {
showNotice.error(err) showNotice.error(err)
} }
const handleSystemTabClick = useCallback(() => { const handleTabChange = (tab: string) => {
setActiveTab('system') setActiveTab(tab)
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'system') localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab)
}, []) }
const handleTunTabClick = useCallback(() => {
setActiveTab('tun')
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'tun')
}, [])
const tabDescription = useMemo(() => { const tabDescription = useMemo(() => {
if (activeTab === 'system') { if (activeTab === 'system') {
@@ -196,14 +191,14 @@ export const ProxyTunCard: FC = () => {
> >
<TabButton <TabButton
isActive={activeTab === 'system'} isActive={activeTab === 'system'}
onClick={handleSystemTabClick} onClick={() => handleTabChange('system')}
icon={ComputerRounded} icon={ComputerRounded}
label={t('settings.sections.system.toggles.systemProxy')} label={t('settings.sections.system.toggles.systemProxy')}
hasIndicator={systemProxyIndicator} hasIndicator={systemProxyIndicator}
/> />
<TabButton <TabButton
isActive={activeTab === 'tun'} isActive={activeTab === 'tun'}
onClick={handleTunTabClick} onClick={() => handleTabChange('tun')}
icon={TroubleshootRounded} icon={TroubleshootRounded}
label={t('settings.sections.system.toggles.tunMode')} label={t('settings.sections.system.toggles.tunMode')}
hasIndicator={enable_tun_mode! && isTunModeAvailable} hasIndicator={enable_tun_mode! && isTunModeAvailable}
+20 -18
View File
@@ -8,7 +8,7 @@ import {
} from '@mui/icons-material' } from '@mui/icons-material'
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material' import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
@@ -22,23 +22,10 @@ import {
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { getSystemInfo } from '@/services/cmds' import { getSystemInfo } from '@/services/cmds'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { version as appVersion } from '@root/package.json' import { version as appVersion } from '@root/package.json'
import { EnhancedCard } from './enhanced-card' 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 = () => { export const SystemInfoCard = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
@@ -49,16 +36,31 @@ export const SystemInfoCard = () => {
const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true) const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true)
const { data: osInfo = '' } = useQuery({ const [osInfo, setOsInfo] = useState('')
queryKey: ['getSystemInfo'],
queryFn: getOsInfo,
})
const lastCheckUpdateText = useMemo( const lastCheckUpdateText = useMemo(
() => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'), () => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'),
[lastCheckUpdate], [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(() => { useEffect(() => {
if (!verge?.auto_check_update) return if (!verge?.auto_check_update) return
if (readLastCheckTime() !== null) return if (readLastCheckTime() !== null) return
+7 -5
View File
@@ -21,7 +21,6 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useEffectEvent,
useReducer, useReducer,
useRef, useRef,
useState, useState,
@@ -114,6 +113,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const setProfileLoading = useSetProfileLoading() const setProfileLoading = useSetProfileLoading()
const [showNextUpdate, setShowNextUpdate] = useState(false) const [showNextUpdate, setShowNextUpdate] = useState(false)
const showNextUpdateRef = useRef(false)
const [nextUpdateTime, setNextUpdateTime] = useState('') const [nextUpdateTime, setNextUpdateTime] = useState('')
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>( const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined, undefined,
@@ -203,6 +203,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
setShowNextUpdate(!showNextUpdate) setShowNextUpdate(!showNextUpdate)
} }
useEffect(() => {
showNextUpdateRef.current = showNextUpdate
}, [showNextUpdate])
useEffect(() => { useEffect(() => {
if (showNextUpdate) { if (showNextUpdate) {
fetchNextUpdateTime() fetchNextUpdateTime()
@@ -214,10 +218,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
updated, updated,
]) ])
const isNextUpdateShown = useEffectEvent(() => showNextUpdate)
useEffect(() => { useEffect(() => {
if (timerUpdateRevision === 0 || !isNextUpdateShown()) return if (timerUpdateRevision === 0 || !showNextUpdateRef.current) return
if (refreshTimeoutRef.current !== undefined) { if (refreshTimeoutRef.current !== undefined) {
clearTimeout(refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current)
@@ -234,7 +236,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
}, [fetchNextUpdateTime, timerUpdateRevision]) }, [fetchNextUpdateTime, timerUpdateRevision])
useEffect(() => { useEffect(() => {
if (completedUpdateRevision === 0 || !isNextUpdateShown()) return if (completedUpdateRevision === 0 || !showNextUpdateRef.current) return
fetchNextUpdateTime() fetchNextUpdateTime()
}, [completedUpdateRevision, fetchNextUpdateTime]) }, [completedUpdateRevision, fetchNextUpdateTime])
+14 -19
View File
@@ -10,7 +10,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' 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 { Controller, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next' 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( const handleOk = useLockFn(
handleSubmit(async (form) => { handleSubmit(async (form) => {
setLoading(true) setLoading(true)
@@ -408,15 +419,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel> <InputLabel>
{t('profiles.modals.profileForm.fields.useSystemProxy')} {t('profiles.modals.profileForm.fields.useSystemProxy')}
</InputLabel> </InputLabel>
<Switch <Switch checked={field.value!} {...field} color="primary" />
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.self_proxy', false)
}}
color="primary"
/>
</StyledBox> </StyledBox>
)} )}
/> />
@@ -429,15 +432,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel> <InputLabel>
{t('profiles.modals.profileForm.fields.useClashProxy')} {t('profiles.modals.profileForm.fields.useClashProxy')}
</InputLabel> </InputLabel>
<Switch <Switch checked={field.value!} {...field} color="primary" />
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.with_proxy', false)
}}
color="primary"
/>
</StyledBox> </StyledBox>
)} )}
/> />
-30
View File
@@ -1,5 +1,3 @@
import * as yaml from 'js-yaml'
import { import {
getRecord, getRecord,
rebindNode, rebindNode,
@@ -36,31 +34,3 @@ export const rebindProxyChainItems = (
delay: record?.history.at(-1)?.delay, 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, keyframes,
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import * as yaml from 'js-yaml'
import { import {
type Ref, type Ref,
useCallback, useCallback,
@@ -59,9 +60,18 @@ const chainPointerSensor = PointerSensor.configure({
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown } type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
interface ProxyChainProps { interface ProxyChainProps {
proxyChain: ProxyChainItem[] proxyChain: ProxyChainItem[]
onUpdateChain: (chain: ProxyChainItem[]) => void onUpdateChain: (chain: ProxyChainItem[]) => void
chainConfigData?: string | null
onMarkUnsavedChanges?: () => void onMarkUnsavedChanges?: () => void
mode?: string mode?: string
selectedGroup?: string | null 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 { interface ChainCardProps {
proxy: ProxyChainItem proxy: ProxyChainItem
index: number index: number
@@ -303,6 +328,7 @@ const SortableProxyChainItem = ({
export const ProxyChain = ({ export const ProxyChain = ({
proxyChain, proxyChain,
onUpdateChain, onUpdateChain,
chainConfigData,
onMarkUnsavedChanges, onMarkUnsavedChanges,
mode, mode,
selectedGroup, selectedGroup,
@@ -554,6 +580,23 @@ export const ProxyChain = ({
recordSelection, 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 ( return (
<Paper <Paper
elevation={1} elevation={1}
+3 -12
View File
@@ -76,7 +76,7 @@ interface GroupSelectMenuProps {
interface ProxyGroupsChainProps { interface ProxyGroupsChainProps {
mode: string mode: string
chainConfigItems?: ProxyChainItem[] | null chainConfigData?: string | null
onExitChainMode: () => void onExitChainMode: () => void
availableGroups: any[] availableGroups: any[]
activeSelectedGroup: string | null activeSelectedGroup: string | null
@@ -305,7 +305,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
mode, mode,
chainConfigItems, chainConfigData,
onExitChainMode, onExitChainMode,
availableGroups, availableGroups,
activeSelectedGroup, activeSelectedGroup,
@@ -338,16 +338,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
return [] 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( const candidateNodes = useMemo(
() => () =>
renderList.flatMap((item) => { renderList.flatMap((item) => {
@@ -530,6 +520,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
<ProxyChain <ProxyChain
proxyChain={currentProxyChain} proxyChain={currentProxyChain}
onUpdateChain={setProxyChain} onUpdateChain={setProxyChain}
chainConfigData={chainConfigData}
mode={mode} mode={mode}
selectedGroup={activeSelectedGroup} selectedGroup={activeSelectedGroup}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
+8 -34
View File
@@ -32,7 +32,6 @@ import {
} from '@/types/proxy-view' } from '@/types/proxy-view'
import { debugLog } from '@/utils/debug' import { debugLog } from '@/utils/debug'
import type { ProxyChainItem } from './proxy-chain-model'
import { ProxyEmptyState } from './proxy-empty-state' import { ProxyEmptyState } from './proxy-empty-state'
import { import {
resolveEmptyListReason, resolveEmptyListReason,
@@ -69,7 +68,7 @@ function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
interface Props { interface Props {
mode: string mode: string
isChainMode?: boolean isChainMode?: boolean
chainConfigItems?: ProxyChainItem[] | null chainConfigData?: string | null
onExitChainMode: () => void onExitChainMode: () => void
} }
@@ -184,7 +183,6 @@ function useProxyRenderState(
renderList, renderList,
onProxies, onProxies,
onHeadState, onHeadState,
setHeadState: setListHeadState,
handleCheckAll, handleCheckAll,
saveScrollPosition, saveScrollPosition,
getScrollPosition, getScrollPosition,
@@ -193,10 +191,10 @@ function useProxyRenderState(
function ChainProxyGroups(props: { function ChainProxyGroups(props: {
mode: string mode: string
chainConfigItems?: ProxyChainItem[] | null chainConfigData?: string | null
onExitChainMode: () => void onExitChainMode: () => void
}) { }) {
const { mode, chainConfigItems, onExitChainMode } = props const { mode, chainConfigData, onExitChainMode } = props
const { proxyView } = useProxiesData() const { proxyView } = useProxiesData()
const [selectedGroup, setSelectedGroup] = useState<string | null>(null) const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
@@ -350,7 +348,7 @@ function ChainProxyGroups(props: {
<Suspense fallback={<BaseLoading />}> <Suspense fallback={<BaseLoading />}>
<ProxyGroupsChain <ProxyGroupsChain
mode={mode} mode={mode}
chainConfigItems={chainConfigItems} chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
availableGroups={availableGroups} availableGroups={availableGroups}
activeSelectedGroup={activeSelectedGroup} activeSelectedGroup={activeSelectedGroup}
@@ -379,17 +377,12 @@ function NormalProxyGroups(props: { mode: string }) {
renderList, renderList,
onProxies, onProxies,
onHeadState, onHeadState,
setHeadState,
handleCheckAll, handleCheckAll,
getScrollPosition, getScrollPosition,
saveScrollPosition, saveScrollPosition,
} = useProxyRenderState(mode, false, null) } = useProxyRenderState(mode, false, null)
const emptyList = useEmptyRenderList() const emptyList = useEmptyRenderList()
const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout() 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) const renderFirstRef = useRef(true)
// Do not persist intermediate positions produced while restoring virtual scroll. // Do not persist intermediate positions produced while restoring virtual scroll.
const isRestoringRef = useRef(false) const isRestoringRef = useRef(false)
@@ -484,17 +477,9 @@ function NormalProxyGroups(props: { mode: string }) {
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
if (!isInteractableMember(member)) 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!) select(group.name, member.ref.name, group.now!, group.fixed!)
}, },
[globalGroupName, select, setHeadState], [select],
) )
const handleLocation = useStableCallback((group: ProxyGroupView) => { const handleLocation = useStableCallback((group: ProxyGroupView) => {
@@ -540,17 +525,6 @@ function NormalProxyGroups(props: { mode: string }) {
return Array.from(new Set(names)) return Array.from(new Set(names))
}, [renderList]) }, [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( const handleGroupToggle = useCallback(
async (group: ProxyGroupView) => { async (group: ProxyGroupView) => {
const index = renderList.findIndex( const index = renderList.findIndex(
@@ -634,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
/> />
</DragDropProvider> </DragDropProvider>
{mode === 'rule' && ( {(mode === 'rule' || mode === 'global') && (
<ProxyGroupNavigator <ProxyGroupNavigator
proxyGroupNames={proxyGroupNames} proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName} onGroupLocation={handleGroupLocationByName}
@@ -647,7 +621,7 @@ function NormalProxyGroups(props: { mode: string }) {
} }
export const ProxyGroups = (props: Props) => { 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 { profiles, isLoading: isProfilesLoading } = useProfiles()
const { isProxyViewPending } = useProxiesData() const { isProxyViewPending } = useProxiesData()
const { isRunningModePending } = useSystemData() const { isRunningModePending } = useSystemData()
@@ -671,7 +645,7 @@ export const ProxyGroups = (props: Props) => {
return isChainMode ? ( return isChainMode ? (
<ChainProxyGroups <ChainProxyGroups
mode={mode} mode={mode}
chainConfigItems={chainConfigItems} chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode} 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 { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
import delayManager, { type DelaySnapshot } from '@/services/delay' import delayManager, { type DelaySnapshot } from '@/services/delay'
import { import {
findCurrentGroupMember,
isInteractableMember, isInteractableMember,
resolveMember, resolveMember,
selectGlobalChainNodes, selectGlobalChainNodes,
@@ -210,10 +209,8 @@ export const useRenderList = (
const renderGroups = useMemo(() => { const renderGroups = useMemo(() => {
if (!proxyView) return [] if (!proxyView) return []
if (mode === 'rule') return proxyView.groups if (mode === 'rule') return proxyView.groups
const { global } = proxyView const global = proxyView.global ? [proxyView.global] : []
if (!global) return [] return mode === 'global' ? [...global, ...proxyView.groups] : global
const current = findCurrentGroupMember(proxyView, global)?.member
return current?.kind === 'group' ? [global, current.group] : [global]
}, [mode, proxyView]) }, [mode, proxyView])
// Every group this list draws, so a test settling in any of them re-sorts that group. // 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 cache = groupCacheRef.current
const retList = renderGroups.flatMap((group) => { const retList = renderGroups.flatMap((group) => {
const isFlat = group === proxyView.global
const headState = headStates[group.name] || DEFAULT_STATE const headState = headStates[group.name] || DEFAULT_STATE
const cached = cache.get(group.name) const cached = cache.get(group.name)
if ( if (
@@ -282,9 +279,7 @@ export const useRenderList = (
} }
const ret: IRenderItem[] = [ const ret: IRenderItem[] = [
isFlat {
? { type: 1, key: `head-${group.name}`, group, headState }
: {
type: 0, type: 0,
key: group.name, key: group.name,
group, group,
@@ -294,7 +289,7 @@ export const useRenderList = (
}, },
] ]
if (headState.open || isFlat) { if (headState.open || !useRule) {
const occurrences = filterSort( const occurrences = filterSort(
resolveOccurrences(proxyView, group), resolveOccurrences(proxyView, group),
group.name, group.name,
@@ -307,6 +302,9 @@ export const useRenderList = (
useRegularExpression: headState.filterUseRegularExpression, useRegularExpression: headState.filterUseRegularExpression,
}, },
) )
if (!useRule) {
ret.push({ type: 1, key: `head-${group.name}`, group, headState })
}
if (occurrences.length === 0) { if (occurrences.length === 0) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState }) ret.push({ type: 3, key: `empty-${group.name}`, group, headState })
} else if (col > 1) { } else if (col > 1) {
@@ -345,8 +343,9 @@ export const useRenderList = (
return ret return ret
}) })
const filtered = const filtered = !useRule
mode === 'rule' ? retList.filter((item) => !item.group.hidden) : retList ? retList.slice(1)
: retList.filter((item) => !item.group.hidden)
if ( if (
prevListRef.current.length === filtered.length && prevListRef.current.length === filtered.length &&
filtered.every((item, index) => item === prevListRef.current[index]) filtered.every((item, index) => item === prevListRef.current[index])
@@ -146,12 +146,7 @@ export const BackupHistoryViewer = ({
setRows([]) setRows([])
return return
} }
// Read the status here: as a dependency, the write below re-runs the fetch. if (!isLocal && webdavStatus === 'failed' && !options?.force) {
if (
!isLocal &&
getWebdavStatus(webdavSignature) === 'failed' &&
!options?.force
) {
setRows([]) setRows([])
return return
} }
@@ -185,7 +180,7 @@ export const BackupHistoryViewer = ({
setLoading(false) setLoading(false)
} }
}, },
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature], [buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus],
) )
useEffect(() => { 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 enable: boolean
listen: string listen: string
enhancedMode: 'fake-ip' | 'redir-host' enhancedMode: 'fake-ip' | 'redir-host'
@@ -215,9 +231,7 @@ type DnsFormValues = {
fallbackDomain: string fallbackDomain: string
nameserverPolicy: string nameserverPolicy: string
hosts: string // hosts设置,独立于dns hosts: string // hosts设置,独立于dns
} }>({
const DEFAULT_FORM_VALUES: DnsFormValues = {
enable: DEFAULT_DNS_CONFIG.enable, enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen, listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'], enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
@@ -246,70 +260,7 @@ const DEFAULT_FORM_VALUES: DnsFormValues = {
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '', DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '', nameserverPolicy: '',
hosts: '', 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. // Prevent YAML-to-form sync before configuration loads.
const [yamlContent, setYamlContent] = useReducer( const [yamlContent, setYamlContent] = useReducer(
@@ -318,7 +269,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
) )
const updateValuesFromConfig = useCallback((config: any) => { const updateValuesFromConfig = useCallback((config: any) => {
if (!config) return undefined if (!config) return
const dnsConfig = config.dns || {} const dnsConfig = config.dns || {}
const hostsConfig = config.hosts || {} const hostsConfig = config.hosts || {}
@@ -338,7 +289,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? fakeIpFilterMode ? fakeIpFilterMode
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'] : DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
const next: DnsFormValues = { setValues({
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable, enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen, listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode, enhancedMode: validEnhancedMode,
@@ -390,27 +341,127 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
nameserverPolicy: nameserverPolicy:
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '', formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
hosts: formatHosts(hostsConfig) || '', 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(() => { const resetToDefaults = useCallback(() => {
setValues(DEFAULT_FORM_VALUES) setValues({
setYamlContent(buildDnsYaml(DEFAULT_FORM_VALUES)) 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(() => { const updateValuesFromYaml = useCallback(() => {
try { try {
const parsedYaml = yaml.load(yamlContent) as any const parsedYaml = yaml.load(yamlContent) as any
if (!parsedYaml || parsedYaml.isInitValue) return if (!parsedYaml || parsedYaml.isInitValue) return
skipYamlSyncRef.current = true
updateValuesFromConfig(parsedYaml) updateValuesFromConfig(parsedYaml)
} catch { } catch {
showNotice.error('settings.modals.dns.errors.invalidYaml') showNotice.error('settings.modals.dns.errors.invalidYaml')
} }
}, [yamlContent, updateValuesFromConfig]) }, [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(() => { useEffect(() => {
return () => { return () => {
editorRef.current?.dispose() editorRef.current?.dispose()
@@ -425,8 +476,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (dnsConfig !== null) { if (dnsConfig !== null) {
const config = yaml.load(dnsConfig) as any const config = yaml.load(dnsConfig) as any
const loaded = updateValuesFromConfig(config) updateValuesFromConfig(config)
setYamlContent(loaded ? buildDnsYaml(loaded) : dnsConfig) setYamlContent(dnsConfig)
} else { } else {
resetToDefaults() resetToDefaults()
} }
@@ -455,7 +506,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (visualization) { if (visualization) {
config = {} config = {}
const dnsConfig = buildDnsConfig(values) const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) { if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig config.dns = dnsConfig
} }
@@ -535,10 +586,20 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? event.target.checked ? event.target.checked
: event.target.value : event.target.value
setValues((prev) => ({ setValues((prev) => {
const newValues = {
...prev, ...prev,
[field]: value, [field]: value,
})) }
if (visualization) {
setTimeout(() => {
updateYamlFromValues()
}, 0)
}
return newValues
})
} }
return ( return (
@@ -568,12 +629,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="contained" variant="contained"
size="small" size="small"
onClick={() => { onClick={() => {
if (visualization) { setVisualization((prev) => !prev)
setYamlContent(buildDnsYaml(values))
} else {
updateValuesFromYaml()
}
setVisualization(!visualization)
}} }}
> >
{visualization {visualization
@@ -13,7 +13,13 @@ import {
Typography, Typography,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' 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 { useTranslation } from 'react-i18next'
import { import {
@@ -35,7 +41,6 @@ import {
getSystemHostname, getSystemHostname,
} from '@/services/cmds' } from '@/services/cmds'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { debugLog } from '@/utils/debug' import { debugLog } from '@/utils/debug'
import getSystem from '@/utils/get-system' import getSystem from '@/utils/get-system'
@@ -123,10 +128,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
} = verge ?? {} } = verge ?? {}
const [value, setValue] = useState(() => buildInitialValues(null)) const [value, setValue] = useState(() => buildInitialValues(null))
const { data: embeddedServerPort } = useQuery({ const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
queryKey: ['getEmbeddedServerPort'], null,
queryFn: getEmbeddedServerPort, )
})
useEffect(() => {
getEmbeddedServerPort().then(setEmbeddedServerPort).catch(console.error)
}, [])
const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows]) 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 { Delete, ExpandLess, ExpandMore } from '@mui/icons-material'
import { import {
Button, Button,
@@ -13,6 +14,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { import {
forwardRef, forwardRef,
useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
@@ -155,20 +157,29 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const selectedProxyToken = selectedProxyOption const selectedProxyToken = selectedProxyOption
? proxyOptionToken(selectedProxyOption) ? proxyOptionToken(selectedProxyOption)
: '' : ''
// A group or member missing from the proxy view reads as the default. const selectedGroupExists =
const effectiveGroup = proxyGroups.some(({ name }) => name === values.group) !values.group || proxyGroups.some(({ name }) => name === values.group)
? values.group
: ''
const effectiveProxy = selectedProxyOption ? values.proxy : null
const latestProxyStateRef = useRef({ const latestProxyStateRef = useRef({
binding: effectiveProxy, binding: values.proxy,
options: proxyOptions, options: proxyOptions,
}) })
latestProxyStateRef.current = { latestProxyStateRef.current = {
binding: effectiveProxy, binding: values.proxy,
options: proxyOptions, 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 () => { const handleSave = async () => {
try { try {
await patchClash({ tunnels: draftTunnels }) await patchClash({ tunnels: draftTunnels })
@@ -479,7 +490,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
<Select <Select
size="small" size="small"
sx={{ width: 200, '> div': { py: '7.5px' } }} sx={{ width: 200, '> div': { py: '7.5px' } }}
value={effectiveGroup} value={values.group}
displayEmpty displayEmpty
onChange={(e) => { onChange={(e) => {
const nextGroup = e.target.value as string const nextGroup = e.target.value as string
@@ -558,7 +569,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
: null, : null,
})) }))
}} }}
disabled={!effectiveGroup} // 没选组就禁用 disabled={!values.group} // 没选组就禁用
> >
<MenuItem value=""> <MenuItem value="">
{t('settings.sections.clash.form.fields.tunnels.default')} {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 { BaseDialog } from '@/components/base'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import type {
IVergeTestItem_Deserialize,
IVergeTestItem_Serialize,
} from '@/services/bindings'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
interface Props { interface Props {
@@ -31,7 +27,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
const testList = verge?.test_list ?? [] const testList = verge?.test_list ?? []
const { control, ...formIns } = useForm<IVergeTestItem_Deserialize>({ const { control, ...formIns } = useForm<IVergeTestItem>({
defaultValues: { defaultValues: {
name: '', name: '',
icon: '', icon: '',
@@ -41,16 +37,11 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const patchTestList = async ( const patchTestList = async (
uid: string, uid: string,
patch: Partial<IVergeTestItem_Deserialize>, patch: Partial<IVergeTestItem>,
) => { ) => {
const newList = testList.map((x) => { const newList = testList.map((x) => {
if (x.uid === uid) { if (x.uid === uid) {
return { return { ...x, ...patch }
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 return x
}) })
@@ -86,12 +77,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
if (openType === 'new') { if (openType === 'new') {
const uid = nanoid() const uid = nanoid()
const item: IVergeTestItem_Serialize = { const item = { ...form, uid }
uid,
name: form.name ?? null,
icon: form.icon ?? null,
url: form.url ?? null,
}
const newList = [...testList, item] const newList = [...testList, item]
await patchVerge({ test_list: newList }) await patchVerge({ test_list: newList })
onChange(uid) onChange(uid)
+1 -6
View File
@@ -17,12 +17,7 @@ const PRESET_PROXY_NAMES = [
'COMPATIBLE', 'COMPATIBLE',
] ]
const identity = (previous: DelayUpdate, next: DelayUpdate): DelayUpdate => const identity = (_: DelayUpdate, next: DelayUpdate): DelayUpdate => next
previous.delay === next.delay &&
previous.elapsed === next.elapsed &&
previous.updatedAt === next.updatedAt
? previous
: next
const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 } const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 }
@@ -1,7 +1,7 @@
import { arrayMove } from '@dnd-kit/helpers' import { arrayMove } from '@dnd-kit/helpers'
import type { DragEndEvent } from '@dnd-kit/react' import type { DragEndEvent } from '@dnd-kit/react'
import { isSortable } from '@dnd-kit/react/sortable' 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[] } type MenuOrderAction = { type: 'sync'; payload: string[] }
@@ -79,11 +79,9 @@ export const useNavMenuOrder = <T extends { path: string }>({
configMenuOrder, configMenuOrder,
) )
const [syncedConfigOrder, setSyncedConfigOrder] = useState(configMenuOrder) useEffect(() => {
if (!areOrdersEqual(syncedConfigOrder, configMenuOrder)) {
setSyncedConfigOrder(configMenuOrder)
dispatchMenuOrder({ type: 'sync', payload: configMenuOrder }) dispatchMenuOrder({ type: 'sync', payload: configMenuOrder })
} }, [configMenuOrder])
const isDefaultOrder = useMemo( const isDefaultOrder = useMemo(
() => areOrdersEqual(menuOrder, defaultOrder), () => areOrdersEqual(menuOrder, defaultOrder),
+18 -17
View File
@@ -1,15 +1,11 @@
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material' import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
import { Box, Button, ButtonGroup, Typography } from '@mui/material' import { Box, Button, ButtonGroup, Typography } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { useEffect, useState } from 'react' import { useCallback, useEffect, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BaseDialog, BasePage, TooltipIcon } from '@/components/base' import { BaseDialog, BasePage, TooltipIcon } from '@/components/base'
import { ProviderButton } from '@/components/proxy/provider-button' 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 { ProxyGroups } from '@/components/proxy/proxy-groups'
import { import {
useAppRefreshers, useAppRefreshers,
@@ -44,13 +40,18 @@ const ProxyPage = () => {
}) })
const [chainConfirmOpen, setChainConfirmOpen] = useState(false) const [chainConfirmOpen, setChainConfirmOpen] = useState(false)
const [chainConfigItems, setChainConfigItems] = useState< const [chainConfigData, dispatchChainConfigData] = useReducer(
ProxyChainItem[] | null (_: string | null, action: string | null) => action,
>(null) null as string | null,
)
const { clashConfig } = useClashConfigData() const { clashConfig } = useClashConfigData()
const { refreshClashConfig } = useAppRefreshers() const { refreshClashConfig } = useAppRefreshers()
const updateChainConfigData = useCallback((value: string | null) => {
dispatchChainConfigData(value)
}, [])
const normalizedMode = clashConfig?.mode?.toLowerCase() const normalizedMode = clashConfig?.mode?.toLowerCase()
const curMode = isMode(normalizedMode) ? normalizedMode : undefined const curMode = isMode(normalizedMode) ? normalizedMode : undefined
const chainWarning = t('proxies.page.chain.warning') const chainWarning = t('proxies.page.chain.warning')
@@ -70,7 +71,6 @@ const ProxyPage = () => {
const applyChainMode = (enabled: boolean) => { const applyChainMode = (enabled: boolean) => {
setIsChainMode(enabled) setIsChainMode(enabled)
if (!enabled) setChainConfigItems(null)
localStorage.setItem('proxy-chain-mode-enabled', enabled.toString()) localStorage.setItem('proxy-chain-mode-enabled', enabled.toString())
} }
@@ -99,7 +99,10 @@ const ProxyPage = () => {
// 当开启链式代理模式时,获取配置数据 // 当开启链式代理模式时,获取配置数据
useEffect(() => { useEffect(() => {
if (!isChainMode) return if (!isChainMode) {
updateChainConfigData(null)
return
}
let cancelled = false let cancelled = false
@@ -110,21 +113,19 @@ const ProxyPage = () => {
if (!exitNode) { if (!exitNode) {
console.error('No proxy chain exit node found in localStorage') console.error('No proxy chain exit node found in localStorage')
if (!cancelled) { if (!cancelled) {
setChainConfigItems([]) updateChainConfigData('')
} }
return return
} }
const configData = await getRuntimeProxyChainConfig(exitNode) const configData = await getRuntimeProxyChainConfig(exitNode)
if (!cancelled) { if (!cancelled) {
setChainConfigItems( updateChainConfigData(configData || '')
configData ? parseChainConfigItems(configData) : [],
)
} }
} catch (error) { } catch (error) {
console.error('Failed to get runtime proxy chain config:', error) console.error('Failed to get runtime proxy chain config:', error)
if (!cancelled) { if (!cancelled) {
setChainConfigItems([]) updateChainConfigData('')
} }
} }
} }
@@ -134,7 +135,7 @@ const ProxyPage = () => {
return () => { return () => {
cancelled = true cancelled = true
} }
}, [isChainMode]) }, [isChainMode, updateChainConfigData])
useEffect(() => { useEffect(() => {
if (normalizedMode && !isMode(normalizedMode)) { if (normalizedMode && !isMode(normalizedMode)) {
@@ -203,7 +204,7 @@ const ProxyPage = () => {
<ProxyGroups <ProxyGroups
mode={curMode ?? 'rule'} mode={curMode ?? 'rule'}
isChainMode={isChainMode} isChainMode={isChainMode}
chainConfigItems={chainConfigItems} chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
/> />
<BaseDialog <BaseDialog
+149 -255
View File
@@ -23,10 +23,7 @@ export const commands = {
__TAURI_INVOKE<string[]>('get_network_interfaces'), __TAURI_INVOKE<string[]>('get_network_interfaces'),
getSystemHostname: () => __TAURI_INVOKE<string>('get_system_hostname'), getSystemHostname: () => __TAURI_INVOKE<string>('get_system_hostname'),
restartApp: () => __TAURI_INVOKE<null>('restart_app'), restartApp: () => __TAURI_INVOKE<null>('restart_app'),
installUpdate: ( installUpdate: (version: string, onEvent: Channel<DownloadEvent>) =>
version: string,
onEvent: Channel<DownloadEvent_Deserialize>,
) =>
__TAURI_INVOKE<InstallUpdateOutcome>('install_update', { __TAURI_INVOKE<InstallUpdateOutcome>('install_update', {
version, version,
onEvent, onEvent,
@@ -37,12 +34,11 @@ export const commands = {
upgradeClashCore: (force: boolean) => upgradeClashCore: (force: boolean) =>
__TAURI_INVOKE<CoreUpgradeReport>('upgrade_clash_core', { force }), __TAURI_INVOKE<CoreUpgradeReport>('upgrade_clash_core', { force }),
/** Returns one coherent core/service snapshot instead of independently refreshed state. */ /** Returns one coherent core/service snapshot instead of independently refreshed state. */
getRuntimeState: () => getRuntimeState: () => __TAURI_INVOKE<RunStateView>('get_runtime_state'),
__TAURI_INVOKE<RunStateView_Serialize>('get_runtime_state'),
getPendingFailures: () => getPendingFailures: () =>
__TAURI_INVOKE<PendingFailure[]>('get_pending_failures'), __TAURI_INVOKE<PendingFailure[]>('get_pending_failures'),
getSidecarFailure: () => getSidecarFailure: () =>
__TAURI_INVOKE<SidecarFailureSnapshot_Serialize>('get_sidecar_failure'), __TAURI_INVOKE<SidecarFailureSnapshot>('get_sidecar_failure'),
entryLightweightMode: () => __TAURI_INVOKE<null>('entry_lightweight_mode'), entryLightweightMode: () => __TAURI_INVOKE<null>('entry_lightweight_mode'),
installService: () => installService: () =>
__TAURI_INVOKE<ServiceInstallOutcome>('install_service'), __TAURI_INVOKE<ServiceInstallOutcome>('install_service'),
@@ -52,7 +48,7 @@ export const commands = {
repairService: () => __TAURI_INVOKE<ServiceInstallOutcome>('repair_service'), repairService: () => __TAURI_INVOKE<ServiceInstallOutcome>('repair_service'),
continueWithSidecar: () => __TAURI_INVOKE<null>('continue_with_sidecar'), continueWithSidecar: () => __TAURI_INVOKE<null>('continue_with_sidecar'),
syncRuntimeProviders: () => __TAURI_INVOKE<void>('sync_runtime_providers'), 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) => patchClashConfig: (payload: YamlMapping) =>
__TAURI_INVOKE<null>('patch_clash_config', { payload }), __TAURI_INVOKE<null>('patch_clash_config', { payload }),
patchClashMode: (payload: string) => patchClashMode: (payload: string) =>
@@ -60,11 +56,16 @@ export const commands = {
/** Reads the saved mode without depending on strict mihomo `/configs` deserialization. */ /** Reads the saved mode without depending on strict mihomo `/configs` deserialization. */
getClashMode: () => __TAURI_INVOKE<string | null>('get_clash_mode'), getClashMode: () => __TAURI_INVOKE<string | null>('get_clash_mode'),
changeClashCore: (clashCore: string) => changeClashCore: (clashCore: string) =>
__TAURI_INVOKE<CommandFailure_Serialize | null>('change_clash_core', { __TAURI_INVOKE<{
clashCore, /** 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: () => 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'), getProxyView: () => __TAURI_INVOKE<ProxyViewV1_Serialize>('get_proxy_view'),
getRuntimeYaml: () => __TAURI_INVOKE<string>('get_runtime_yaml'), getRuntimeYaml: () => __TAURI_INVOKE<string>('get_runtime_yaml'),
getRuntimeLogs: () => __TAURI_INVOKE<RuntimeLogs>('get_runtime_logs'), getRuntimeLogs: () => __TAURI_INVOKE<RuntimeLogs>('get_runtime_logs'),
@@ -72,7 +73,7 @@ export const commands = {
__TAURI_INVOKE<string>('get_runtime_proxy_chain_config', { __TAURI_INVOKE<string>('get_runtime_proxy_chain_config', {
proxyChainExitNode, proxyChainExitNode,
}), }),
updateProxyChainConfigInRuntime: (proxyChainConfig: YamlValue | null) => updateProxyChainConfigInRuntime: (proxyChainConfig: JsonValue | null) =>
__TAURI_INVOKE<ValidationOutcome>('update_proxy_chain_config_in_runtime', { __TAURI_INVOKE<ValidationOutcome>('update_proxy_chain_config_in_runtime', {
proxyChainConfig, proxyChainConfig,
}), }),
@@ -102,11 +103,18 @@ export const commands = {
takeDnsOverrideNotice: () => takeDnsOverrideNotice: () =>
__TAURI_INVOKE<boolean>('take_dns_override_notice'), __TAURI_INVOKE<boolean>('take_dns_override_notice'),
takeServiceFallbackNotice: () => takeServiceFallbackNotice: () =>
__TAURI_INVOKE<ServiceFallbackNotice | null>( __TAURI_INVOKE<
'take_service_fallback_notice', | { kind: 'unavailable' }
), | { kind: 'coreRejected'; reason: string }
| { kind: 'notAutoStarted' }
| null
>('take_service_fallback_notice'),
getCoreStartupError: () => 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: () => takeServiceRepairNotice: () =>
__TAURI_INVOKE<boolean>('take_service_repair_notice'), __TAURI_INVOKE<boolean>('take_service_repair_notice'),
takeServiceOwnerNotice: () => takeServiceOwnerNotice: () =>
@@ -132,12 +140,10 @@ export const commands = {
openDevtools: () => __TAURI_INVOKE<void>('open_devtools'), openDevtools: () => __TAURI_INVOKE<void>('open_devtools'),
exitApp: () => __TAURI_INVOKE<void>('exit_app'), exitApp: () => __TAURI_INVOKE<void>('exit_app'),
getNetworkInterfacesInfo: () => getNetworkInterfacesInfo: () =>
__TAURI_INVOKE<NetworkInterfaceView_Serialize[]>( __TAURI_INVOKE<NetworkInterfaceView[]>('get_network_interfaces_info'),
'get_network_interfaces_info',
),
getProfiles: () => __TAURI_INVOKE<IProfiles_Serialize>('get_profiles'), getProfiles: () => __TAURI_INVOKE<IProfiles_Serialize>('get_profiles'),
enhanceProfiles: () => __TAURI_INVOKE<ValidationOutcome>('enhance_profiles'), enhanceProfiles: () => __TAURI_INVOKE<ValidationOutcome>('enhance_profiles'),
patchProfilesConfig: (profiles: ProfileSelection_Deserialize) => patchProfilesConfig: (profiles: ProfileSelection) =>
__TAURI_INVOKE<ValidationOutcome>('patch_profiles_config', { profiles }), __TAURI_INVOKE<ValidationOutcome>('patch_profiles_config', { profiles }),
viewProfile: (index: string) => viewProfile: (index: string) =>
__TAURI_INVOKE<null>('view_profile', { index }), __TAURI_INVOKE<null>('view_profile', { index }),
@@ -145,12 +151,52 @@ export const commands = {
__TAURI_INVOKE<null>('patch_profile', { index, profile }), __TAURI_INVOKE<null>('patch_profile', { index, profile }),
createProfile: (item: ProfileCreate_Deserialize, fileData: string | null) => createProfile: (item: ProfileCreate_Deserialize, fileData: string | null) =>
__TAURI_INVOKE<null>('create_profile', { item, fileData }), __TAURI_INVOKE<null>('create_profile', { item, fileData }),
importProfile: (url: string, option: PrfOption_Deserialize | null) => importProfile: (
__TAURI_INVOKE<null>('import_profile', { url, option }), 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) => reorderProfile: (activeId: string, overId: string) =>
__TAURI_INVOKE<null>('reorder_profile', { activeId, overId }), __TAURI_INVOKE<null>('reorder_profile', { activeId, overId }),
updateProfile: (index: string, option: PrfOption_Deserialize | null) => updateProfile: (
__TAURI_INVOKE<null>('update_profile', { index, option }), 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) => deleteProfile: (index: string) =>
__TAURI_INVOKE<null>('delete_profile', { index }), __TAURI_INVOKE<null>('delete_profile', { index }),
readProfileFile: (index: string) => readProfileFile: (index: string) =>
@@ -178,14 +224,11 @@ export const commands = {
__TAURI_INVOKE<null>('delete_webdav_backup', { filename }), __TAURI_INVOKE<null>('delete_webdav_backup', { filename }),
restoreWebdavBackup: (filename: string) => restoreWebdavBackup: (filename: string) =>
__TAURI_INVOKE<null>('restore_webdav_backup', { filename }), __TAURI_INVOKE<null>('restore_webdav_backup', { filename }),
getUnlockItems: () => getUnlockItems: () => __TAURI_INVOKE<UnlockItem[]>('get_unlock_items'),
__TAURI_INVOKE<UnlockItem_Serialize[]>('get_unlock_items'), checkMediaUnlock: (onComplete: Channel<UnlockItem>) =>
checkMediaUnlock: (onComplete: Channel<UnlockItem_Deserialize>) => __TAURI_INVOKE<UnlockItem[]>('check_media_unlock', { onComplete }),
__TAURI_INVOKE<UnlockItem_Serialize[]>('check_media_unlock', {
onComplete,
}),
checkMediaUnlockItem: (name: string) => checkMediaUnlockItem: (name: string) =>
__TAURI_INVOKE<UnlockItem_Serialize>('check_media_unlock_item', { name }), __TAURI_INVOKE<UnlockItem>('check_media_unlock_item', { name }),
} }
/* Types */ /* Types */
@@ -194,20 +237,7 @@ export type AutoProxy = {
url: string url: string
} }
export type ClashInfo = ClashInfo_Serialize | ClashInfo_Deserialize export type ClashInfo = {
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 = {
/** clash core port */ /** clash core port */
mixed_port: number mixed_port: number
socks_port: number socks_port: number
@@ -226,21 +256,21 @@ export type CommandFailure =
/** Structured command failure returned to the frontend. */ /** Structured command failure returned to the frontend. */
export type CommandFailure_Deserialize = { export type CommandFailure_Deserialize = {
/** Stable frontend identifier, when classified. */ /** Stable frontend identifier, when classified. */
code?: string | null code: string | null
/** Human-readable diagnostic detail. */ /** Human-readable diagnostic detail. */
detail: string detail: string
/** Requested operation when known by the command. */ /** Requested operation when known by the command. */
operation?: FailedOperation | null operation: FailedOperation | null
} }
/** Structured command failure returned to the frontend. */ /** Structured command failure returned to the frontend. */
export type CommandFailure_Serialize = { export type CommandFailure_Serialize = {
/** Stable frontend identifier, when classified. */ /** Stable frontend identifier, when classified. */
code?: string code?: string | null
/** Human-readable diagnostic detail. */ /** Human-readable diagnostic detail. */
detail: string detail: string
/** Requested operation when known by the command. */ /** Requested operation when known by the command. */
operation?: FailedOperation operation?: FailedOperation | null
} }
/** Why the Core is not running, kept until a Core starts again. */ /** Why the Core is not running, kept until a Core starts again. */
@@ -263,26 +293,7 @@ export type DnsOverrideOutcome =
| { status: 'confirmation_required'; source: string } | { status: 'confirmation_required'; source: string }
/** Same wire shape as the updater plugin's JS `DownloadEvent`. */ /** Same wire shape as the updater plugin's JS `DownloadEvent`. */
export type DownloadEvent = DownloadEvent_Serialize | DownloadEvent_Deserialize export type DownloadEvent =
/** 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 =
| { | {
event: 'Started' event: 'Started'
data: { data: {
@@ -322,27 +333,14 @@ export type IProfiles_Serialize = {
/** ### `verge.yaml` schema */ /** ### `verge.yaml` schema */
export type IVerge = IVerge_Serialize | IVerge_Deserialize export type IVerge = IVerge_Serialize | IVerge_Deserialize
export type IVergeTestItem = export type IVergeTestItem = {
| IVergeTestItem_Serialize
| IVergeTestItem_Deserialize
export type IVergeTestItem_Deserialize = {
uid?: string | null uid?: string | null
name?: string | null name?: string | null
icon?: string | null icon?: string | null
url?: string | null url?: string | null
} }
export type IVergeTestItem_Serialize = { export type IVergeTheme = {
uid: string | null
name: string | null
icon: string | null
url: string | null
}
export type IVergeTheme = IVergeTheme_Serialize | IVergeTheme_Deserialize
export type IVergeTheme_Deserialize = {
primary_color?: string | null primary_color?: string | null
secondary_color?: string | null secondary_color?: string | null
primary_text?: string | null primary_text?: string | null
@@ -355,19 +353,6 @@ export type IVergeTheme_Deserialize = {
css_injection?: string | null 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 */ /** ### `verge.yaml` schema */
export type IVerge_Deserialize = { export type IVerge_Deserialize = {
/** silent | error | warn | info | debug | trace */ /** silent | error | warn | info | debug | trace */
@@ -410,7 +395,7 @@ export type IVerge_Deserialize = {
proxy_auto_config?: boolean | null proxy_auto_config?: boolean | null
pac_file_content?: string | null pac_file_content?: string | null
proxy_host?: string | null proxy_host?: string | null
theme_setting?: IVergeTheme_Deserialize | null theme_setting?: IVergeTheme | null
web_ui_list?: string[] | null web_ui_list?: string[] | null
clash_core?: string | null clash_core?: string | null
/** format: {func},{key} */ /** format: {func},{key} */
@@ -426,7 +411,7 @@ export type IVerge_Deserialize = {
auto_delay_detection_interval_minutes?: number | null auto_delay_detection_interval_minutes?: number | null
enable_builtin_enhanced?: boolean | null enable_builtin_enhanced?: boolean | null
proxy_layout_column?: number | null proxy_layout_column?: number | null
test_list?: IVergeTestItem_Deserialize[] | null test_list?: IVergeTestItem[] | null
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */ /** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
auto_log_clean?: number | null auto_log_clean?: number | null
enable_auto_backup_schedule?: boolean | null enable_auto_backup_schedule?: boolean | null
@@ -475,13 +460,13 @@ export type IVerge_Serialize = {
traffic_graph: boolean | null traffic_graph: boolean | null
/** show memory info (only for Mihomo) */ /** show memory info (only for Mihomo) */
enable_memory_usage: boolean | null enable_memory_usage: boolean | null
enable_group_icon?: boolean enable_group_icon?: boolean | null
pause_render_traffic_stats_on_blur: boolean | null pause_render_traffic_stats_on_blur: boolean | null
common_tray_icon?: boolean common_tray_icon?: boolean | null
menu_icon?: string menu_icon?: string | null
menu_order?: string[] menu_order?: string[] | null
proxy_group_tools_position?: string proxy_group_tools_position?: string | null
notice_position?: string notice_position?: string | null
collapse_navbar: boolean | null collapse_navbar: boolean | null
sysproxy_tray_icon: boolean | null sysproxy_tray_icon: boolean | null
tun_tray_icon: boolean | null tun_tray_icon: boolean | null
@@ -500,11 +485,11 @@ export type IVerge_Serialize = {
proxy_auto_config: boolean | null proxy_auto_config: boolean | null
pac_file_content: string | null pac_file_content: string | null
proxy_host: string | null proxy_host: string | null
theme_setting: IVergeTheme_Serialize | null theme_setting: IVergeTheme | null
web_ui_list: string[] | null web_ui_list: string[] | null
clash_core?: string clash_core?: string | null
/** format: {func},{key} */ /** format: {func},{key} */
hotkeys?: string[] hotkeys?: string[] | null
enable_global_hotkey: boolean | null enable_global_hotkey: boolean | null
home_cards: JsonValue | null home_cards: JsonValue | null
auto_close_connection: boolean | null auto_close_connection: boolean | null
@@ -516,7 +501,7 @@ export type IVerge_Serialize = {
auto_delay_detection_interval_minutes: number | null auto_delay_detection_interval_minutes: number | null
enable_builtin_enhanced: boolean | null enable_builtin_enhanced: boolean | null
proxy_layout_column: number | null proxy_layout_column: number | null
test_list: IVergeTestItem_Serialize[] | null test_list: IVergeTestItem[] | null
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */ /** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
auto_log_clean: number | null auto_log_clean: number | null
enable_auto_backup_schedule: boolean | null enable_auto_backup_schedule: boolean | null
@@ -528,10 +513,10 @@ export type IVerge_Serialize = {
verge_socks_enabled: boolean | null verge_socks_enabled: boolean | null
verge_port: number | null verge_port: number | null
verge_http_enabled: boolean | null verge_http_enabled: boolean | null
webdav_url?: string webdav_url?: string | null
webdav_username?: string webdav_username?: string | null
webdav_password?: string webdav_password?: string | null
tray_proxy_groups_display_mode?: string tray_proxy_groups_display_mode?: string | null
tray_inline_outbound_modes: boolean | null tray_inline_outbound_modes: boolean | null
enable_auto_light_weight_mode: boolean | null enable_auto_light_weight_mode: boolean | null
/** 自动进入轻量模式的延迟(分钟) */ /** 自动进入轻量模式的延迟(分钟) */
@@ -593,17 +578,7 @@ export type LocalBackupFile = {
content_length: number content_length: number
} }
export type NetworkInterfaceView = export type NetworkInterfaceView = {
| NetworkInterfaceView_Serialize
| NetworkInterfaceView_Deserialize
export type NetworkInterfaceView_Deserialize = {
name: string
addr: InterfaceAddress[]
mac_addr?: string | null
}
export type NetworkInterfaceView_Serialize = {
name: string name: string
addr: InterfaceAddress[] addr: InterfaceAddress[]
mac_addr: string | null mac_addr: string | null
@@ -662,20 +637,7 @@ export type PendingFailure = {
sequence: number sequence: number
} }
export type PlatformVergeFields = 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 = {
tray_icon?: string | null tray_icon?: string | null
verge_redir_port?: number | null verge_redir_port?: number | null
verge_redir_enabled?: boolean | null verge_redir_enabled?: boolean | null
@@ -701,7 +663,7 @@ export type PrfItem_Deserialize = {
file?: string | null file?: string | null
desc?: string | null desc?: string | null
url?: string | null url?: string | null
selected?: PrfSelected_Deserialize[] | null selected?: PrfSelected[] | null
extra?: PrfExtra | null extra?: PrfExtra | null
updated?: number | null updated?: number | null
option?: PrfOption_Deserialize | null option?: PrfOption_Deserialize | null
@@ -714,13 +676,13 @@ export type PrfItem_Serialize = {
type: string | null type: string | null
name: string | null name: string | null
file: string | null file: string | null
desc?: string desc?: string | null
url?: string url?: string | null
selected?: PrfSelected_Serialize[] selected?: PrfSelected[] | null
extra?: PrfExtra extra?: PrfExtra | null
updated: number | null updated: number | null
option?: PrfOption_Serialize option?: PrfOption_Serialize | null
home?: string home?: string | null
} }
export type PrfOption = PrfOption_Serialize | PrfOption_Deserialize export type PrfOption = PrfOption_Serialize | PrfOption_Deserialize
@@ -746,18 +708,18 @@ export type PrfOption_Deserialize = {
} }
export type PrfOption_Serialize = { export type PrfOption_Serialize = {
user_agent?: string user_agent?: string | null
with_proxy?: boolean with_proxy?: boolean | null
self_proxy?: boolean self_proxy?: boolean | null
update_interval?: number update_interval?: number | null
/** /**
* HTTP request timeout in seconds * HTTP request timeout in seconds
* default is 60 seconds * default is 60 seconds
*/ */
timeout_seconds?: number timeout_seconds?: number | null
/** default is `false` */ /** default is `false` */
danger_accept_invalid_certs?: boolean danger_accept_invalid_certs?: boolean | null
allow_auto_update?: boolean allow_auto_update?: boolean | null
merge: string | null merge: string | null
script: string | null script: string | null
rules: string | null rules: string | null
@@ -765,18 +727,11 @@ export type PrfOption_Serialize = {
groups: string | null groups: string | null
} }
export type PrfSelected = PrfSelected_Serialize | PrfSelected_Deserialize export type PrfSelected = {
export type PrfSelected_Deserialize = {
name?: string | null name?: string | null
now?: 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 = ProfileCreate_Serialize | ProfileCreate_Deserialize
export type ProfileCreate_Deserialize = { export type ProfileCreate_Deserialize = {
@@ -806,7 +761,7 @@ export type ProfileDnsSettings_Deserialize = {
export type ProfileDnsSettings_Serialize = { export type ProfileDnsSettings_Serialize = {
enabled: boolean enabled: boolean
confirmation?: string confirmation?: string | null
} }
export type ProfilePatch = ProfilePatch_Serialize | ProfilePatch_Deserialize export type ProfilePatch = ProfilePatch_Serialize | ProfilePatch_Deserialize
@@ -817,7 +772,7 @@ export type ProfilePatch_Deserialize = {
desc?: string | null desc?: string | null
file?: string | null file?: string | null
url?: string | null url?: string | null
selected?: PrfSelected_Deserialize[] | null selected?: PrfSelected[] | null
extra?: PrfExtra | null extra?: PrfExtra | null
updated?: number | null updated?: number | null
option?: PrfOption_Deserialize | null option?: PrfOption_Deserialize | null
@@ -829,24 +784,16 @@ export type ProfilePatch_Serialize = {
desc: string | null desc: string | null
file: string | null file: string | null
url: string | null url: string | null
selected: PrfSelected_Serialize[] | null selected: PrfSelected[] | null
extra: PrfExtra | null extra: PrfExtra | null
updated: number | null updated: number | null
option: PrfOption_Serialize | null option: PrfOption_Serialize | null
} }
export type ProfileSelection = export type ProfileSelection = {
| ProfileSelection_Serialize
| ProfileSelection_Deserialize
export type ProfileSelection_Deserialize = {
current?: string | null current?: string | null
} }
export type ProfileSelection_Serialize = {
current: string | null
}
export type ProfileUpdatePayload = { export type ProfileUpdatePayload = {
uid: string uid: string
} }
@@ -866,11 +813,11 @@ export type ProxyGroupView =
export type ProxyGroupView_Deserialize = { export type ProxyGroupView_Deserialize = {
name: string name: string
type: string type: string
now?: string | null now: string | null
fixed?: string | null fixed: string | null
hidden?: boolean | null hidden: boolean | null
icon?: string | null icon: string | null
testUrl?: string | null testUrl: string | null
history: DelayHistory[] history: DelayHistory[]
members: ProxyMemberRef[] members: ProxyMemberRef[]
} & ProxyCapabilities } & ProxyCapabilities
@@ -878,11 +825,11 @@ export type ProxyGroupView_Deserialize = {
export type ProxyGroupView_Serialize = { export type ProxyGroupView_Serialize = {
name: string name: string
type: string type: string
now?: string now?: string | null
fixed?: string fixed?: string | null
hidden?: boolean hidden?: boolean | null
icon?: string icon?: string | null
testUrl?: string testUrl?: string | null
history: DelayHistory[] history: DelayHistory[]
members: ProxyMemberRef[] members: ProxyMemberRef[]
} & ProxyCapabilities } & ProxyCapabilities
@@ -908,10 +855,10 @@ export type ProxyNodeView_Deserialize = {
name: string name: string
type: string type: string
history: DelayHistory[] history: DelayHistory[]
id?: string | null id: string | null
hidden?: boolean | null hidden: boolean | null
icon?: string | null icon: string | null
testUrl?: string | null testUrl: string | null
source: ProxyNodeSource source: ProxyNodeSource
} & ProxyCapabilities } & ProxyCapabilities
@@ -920,10 +867,10 @@ export type ProxyNodeView_Serialize = {
name: string name: string
type: string type: string
history: DelayHistory[] history: DelayHistory[]
id?: string id?: string | null
hidden?: boolean hidden?: boolean | null
icon?: string icon?: string | null
testUrl?: string testUrl?: string | null
source: ProxyNodeSource source: ProxyNodeSource
} & ProxyCapabilities } & ProxyCapabilities
@@ -944,16 +891,16 @@ export type ProxyProviderView =
export type ProxyProviderView_Deserialize = { export type ProxyProviderView_Deserialize = {
name: string name: string
vehicleType: ProxyProviderVehicleType vehicleType: ProxyProviderVehicleType
updatedAt?: string | null updatedAt: string | null
subscriptionInfo?: ProxySubscriptionInfo | null subscriptionInfo: ProxySubscriptionInfo | null
proxyRecordIds: string[] proxyRecordIds: string[]
} }
export type ProxyProviderView_Serialize = { export type ProxyProviderView_Serialize = {
name: string name: string
vehicleType: ProxyProviderVehicleType vehicleType: ProxyProviderVehicleType
updatedAt?: string updatedAt?: string | null
subscriptionInfo?: ProxySubscriptionInfo subscriptionInfo?: ProxySubscriptionInfo | null
proxyRecordIds: string[] proxyRecordIds: string[]
} }
@@ -974,8 +921,8 @@ export type ProxyViewV1_Deserialize = {
schemaVersion: number schemaVersion: number
orderSource: ProxyViewOrderSource orderSource: ProxyViewOrderSource
providerState: ProxyViewProviderState providerState: ProxyViewProviderState
global?: ProxyGroupView_Deserialize | null global: ProxyGroupView_Deserialize | null
direct?: string | null direct: string | null
groups: ProxyGroupView_Deserialize[] groups: ProxyGroupView_Deserialize[]
records: { [key in string]: ProxyNodeView_Deserialize } records: { [key in string]: ProxyNodeView_Deserialize }
standalone: string[] standalone: string[]
@@ -994,22 +941,7 @@ export type ProxyViewV1_Serialize = {
providers: ProxyProviderView_Serialize[] providers: ProxyProviderView_Serialize[]
} }
export type RunStateView = RunStateView_Serialize | RunStateView_Deserialize export type RunStateView = {
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 = {
mode: RunningMode mode: RunningMode
service: ServiceHealthKind service: ServiceHealthKind
serviceUnavailableReason: string | null serviceUnavailableReason: string | null
@@ -1048,18 +980,7 @@ export type ServiceInstallOutcome =
| { status: 'sidecar'; reason: string } | { status: 'sidecar'; reason: string }
/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */ /** A removal has a revision too, so a delayed read cannot restore a resolved failure. */
export type SidecarFailureSnapshot = 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 = {
revision: number revision: number
detail: string | null detail: string | null
} }
@@ -1080,16 +1001,7 @@ export type ToggleableProxyPort = {
port: number port: number
} }
export type UnlockItem = UnlockItem_Serialize | UnlockItem_Deserialize export type UnlockItem = {
export type UnlockItem_Deserialize = {
name: string
status: string
region?: string | null
check_time?: string | null
}
export type UnlockItem_Serialize = {
name: string name: string
status: string status: string
region: string | null region: string | null
@@ -1115,11 +1027,7 @@ export type ValidationOutcome =
export type ValidationSkipReason = 'exiting' | 'debounced' export type ValidationSkipReason = 'exiting' | 'debounced'
export type VergeEventPayloads = export type VergeEventPayloads = {
| VergeEventPayloads_Serialize
| VergeEventPayloads_Deserialize
export type VergeEventPayloads_Deserialize = {
'verge://refresh-clash-config': null 'verge://refresh-clash-config': null
'verge://refresh-verge-config': null 'verge://refresh-verge-config': null
'verge://refresh-profiles': null 'verge://refresh-profiles': null
@@ -1129,21 +1037,7 @@ export type VergeEventPayloads_Deserialize = {
'verge://timer-updated': string 'verge://timer-updated': string
'profile-update-started': ProfileUpdatePayload 'profile-update-started': ProfileUpdatePayload
'profile-update-completed': ProfileUpdatePayload 'profile-update-completed': ProfileUpdatePayload
'verge://run-state-changed': RunStateView_Deserialize 'verge://run-state-changed': RunStateView
'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://pending-failures-changed': null 'verge://pending-failures-changed': null
} }
+1 -1
View File
@@ -19,7 +19,7 @@ import type {
export type { export type {
RunningMode, RunningMode,
RunStateView_Serialize as RunState, RunStateView as RunState,
FailedOperation, FailedOperation,
PendingFailure, PendingFailure,
SidecarFailureSnapshot, SidecarFailureSnapshot,
+1 -4
View File
@@ -1,4 +1 @@
export type { export type { NoticeStatus, VergeEventPayloads } from './bindings'
NoticeStatus,
VergeEventPayloads_Serialize as VergeEventPayloads,
} from './bindings'
+4
View File
@@ -5,12 +5,16 @@ import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import svgr from 'vite-plugin-svgr' import svgr from 'vite-plugin-svgr'
import { reactDiagnostics } from './scripts/react/vite.mts'
export default defineConfig({ export default defineConfig({
root: 'src', root: 'src',
cacheDir: path.resolve('node_modules/.vite/default'),
server: { port: 3000 }, server: { port: 3000 },
plugins: [ plugins: [
svgr(), svgr(),
react(), react(),
...reactDiagnostics(process.env.CVR_REACT_DIAGNOSTICS),
legacy({ legacy({
modernTargets: ['edge>=109', 'safari>=14'], modernTargets: ['edge>=109', 'safari>=14'],
renderLegacyChunks: false, renderLegacyChunks: false,