Refactor VPN proxy routing and session handling
This commit is contained in:
20
apps/windows-client/package-lock.json
generated
20
apps/windows-client/package-lock.json
generated
@@ -9,6 +9,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
@@ -1433,6 +1435,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-dialog": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1771,6 +1782,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "1.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
|
||||||
|
"integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
@@ -24,4 +26,3 @@
|
|||||||
"vite": "^7.0.0"
|
"vite": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
apps/windows-client/src-tauri/Cargo.lock
generated
160
apps/windows-client/src-tauri/Cargo.lock
generated
@@ -1932,6 +1932,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2396,6 +2397,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfd"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||||
|
dependencies = [
|
||||||
|
"block2",
|
||||||
|
"dispatch2",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"gtk-sys",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation",
|
||||||
|
"raw-window-handle",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
@@ -3040,6 +3065,64 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin"
|
||||||
|
version = "2.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"glob",
|
||||||
|
"plist",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-dialog"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"raw-window-handle",
|
||||||
|
"rfd",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-fs"
|
||||||
|
version = "2.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"log",
|
||||||
|
"objc2-foundation",
|
||||||
|
"percent-encoding",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"toml 1.1.2+spec-1.1.0",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.11.3"
|
version = "2.11.3"
|
||||||
@@ -3618,6 +3701,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4047,6 +4131,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.53.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -4080,13 +4173,30 @@ dependencies = [
|
|||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm 0.52.6",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
|
"windows_aarch64_msvc 0.53.1",
|
||||||
|
"windows_i686_gnu 0.53.1",
|
||||||
|
"windows_i686_gnullvm 0.53.1",
|
||||||
|
"windows_i686_msvc 0.53.1",
|
||||||
|
"windows_x86_64_gnu 0.53.1",
|
||||||
|
"windows_x86_64_gnullvm 0.53.1",
|
||||||
|
"windows_x86_64_msvc 0.53.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-threading"
|
name = "windows-threading"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4117,6 +4227,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4129,6 +4245,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4141,12 +4263,24 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4159,6 +4293,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4171,6 +4311,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4183,6 +4329,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4195,6 +4347,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.5.40"
|
version = "0.5.40"
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
tauri-plugin-dialog = "2.7.1"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.",
|
"description": "Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": ["core:default"]
|
"permissions": ["core:default", "dialog:allow-open"]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.","local":true,"windows":["main"],"permissions":["core:default"]}}
|
{"default":{"identifier":"default","description":"Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.","local":true,"windows":["main"],"permissions":["core:default","dialog:allow-open"]}}
|
||||||
@@ -2191,6 +2191,72 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:deny-unminimize",
|
"const": "core:window:deny-unminimize",
|
||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:default",
|
||||||
|
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-ask",
|
||||||
|
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-confirm",
|
||||||
|
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-message",
|
||||||
|
"markdownDescription": "Enables the message command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-open",
|
||||||
|
"markdownDescription": "Enables the open command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-save",
|
||||||
|
"markdownDescription": "Enables the save command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-ask",
|
||||||
|
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-confirm",
|
||||||
|
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-message",
|
||||||
|
"markdownDescription": "Denies the message command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-open",
|
||||||
|
"markdownDescription": "Denies the open command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-save",
|
||||||
|
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2191,6 +2191,72 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:deny-unminimize",
|
"const": "core:window:deny-unminimize",
|
||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:default",
|
||||||
|
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-ask",
|
||||||
|
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-confirm",
|
||||||
|
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-message",
|
||||||
|
"markdownDescription": "Enables the message command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-open",
|
||||||
|
"markdownDescription": "Enables the open command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:allow-save",
|
||||||
|
"markdownDescription": "Enables the save command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-ask",
|
||||||
|
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-confirm",
|
||||||
|
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the message command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-message",
|
||||||
|
"markdownDescription": "Denies the message command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the open command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-open",
|
||||||
|
"markdownDescription": "Denies the open command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the save command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "dialog:deny-save",
|
||||||
|
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
#[cfg(not(test))]
|
||||||
|
use crate::adapters::proxy_router::{
|
||||||
|
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
||||||
|
ProxyRouterRequest,
|
||||||
|
};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItemType, Protocol,
|
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItemType, Protocol,
|
||||||
ProxyProtocol, Target,
|
ProxyProtocol, Target,
|
||||||
@@ -7,11 +12,6 @@ use crate::proxy_router::{
|
|||||||
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
||||||
ProxyRouterRequest,
|
ProxyRouterRequest,
|
||||||
};
|
};
|
||||||
#[cfg(not(test))]
|
|
||||||
use crate::adapters::proxy_router::{
|
|
||||||
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
|
||||||
ProxyRouterRequest,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const PROXIFYRE_ADAPTER_ID: &str = "proxifyre";
|
pub const PROXIFYRE_ADAPTER_ID: &str = "proxifyre";
|
||||||
@@ -45,7 +45,10 @@ impl ProxiFyreAdapter {
|
|||||||
if app_names.is_empty() {
|
if app_names.is_empty() {
|
||||||
return Err(ProxyRouterError::new(
|
return Err(ProxyRouterError::new(
|
||||||
ProxyRouterErrorKind::EmptyProfileItems,
|
ProxyRouterErrorKind::EmptyProfileItems,
|
||||||
format!("В профиле '{}' нет приложений для маршрутизации", profile.id),
|
format!(
|
||||||
|
"В профиле '{}' нет приложений для маршрутизации",
|
||||||
|
profile.id
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,7 +206,10 @@ impl SingBoxConfigChecker for SingBoxCommandChecker {
|
|||||||
let _ = fs::remove_file(&config_path);
|
let _ = fs::remove_file(&config_path);
|
||||||
SingBoxConfigError::new(
|
SingBoxConfigError::new(
|
||||||
SingBoxConfigErrorKind::CheckFailed,
|
SingBoxConfigErrorKind::CheckFailed,
|
||||||
format!("Не удалось выполнить '{} check': {error}", binary_path.display()),
|
format!(
|
||||||
|
"Не удалось выполнить '{} check': {error}",
|
||||||
|
binary_path.display()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let _ = fs::remove_file(&config_path);
|
let _ = fs::remove_file(&config_path);
|
||||||
@@ -320,14 +323,20 @@ fn ensure_local_singbox_target(
|
|||||||
else {
|
else {
|
||||||
return Err(SingBoxConfigError::new(
|
return Err(SingBoxConfigError::new(
|
||||||
SingBoxConfigErrorKind::MissingRequiredComponent,
|
SingBoxConfigErrorKind::MissingRequiredComponent,
|
||||||
format!("Локальная цель '{}' требует состояние компонента sing-box", target.id),
|
format!(
|
||||||
|
"Локальная цель '{}' требует состояние компонента sing-box",
|
||||||
|
target.id
|
||||||
|
),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
if !component_is_running(status) {
|
if !component_is_running(status) {
|
||||||
return Err(SingBoxConfigError::new(
|
return Err(SingBoxConfigError::new(
|
||||||
SingBoxConfigErrorKind::RequiredComponentNotRunning,
|
SingBoxConfigErrorKind::RequiredComponentNotRunning,
|
||||||
format!("Локальная цель '{}' требует установленный и запущенный sing-box", target.id),
|
format!(
|
||||||
|
"Локальная цель '{}' требует установленный и запущенный sing-box",
|
||||||
|
target.id
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,6 @@ use crate::adapters::proxy_router::{
|
|||||||
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
||||||
ProxyRouterRequest,
|
ProxyRouterRequest,
|
||||||
};
|
};
|
||||||
#[cfg(test)]
|
|
||||||
use crate::proxifyre::ProxiFyreAdapter;
|
|
||||||
#[cfg(test)]
|
|
||||||
use crate::proxy_router::{
|
|
||||||
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
|
||||||
ProxyRouterRequest,
|
|
||||||
};
|
|
||||||
use crate::component_detection::{
|
use crate::component_detection::{
|
||||||
detect_proxyfier_install, detect_proxyfier_install_with_host,
|
detect_proxyfier_install, detect_proxyfier_install_with_host,
|
||||||
proxyfier_component_from_detection, DetectedProxyfier, ProxyfierDetectionHost,
|
proxyfier_component_from_detection, DetectedProxyfier, ProxyfierDetectionHost,
|
||||||
@@ -22,9 +15,17 @@ use crate::models::{
|
|||||||
ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol, Target,
|
ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol, Target,
|
||||||
TargetInput, TargetKind,
|
TargetInput, TargetKind,
|
||||||
};
|
};
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::proxifyre::ProxiFyreAdapter;
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::proxy_router::{
|
||||||
|
ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
|
||||||
|
ProxyRouterRequest,
|
||||||
|
};
|
||||||
use crate::storage::{default_config_root, JsonStorage};
|
use crate::storage::{default_config_root, JsonStorage};
|
||||||
use crate::validation::{normalize_profile, normalize_target, ValidationError};
|
use crate::validation::{normalize_profile, normalize_target, ValidationError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -101,6 +102,14 @@ pub struct StatusResponse {
|
|||||||
pub generated_config_path: String,
|
pub generated_config_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SavedStateResponse {
|
||||||
|
pub profiles: Vec<ProfileDto>,
|
||||||
|
pub targets: Vec<TargetDto>,
|
||||||
|
pub generated_config_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ProfileInputDto {
|
pub struct ProfileInputDto {
|
||||||
@@ -321,8 +330,20 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_status(state: tauri::State<'_, CommandState>) -> Result<StatusResponse, CommandError> {
|
pub async fn get_status(
|
||||||
build_status(&state.storage())
|
state: tauri::State<'_, CommandState>,
|
||||||
|
) -> Result<StatusResponse, CommandError> {
|
||||||
|
let storage = state.storage();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || build_status(&storage))
|
||||||
|
.await
|
||||||
|
.map_err(background_task_error)?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_saved_state(
|
||||||
|
state: tauri::State<'_, CommandState>,
|
||||||
|
) -> Result<SavedStateResponse, CommandError> {
|
||||||
|
read_saved_state(&state.storage())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -354,10 +375,13 @@ pub fn save_target(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_components(
|
pub async fn get_components(
|
||||||
state: tauri::State<'_, CommandState>,
|
state: tauri::State<'_, CommandState>,
|
||||||
) -> Result<Vec<ComponentStatusDto>, CommandError> {
|
) -> Result<Vec<ComponentStatusDto>, CommandError> {
|
||||||
read_components(&state.storage())
|
let storage = state.storage();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || read_components(&storage))
|
||||||
|
.await
|
||||||
|
.map_err(background_task_error)?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -389,7 +413,10 @@ pub fn get_logs(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result<String, CommandError> {
|
pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result<String, CommandError> {
|
||||||
let storage = state.storage();
|
let storage = state.storage();
|
||||||
let generated_path = storage.paths().generated_dir.join("proxifyre-app-config.json");
|
let generated_path = storage
|
||||||
|
.paths()
|
||||||
|
.generated_dir
|
||||||
|
.join("proxifyre-app-config.json");
|
||||||
let config_path = detect_proxyfier_install()
|
let config_path = detect_proxyfier_install()
|
||||||
.and_then(|detected| detected.config_path)
|
.and_then(|detected| detected.config_path)
|
||||||
.filter(|path| path.exists())
|
.filter(|path| path.exists())
|
||||||
@@ -403,6 +430,20 @@ pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result<Str
|
|||||||
Ok(config_path.display().to_string())
|
Ok(config_path.display().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn start_proxifyre_service() -> Result<ComponentStatusDto, CommandError> {
|
||||||
|
tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Start))
|
||||||
|
.await
|
||||||
|
.map_err(background_task_error)?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stop_proxifyre_service() -> Result<ComponentStatusDto, CommandError> {
|
||||||
|
tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Stop))
|
||||||
|
.await
|
||||||
|
.map_err(background_task_error)?
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_status(storage: &JsonStorage) -> Result<StatusResponse, CommandError> {
|
pub fn build_status(storage: &JsonStorage) -> Result<StatusResponse, CommandError> {
|
||||||
let profiles = storage.read_profiles().map_err(storage_error)?;
|
let profiles = storage.read_profiles().map_err(storage_error)?;
|
||||||
let targets = storage.read_targets().map_err(storage_error)?;
|
let targets = storage.read_targets().map_err(storage_error)?;
|
||||||
@@ -426,7 +467,11 @@ pub fn build_status(storage: &JsonStorage) -> Result<StatusResponse, CommandErro
|
|||||||
routed_app_count,
|
routed_app_count,
|
||||||
active_target: active_target.map(TargetDto::from),
|
active_target: active_target.map(TargetDto::from),
|
||||||
components: components.iter().map(ComponentStatusDto::from).collect(),
|
components: components.iter().map(ComponentStatusDto::from).collect(),
|
||||||
recent_activity: activity.iter().take(10).map(ActivityEntryDto::from).collect(),
|
recent_activity: activity
|
||||||
|
.iter()
|
||||||
|
.take(10)
|
||||||
|
.map(ActivityEntryDto::from)
|
||||||
|
.collect(),
|
||||||
generated_config_path: storage
|
generated_config_path: storage
|
||||||
.paths()
|
.paths()
|
||||||
.generated_dir
|
.generated_dir
|
||||||
@@ -501,6 +546,19 @@ pub fn read_activity(storage: &JsonStorage) -> Result<Vec<ActivityEntryDto>, Com
|
|||||||
.map(|entries| entries.iter().map(ActivityEntryDto::from).collect())
|
.map(|entries| entries.iter().map(ActivityEntryDto::from).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_saved_state(storage: &JsonStorage) -> Result<SavedStateResponse, CommandError> {
|
||||||
|
Ok(SavedStateResponse {
|
||||||
|
profiles: read_profiles(storage)?,
|
||||||
|
targets: read_targets(storage)?,
|
||||||
|
generated_config_path: storage
|
||||||
|
.paths()
|
||||||
|
.generated_dir
|
||||||
|
.join("proxifyre-app-config.json")
|
||||||
|
.display()
|
||||||
|
.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_preview(
|
pub fn resolve_preview(
|
||||||
input: ProfileInputDto,
|
input: ProfileInputDto,
|
||||||
) -> Result<ResolveProfilePreviewResponse, CommandError> {
|
) -> Result<ResolveProfilePreviewResponse, CommandError> {
|
||||||
@@ -608,7 +666,10 @@ fn default_components() -> Vec<ComponentStatus> {
|
|||||||
version: None,
|
version: None,
|
||||||
path: None,
|
path: None,
|
||||||
problems: Vec::new(),
|
problems: Vec::new(),
|
||||||
actions: vec!["Открыть журнал".to_string(), "Скопировать диагностику".to_string()],
|
actions: vec![
|
||||||
|
"Открыть журнал".to_string(),
|
||||||
|
"Скопировать диагностику".to_string(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
ComponentStatus {
|
ComponentStatus {
|
||||||
id: ComponentId::Proxyfier,
|
id: ComponentId::Proxyfier,
|
||||||
@@ -720,6 +781,386 @@ fn open_folder(path: &Path) -> Result<String, CommandError> {
|
|||||||
Ok(path.display().to_string())
|
Ok(path.display().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum ServiceControlAction {
|
||||||
|
Start,
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceControlAction {
|
||||||
|
fn error_code(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ServiceControlAction::Start => "proxifyre_service_start_failed",
|
||||||
|
ServiceControlAction::Stop => "proxifyre_service_stop_failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ServiceControlAction::Start => "запустить",
|
||||||
|
ServiceControlAction::Stop => "остановить",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_proxifyre_service(
|
||||||
|
action: ServiceControlAction,
|
||||||
|
) -> Result<ComponentStatusDto, CommandError> {
|
||||||
|
let Some(detected) = detect_proxyfier_install() else {
|
||||||
|
return Err(CommandError::new(
|
||||||
|
"proxifyre_not_found",
|
||||||
|
"ProxiFyre не найден на компьютере.",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
run_proxifyre_service_command(action, &service_name_candidates(&detected))?;
|
||||||
|
let refreshed = detect_proxyfier_install();
|
||||||
|
let component = proxyfier_component_from_detection(refreshed.as_ref());
|
||||||
|
|
||||||
|
Ok(ComponentStatusDto::from(&component))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn service_name_candidates(detected: &DetectedProxyfier) -> Vec<String> {
|
||||||
|
let mut names = Vec::new();
|
||||||
|
|
||||||
|
if let Some(service_name) = &detected.service_name {
|
||||||
|
names.push(service_name.clone());
|
||||||
|
}
|
||||||
|
for service_name in ["ProxiFyreService", "ProxiFyre"] {
|
||||||
|
if !names
|
||||||
|
.iter()
|
||||||
|
.any(|existing| existing.eq_ignore_ascii_case(service_name))
|
||||||
|
{
|
||||||
|
names.push(service_name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct ServiceCommandOutput {
|
||||||
|
success: bool,
|
||||||
|
code: String,
|
||||||
|
service_name: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
|
process_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_proxifyre_service_command(
|
||||||
|
action: ServiceControlAction,
|
||||||
|
service_names: &[String],
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let names = service_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| format!("'{}'", escape_powershell_single(name)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let action_name = match action {
|
||||||
|
ServiceControlAction::Start => "start",
|
||||||
|
ServiceControlAction::Stop => "stop",
|
||||||
|
};
|
||||||
|
let script = format!(
|
||||||
|
r#"
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$names = @({names})
|
||||||
|
$action = '{action_name}'
|
||||||
|
$service = $null
|
||||||
|
|
||||||
|
function Find-ProxiFyreService {{
|
||||||
|
foreach ($name in $names) {{
|
||||||
|
$candidate = Get-Service -Name $name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $candidate) {{ return $candidate }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
return Get-Service |
|
||||||
|
Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
|
||||||
|
Select-Object -First 1
|
||||||
|
}}
|
||||||
|
|
||||||
|
function Get-ServiceProcessId([string]$name) {{
|
||||||
|
$escapedName = $name.Replace("'", "''")
|
||||||
|
$record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $record) {{ return 0 }}
|
||||||
|
return [int]$record.ProcessId
|
||||||
|
}}
|
||||||
|
|
||||||
|
function Get-ServiceStatus([string]$name) {{
|
||||||
|
$current = Get-Service -Name $name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $current) {{ return $null }}
|
||||||
|
return $current.Status.ToString()
|
||||||
|
}}
|
||||||
|
|
||||||
|
function Write-ServiceResult([bool]$success, [string]$code, [string]$status, [int]$processId) {{
|
||||||
|
[PSCustomObject]@{{
|
||||||
|
success = $success
|
||||||
|
code = $code
|
||||||
|
serviceName = if ($null -ne $service) {{ $service.Name }} else {{ $null }}
|
||||||
|
status = $status
|
||||||
|
processId = $processId
|
||||||
|
}} | ConvertTo-Json -Compress
|
||||||
|
exit 0
|
||||||
|
}}
|
||||||
|
|
||||||
|
$service = Find-ProxiFyreService
|
||||||
|
if ($null -eq $service) {{
|
||||||
|
Write-ServiceResult $false 'service_not_found' $null 0
|
||||||
|
}}
|
||||||
|
|
||||||
|
$status = $service.Status.ToString()
|
||||||
|
$processId = Get-ServiceProcessId $service.Name
|
||||||
|
|
||||||
|
if ($action -eq 'start') {{
|
||||||
|
if ($status -eq 'Running') {{
|
||||||
|
Write-ServiceResult $true 'already_running' $status $processId
|
||||||
|
}}
|
||||||
|
|
||||||
|
try {{
|
||||||
|
Start-Service -Name $service.Name -ErrorAction Stop
|
||||||
|
$service = Get-Service -Name $service.Name
|
||||||
|
$service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15))
|
||||||
|
}} catch {{
|
||||||
|
Write-ServiceResult $false 'start_failed' (Get-ServiceStatus $service.Name) (Get-ServiceProcessId $service.Name)
|
||||||
|
}}
|
||||||
|
|
||||||
|
Write-ServiceResult ($service.Status -eq 'Running') 'started' $service.Status.ToString() (Get-ServiceProcessId $service.Name)
|
||||||
|
}}
|
||||||
|
|
||||||
|
if ($status -eq 'Stopped') {{
|
||||||
|
Write-ServiceResult $true 'already_stopped' $status $processId
|
||||||
|
}}
|
||||||
|
|
||||||
|
try {{
|
||||||
|
if ($service.CanStop) {{
|
||||||
|
Stop-Service -Name $service.Name -Force -ErrorAction Stop
|
||||||
|
}}
|
||||||
|
}} catch {{}}
|
||||||
|
|
||||||
|
try {{
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
|
||||||
|
$null = & sc.exe stop $service.Name 2>$null
|
||||||
|
}}
|
||||||
|
}} catch {{}}
|
||||||
|
|
||||||
|
try {{
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
|
||||||
|
$service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
|
||||||
|
}}
|
||||||
|
}} catch {{}}
|
||||||
|
|
||||||
|
$status = Get-ServiceStatus $service.Name
|
||||||
|
$processId = Get-ServiceProcessId $service.Name
|
||||||
|
if ($status -ne 'Stopped' -and $processId -gt 0) {{
|
||||||
|
try {{
|
||||||
|
$null = & taskkill.exe /PID $processId /F 2>$null
|
||||||
|
Start-Sleep -Milliseconds 700
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service) {{
|
||||||
|
$service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
|
||||||
|
}}
|
||||||
|
}} catch {{}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
$status = Get-ServiceStatus $service.Name
|
||||||
|
$processId = Get-ServiceProcessId $service.Name
|
||||||
|
if ($status -eq 'Stopped') {{
|
||||||
|
Write-ServiceResult $true 'stopped' $status $processId
|
||||||
|
}}
|
||||||
|
|
||||||
|
Write-ServiceResult $false 'stop_failed' $status $processId
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = Command::new("powershell")
|
||||||
|
.args([
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
script.as_str(),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.map_err(|error| {
|
||||||
|
CommandError::new(
|
||||||
|
action.error_code(),
|
||||||
|
format!("Не удалось {} службу ProxiFyre: {error}", action.label()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let result = parse_service_command_output(&output.stdout).ok_or_else(|| {
|
||||||
|
CommandError::new(
|
||||||
|
action.error_code(),
|
||||||
|
service_script_failed_message(action, output.status.code()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if result.success {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(result.code.as_str(), "start_failed" | "stop_failed") {
|
||||||
|
run_elevated_proxifyre_service_command(action, service_names, &result)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(CommandError::new(
|
||||||
|
action.error_code(),
|
||||||
|
service_command_failed_message(action, &result),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_elevated_proxifyre_service_command(
|
||||||
|
action: ServiceControlAction,
|
||||||
|
service_names: &[String],
|
||||||
|
direct_result: &ServiceCommandOutput,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
|
let script_path = write_elevated_service_script(action, service_names)?;
|
||||||
|
let launch_script = format!(
|
||||||
|
"$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
|
||||||
|
escape_powershell_single(&script_path.display().to_string())
|
||||||
|
);
|
||||||
|
let output = Command::new("powershell")
|
||||||
|
.args([
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
launch_script.as_str(),
|
||||||
|
])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
let _ = fs::remove_file(&script_path);
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) if output.status.success() => Ok(()),
|
||||||
|
Ok(output) => Err(CommandError::new(
|
||||||
|
action.error_code(),
|
||||||
|
elevated_service_failed_message(action, direct_result, output.status.code()),
|
||||||
|
)),
|
||||||
|
Err(error) => Err(CommandError::new(
|
||||||
|
action.error_code(),
|
||||||
|
format!(
|
||||||
|
"Не удалось запросить права администратора, чтобы {} службу ProxiFyre: {error}",
|
||||||
|
action.label()
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_elevated_service_script(
|
||||||
|
action: ServiceControlAction,
|
||||||
|
service_names: &[String],
|
||||||
|
) -> Result<PathBuf, CommandError> {
|
||||||
|
let nonce = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let script_path = env::temp_dir().join(format!("vpn-proxy-proxifyre-service-{nonce}.ps1"));
|
||||||
|
let script = elevated_service_script(action, service_names);
|
||||||
|
|
||||||
|
fs::write(&script_path, script).map_err(|error| {
|
||||||
|
CommandError::new(
|
||||||
|
action.error_code(),
|
||||||
|
format!(
|
||||||
|
"Не удалось подготовить временный скрипт для управления ProxiFyre '{}': {error}",
|
||||||
|
script_path.display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(script_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn elevated_service_script(action: ServiceControlAction, service_names: &[String]) -> String {
|
||||||
|
let names = service_names
|
||||||
|
.iter()
|
||||||
|
.map(|name| format!("'{}'", escape_powershell_single(name)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let action_name = match action {
|
||||||
|
ServiceControlAction::Start => "start",
|
||||||
|
ServiceControlAction::Stop => "stop",
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
$names = @({names})
|
||||||
|
$action = '{action_name}'
|
||||||
|
$service = $null
|
||||||
|
|
||||||
|
foreach ($name in $names) {{
|
||||||
|
$service = Get-Service -Name $name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service) {{ break }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
if ($null -eq $service) {{
|
||||||
|
$service = Get-Service |
|
||||||
|
Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
|
||||||
|
Select-Object -First 1
|
||||||
|
}}
|
||||||
|
|
||||||
|
if ($null -eq $service) {{ exit 2 }}
|
||||||
|
|
||||||
|
function Get-ServiceProcessId([string]$name) {{
|
||||||
|
$escapedName = $name.Replace("'", "''")
|
||||||
|
$record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $record) {{ return 0 }}
|
||||||
|
return [int]$record.ProcessId
|
||||||
|
}}
|
||||||
|
|
||||||
|
if ($action -eq 'start') {{
|
||||||
|
if ($service.Status -eq 'Running') {{ exit 0 }}
|
||||||
|
Start-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service) {{
|
||||||
|
try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}}
|
||||||
|
if ($service.Status -eq 'Running') {{ exit 0 }}
|
||||||
|
}}
|
||||||
|
exit 3
|
||||||
|
}}
|
||||||
|
|
||||||
|
if ($service.Status -eq 'Stopped') {{ exit 0 }}
|
||||||
|
|
||||||
|
if ($service.CanStop) {{
|
||||||
|
Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
|
||||||
|
}}
|
||||||
|
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
|
||||||
|
$null = & sc.exe stop $service.Name 2>$null
|
||||||
|
}}
|
||||||
|
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
|
||||||
|
try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
|
||||||
|
$processId = Get-ServiceProcessId $service.Name
|
||||||
|
if ($processId -gt 0) {{
|
||||||
|
$null = & taskkill.exe /PID $processId /F 2>$null
|
||||||
|
Start-Sleep -Milliseconds 700
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $service) {{
|
||||||
|
try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $service -or $service.Status -eq 'Stopped') {{ exit 0 }}
|
||||||
|
exit 4
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_to_detected_proxyfier(
|
fn apply_to_detected_proxyfier(
|
||||||
request: HelperApplyRequest<'_>,
|
request: HelperApplyRequest<'_>,
|
||||||
detected: &DetectedProxyfier,
|
detected: &DetectedProxyfier,
|
||||||
@@ -832,6 +1273,104 @@ fn storage_error(error: std::io::Error) -> CommandError {
|
|||||||
CommandError::new("storage_error", error.to_string())
|
CommandError::new("storage_error", error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn background_task_error(error: impl std::fmt::Display) -> CommandError {
|
||||||
|
CommandError::new(
|
||||||
|
"background_task_failed",
|
||||||
|
format!("Фоновая проверка не завершилась: {error}"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_service_command_output(stdout: &[u8]) -> Option<ServiceCommandOutput> {
|
||||||
|
let stdout = String::from_utf8_lossy(stdout);
|
||||||
|
let payload = stdout
|
||||||
|
.lines()
|
||||||
|
.rev()
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|line| line.starts_with('{') && line.ends_with('}'))?;
|
||||||
|
|
||||||
|
serde_json::from_str(payload).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn service_script_failed_message(action: ServiceControlAction, exit_code: Option<i32>) -> String {
|
||||||
|
let exit_code = exit_code
|
||||||
|
.map(|code| format!(" Код выхода PowerShell: {code}."))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Не удалось {} службу ProxiFyre: команда управления службой не вернула корректный результат.{exit_code}",
|
||||||
|
action.label()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn service_command_failed_message(
|
||||||
|
action: ServiceControlAction,
|
||||||
|
result: &ServiceCommandOutput,
|
||||||
|
) -> String {
|
||||||
|
let service_name = result
|
||||||
|
.service_name
|
||||||
|
.as_deref()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or("ProxiFyre");
|
||||||
|
let status = result
|
||||||
|
.status
|
||||||
|
.as_deref()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or("неизвестен");
|
||||||
|
let pid = result
|
||||||
|
.process_id
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.map(|value| format!(", PID: {value}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
match result.code.as_str() {
|
||||||
|
"service_not_found" => "Служба ProxiFyre не найдена.".to_string(),
|
||||||
|
"start_failed" => format!(
|
||||||
|
"Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора."
|
||||||
|
),
|
||||||
|
"stop_failed" => format!(
|
||||||
|
"Не удалось остановить службу {service_name} даже после принудительной попытки. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc."
|
||||||
|
),
|
||||||
|
_ => format!(
|
||||||
|
"Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.",
|
||||||
|
action.label()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn elevated_service_failed_message(
|
||||||
|
action: ServiceControlAction,
|
||||||
|
direct_result: &ServiceCommandOutput,
|
||||||
|
exit_code: Option<i32>,
|
||||||
|
) -> String {
|
||||||
|
let service_name = direct_result
|
||||||
|
.service_name
|
||||||
|
.as_deref()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or("ProxiFyre");
|
||||||
|
let status = direct_result
|
||||||
|
.status
|
||||||
|
.as_deref()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or("неизвестен");
|
||||||
|
let pid = direct_result
|
||||||
|
.process_id
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.map(|value| format!(", PID: {value}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let exit_code = exit_code
|
||||||
|
.map(|code| format!(" Код elevated-команды: {code}."))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Не удалось {} службу {service_name} даже после запроса прав администратора. До запроса UAC статус был: {status}{pid}.{exit_code} Если появлялось окно UAC, проверь, что оно было подтверждено.",
|
||||||
|
action.label()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_powershell_single(value: &str) -> String {
|
||||||
|
value.replace('\'', "''")
|
||||||
|
}
|
||||||
|
|
||||||
fn validation_error(errors: Vec<ValidationError>) -> CommandError {
|
fn validation_error(errors: Vec<ValidationError>) -> CommandError {
|
||||||
CommandError::with_details(
|
CommandError::with_details(
|
||||||
"validation_error",
|
"validation_error",
|
||||||
|
|||||||
@@ -94,9 +94,7 @@ pub fn detect_proxyfier_install_with_host(
|
|||||||
.next()
|
.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn proxyfier_component_from_detection(
|
pub fn proxyfier_component_from_detection(detected: Option<&DetectedProxyfier>) -> ComponentStatus {
|
||||||
detected: Option<&DetectedProxyfier>,
|
|
||||||
) -> ComponentStatus {
|
|
||||||
match detected {
|
match detected {
|
||||||
Some(proxyfier) => detected_proxyfier_component(proxyfier),
|
Some(proxyfier) => detected_proxyfier_component(proxyfier),
|
||||||
None => missing_proxyfier_component(),
|
None => missing_proxyfier_component(),
|
||||||
@@ -115,7 +113,7 @@ fn detected_proxyfier_component(proxyfier: &DetectedProxyfier) -> ComponentStatu
|
|||||||
vec![
|
vec![
|
||||||
"Применить сгенерированный конфиг".to_string(),
|
"Применить сгенерированный конфиг".to_string(),
|
||||||
"Открыть папку конфига".to_string(),
|
"Открыть папку конфига".to_string(),
|
||||||
"Перезапустить".to_string(),
|
"Остановить".to_string(),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
vec![
|
vec![
|
||||||
@@ -255,7 +253,8 @@ fn push_env_candidate(
|
|||||||
|
|
||||||
fn push_candidate(candidates: &mut Vec<ProxyfierCandidate>, candidate: ProxyfierCandidate) {
|
fn push_candidate(candidates: &mut Vec<ProxyfierCandidate>, candidate: ProxyfierCandidate) {
|
||||||
if !candidates.iter().any(|existing| {
|
if !candidates.iter().any(|existing| {
|
||||||
existing.engine == candidate.engine && same_path(&existing.install_dir, &candidate.install_dir)
|
existing.engine == candidate.engine
|
||||||
|
&& same_path(&existing.install_dir, &candidate.install_dir)
|
||||||
}) {
|
}) {
|
||||||
candidates.push(candidate);
|
candidates.push(candidate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
mod activity;
|
mod activity;
|
||||||
mod component_detection;
|
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod component_detection;
|
||||||
mod models;
|
mod models;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod validation;
|
mod validation;
|
||||||
@@ -24,10 +24,12 @@ pub(crate) mod proxy_router {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.manage(commands::CommandState::default())
|
.manage(commands::CommandState::default())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::get_status,
|
commands::get_status,
|
||||||
commands::get_profiles,
|
commands::get_profiles,
|
||||||
|
commands::get_saved_state,
|
||||||
commands::save_profile,
|
commands::save_profile,
|
||||||
commands::get_targets,
|
commands::get_targets,
|
||||||
commands::save_target,
|
commands::save_target,
|
||||||
@@ -35,7 +37,9 @@ fn main() {
|
|||||||
commands::resolve_profile_preview,
|
commands::resolve_profile_preview,
|
||||||
commands::apply_profiles,
|
commands::apply_profiles,
|
||||||
commands::get_logs,
|
commands::get_logs,
|
||||||
commands::open_config_location
|
commands::open_config_location,
|
||||||
|
commands::start_proxifyre_service,
|
||||||
|
commands::stop_proxifyre_service
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("не удалось запустить клиент VPN Proxy для Windows");
|
.expect("не удалось запустить клиент VPN Proxy для Windows");
|
||||||
|
|||||||
@@ -45,12 +45,7 @@ fn slug(value: &str, fallback: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_name(value: &str) -> String {
|
fn process_name(value: &str) -> String {
|
||||||
let base = value
|
let base = value.trim().rsplit(['\\', '/']).next().unwrap_or("").trim();
|
||||||
.trim()
|
|
||||||
.rsplit(['\\', '/'])
|
|
||||||
.next()
|
|
||||||
.unwrap_or("")
|
|
||||||
.trim();
|
|
||||||
base.strip_suffix(".exe")
|
base.strip_suffix(".exe")
|
||||||
.or_else(|| base.strip_suffix(".EXE"))
|
.or_else(|| base.strip_suffix(".EXE"))
|
||||||
.unwrap_or(base)
|
.unwrap_or(base)
|
||||||
@@ -62,7 +57,10 @@ pub fn parse_protocol(value: &str) -> Result<Protocol, ValidationError> {
|
|||||||
match value.trim().to_ascii_uppercase().as_str() {
|
match value.trim().to_ascii_uppercase().as_str() {
|
||||||
"TCP" => Ok(Protocol::Tcp),
|
"TCP" => Ok(Protocol::Tcp),
|
||||||
"UDP" => Ok(Protocol::Udp),
|
"UDP" => Ok(Protocol::Udp),
|
||||||
_ => Err(error("protocols", format!("Неподдерживаемый протокол: {value}"))),
|
_ => Err(error(
|
||||||
|
"protocols",
|
||||||
|
format!("Неподдерживаемый протокол: {value}"),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +69,10 @@ pub fn parse_profile_item_type(value: &str) -> Result<ProfileItemType, Validatio
|
|||||||
"process" => Ok(ProfileItemType::Process),
|
"process" => Ok(ProfileItemType::Process),
|
||||||
"folder" => Ok(ProfileItemType::Folder),
|
"folder" => Ok(ProfileItemType::Folder),
|
||||||
"exe" => Ok(ProfileItemType::Exe),
|
"exe" => Ok(ProfileItemType::Exe),
|
||||||
_ => Err(error("items.type", format!("Неподдерживаемый тип элемента: {value}"))),
|
_ => Err(error(
|
||||||
|
"items.type",
|
||||||
|
format!("Неподдерживаемый тип элемента: {value}"),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +150,8 @@ pub fn normalize_profile(input: ProfileInput) -> ValidationResult<Profile> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let recursive = matches!(item_type, ProfileItemType::Folder)
|
let recursive =
|
||||||
&& raw_item.recursive.unwrap_or(true);
|
matches!(item_type, ProfileItemType::Folder) && raw_item.recursive.unwrap_or(true);
|
||||||
items.push(ProfileItem {
|
items.push(ProfileItem {
|
||||||
item_type,
|
item_type,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#[path = "../src/activity.rs"]
|
#[path = "../src/activity.rs"]
|
||||||
mod activity;
|
mod activity;
|
||||||
#[path = "../src/component_detection.rs"]
|
|
||||||
mod component_detection;
|
|
||||||
#[path = "../src/commands.rs"]
|
#[path = "../src/commands.rs"]
|
||||||
mod commands;
|
mod commands;
|
||||||
|
#[path = "../src/component_detection.rs"]
|
||||||
|
mod component_detection;
|
||||||
#[path = "../src/models.rs"]
|
#[path = "../src/models.rs"]
|
||||||
mod models;
|
mod models;
|
||||||
#[path = "../src/adapters/proxifyre.rs"]
|
#[path = "../src/adapters/proxifyre.rs"]
|
||||||
@@ -17,9 +17,9 @@ mod validation;
|
|||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
apply_profiles_with_services, build_status, resolve_component_statuses, resolve_preview,
|
apply_profiles_with_services, build_status, resolve_component_statuses, resolve_preview,
|
||||||
save_profile_to_storage, save_target_to_storage, Clock, CommandError,
|
save_profile_to_storage, save_target_to_storage, Clock, CommandError, DetectedProxyApplyHelper,
|
||||||
DetectedProxyApplyHelper, HelperApplyRequest, HelperApplyResult, ProfileInputDto,
|
HelperApplyRequest, HelperApplyResult, ProfileInputDto, ProfileItemInputDto, ProxyApplyHelper,
|
||||||
ProfileItemInputDto, ProxyApplyHelper, TargetInputDto,
|
TargetInputDto,
|
||||||
};
|
};
|
||||||
use component_detection::{
|
use component_detection::{
|
||||||
DetectedProxyfier, ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry,
|
DetectedProxyfier, ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry,
|
||||||
@@ -240,8 +240,8 @@ fn detected_proxy_apply_helper_writes_proxifyre_app_config() {
|
|||||||
})
|
})
|
||||||
.expect("detected helper should apply");
|
.expect("detected helper should apply");
|
||||||
|
|
||||||
let applied = fs::read_to_string(install_dir.join("app-config.json"))
|
let applied =
|
||||||
.expect("read applied app-config");
|
fs::read_to_string(install_dir.join("app-config.json")).expect("read applied app-config");
|
||||||
|
|
||||||
assert!(result.success);
|
assert!(result.success);
|
||||||
assert!(result.changed);
|
assert!(result.changed);
|
||||||
@@ -276,7 +276,9 @@ fn detected_proxy_apply_helper_ignores_plain_proxifier_install() {
|
|||||||
assert!(result.success);
|
assert!(result.success);
|
||||||
assert!(result.changed);
|
assert!(result.changed);
|
||||||
assert_eq!(result.action, "proxifyre.stage-generated-config");
|
assert_eq!(result.action, "proxifyre.stage-generated-config");
|
||||||
assert!(result.message.contains("совместимая установка ProxiFyre не найдена"));
|
assert!(result
|
||||||
|
.message
|
||||||
|
.contains("совместимая установка ProxiFyre не найдена"));
|
||||||
|
|
||||||
cleanup(&root);
|
cleanup(&root);
|
||||||
}
|
}
|
||||||
@@ -358,7 +360,10 @@ impl ProxyfierDetectionHost for DetectionHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_path(path: &Path) -> String {
|
fn normalize_path(path: &Path) -> String {
|
||||||
path.display().to_string().replace('/', "\\").to_ascii_lowercase()
|
path.display()
|
||||||
|
.to_string()
|
||||||
|
.replace('/', "\\")
|
||||||
|
.to_ascii_lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_root(name: &str) -> PathBuf {
|
fn test_root(name: &str) -> PathBuf {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use component_detection::{
|
|||||||
detect_proxyfier_install_with_host, proxyfier_component_from_detection, ProxyfierDetectionHost,
|
detect_proxyfier_install_with_host, proxyfier_component_from_detection, ProxyfierDetectionHost,
|
||||||
ProxyfierEngine, RegistryInstallEntry,
|
ProxyfierEngine, RegistryInstallEntry,
|
||||||
};
|
};
|
||||||
use models::{ComponentState};
|
use models::ComponentState;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -25,7 +25,10 @@ fn detects_existing_proxifyre_from_registry_install_location() {
|
|||||||
|
|
||||||
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
|
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
|
||||||
assert_eq!(detected.install_dir, PathBuf::from(r"C:\Tools\ProxiFyre"));
|
assert_eq!(detected.install_dir, PathBuf::from(r"C:\Tools\ProxiFyre"));
|
||||||
assert_eq!(detected.config_path, Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")));
|
assert_eq!(
|
||||||
|
detected.config_path,
|
||||||
|
Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json"))
|
||||||
|
);
|
||||||
assert!(detected.running);
|
assert!(detected.running);
|
||||||
|
|
||||||
let component = proxyfier_component_from_detection(Some(&detected));
|
let component = proxyfier_component_from_detection(Some(&detected));
|
||||||
@@ -56,7 +59,10 @@ fn env_override_can_point_to_portable_proxifyre_install() {
|
|||||||
.expect("env override should be checked before common paths");
|
.expect("env override should be checked before common paths");
|
||||||
|
|
||||||
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
|
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
|
||||||
assert_eq!(detected.executable_path, PathBuf::from(r"D:\Portable\ProxiFyre\ProxiFyre.exe"));
|
assert_eq!(
|
||||||
|
detected.executable_path,
|
||||||
|
PathBuf::from(r"D:\Portable\ProxiFyre\ProxiFyre.exe")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -118,17 +124,16 @@ impl ProxyfierDetectionHost for MockHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn path_exists(&self, path: &Path) -> bool {
|
fn path_exists(&self, path: &Path) -> bool {
|
||||||
self.paths.contains(&normalize_path(&path.display().to_string()))
|
self.paths
|
||||||
|
.contains(&normalize_path(&path.display().to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_running(&self, process_name: &str) -> bool {
|
fn process_running(&self, process_name: &str) -> bool {
|
||||||
self.processes
|
self.processes.contains(&process_name.to_ascii_lowercase())
|
||||||
.contains(&process_name.to_ascii_lowercase())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn service_running(&self, service_name: &str) -> bool {
|
fn service_running(&self, service_name: &str) -> bool {
|
||||||
self.services
|
self.services.contains(&service_name.to_ascii_lowercase())
|
||||||
.contains(&service_name.to_ascii_lowercase())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
|
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
|
||||||
|
|||||||
@@ -124,7 +124,5 @@ fn rejects_malformed_target_fields() {
|
|||||||
assert!(error.iter().any(|item| item.field == "host"));
|
assert!(error.iter().any(|item| item.field == "host"));
|
||||||
assert!(error.iter().any(|item| item.field == "port"));
|
assert!(error.iter().any(|item| item.field == "port"));
|
||||||
assert!(error.iter().any(|item| item.field == "protocol"));
|
assert!(error.iter().any(|item| item.field == "protocol"));
|
||||||
assert!(error
|
assert!(error.iter().any(|item| item.field == "requires_component"));
|
||||||
.iter()
|
|
||||||
.any(|item| item.field == "requires_component"));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#[path = "../src/models.rs"]
|
#[path = "../src/models.rs"]
|
||||||
mod models;
|
mod models;
|
||||||
#[path = "../src/adapters/proxy_router.rs"]
|
|
||||||
mod proxy_router;
|
|
||||||
#[path = "../src/adapters/proxifyre.rs"]
|
#[path = "../src/adapters/proxifyre.rs"]
|
||||||
mod proxifyre;
|
mod proxifyre;
|
||||||
|
#[path = "../src/adapters/proxy_router.rs"]
|
||||||
|
mod proxy_router;
|
||||||
|
|
||||||
use models::{
|
use models::{
|
||||||
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType, Protocol,
|
ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType, Protocol,
|
||||||
@@ -33,7 +33,10 @@ fn generates_proxifyre_config_for_discord_external_socks5_target() {
|
|||||||
assert!(config.bypass_lan);
|
assert!(config.bypass_lan);
|
||||||
assert_eq!(config.proxies.len(), 1);
|
assert_eq!(config.proxies.len(), 1);
|
||||||
assert_eq!(config.proxies[0].app_names, vec!["Discord"]);
|
assert_eq!(config.proxies[0].app_names, vec!["Discord"]);
|
||||||
assert_eq!(config.proxies[0].socks5_proxy_endpoint, "192.168.50.111:8080");
|
assert_eq!(
|
||||||
|
config.proxies[0].socks5_proxy_endpoint,
|
||||||
|
"192.168.50.111:8080"
|
||||||
|
);
|
||||||
assert_eq!(config.proxies[0].supported_protocols, vec!["TCP", "UDP"]);
|
assert_eq!(config.proxies[0].supported_protocols, vec!["TCP", "UDP"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,32 @@ fn skips_disabled_profiles_when_generating_proxifyre_config() {
|
|||||||
assert!(config.proxies.is_empty());
|
assert!(config.proxies.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn includes_folder_paths_when_generating_proxifyre_config() {
|
||||||
|
let adapter = ProxiFyreAdapter::default();
|
||||||
|
let mut profile = discord_profile("home-gateway");
|
||||||
|
profile.items.push(ProfileItem {
|
||||||
|
item_type: ProfileItemType::Folder,
|
||||||
|
value: r"C:\Games\MyGame".to_string(),
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
let profiles = vec![profile];
|
||||||
|
let targets = vec![external_socks5_target()];
|
||||||
|
let components = Vec::new();
|
||||||
|
|
||||||
|
let generated = adapter
|
||||||
|
.generate_config(ProxyRouterRequest::new(&profiles, &targets, &components))
|
||||||
|
.expect("folder paths should be accepted by ProxiFyre config generation");
|
||||||
|
let config: ProxiFyreConfig =
|
||||||
|
serde_json::from_str(&generated.contents).expect("generated config json");
|
||||||
|
|
||||||
|
assert_eq!(generated.routed_apps, 2);
|
||||||
|
assert_eq!(
|
||||||
|
config.proxies[0].app_names,
|
||||||
|
vec!["Discord", r"C:\Games\MyGame"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocks_local_singbox_target_when_required_component_is_missing() {
|
fn blocks_local_singbox_target_when_required_component_is_missing() {
|
||||||
let adapter = ProxiFyreAdapter::default();
|
let adapter = ProxiFyreAdapter::default();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#[path = "../src/models.rs"]
|
#[path = "../src/models.rs"]
|
||||||
mod models;
|
mod models;
|
||||||
#[path = "../src/adapters/proxy_router.rs"]
|
|
||||||
mod proxy_router;
|
|
||||||
#[path = "../src/adapters/proxifyre.rs"]
|
#[path = "../src/adapters/proxifyre.rs"]
|
||||||
mod proxifyre;
|
mod proxifyre;
|
||||||
|
#[path = "../src/adapters/proxy_router.rs"]
|
||||||
|
mod proxy_router;
|
||||||
#[path = "../src/adapters/singbox.rs"]
|
#[path = "../src/adapters/singbox.rs"]
|
||||||
mod singbox;
|
mod singbox;
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ use models::{
|
|||||||
use proxifyre::{ProxiFyreAdapter, ProxiFyreConfig};
|
use proxifyre::{ProxiFyreAdapter, ProxiFyreConfig};
|
||||||
use proxy_router::{ProxyRouterAdapter, ProxyRouterRequest};
|
use proxy_router::{ProxyRouterAdapter, ProxyRouterRequest};
|
||||||
use singbox::{
|
use singbox::{
|
||||||
SingBoxAdapter, SingBoxCheckResult, SingBoxConfig, SingBoxConfigChecker,
|
SingBoxAdapter, SingBoxCheckResult, SingBoxConfig, SingBoxConfigChecker, SingBoxConfigError,
|
||||||
SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGenerationRequest, SINGBOX_OUTPUT_FILE,
|
SingBoxConfigErrorKind, SingBoxGenerationRequest, SINGBOX_OUTPUT_FILE,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
@@ -116,7 +116,10 @@ fn blocks_local_singbox_config_when_component_is_not_running() {
|
|||||||
)
|
)
|
||||||
.expect_err("local sing-box target requires running component");
|
.expect_err("local sing-box target requires running component");
|
||||||
|
|
||||||
assert_eq!(error.kind, SingBoxConfigErrorKind::RequiredComponentNotRunning);
|
assert_eq!(
|
||||||
|
error.kind,
|
||||||
|
SingBoxConfigErrorKind::RequiredComponentNotRunning
|
||||||
|
);
|
||||||
assert!(checker.calls.borrow().is_empty());
|
assert!(checker.calls.borrow().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +132,7 @@ fn propagates_failed_singbox_check_as_structured_error() {
|
|||||||
|
|
||||||
let error = adapter
|
let error = adapter
|
||||||
.generate_config(
|
.generate_config(
|
||||||
SingBoxGenerationRequest::new(
|
SingBoxGenerationRequest::new(&targets, &components, Some(Path::new("sing-box.exe"))),
|
||||||
&targets,
|
|
||||||
&components,
|
|
||||||
Some(Path::new("sing-box.exe")),
|
|
||||||
),
|
|
||||||
&checker,
|
&checker,
|
||||||
)
|
)
|
||||||
.expect_err("failed sing-box check should block generated config");
|
.expect_err("failed sing-box check should block generated config");
|
||||||
@@ -156,7 +155,10 @@ fn external_proxifyre_apply_does_not_require_singbox_component() {
|
|||||||
serde_json::from_str(&generated.contents).expect("generated proxifyre json");
|
serde_json::from_str(&generated.contents).expect("generated proxifyre json");
|
||||||
|
|
||||||
assert_eq!(config.proxies.len(), 1);
|
assert_eq!(config.proxies.len(), 1);
|
||||||
assert_eq!(config.proxies[0].socks5_proxy_endpoint, "192.168.50.111:8080");
|
assert_eq!(
|
||||||
|
config.proxies[0].socks5_proxy_endpoint,
|
||||||
|
"192.168.50.111:8080"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RecordingChecker {
|
struct RecordingChecker {
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ fn roundtrips_profiles_targets_components_and_activity() {
|
|||||||
|
|
||||||
assert_eq!(storage.read_profiles().expect("read profiles"), profiles);
|
assert_eq!(storage.read_profiles().expect("read profiles"), profiles);
|
||||||
assert_eq!(storage.read_targets().expect("read targets"), targets);
|
assert_eq!(storage.read_targets().expect("read targets"), targets);
|
||||||
assert_eq!(storage.read_components().expect("read components"), components);
|
assert_eq!(
|
||||||
|
storage.read_components().expect("read components"),
|
||||||
|
components
|
||||||
|
);
|
||||||
assert_eq!(storage.read_activity().expect("read activity"), activity);
|
assert_eq!(storage.read_activity().expect("read activity"), activity);
|
||||||
|
|
||||||
cleanup(&root);
|
cleanup(&root);
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export interface StatusResponse {
|
|||||||
generatedConfigPath: string;
|
generatedConfigPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SavedStateResponse {
|
||||||
|
profiles: Profile[];
|
||||||
|
targets: Target[];
|
||||||
|
generatedConfigPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HelperApplyResult {
|
export interface HelperApplyResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
changed: boolean;
|
changed: boolean;
|
||||||
@@ -50,6 +56,10 @@ export function getStatus(): Promise<StatusResponse> {
|
|||||||
return invoke<StatusResponse>('get_status');
|
return invoke<StatusResponse>('get_status');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSavedState(): Promise<SavedStateResponse> {
|
||||||
|
return invoke<SavedStateResponse>('get_saved_state');
|
||||||
|
}
|
||||||
|
|
||||||
export function getProfiles(): Promise<Profile[]> {
|
export function getProfiles(): Promise<Profile[]> {
|
||||||
return invoke<Profile[]>('get_profiles');
|
return invoke<Profile[]>('get_profiles');
|
||||||
}
|
}
|
||||||
@@ -77,3 +87,11 @@ export function applyProfiles(): Promise<ApplyProfilesResponse> {
|
|||||||
export function openConfigLocation(): Promise<string> {
|
export function openConfigLocation(): Promise<string> {
|
||||||
return invoke<string>('open_config_location');
|
return invoke<string>('open_config_location');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function startProxiFyreService(): Promise<ComponentStatus> {
|
||||||
|
return invoke<ComponentStatus>('start_proxifyre_service');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopProxiFyreService(): Promise<ComponentStatus> {
|
||||||
|
return invoke<ComponentStatus>('stop_proxifyre_service');
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import { Cpu, FileCode2, FolderOpen } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
applyProfiles,
|
applyProfiles,
|
||||||
getComponents,
|
getComponents,
|
||||||
getProfiles,
|
getSavedState,
|
||||||
getStatus,
|
|
||||||
getTargets,
|
|
||||||
openConfigLocation,
|
openConfigLocation,
|
||||||
saveProfile,
|
saveProfile,
|
||||||
saveTarget,
|
saveTarget,
|
||||||
|
startProxiFyreService,
|
||||||
|
stopProxiFyreService,
|
||||||
type ApplyProfilesResponse,
|
type ApplyProfilesResponse,
|
||||||
} from '../api/tauriCommands';
|
} from '../api/tauriCommands';
|
||||||
import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, Target } from '../domain/types';
|
import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, Target } from '../domain/types';
|
||||||
|
|
||||||
type DraftItemType = Extract<ProfileItemType, 'process' | 'exe'>;
|
type DraftItemType = Extract<ProfileItemType, 'process' | 'folder' | 'exe'>;
|
||||||
|
type ServiceVisualState = 'active' | 'settling' | null;
|
||||||
|
|
||||||
interface DraftItem {
|
interface DraftItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,8 +29,14 @@ interface Notice {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LogEntry extends Notice {
|
||||||
|
id: string;
|
||||||
|
at: number;
|
||||||
|
}
|
||||||
|
|
||||||
const MAIN_TARGET_ID = 'main-proxy';
|
const MAIN_TARGET_ID = 'main-proxy';
|
||||||
const MAIN_PROFILE_ID = 'main-profile';
|
const MAIN_PROFILE_ID = 'main-profile';
|
||||||
|
const LOG_VISIBLE_MS = 6500;
|
||||||
|
|
||||||
const fallbackComponents: ComponentStatus[] = [
|
const fallbackComponents: ComponentStatus[] = [
|
||||||
{
|
{
|
||||||
@@ -47,50 +56,63 @@ export function App() {
|
|||||||
const [targetId, setTargetId] = useState(MAIN_TARGET_ID);
|
const [targetId, setTargetId] = useState(MAIN_TARGET_ID);
|
||||||
const [items, setItems] = useState<DraftItem[]>([]);
|
const [items, setItems] = useState<DraftItem[]>([]);
|
||||||
const [loadedProfiles, setLoadedProfiles] = useState<Profile[]>([]);
|
const [loadedProfiles, setLoadedProfiles] = useState<Profile[]>([]);
|
||||||
const [newItemType, setNewItemType] = useState<DraftItemType>('process');
|
const [isProcessInputOpen, setIsProcessInputOpen] = useState(false);
|
||||||
const [newItemValue, setNewItemValue] = useState('');
|
const [processInput, setProcessInput] = useState('');
|
||||||
|
const [pickerAction, setPickerAction] = useState<'exe' | 'folder' | null>(null);
|
||||||
const [components, setComponents] = useState<ComponentStatus[]>(fallbackComponents);
|
const [components, setComponents] = useState<ComponentStatus[]>(fallbackComponents);
|
||||||
const [generatedConfigPath, setGeneratedConfigPath] = useState('');
|
const [generatedConfigPath, setGeneratedConfigPath] = useState('');
|
||||||
const [notice, setNotice] = useState<Notice | null>(null);
|
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
|
||||||
|
const [activeLogId, setActiveLogId] = useState<string | null>(null);
|
||||||
|
const [isLogOpen, setIsLogOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isDetectingComponents, setIsDetectingComponents] = useState(true);
|
||||||
const [isApplying, setIsApplying] = useState(false);
|
const [isApplying, setIsApplying] = useState(false);
|
||||||
const [isOpeningConfig, setIsOpeningConfig] = useState(false);
|
const [isOpeningConfig, setIsOpeningConfig] = useState(false);
|
||||||
|
const [serviceAction, setServiceAction] = useState<'start' | 'stop' | null>(null);
|
||||||
|
const [serviceVisualState, setServiceVisualState] = useState<ServiceVisualState>(null);
|
||||||
|
const serviceVisualTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const proxyfier = useMemo(
|
const proxyfier = useMemo(
|
||||||
() => components.find((component) => component.id === 'proxyfier'),
|
() => components.find((component) => component.id === 'proxyfier'),
|
||||||
[components],
|
[components],
|
||||||
);
|
);
|
||||||
|
const activeLog = useMemo(
|
||||||
|
() => logEntries.find((entry) => entry.id === activeLogId) ?? null,
|
||||||
|
[activeLogId, logEntries],
|
||||||
|
);
|
||||||
|
const finderStateClass = isDetectingComponents ? 'checking' : proxyfier?.installed ? 'found' : 'missing';
|
||||||
|
const finderVisualClass =
|
||||||
|
serviceVisualState === 'active' ? 'working' : serviceVisualState === 'settling' ? 'settling' : '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh();
|
void refresh();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (serviceVisualTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(serviceVisualTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeLogId) return undefined;
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
setActiveLogId((current) => (current === activeLogId ? null : current));
|
||||||
|
}, LOG_VISIBLE_MS);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [activeLogId]);
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [status, profiles, targets, detectedComponents] = await Promise.all([
|
const saved = await getSavedState();
|
||||||
getStatus(),
|
applySavedState(saved.profiles, saved.targets, saved.generatedConfigPath);
|
||||||
getProfiles(),
|
|
||||||
getTargets(),
|
|
||||||
getComponents(),
|
|
||||||
]);
|
|
||||||
const activeProfiles = profiles.filter((profile) => profile.enabled);
|
|
||||||
const mainProfile = profiles.find((profile) => profile.id === MAIN_PROFILE_ID);
|
|
||||||
const activeProfile = mainProfile ?? activeProfiles[0];
|
|
||||||
const activeTarget = targetForUi(targets, status.activeTarget, activeProfile);
|
|
||||||
const editableProfiles = mainProfile ? [mainProfile] : activeProfiles;
|
|
||||||
|
|
||||||
if (activeTarget) setProxyInput(formatProxy(activeTarget));
|
|
||||||
setItems(itemsForProfiles(editableProfiles));
|
|
||||||
setLoadedProfiles(profiles);
|
|
||||||
setProfileId(mainProfile?.id ?? MAIN_PROFILE_ID);
|
|
||||||
setTargetId(activeTarget?.id ?? activeProfile?.targetId ?? MAIN_TARGET_ID);
|
|
||||||
|
|
||||||
setComponents(detectedComponents);
|
|
||||||
setGeneratedConfigPath(status.generatedConfigPath);
|
|
||||||
setNotice(null);
|
|
||||||
} catch {
|
} catch {
|
||||||
setNotice({
|
showNotice({
|
||||||
kind: 'info',
|
kind: 'info',
|
||||||
title: 'Режим предпросмотра',
|
title: 'Режим предпросмотра',
|
||||||
text: 'Запусти приложение через Tauri, чтобы увидеть найденный ProxiFyre и применить конфиг.',
|
text: 'Запусти приложение через Tauri, чтобы увидеть найденный ProxiFyre и применить конфиг.',
|
||||||
@@ -98,53 +120,108 @@ export function App() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void refreshComponents();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addItem() {
|
async function refreshComponents() {
|
||||||
const value = normalizeItemValue(newItemValue, newItemType);
|
setIsDetectingComponents(true);
|
||||||
|
try {
|
||||||
|
const detectedComponents = await getComponents();
|
||||||
|
setComponents(detectedComponents);
|
||||||
|
} catch (error) {
|
||||||
|
showNotice({
|
||||||
|
kind: 'error',
|
||||||
|
title: 'ProxiFyre не проверен',
|
||||||
|
text: errorMessage(error),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsDetectingComponents(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySavedState(profiles: Profile[], targets: Target[], generatedPath: string) {
|
||||||
|
const activeProfiles = profiles.filter((profile) => profile.enabled);
|
||||||
|
const mainProfile = profiles.find((profile) => profile.id === MAIN_PROFILE_ID);
|
||||||
|
const activeProfile = mainProfile ?? activeProfiles[0];
|
||||||
|
const activeTarget = targetForUi(targets, activeProfile);
|
||||||
|
const editableProfiles = mainProfile ? [mainProfile] : activeProfiles;
|
||||||
|
|
||||||
|
if (activeTarget) setProxyInput(formatProxy(activeTarget));
|
||||||
|
setItems(itemsForProfiles(editableProfiles));
|
||||||
|
setLoadedProfiles(profiles);
|
||||||
|
setProfileId(mainProfile?.id ?? MAIN_PROFILE_ID);
|
||||||
|
setTargetId(activeTarget?.id ?? activeProfile?.targetId ?? MAIN_TARGET_ID);
|
||||||
|
setGeneratedConfigPath(generatedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem(type: DraftItemType, rawValue: string) {
|
||||||
|
const value = normalizeItemValue(rawValue, type);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
setNotice({
|
showNotice({
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
title: 'Нечего добавить',
|
title: 'Нечего добавить',
|
||||||
text: newItemType === 'process' ? 'Введи имя процесса.' : 'Введи путь к EXE-файлу.',
|
text: emptyItemMessage(type),
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.some((item) => item.type === newItemType && sameValue(item.value, value))) {
|
if (items.some((item) => item.type === type && sameValue(item.value, value))) {
|
||||||
setNotice({
|
showNotice({
|
||||||
kind: 'info',
|
kind: 'info',
|
||||||
title: 'Уже добавлено',
|
title: 'Уже добавлено',
|
||||||
text: value,
|
text: value,
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setItems((current) => [
|
setItems((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
id: `${newItemType}-${Date.now()}`,
|
id: `${type}-${Date.now()}`,
|
||||||
type: newItemType,
|
type,
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setNewItemValue('');
|
return true;
|
||||||
setNotice(null);
|
}
|
||||||
|
|
||||||
|
function addProcess() {
|
||||||
|
if (addItem('process', processInput)) {
|
||||||
|
setProcessInput('');
|
||||||
|
setIsProcessInputOpen(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeItem(id: string) {
|
function removeItem(id: string) {
|
||||||
setItems((current) => current.filter((item) => item.id !== id));
|
setItems((current) => current.filter((item) => item.id !== id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pickAndAddItem(type: Extract<DraftItemType, 'exe' | 'folder'>) {
|
||||||
|
setPickerAction(type);
|
||||||
|
try {
|
||||||
|
const selectedPath = await pickPath(type);
|
||||||
|
if (selectedPath) {
|
||||||
|
addItem(type, selectedPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotice({
|
||||||
|
kind: 'error',
|
||||||
|
title: type === 'exe' ? 'EXE не выбран' : 'Папка не выбрана',
|
||||||
|
text: errorMessage(error),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setPickerAction(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateConfig() {
|
async function updateConfig() {
|
||||||
let parsedProxy: ParsedProxy;
|
let parsedProxy: ParsedProxy;
|
||||||
try {
|
try {
|
||||||
parsedProxy = parseProxy(proxyInput);
|
parsedProxy = parseProxy(proxyInput);
|
||||||
if (!items.length) {
|
if (!items.length) throw new Error('Добавь хотя бы один процесс, EXE-файл или папку.');
|
||||||
throw new Error('Добавь хотя бы один процесс или EXE-файл.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotice({
|
showNotice({
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
title: 'Проверь данные',
|
title: 'Проверь данные',
|
||||||
text: errorMessage(error),
|
text: errorMessage(error),
|
||||||
@@ -177,18 +254,16 @@ export function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const result = await applyProfiles();
|
const result = await applyProfiles();
|
||||||
const [status, detectedComponents, profiles] = await Promise.all([
|
const [saved, detectedComponents] = await Promise.all([
|
||||||
getStatus(),
|
getSavedState(),
|
||||||
getComponents(),
|
getComponents(),
|
||||||
getProfiles(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
applySavedState(saved.profiles, saved.targets, result.generatedConfigPath);
|
||||||
setComponents(detectedComponents);
|
setComponents(detectedComponents);
|
||||||
setGeneratedConfigPath(status.generatedConfigPath);
|
showNotice(noticeFromApply(result));
|
||||||
setLoadedProfiles(profiles);
|
|
||||||
setNotice(noticeFromApply(result));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotice({
|
showNotice({
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
title: 'Конфиг не обновлен',
|
title: 'Конфиг не обновлен',
|
||||||
text: errorMessage(error),
|
text: errorMessage(error),
|
||||||
@@ -202,13 +277,13 @@ export function App() {
|
|||||||
setIsOpeningConfig(true);
|
setIsOpeningConfig(true);
|
||||||
try {
|
try {
|
||||||
const openedPath = await openConfigLocation();
|
const openedPath = await openConfigLocation();
|
||||||
setNotice({
|
showNotice({
|
||||||
kind: 'info',
|
kind: 'info',
|
||||||
title: 'Конфиг открыт',
|
title: 'Конфиг открыт',
|
||||||
text: openedPath,
|
text: openedPath,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotice({
|
showNotice({
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
title: 'Не удалось открыть конфиг',
|
title: 'Не удалось открыть конфиг',
|
||||||
text: errorMessage(error),
|
text: errorMessage(error),
|
||||||
@@ -218,6 +293,66 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setProxiFyreServiceRunning(shouldRun: boolean) {
|
||||||
|
const action = shouldRun ? 'start' : 'stop';
|
||||||
|
setServiceAction(action);
|
||||||
|
startServiceVisual();
|
||||||
|
try {
|
||||||
|
await nextFrame();
|
||||||
|
const component = shouldRun
|
||||||
|
? await startProxiFyreService()
|
||||||
|
: await stopProxiFyreService();
|
||||||
|
|
||||||
|
setComponents((current) => upsertComponent(current, component));
|
||||||
|
showNotice({
|
||||||
|
kind: 'success',
|
||||||
|
title: shouldRun ? 'Служба запущена' : 'Служба остановлена',
|
||||||
|
text: proxyfierDetails(component, false),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showNotice({
|
||||||
|
kind: 'error',
|
||||||
|
title: shouldRun ? 'Служба не запущена' : 'Служба не остановлена',
|
||||||
|
text: errorMessage(error),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setServiceAction(null);
|
||||||
|
settleServiceVisual();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startServiceVisual() {
|
||||||
|
if (serviceVisualTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(serviceVisualTimerRef.current);
|
||||||
|
serviceVisualTimerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setServiceVisualState('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function settleServiceVisual() {
|
||||||
|
if (serviceVisualTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(serviceVisualTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setServiceVisualState('settling');
|
||||||
|
serviceVisualTimerRef.current = window.setTimeout(() => {
|
||||||
|
setServiceVisualState(null);
|
||||||
|
serviceVisualTimerRef.current = null;
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotice(notice: Notice) {
|
||||||
|
const entry: LogEntry = {
|
||||||
|
...notice,
|
||||||
|
id: `log-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
at: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setLogEntries((current) => [entry, ...current].slice(0, 40));
|
||||||
|
setActiveLogId(entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="simple-shell">
|
<main className="simple-shell">
|
||||||
<section className="simple-panel">
|
<section className="simple-panel">
|
||||||
@@ -226,16 +361,45 @@ export function App() {
|
|||||||
<small>VPN Proxy</small>
|
<small>VPN Proxy</small>
|
||||||
<h1>Прокси для приложений</h1>
|
<h1>Прокси для приложений</h1>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="ghost-button" onClick={refresh} disabled={isLoading}>
|
<button
|
||||||
{isLoading ? 'Ищу...' : 'Обновить'}
|
type="button"
|
||||||
|
className="ghost-button"
|
||||||
|
onClick={refresh}
|
||||||
|
disabled={isLoading || isDetectingComponents}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Загружаю...' : isDetectingComponents ? 'Проверяю...' : 'Обновить'}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className={`finder-card ${proxyfier?.installed ? 'found' : 'missing'}`}>
|
<div className={`finder-card ${finderStateClass} ${finderVisualClass}`.trim()}>
|
||||||
|
<span className="finder-border-glow" aria-hidden="true">
|
||||||
|
<span className="finder-border-glow-segment top" />
|
||||||
|
<span className="finder-border-glow-segment right" />
|
||||||
|
<span className="finder-border-glow-segment bottom" />
|
||||||
|
<span className="finder-border-glow-segment left" />
|
||||||
|
</span>
|
||||||
<span className="status-light" />
|
<span className="status-light" />
|
||||||
<div>
|
<div className="finder-text">
|
||||||
<strong>{proxyfierTitle(proxyfier)}</strong>
|
<strong>{proxyfierTitle(proxyfier, isDetectingComponents)}</strong>
|
||||||
<span>{proxyfierDetails(proxyfier)}</span>
|
<span>{proxyfierDetails(proxyfier, isDetectingComponents)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="service-actions" aria-label="Управление службой ProxiFyre">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="service-button"
|
||||||
|
onClick={() => setProxiFyreServiceRunning(true)}
|
||||||
|
disabled={isDetectingComponents || Boolean(serviceAction) || !proxyfier?.installed || Boolean(proxyfier?.running)}
|
||||||
|
>
|
||||||
|
{serviceAction === 'start' ? '...' : 'Запустить'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="service-button stop"
|
||||||
|
onClick={() => setProxiFyreServiceRunning(false)}
|
||||||
|
disabled={isDetectingComponents || Boolean(serviceAction) || !proxyfier?.installed || !proxyfier?.running}
|
||||||
|
>
|
||||||
|
{serviceAction === 'stop' ? '...' : 'Остановить'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -255,35 +419,90 @@ export function App() {
|
|||||||
<span>{items.length}</span>
|
<span>{items.length}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="add-line">
|
{isProcessInputOpen ? (
|
||||||
<select
|
<div className="process-add-line">
|
||||||
value={newItemType}
|
|
||||||
onChange={(event) => setNewItemType(event.target.value as DraftItemType)}
|
|
||||||
>
|
|
||||||
<option value="process">Процесс</option>
|
|
||||||
<option value="exe">EXE-файл</option>
|
|
||||||
</select>
|
|
||||||
<input
|
<input
|
||||||
value={newItemValue}
|
value={processInput}
|
||||||
onChange={(event) => setNewItemValue(event.target.value)}
|
onChange={(event) => setProcessInput(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') addItem();
|
if (event.key === 'Enter') addProcess();
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setProcessInput('');
|
||||||
|
setIsProcessInputOpen(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={newItemType === 'process' ? 'Discord' : 'C:\\Apps\\app.exe'}
|
placeholder="Discord"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={addItem}>
|
<button type="button" onClick={addProcess}>
|
||||||
Добавить
|
OK
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="process-cancel-button"
|
||||||
|
onClick={() => {
|
||||||
|
setProcessInput('');
|
||||||
|
setIsProcessInputOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="add-toolbar" aria-label="Добавить приложение">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="add-tile"
|
||||||
|
onClick={() => setIsProcessInputOpen(true)}
|
||||||
|
aria-label="Добавить процесс"
|
||||||
|
title="Процесс"
|
||||||
|
>
|
||||||
|
<Cpu size={22} strokeWidth={1.8} />
|
||||||
|
<span className="sr-only">Процесс</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="add-tile"
|
||||||
|
onClick={() => void pickAndAddItem('exe')}
|
||||||
|
disabled={Boolean(pickerAction)}
|
||||||
|
aria-label="Добавить EXE-файл"
|
||||||
|
title="EXE-файл"
|
||||||
|
>
|
||||||
|
{pickerAction === 'exe' ? <span className="tile-loading">...</span> : <FileCode2 size={22} strokeWidth={1.8} />}
|
||||||
|
<span className="sr-only">EXE-файл</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="add-tile"
|
||||||
|
onClick={() => void pickAndAddItem('folder')}
|
||||||
|
disabled={Boolean(pickerAction)}
|
||||||
|
aria-label="Добавить папку"
|
||||||
|
title="Папка"
|
||||||
|
>
|
||||||
|
{pickerAction === 'folder' ? <span className="tile-loading">...</span> : <FolderOpen size={22} strokeWidth={1.8} />}
|
||||||
|
<span className="sr-only">Папка</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="app-list">
|
<div className="app-list">
|
||||||
{items.length ? (
|
{isLoading ? (
|
||||||
|
<div className="list-skeleton" aria-label="Загрузка приложений">
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
) : items.length ? (
|
||||||
items.map((item) => (
|
items.map((item) => (
|
||||||
<div className="app-row" key={item.id}>
|
<div className="app-row" key={item.id}>
|
||||||
|
<div className="app-row-main">
|
||||||
|
<span className="item-icon" aria-hidden="true">
|
||||||
|
{itemIcon(item.type)}
|
||||||
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>{item.value}</strong>
|
<strong>{item.value}</strong>
|
||||||
<span>{item.type === 'process' ? 'процесс' : 'EXE-файл'}</span>
|
<span>{itemTypeLabel(item.type)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={() => removeItem(item.id)} aria-label={`Удалить ${item.value}`}>
|
<button type="button" onClick={() => removeItem(item.id)} aria-label={`Удалить ${item.value}`}>
|
||||||
Удалить
|
Удалить
|
||||||
@@ -291,18 +510,11 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="empty-state">Добавь процесс или путь к EXE-файлу.</div>
|
<div className="empty-state">Добавь процесс, папку или путь к EXE-файлу.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{notice ? (
|
|
||||||
<div className={`notice-line ${notice.kind}`}>
|
|
||||||
<strong>{notice.title}</strong>
|
|
||||||
<span>{notice.text}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="command-row">
|
<div className="command-row">
|
||||||
<button type="button" className="apply-button" onClick={updateConfig} disabled={isApplying}>
|
<button type="button" className="apply-button" onClick={updateConfig} disabled={isApplying}>
|
||||||
{isApplying ? 'Обновляю...' : 'Обновить конфиг'}
|
{isApplying ? 'Обновляю...' : 'Обновить конфиг'}
|
||||||
@@ -319,6 +531,38 @@ export function App() {
|
|||||||
|
|
||||||
{generatedConfigPath ? <p className="config-path">{generatedConfigPath}</p> : null}
|
{generatedConfigPath ? <p className="config-path">{generatedConfigPath}</p> : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{logEntries.length ? (
|
||||||
|
<aside className={`log-dock ${activeLog?.kind ?? 'idle'}`} aria-live="polite">
|
||||||
|
<div className={`log-current ${activeLog ? 'visible' : 'hidden'}`}>
|
||||||
|
{activeLog ? (
|
||||||
|
<>
|
||||||
|
<strong>{activeLog.title}</strong>
|
||||||
|
<span>{activeLog.text}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="log-muted">Журнал событий</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button type="button" className="log-toggle" onClick={() => setIsLogOpen((current) => !current)}>
|
||||||
|
{isLogOpen ? 'Скрыть' : 'Посмотреть'}
|
||||||
|
<span>{logEntries.length}</span>
|
||||||
|
</button>
|
||||||
|
{isLogOpen ? (
|
||||||
|
<div className="log-history">
|
||||||
|
{logEntries.map((entry) => (
|
||||||
|
<div className={`log-history-row ${entry.kind}`} key={entry.id}>
|
||||||
|
<time>{formatLogTime(entry.at)}</time>
|
||||||
|
<div>
|
||||||
|
<strong>{entry.title}</strong>
|
||||||
|
<span>{entry.text}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</aside>
|
||||||
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -358,8 +602,7 @@ function parseProxy(rawValue: string): ParsedProxy {
|
|||||||
return { protocol: 'socks5', host, port };
|
return { protocol: 'socks5', host, port };
|
||||||
}
|
}
|
||||||
|
|
||||||
function targetForUi(targets: Target[], activeTarget: Target | undefined, profile: Profile | undefined) {
|
function targetForUi(targets: Target[], profile: Profile | undefined) {
|
||||||
if (activeTarget) return activeTarget;
|
|
||||||
if (profile) return targets.find((target) => target.id === profile.targetId);
|
if (profile) return targets.find((target) => target.id === profile.targetId);
|
||||||
return targets.find((target) => target.id === MAIN_TARGET_ID) ?? targets.find((target) => target.kind === 'external');
|
return targets.find((target) => target.id === MAIN_TARGET_ID) ?? targets.find((target) => target.kind === 'external');
|
||||||
}
|
}
|
||||||
@@ -370,7 +613,7 @@ function itemsForProfiles(profiles: Profile[]): DraftItem[] {
|
|||||||
|
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
for (const item of profile.items) {
|
for (const item of profile.items) {
|
||||||
if (item.type !== 'process' && item.type !== 'exe') continue;
|
if (item.type !== 'process' && item.type !== 'folder' && item.type !== 'exe') continue;
|
||||||
|
|
||||||
const key = `${item.type}:${item.value.trim().toLowerCase()}`;
|
const key = `${item.type}:${item.value.trim().toLowerCase()}`;
|
||||||
if (seen.has(key)) continue;
|
if (seen.has(key)) continue;
|
||||||
@@ -395,7 +638,7 @@ function formatProxy(target: Target) {
|
|||||||
function normalizeItemValue(value: string, type: DraftItemType) {
|
function normalizeItemValue(value: string, type: DraftItemType) {
|
||||||
const clean = value.trim().replace(/^"|"$/g, '');
|
const clean = value.trim().replace(/^"|"$/g, '');
|
||||||
if (!clean) return '';
|
if (!clean) return '';
|
||||||
if (type === 'exe') return clean;
|
if (type === 'folder' || type === 'exe') return clean;
|
||||||
|
|
||||||
return clean
|
return clean
|
||||||
.split(/[\\/]/)
|
.split(/[\\/]/)
|
||||||
@@ -404,14 +647,58 @@ function normalizeItemValue(value: string, type: DraftItemType) {
|
|||||||
.trim() ?? '';
|
.trim() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pickPath(type: Extract<DraftItemType, 'exe' | 'folder'>) {
|
||||||
|
const selected = await open(
|
||||||
|
type === 'folder'
|
||||||
|
? {
|
||||||
|
title: 'Выбери папку',
|
||||||
|
directory: true,
|
||||||
|
multiple: false,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: 'Выбери EXE-файл',
|
||||||
|
directory: false,
|
||||||
|
multiple: false,
|
||||||
|
filters: [{ name: 'EXE-файлы', extensions: ['exe'] }],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(selected)) return selected[0] ?? null;
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFrame() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
window.requestAnimationFrame(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function profileItemInput(item: DraftItem): ProfileItemInput {
|
function profileItemInput(item: DraftItem): ProfileItemInput {
|
||||||
return {
|
return {
|
||||||
type: item.type,
|
type: item.type,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
recursive: false,
|
recursive: item.type === 'folder',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emptyItemMessage(type: DraftItemType) {
|
||||||
|
if (type === 'process') return 'Введи имя процесса.';
|
||||||
|
if (type === 'folder') return 'Введи путь к папке.';
|
||||||
|
return 'Введи путь к EXE-файлу.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemTypeLabel(type: DraftItemType) {
|
||||||
|
if (type === 'process') return 'процесс';
|
||||||
|
if (type === 'folder') return 'папка';
|
||||||
|
return 'EXE-файл';
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemIcon(type: DraftItemType) {
|
||||||
|
if (type === 'process') return <Cpu size={18} strokeWidth={1.9} />;
|
||||||
|
if (type === 'folder') return <FolderOpen size={18} strokeWidth={1.9} />;
|
||||||
|
return <FileCode2 size={18} strokeWidth={1.9} />;
|
||||||
|
}
|
||||||
|
|
||||||
function profileInputFromProfile(profile: Profile, enabled: boolean) {
|
function profileInputFromProfile(profile: Profile, enabled: boolean) {
|
||||||
return {
|
return {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
@@ -427,14 +714,16 @@ function profileInputFromProfile(profile: Profile, enabled: boolean) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function proxyfierTitle(component: ComponentStatus | undefined) {
|
function proxyfierTitle(component: ComponentStatus | undefined, checking: boolean) {
|
||||||
|
if (checking) return 'Проверяю ProxiFyre';
|
||||||
if (!component) return 'ProxiFyre не проверен';
|
if (!component) return 'ProxiFyre не проверен';
|
||||||
if (component.running) return 'ProxiFyre найден и запущен';
|
if (component.running) return 'ProxiFyre найден и запущен';
|
||||||
if (component.installed) return 'ProxiFyre найден';
|
if (component.installed) return 'ProxiFyre найден';
|
||||||
return 'ProxiFyre не найден';
|
return 'ProxiFyre не найден';
|
||||||
}
|
}
|
||||||
|
|
||||||
function proxyfierDetails(component: ComponentStatus | undefined) {
|
function proxyfierDetails(component: ComponentStatus | undefined, checking: boolean) {
|
||||||
|
if (checking) return 'Ищу установленный клиент и состояние службы.';
|
||||||
if (!component) return 'Нажми «Обновить», чтобы проверить компьютер.';
|
if (!component) return 'Нажми «Обновить», чтобы проверить компьютер.';
|
||||||
if (component.path) return component.path;
|
if (component.path) return component.path;
|
||||||
return component.problems[0] ?? 'Путь установки не найден.';
|
return component.problems[0] ?? 'Путь установки не найден.';
|
||||||
@@ -448,10 +737,29 @@ function noticeFromApply(result: ApplyProfilesResponse): Notice {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upsertComponent(components: ComponentStatus[], component: ComponentStatus) {
|
||||||
|
const index = components.findIndex((current) => current.id === component.id);
|
||||||
|
if (index === -1) return [...components, component];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...components.slice(0, index),
|
||||||
|
component,
|
||||||
|
...components.slice(index + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function sameValue(left: string, right: string) {
|
function sameValue(left: string, right: string) {
|
||||||
return left.trim().toLowerCase() === right.trim().toLowerCase();
|
return left.trim().toLowerCase() === right.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLogTime(timestamp: number) {
|
||||||
|
return new Date(timestamp).toLocaleTimeString('ru-RU', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function errorMessage(error: unknown) {
|
function errorMessage(error: unknown) {
|
||||||
if (error instanceof Error) return error.message;
|
if (error instanceof Error) return error.message;
|
||||||
if (typeof error === 'string') return error;
|
if (typeof error === 'string') return error;
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input {
|
||||||
select {
|
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +50,15 @@ button:disabled {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: #101216;
|
background: #101216;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 18px;
|
padding: 18px 18px 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-header,
|
.simple-header,
|
||||||
.finder-card,
|
.finder-card,
|
||||||
.section-head,
|
.section-head,
|
||||||
.add-line,
|
.add-toolbar,
|
||||||
.app-row,
|
.process-add-line,
|
||||||
.notice-line {
|
.app-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -75,7 +74,7 @@ button:disabled {
|
|||||||
|
|
||||||
.simple-header small,
|
.simple-header small,
|
||||||
.simple-field span,
|
.simple-field span,
|
||||||
.app-row span,
|
.app-row-main > div > span,
|
||||||
.config-path,
|
.config-path,
|
||||||
.finder-card span {
|
.finder-card span {
|
||||||
color: #8d99ae;
|
color: #8d99ae;
|
||||||
@@ -99,9 +98,11 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ghost-button,
|
.ghost-button,
|
||||||
.add-line button,
|
.add-toolbar button,
|
||||||
|
.process-add-line button,
|
||||||
.app-row button,
|
.app-row button,
|
||||||
.open-config-button {
|
.open-config-button,
|
||||||
|
.service-button {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
border: 1px solid #343b49;
|
border: 1px solid #343b49;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -112,30 +113,112 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ghost-button:hover,
|
.ghost-button:hover,
|
||||||
.add-line button:hover,
|
.add-toolbar button:hover,
|
||||||
|
.process-add-line button:hover,
|
||||||
.app-row button:hover,
|
.app-row button:hover,
|
||||||
.open-config-button:hover {
|
.open-config-button:hover,
|
||||||
|
.service-button:hover {
|
||||||
background: #2d3543;
|
background: #2d3543;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finder-card {
|
.finder-card {
|
||||||
justify-content: flex-start;
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
justify-content: stretch;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
|
overflow: hidden;
|
||||||
border: 1px solid #2b3342;
|
border: 1px solid #2b3342;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #151923;
|
background: #151923;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finder-card > div,
|
.finder-border-glow {
|
||||||
.app-row > div {
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: inherit;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-card > :not(.finder-border-glow) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-card.checking .finder-border-glow,
|
||||||
|
.finder-card.working .finder-border-glow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-card.checking .finder-border-glow {
|
||||||
|
opacity: 0.78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-card.settling .finder-border-glow {
|
||||||
|
opacity: 0;
|
||||||
|
transition-duration: 0.7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-border-glow-segment {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
background: #93c5fd;
|
||||||
|
box-shadow:
|
||||||
|
0 0 8px rgba(96, 165, 250, 0.95),
|
||||||
|
0 0 16px rgba(34, 197, 94, 0.36);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-border-glow-segment.top,
|
||||||
|
.finder-border-glow-segment.bottom {
|
||||||
|
width: 108px;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, transparent, #60a5fa 24%, #bbf7d0 54%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-border-glow-segment.right,
|
||||||
|
.finder-border-glow-segment.left {
|
||||||
|
width: 2px;
|
||||||
|
height: 64px;
|
||||||
|
background: linear-gradient(180deg, transparent, #60a5fa 24%, #bbf7d0 54%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-border-glow-segment.top {
|
||||||
|
top: 0;
|
||||||
|
animation: finder-border-top 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-border-glow-segment.right {
|
||||||
|
right: 0;
|
||||||
|
animation: finder-border-right 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-border-glow-segment.bottom {
|
||||||
|
bottom: 0;
|
||||||
|
animation: finder-border-bottom 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-border-glow-segment.left {
|
||||||
|
left: 0;
|
||||||
|
animation: finder-border-left 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finder-text,
|
||||||
|
.app-row > div,
|
||||||
|
.app-row-main > div {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finder-card strong,
|
.finder-card strong,
|
||||||
.finder-card span,
|
.finder-card span,
|
||||||
.app-row strong,
|
.app-row-main strong,
|
||||||
.app-row span {
|
.app-row-main > div > span {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
@@ -158,6 +241,29 @@ button:disabled {
|
|||||||
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12);
|
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.finder-card.checking .status-light {
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
border-top-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
animation: spin 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-button {
|
||||||
|
min-width: 102px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-button.stop {
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
.simple-field {
|
.simple-field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
@@ -165,8 +271,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.simple-field input,
|
.simple-field input,
|
||||||
.add-line input,
|
.process-add-line input {
|
||||||
.add-line select {
|
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #343b49;
|
border: 1px solid #343b49;
|
||||||
@@ -178,8 +283,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.simple-field input:focus,
|
.simple-field input:focus,
|
||||||
.add-line input:focus,
|
.process-add-line input:focus {
|
||||||
.add-line select:focus {
|
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
box-shadow: 0 0 0 1px #3b82f6;
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
}
|
}
|
||||||
@@ -202,29 +306,99 @@ button:disabled {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-line {
|
.add-toolbar {
|
||||||
display: grid;
|
justify-content: center;
|
||||||
grid-template-columns: 130px minmax(0, 1fr) auto;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-line button {
|
.add-tile {
|
||||||
min-width: 104px;
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 74px;
|
||||||
|
height: 48px;
|
||||||
|
border-color: #2b3342;
|
||||||
|
background: #131720;
|
||||||
|
color: #dbeafe;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tile svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tile:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: #182033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tile[aria-pressed="true"] {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-loading {
|
||||||
|
color: #dbeafe;
|
||||||
|
font-weight: 800;
|
||||||
|
animation: pulse 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-add-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 72px 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-add-line button {
|
||||||
border-color: #166534;
|
border-color: #166534;
|
||||||
background: #14532d;
|
background: #14532d;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-line button:hover {
|
.process-add-line .process-cancel-button {
|
||||||
|
border-color: #343b49;
|
||||||
|
background: #242a35;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-add-line button:hover {
|
||||||
background: #166534;
|
background: #166534;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.process-add-line .process-cancel-button:hover {
|
||||||
|
background: #2d3543;
|
||||||
|
}
|
||||||
|
|
||||||
.app-list {
|
.app-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-row-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-row .item-icon {
|
||||||
|
display: grid;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid #2b3342;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #0d1016;
|
||||||
|
color: #dbeafe;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-row .item-icon svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.app-row,
|
.app-row,
|
||||||
.empty-state,
|
.empty-state {
|
||||||
.notice-line {
|
|
||||||
border: 1px solid #2b3342;
|
border: 1px solid #2b3342;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #131720;
|
background: #131720;
|
||||||
@@ -240,30 +414,20 @@ button:disabled {
|
|||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-line {
|
.list-skeleton {
|
||||||
align-items: flex-start;
|
display: grid;
|
||||||
justify-content: flex-start;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-line strong,
|
.list-skeleton span {
|
||||||
.notice-line span {
|
min-height: 56px;
|
||||||
display: block;
|
border: 1px solid #2b3342;
|
||||||
}
|
border-radius: 4px;
|
||||||
|
background:
|
||||||
.notice-line.success {
|
linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.12), transparent),
|
||||||
border-color: rgba(34, 197, 94, 0.42);
|
#131720;
|
||||||
background: rgba(20, 83, 45, 0.32);
|
background-size: 220% 100%;
|
||||||
}
|
animation: shimmer 1.15s linear infinite;
|
||||||
|
|
||||||
.notice-line.error {
|
|
||||||
border-color: rgba(239, 68, 68, 0.42);
|
|
||||||
background: rgba(127, 29, 29, 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notice-line.info {
|
|
||||||
border-color: rgba(59, 130, 246, 0.42);
|
|
||||||
background: rgba(30, 58, 138, 0.26);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-row {
|
.command-row {
|
||||||
@@ -299,27 +463,314 @@ button:disabled {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-dock {
|
||||||
|
position: fixed;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 30;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 48px;
|
||||||
|
border: 1px solid #2b3342;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(19, 23, 32, 0.98);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.34);
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-dock.error {
|
||||||
|
border-color: rgba(239, 68, 68, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-dock.success {
|
||||||
|
border-color: rgba(34, 197, 94, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-dock.info {
|
||||||
|
border-color: rgba(59, 130, 246, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-current {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 0 8px;
|
||||||
|
transition: opacity 180ms ease, transform 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-current.hidden {
|
||||||
|
color: #8d99ae;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-current.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-current strong,
|
||||||
|
.log-current span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-current strong {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-current span {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-muted {
|
||||||
|
color: #8d99ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
border: 1px solid #343b49;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #242a35;
|
||||||
|
color: #eef2ff;
|
||||||
|
padding: 7px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-toggle:hover {
|
||||||
|
background: #2d3543;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-toggle span {
|
||||||
|
min-width: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1b202b;
|
||||||
|
color: #dbeafe;
|
||||||
|
padding: 2px 6px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 230px;
|
||||||
|
overflow: auto;
|
||||||
|
border-top: 1px solid #2b3342;
|
||||||
|
padding-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 76px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
border: 1px solid #2b3342;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #0d1016;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row.error {
|
||||||
|
border-color: rgba(239, 68, 68, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row.success {
|
||||||
|
border-color: rgba(34, 197, 94, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row.info {
|
||||||
|
border-color: rgba(59, 130, 246, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row time {
|
||||||
|
color: #8d99ae;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row strong,
|
||||||
|
.log-history-row span {
|
||||||
|
display: block;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row span {
|
||||||
|
color: #b6c2d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
50% {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes finder-border-top {
|
||||||
|
0% {
|
||||||
|
left: -116px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
4%,
|
||||||
|
23% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
25%,
|
||||||
|
100% {
|
||||||
|
left: calc(100% + 8px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes finder-border-right {
|
||||||
|
0%,
|
||||||
|
25% {
|
||||||
|
top: -72px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
29%,
|
||||||
|
48% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes finder-border-bottom {
|
||||||
|
0%,
|
||||||
|
50% {
|
||||||
|
right: -116px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
54%,
|
||||||
|
73% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
75%,
|
||||||
|
100% {
|
||||||
|
right: calc(100% + 8px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes finder-border-left {
|
||||||
|
0%,
|
||||||
|
75% {
|
||||||
|
bottom: -72px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
79%,
|
||||||
|
98% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
from {
|
||||||
|
background-position: 220% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background-position: -220% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (max-width: 680px) {
|
||||||
.simple-shell {
|
.simple-shell {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-panel {
|
.simple-panel {
|
||||||
padding: 14px;
|
padding: 14px 14px 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-header,
|
.simple-header,
|
||||||
.app-row,
|
.app-row {
|
||||||
.notice-line {
|
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-line {
|
.add-toolbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tile {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-add-line {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.finder-card {
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.command-row {
|
.command-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-dock {
|
||||||
|
right: 8px;
|
||||||
|
left: 8px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-current {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-history-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user