Compare commits

...

38 Commits

Author SHA1 Message Date
2dust
06cec89ec9 up 7.19.3 2026-03-10 17:38:03 +08:00
2dust
26f65dd3b2 Code cleanup: pattern matching and minor fixes 2026-03-10 17:19:21 +08:00
2dust
0c2114d2e1 Refactor SRS rule collection and add DNS parsing 2026-03-10 15:29:36 +08:00
DHR60
4af528f8e2 Typo (#8917) 2026-03-10 14:10:23 +08:00
DHR60
588e82f0d9 Tun ech protect (#8915) 2026-03-10 09:16:16 +08:00
DHR60
0c13488410 Fix (#8914)
* Fix

* Fix
2026-03-10 09:14:57 +08:00
DHR60
a88396c11d Fix custom config sub chain (#8913) 2026-03-10 09:13:52 +08:00
DHR60
ef5fee9975 Fix (#8909) 2026-03-08 19:00:27 +08:00
2dust
df800a60c2 Persist DataGrid column layout for ClashConnectionsView 2026-03-08 18:59:56 +08:00
2dust
679bd8afcc Persist DataGrid column layout for ClashConnectionsView
https://github.com/2dust/v2rayN/issues/8893
2026-03-08 17:57:09 +08:00
DHR60
66e1aeae1f Fix speedtest core type (#8900)
* Fix speedtest core type

* Simplify code
2026-03-07 16:36:03 +08:00
dependabot[bot]
e03c22092f Bump actions/setup-dotnet from 5.0.1 to 5.2.0 (#8891)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.0.1 to 5.2.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.0.1...v5.2.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 16:33:41 +08:00
2dust
c0aa829abb up 7.19.2 2026-03-05 09:49:46 +08:00
DHR60
b8f7cc0768 Fix hosts matching (#8890)
* Fix hosts matching

* Fix hosts resolve rule

* Fix
2026-03-04 20:13:06 +08:00
2dust
81da72bb39 Fix
https://github.com/2dust/v2rayN/issues/8881
2026-03-04 19:11:31 +08:00
2dust
d9201157c8 Bug fix
https://github.com/2dust/v2rayN/issues/8875
2026-03-04 18:58:59 +08:00
2dust
e179d5bc42 Fix
https://github.com/2dust/v2rayN/issues/8870
2026-03-03 16:03:23 +08:00
2dust
4d2f32099e Bug fix
https://github.com/2dust/v2rayN/issues/8879
2026-03-03 10:07:48 +08:00
2dust
f24a79aa2c Bug fix
https://github.com/2dust/v2rayN/issues/8875
2026-03-02 20:18:07 +08:00
2dust
9a3604e89b Bug fix
https://github.com/2dust/v2rayN/issues/8874
2026-03-02 20:00:02 +08:00
DHR60
fd7cf0d453 Fix xray custom dns (#8872) 2026-03-02 19:46:01 +08:00
tt2563
65cf782eb0 更新繁體中文翻譯 (#8873)
Co-authored-by: ertet <sfsa@sdaf.cc>
2026-03-02 19:44:53 +08:00
2dust
bfa9eaa5ec up 7.19.1 2026-03-02 09:31:57 +08:00
2dust
cea725ae3d Update Directory.Packages.props 2026-03-02 09:31:34 +08:00
DHR60
c9df9a0001 Fix (#8868) 2026-03-01 19:44:44 +08:00
DHR60
56f1794e47 Fix DNS rule (#8866) 2026-03-01 18:38:16 +08:00
DHR60
a71ebbd01c Optimize (#8862)
* Relax group type restrictions

* Optimize db read
2026-03-01 17:41:59 +08:00
DHR60
9f6237fb21 Speedtest respect user-set core type (#8856)
* Respect user-set core type

* Allow test group
2026-03-01 16:11:40 +08:00
DHR60
99d67ca3f1 Fix DNS routing (#8841) 2026-03-01 14:41:02 +08:00
2dust
d56e896f07 Ignore json file extensions in IsDomain 2026-03-01 14:20:05 +08:00
DHR60
6b4ae5a386 Fix (#8864)
* Fix

* Build all contexts

* Notify validator result

* Fix
2026-03-01 13:47:48 +08:00
DHR60
a3ff31088e Perf Policy Group generate (#8855)
* Perf Policy Group generate

* Scroll to new group node

* Add region group

* I18n

* Fix

* Fix

* Move default filter to Global

* Default Filter List

* Increase UI column widths for AddGroupServerWindow

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-03-01 10:05:03 +08:00
DHR60
584e538623 Refactor node pre check (#8848)
* Refactor node pre check

* Rename method

* I18n

* Add remark not found warnings

* Fix

* Fix

* Fix

* Update v2rayN/ServiceLib/Handler/CoreConfigHandler.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 16:31:02 +08:00
JieXu
67c4ae02ba Fix bugs (#8854)
* Update package-rhel.sh

* Update package-debian.sh

* Update ResUI.fr.resx

* Update package-rhel.sh

* Update package-rhel.sh
2026-02-27 15:15:31 +08:00
2dust
ed1275e29f Upgrade Downloader to 4.1.1 and update API usage 2026-02-27 11:21:51 +08:00
dependabot[bot]
0cf07e925f Bump actions/download-artifact from 7 to 8 (#8851)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 11:04:20 +08:00
dependabot[bot]
49e487886d Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#8850)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6.0.0...v7.0.0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 11:04:16 +08:00
DHR60
ad07f281c7 Fix routing (#8849)
Fix
2026-02-27 11:04:00 +08:00
67 changed files with 2374 additions and 1914 deletions

View File

@@ -37,7 +37,7 @@ jobs:
fetch-depth: '0'
- name: Setup .NET
uses: actions/setup-dotnet@v5.0.1
uses: actions/setup-dotnet@v5.2.0
with:
dotnet-version: '8.0.x'
@@ -50,7 +50,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o "$OutputPathArm64"
- name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0
uses: actions/upload-artifact@v7.0.0
with:
name: v2rayN-linux
path: |
@@ -169,7 +169,7 @@ jobs:
fetch-depth: '0'
- name: Restore build artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: v2rayN-linux
path: ${{ github.workspace }}/v2rayN/Release
@@ -190,7 +190,7 @@ jobs:
ls -R "$GITHUB_WORKSPACE/dist/rpm" || true
- name: Upload RPM artifacts
uses: actions/upload-artifact@v6.0.0
uses: actions/upload-artifact@v7.0.0
with:
name: v2rayN-rpm
path: dist/rpm/**/*.rpm

View File

@@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v5.0.1
uses: actions/setup-dotnet@v5.2.0
with:
dotnet-version: '8.0.x'
@@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0
uses: actions/upload-artifact@v7.0.0
with:
name: v2rayN-macos
path: |

View File

@@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v5.0.1
uses: actions/setup-dotnet@v5.2.0
with:
dotnet-version: '8.0.x'
@@ -45,7 +45,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0
uses: actions/upload-artifact@v7.0.0
with:
name: v2rayN-windows-desktop
path: |

View File

@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup
uses: actions/setup-dotnet@v5.0.1
uses: actions/setup-dotnet@v5.2.0
with:
dotnet-version: '8.0.x'
@@ -42,7 +42,7 @@ jobs:
dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64
- name: Upload build artifacts
uses: actions/upload-artifact@v6.0.0
uses: actions/upload-artifact@v7.0.0
with:
name: v2rayN-windows
path: |

View File

@@ -16,7 +16,7 @@ cp -rf $OutputPath "${PackagePath}/opt/v2rayN"
echo "When this file exists, app will not store configs under this folder" > "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt"
if [ $Arch = "linux-64" ]; then
Arch2="amd64"
Arch2="amd64"
else
Arch2="arm64"
fi
@@ -32,9 +32,8 @@ Depends: libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26),
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF
cat >"${PackagePath}/DEBIAN/postinst" <<-EOF
if [ ! -s /usr/share/applications/v2rayN.desktop ]; then
cat >/usr/share/applications/v2rayN.desktop<<-END
mkdir -p "${PackagePath}/usr/share/applications"
cat >"${PackagePath}/usr/share/applications/v2rayN.desktop" <<-EOF
[Desktop Entry]
Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
@@ -43,10 +42,12 @@ Icon=/opt/v2rayN/v2rayN.png
Terminal=false
Type=Application
Categories=Network;Application;
END
fi
EOF
update-desktop-database
cat >"${PackagePath}/DEBIAN/postinst" <<-'EOF'
set -e
update-desktop-database || true
exit 0
EOF
sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"

View File

@@ -1,45 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
# ====== Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS ======
if [[ -r /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
rhel|rocky|almalinux|fedora|centos)
echo "[OK] Detected supported system: $NAME $VERSION_ID"
;;
*)
echo "[ERROR] Unsupported system: $NAME ($ID)."
echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS."
exit 1
;;
esac
else
echo "[ERROR] Cannot detect system (missing /etc/os-release)."
exit 1
# Require Red Hat base branch
. /etc/os-release
case "${ID:-}" in
rhel|rocky|almalinux|fedora|centos)
echo "Detected supported system: ${NAME:-$ID} ${VERSION_ID:-}"
;;
*)
echo "Unsupported system: ${NAME:-unknown} (${ID:-unknown})."
echo "This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
exit 1
;;
esac
# Kernel version
MIN_KERNEL="6.11"
CURRENT_KERNEL="$(uname -r)"
lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$CURRENT_KERNEL" | sort -V | head -n1)"
if [[ "$lowest" != "$MIN_KERNEL" ]]; then
echo "Kernel $CURRENT_KERNEL is below $MIN_KERNEL"
exit 1
fi
# ======================== Kernel version check (require >= 6.11) =======================
MIN_KERNEL_MAJOR=6
MIN_KERNEL_MINOR=11
KERNEL_FULL=$(uname -r)
KERNEL_MAJOR=$(echo "$KERNEL_FULL" | cut -d. -f1)
KERNEL_MINOR=$(echo "$KERNEL_FULL" | cut -d. -f2)
echo "[OK] Kernel $CURRENT_KERNEL verified."
echo "[INFO] Detected kernel version: $KERNEL_FULL"
if (( KERNEL_MAJOR < MIN_KERNEL_MAJOR )) || { (( KERNEL_MAJOR == MIN_KERNEL_MAJOR )) && (( KERNEL_MINOR < MIN_KERNEL_MINOR )); }; then
echo "[ERROR] Kernel $KERNEL_FULL is too old. Requires Linux >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
echo "Please upgrade your system or use a newer container (e.g. Fedora 42+, RHEL 10+)."
exit 1
fi
echo "[OK] Kernel version >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}."
# ===== Config & Parse arguments =========================================================
# Config & Parse arguments
VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty
WITH_CORE="both" # Default: bundle both xray+sing-box
AUTOSTART=0 # 1 = enable system-wide autostart (/etc/xdg/autostart)
FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads
ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target)
BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively
@@ -55,7 +46,6 @@ if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi
while [[ $# -gt 0 ]]; do
case "$1" in
--with-core) WITH_CORE="${2:-both}"; shift 2;;
--autostart) AUTOSTART=1; shift;;
--xray-ver) XRAY_VER="${2:-}"; shift 2;;
--singbox-ver) SING_VER="${2:-}"; shift 2;;
--netcore) FORCE_NETCORE=1; shift;;
@@ -69,38 +59,26 @@ done
# Conflict: version number AND --buildfrom cannot be used together
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
echo "[ERROR] You cannot specify both an explicit version and --buildfrom at the same time."
echo "You cannot specify both an explicit version and --buildfrom at the same time."
echo " Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
exit 1
fi
# ===== Environment check + Dependencies ========================================
# Check and install dependencies
host_arch="$(uname -m)"
[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; }
install_ok=0
case "$ID" in
rhel|rocky|almalinux|centos)
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
sudo dnf -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
install_ok=1
elif command -v yum >/dev/null 2>&1; then
sudo yum -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \
sudo yum -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync
install_ok=1
fi
;;
*)
;;
esac
if [[ "$install_ok" -ne 1 ]]; then
echo "[WARN] Could not auto-install dependencies for '$ID'. Make sure these are available:"
echo " dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on RPM-based distros)"
if command -v dnf >/dev/null 2>&1; then
sudo dnf -y install rpm-build rpmdevtools curl unzip tar jq rsync dotnet-sdk-8.0 \
&& install_ok=1
fi
command -v curl >/dev/null
if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$ID'. Make sure these are available:"
echo "dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on Red Hat branch)"
fi
# Root directory
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
@@ -119,9 +97,6 @@ if [[ ! -f "$PROJECT" ]]; then
fi
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; }
# Resolve GUI version & auto checkout
VERSION=""
choose_channel() {
# If --buildfrom provided, map it directly and skip interaction.
if [[ -n "${BUILD_FROM:-}" ]]; then
@@ -135,60 +110,35 @@ choose_channel() {
# Print menu to stderr and read from /dev/tty so stdout only carries the token.
local ch="latest" sel=""
if [[ -t 0 ]]; then
echo "[?] Choose v2rayN release channel:" >&2
echo " 1) Latest (stable) [default]" >&2
echo " 2) Pre-release (preview)" >&2
echo " 3) Keep current (do nothing)" >&2
printf "Enter 1, 2 or 3 [default 1]: " >&2
if read -r sel </dev/tty; then
case "${sel:-}" in
2) ch="prerelease" ;;
3) ch="keep" ;;
*) ch="latest" ;;
esac
else
ch="latest"
fi
else
ch="latest"
fi
echo "$ch"
}
get_latest_tag_latest() {
# Resolve /releases/latest → tag_name
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
| grep -Eo '"tag_name":\s*"v?[^"]+"' \
| head -n1 \
| sed -E 's/.*"tag_name":\s*"v?([^"]+)".*/\1/'
| jq -re '.tag_name' \
| sed 's/^v//'
}
get_latest_tag_prerelease() {
# Resolve newest prerelease=true tag; prefer jq, fallback to sed/grep (no awk)
local json tag
json="$(curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20")" || return 1
# 1) Use jq if present
if command -v jq >/dev/null 2>&1; then
tag="$(printf '%s' "$json" \
| jq -r '[.[] | select(.prerelease==true)][0].tag_name' 2>/dev/null \
| sed 's/^v//')" || true
fi
# 2) Fallback to sed/grep only
if [[ -z "${tag:-}" || "${tag:-}" == "null" ]]; then
tag="$(printf '%s' "$json" \
| tr '\n' ' ' \
| sed 's/},[[:space:]]*{/\n/g' \
| grep -m1 -E '"prerelease"[[:space:]]*:[[:space:]]*true' \
| grep -Eo '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' \
| head -n1 \
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/')" || true
fi
[[ -n "${tag:-}" && "${tag:-}" != "null" ]] || return 1
printf '%s\n' "$tag"
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases?per_page=20" \
| jq -re 'first(.[] | select(.prerelease == true) | .tag_name)' \
| sed 's/^v//'
}
git_try_checkout() {
@@ -196,11 +146,7 @@ git_try_checkout() {
local want="$1" ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true
if git rev-parse "refs/tags/v${want}" >/dev/null 2>&1; then
ref="v${want}"
elif git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then
ref="${want}"
elif git rev-parse --verify "${want}" >/dev/null 2>&1; then
if git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then
ref="${want}"
fi
if [[ -n "$ref" ]]; then
@@ -216,88 +162,56 @@ git_try_checkout() {
return 1
}
apply_channel_or_keep() {
local ch="$1" tag
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo '0.0.0+git')"
VERSION="${VERSION#v}"
return 0
fi
echo "[*] Resolving ${ch} tag from GitHub releases..."
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
}
if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then
echo "[*] Trying to switch v2rayN repo to version: ${VERSION_ARG}"
if git_try_checkout "${VERSION_ARG#v}"; then
VERSION="${VERSION_ARG#v}"
clean_ver="${VERSION_ARG#v}"
if git_try_checkout "$clean_ver"; then
VERSION="$clean_ver"
else
echo "[WARN] Tag '${VERSION_ARG}' not found."
ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
apply_channel_or_keep "$ch"
fi
else
ch="$(choose_channel)"
if [[ "$ch" == "keep" ]]; then
echo "[*] Keep current repository state (no checkout)."
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
VERSION="${VERSION#v}"
else
echo "[*] Resolving ${ch} tag from GitHub releases..."
tag=""
if [[ "$ch" == "prerelease" ]]; then
tag="$(get_latest_tag_prerelease || true)"
if [[ -z "$tag" ]]; then
echo "[WARN] Failed to resolve prerelease tag, falling back to latest."
tag="$(get_latest_tag_latest || true)"
fi
else
tag="$(get_latest_tag_latest || true)"
fi
[[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; }
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; }
VERSION="${tag#v}"
fi
apply_channel_or_keep "$ch"
fi
else
echo "[WARN] Current directory is not a git repo; cannot checkout version. Proceeding on current tree."
VERSION="${VERSION_ARG:-}"
if [[ -z "$VERSION" ]]; then
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
VERSION="$(git describe --tags --abbrev=0)"
else
VERSION="0.0.0+git"
fi
fi
VERSION="${VERSION#v}"
echo "Current directory is not a git repo; proceeding on current tree."
VERSION="${VERSION_ARG:-0.0.0}"
fi
VERSION="${VERSION#v}"
echo "[*] GUI version resolved as: ${VERSION}"
# ===== Helpers for core/rules download (use RID_DIR for arch sync) =====================
# Helpers for core
download_xray() {
# Download Xray core and install to outdir/xray
# Download Xray core
local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir"
if [[ -n "${XRAY_VER:-}" ]]; then ver="${XRAY_VER}"; fi
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
@@ -316,10 +230,9 @@ download_xray() {
}
download_singbox() {
# Download sing-box core and install to outdir/sing-box
# Download sing-box
local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir"
if [[ -n "${SING_VER:-}" ]]; then ver="${SING_VER}"; fi
if [[ -z "$ver" ]]; then
ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \
| grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true
@@ -339,7 +252,7 @@ download_singbox() {
install -Dm755 "$bin" "$outdir/sing-box"
}
# Move geo files to a unified path: outroot/bin
# Move geo files to outroot/bin
unify_geo_layout() {
local outroot="$1"
mkdir -p "$outroot/bin"
@@ -351,18 +264,13 @@ unify_geo_layout() {
"geoip.metadb" \
)
for n in "${names[@]}"; do
# If file exists under bin/xray/, move it up to bin/
if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
# If file already in bin/, leave it as-is
if [[ -f "$outroot/bin/$n" ]]; then
:
fi
done
}
# Download geo/rule assets; then unify to bin/
# Download geo/rule assets
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
@@ -396,7 +304,7 @@ download_geo_assets() {
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done
# Unify to bin/
# Unify to bin
unify_geo_layout "$outroot"
}
@@ -427,7 +335,7 @@ download_v2rayn_bundle() {
local nested_dir
nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)"
if [[ -n "${nested_dir:-}" && -d "$nested_dir/bin" ]]; then
if [[ -n "$nested_dir" && -d "$nested_dir/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$nested_dir/bin/" "$outroot/bin/"
rm -rf "$nested_dir"
@@ -451,7 +359,7 @@ build_for_arch() {
case "$short" in
x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;;
arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;;
*) echo "[ERROR] Unknown arch '$short' (use x64|arm64)"; return 1;;
*) echo "Unknown arch '$short' (use x64|arm64)"; return 1;;
esac
echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)"
@@ -464,8 +372,7 @@ build_for_arch() {
dotnet publish "$PROJECT" \
-c Release -r "$rid" \
-p:PublishSingleFile=false \
-p:SelfContained=true \
-p:IncludeNativeLibrariesForSelfExtract=true
-p:SelfContained=true
# Per-arch variables (scoped)
local RID_DIR="$rid"
@@ -502,28 +409,28 @@ build_for_arch() {
mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box"
# Bundle / cores per-arch
fetch_separate_cores_and_rules() {
local outroot="$1"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
}
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT"
fi
else
echo "[*] --netcore specified: use separate core + rules."
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)"
fetch_separate_cores_and_rules "$WORKDIR/$PKGROOT"
fi
# Tarball
@@ -577,12 +484,6 @@ https://github.com/2dust/v2rayN
install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/
install -dm0755 %{buildroot}%{_sysconfdir}/sudoers.d
cat > %{buildroot}%{_sysconfdir}/sudoers.d/v2rayn-mihomo-deny << 'EOF'
ALL ALL=(ALL) !/home/*/.local/share/v2rayN/bin/mihomo/mihomo
EOF
chmod 0440 %{buildroot}%{_sysconfdir}/sudoers.d/v2rayn-mihomo-deny
# Launcher (prefer native ELF first, then DLL fallback)
install -dm0755 %{buildroot}%{_bindir}
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
@@ -636,47 +537,13 @@ fi
/opt/v2rayN
%{_datadir}/applications/v2rayn.desktop
%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
%config(noreplace) /etc/sudoers.d/v2rayn-mihomo-deny
SPEC
# Autostart injection (inside %install) and %files entry
if [[ "$AUTOSTART" -eq 1 ]]; then
awk '
BEGIN{ins=0}
/^%post$/ && !ins {
print "# --- Autostart (.desktop) ---"
print "install -dm0755 %{buildroot}/etc/xdg/autostart"
print "cat > %{buildroot}/etc/xdg/autostart/v2rayn.desktop << '\''EOF'\''"
print "[Desktop Entry]"
print "Type=Application"
print "Name=v2rayN (Autostart)"
print "Exec=v2rayn"
print "X-GNOME-Autostart-enabled=true"
print "NoDisplay=false"
print "EOF"
ins=1
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
awk '
BEGIN{infiles=0; done=0}
/^%files$/ {infiles=1}
infiles && done==0 && $0 ~ /%{_datadir}\/icons\/hicolor\/256x256\/apps\/v2rayn\.png/ {
print
print "%config(noreplace) /etc/xdg/autostart/v2rayn.desktop"
done=1
next
}
{print}
' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE"
fi
# Replace placeholders
sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE"
sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE"
# Build RPM for this arch (force rpm --target to match compile arch)
# Build RPM for this arch
rpmbuild -ba "$SPECFILE" --target "$rpm_target"
echo "Build done for $short. RPM at:"
@@ -690,33 +557,18 @@ SPEC
# ===== Arch selection and build orchestration =========================================
case "${ARCH_OVERRIDE:-}" in
"")
# No --arch: use host architecture
if [[ "$host_arch" == "aarch64" ]]; then
build_for_arch arm64
else
build_for_arch x64
fi
;;
x64|amd64)
build_for_arch x64
;;
arm64|aarch64)
build_for_arch arm64
;;
all)
BUILT_ALL=1
# Build x64 and arm64 separately; each package contains its own arch-only binaries.
build_for_arch x64
build_for_arch arm64
;;
*)
echo "[ERROR] Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."
exit 1
;;
all) targets=(x64 arm64); BUILT_ALL=1 ;;
x64|amd64) targets=(x64) ;;
arm64|aarch64) targets=(arm64) ;;
"") targets=($([[ "$host_arch" == "aarch64" ]] && echo arm64 || echo x64)) ;;
*) echo "Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all."; exit 1 ;;
esac
# ===== Final summary if building both arches ==========================================
for arch in "${targets[@]}"; do
build_for_arch "$arch"
done
# Print Both arches information
if [[ "$BUILT_ALL" -eq 1 ]]; then
echo ""
echo "================ Build Summary (both architectures) ================"
@@ -725,7 +577,7 @@ if [[ "$BUILT_ALL" -eq 1 ]]; then
echo "$rp"
done
else
echo "[WARN] No RPMs detected in summary (check build logs above)."
echo "No RPMs detected in summary (check build logs above)."
fi
echo "==================================================================="
echo "===================================================================="
fi

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.19.0</Version>
<Version>7.19.3</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -11,7 +11,7 @@
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.12" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageVersion Include="CliWrap" Version="3.10.0" />
<PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="Downloader" Version="4.1.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.3.1.1" />
@@ -22,11 +22,11 @@
<PackageVersion Include="Semi.Avalonia" Version="11.3.7.3" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.0" />
<PackageVersion Include="NLog" Version="6.1.1" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
</ItemGroup>
</Project>
</Project>

View File

@@ -497,6 +497,13 @@ public class Utils
return false;
}
var ext = Path.GetExtension(domain);
if (ext.IsNotEmpty()
&& ext[1..].ToLowerInvariant() is "json" or "txt" or "xml" or "cfg" or "ini" or "log" or "yaml" or "yml" or "toml")
{
return false;
}
return Uri.CheckHostName(domain) == UriHostNameType.Dns;
}

View File

@@ -49,6 +49,7 @@ public class Global
public const string DirectTag = "direct";
public const string BlockTag = "block";
public const string DnsTag = "dns-module";
public const string DirectDnsTag = "direct-dns";
public const string BalancerTagSuffix = "-round";
public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality";
@@ -90,6 +91,22 @@ public class Global
public const int Hysteria2DefaultHopInt = 10;
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$";
public static readonly List<string> PolicyGroupDefaultFilterList =
[
// All nodes (exclude traffic/expiry info)
PolicyGroupDefaultAllFilter,
// Low multiplier nodes, e.g. ×0.1, 0.5x, 0.1倍
@"^.*(?:[×xX✕*]\s*0\.[0-9]+|0\.[0-9]+\s*[×xX*]).*$",
// Dedicated line nodes, e.g. IPLC, IEPL
$@"^(?!.*(?:{PolicyGroupExcludeKeywords})).*(?:线|IPLC|IEPL|).*$",
// Japan nodes
$@"^(?!.*(?:{PolicyGroupExcludeKeywords})).*(?:|\\b[Jj][Pp]\\b|🇯🇵|[Jj]apan).*$",
];
public static readonly List<string> IEProxyProtocols =
[
"{ip}:{http_port}",

View File

@@ -24,6 +24,7 @@ global using ServiceLib.Common;
global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler;
global using ServiceLib.Handler.Builder;
global using ServiceLib.Handler.Fmt;
global using ServiceLib.Handler.SysProxy;
global using ServiceLib.Helper;

View File

@@ -0,0 +1,438 @@
namespace ServiceLib.Handler.Builder;
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
{
public bool Success => ValidatorResult.Success;
}
/// <summary>
/// Holds the results of a full context build, including the main context and an optional
/// pre-socks context (e.g. for TUN protection or pre-socks chaining).
/// </summary>
public record CoreConfigContextBuilderAllResult(
CoreConfigContextBuilderResult MainResult,
CoreConfigContextBuilderResult? PreSocksResult)
{
/// <summary>True only when both the main result and (if present) the pre-socks result succeeded.</summary>
public bool Success => MainResult.Success && (PreSocksResult?.Success ?? true);
/// <summary>
/// Merges all errors and warnings from the main result and the optional pre-socks result
/// into a single <see cref="NodeValidatorResult"/> for unified notification.
/// </summary>
public NodeValidatorResult CombinedValidatorResult => new(
[.. MainResult.ValidatorResult.Errors, .. PreSocksResult?.ValidatorResult.Errors ?? []],
[.. MainResult.ValidatorResult.Warnings, .. PreSocksResult?.ValidatorResult.Warnings ?? []]);
/// <summary>
/// The main context with TunProtectSsPort/ProxyRelaySsPort and ProtectDomainList merged in
/// from the pre-socks result (if any). Pass this to the core runner.
/// </summary>
public CoreConfigContext ResolvedMainContext => PreSocksResult is not null
? MainResult.Context with
{
TunProtectSsPort = PreSocksResult.Context.TunProtectSsPort,
ProxyRelaySsPort = PreSocksResult.Context.ProxyRelaySsPort,
ProtectDomainList = [.. MainResult.Context.ProtectDomainList ?? [], .. PreSocksResult.Context.ProtectDomainList ?? []],
}
: MainResult.Context;
}
public class CoreConfigContextBuilder
{
/// <summary>
/// Builds a <see cref="CoreConfigContext"/> for the given node, resolves its proxy map,
/// and processes outbound nodes referenced by routing rules.
/// </summary>
public static async Task<CoreConfigContextBuilderResult> Build(Config config, ProfileItem node)
{
var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
var context = new CoreConfigContext()
{
Node = node,
RunCoreType = runCoreType,
AllProxiesMap = [],
AppConfig = config,
FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType),
IsTunEnabled = config.TunModeItem.EnableTun,
SimpleDnsItem = config.SimpleDNSItem,
ProtectDomainList = [],
TunProtectSsPort = 0,
ProxyRelaySsPort = 0,
RawDnsItem = await AppManager.Instance.GetDNSItem(coreType),
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
};
var validatorResult = NodeValidatorResult.Empty();
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
if (!nodeValidatorResult.Success)
{
return new CoreConfigContextBuilderResult(context, nodeValidatorResult);
}
context = context with { Node = actNode };
validatorResult.Warnings.AddRange(nodeValidatorResult.Warnings);
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet) ?? [];
foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
{
if (ruleItem.OutboundTag.IsNullOrEmpty())
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleEmptyOutboundTag, ruleItem.Remarks));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
if (ruleOutboundNode == null)
{
validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
var (actRuleNode, ruleNodeValidatorResult) = await ResolveNodeAsync(context, ruleOutboundNode, false);
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Warnings.Select(w =>
string.Format(ResUI.MsgRoutingRuleOutboundNodeWarning, ruleItem.Remarks, ruleItem.OutboundTag, w)));
if (!ruleNodeValidatorResult.Success)
{
validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e =>
string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e)));
ruleItem.OutboundTag = Global.ProxyTag;
continue;
}
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
}
}
return new CoreConfigContextBuilderResult(context, validatorResult);
}
/// <summary>
/// Builds the main <see cref="CoreConfigContext"/> for <paramref name="node"/> and, when
/// the main build succeeds, also builds the optional pre-socks context required for TUN
/// protection or pre-socks proxy chaining.
/// </summary>
public static async Task<CoreConfigContextBuilderAllResult> BuildAll(Config config, ProfileItem node)
{
var mainResult = await Build(config, node);
if (!mainResult.Success)
{
return new CoreConfigContextBuilderAllResult(mainResult, null);
}
var preResult = await BuildPreSocksIfNeeded(mainResult.Context);
return new CoreConfigContextBuilderAllResult(mainResult, preResult);
}
/// <summary>
/// Determines whether a pre-socks context is required for <paramref name="nodeContext"/>
/// and, if so, builds and returns it. Returns <c>null</c> when no pre-socks core is needed.
/// </summary>
private static async Task<CoreConfigContextBuilderResult?> BuildPreSocksIfNeeded(CoreConfigContext nodeContext)
{
var config = nodeContext.AppConfig;
var node = nodeContext.Node;
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var preSocksItem = ConfigHandler.GetPreSocksItem(config, node, coreType);
if (preSocksItem != null)
{
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
return preSocksResult with
{
Context = preSocksResult.Context with
{
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
}
};
}
if (!nodeContext.IsTunEnabled
|| coreType != ECoreType.Xray
|| node.ConfigType == EConfigType.Custom)
{
return null;
}
var tunProtectSsPort = Utils.GetFreePort();
var proxyRelaySsPort = Utils.GetFreePort();
var preItem = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.Shadowsocks,
Address = Global.Loopback,
Port = proxyRelaySsPort,
Password = Global.None,
};
preItem.SetProtocolExtra(preItem.GetProtocolExtra() with
{
SsMethod = Global.None,
});
var preResult2 = await Build(nodeContext.AppConfig, preItem);
return preResult2 with
{
Context = preResult2.Context with
{
ProtectDomainList = [.. nodeContext.ProtectDomainList ?? [], .. preResult2.Context.ProtectDomainList ?? []],
TunProtectSsPort = tunProtectSsPort,
ProxyRelaySsPort = proxyRelaySsPort,
}
};
}
/// <summary>
/// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain.
/// Returns the effective (possibly replaced) node and the validation result.
/// </summary>
public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context,
ProfileItem node,
bool includeSubChain = true)
{
if (node.IndexId.IsNullOrEmpty())
{
return (node, NodeValidatorResult.Empty());
}
if (includeSubChain)
{
var (virtualChainNode, chainValidatorResult) = await BuildSubscriptionChainNodeAsync(node);
if (virtualChainNode != null)
{
context.AllProxiesMap[virtualChainNode.IndexId] = virtualChainNode;
var (resolvedNode, resolvedResult) = await ResolveNodeAsync(context, virtualChainNode, false);
resolvedResult.Warnings.InsertRange(0, chainValidatorResult.Warnings);
return (resolvedNode, resolvedResult);
}
// Chain not built but warnings may still exist (e.g. missing profiles)
if (chainValidatorResult.Warnings.Count > 0)
{
var fillResult = await RegisterNodeAsync(context, node);
fillResult.Warnings.InsertRange(0, chainValidatorResult.Warnings);
return (node, fillResult);
}
}
var registerResult = await RegisterNodeAsync(context, node);
return (node, registerResult);
}
/// <summary>
/// If the node's subscription defines prev/next profiles, creates a virtual
/// <see cref="EConfigType.ProxyChain"/> node that wraps them together.
/// Returns <c>null</c> as the chain item when no chain is needed.
/// Any warnings (e.g. missing prev/next profile) are returned in the validator result.
/// </summary>
private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node)
{
var result = NodeValidatorResult.Empty();
if (node.Subid.IsNullOrEmpty() || node.ConfigType == EConfigType.Custom)
{
return (null, result);
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem == null)
{
return (null, result);
}
ProfileItem? prevNode = null;
ProfileItem? nextNode = null;
if (!subItem.PrevProfile.IsNullOrEmpty())
{
prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode == null)
{
result.Warnings.Add(string.Format(ResUI.MsgSubscriptionPrevProfileNotFound, subItem.PrevProfile));
}
}
if (!subItem.NextProfile.IsNullOrEmpty())
{
nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode == null)
{
result.Warnings.Add(string.Format(ResUI.MsgSubscriptionNextProfileNotFound, subItem.NextProfile));
}
}
if (prevNode is null && nextNode is null)
{
return (null, result);
}
// Build new proxy chain node
var chainNode = new ProfileItem()
{
IndexId = $"inner-{Utils.GetGuid(false)}",
ConfigType = EConfigType.ProxyChain,
CoreType = AppManager.Instance.GetCoreType(node, node.ConfigType),
Remarks = node.Remarks,
};
List<string?> childItems = [prevNode?.IndexId, node.IndexId, nextNode?.IndexId];
var chainExtraItem = chainNode.GetProtocolExtra() with
{
GroupType = chainNode.ConfigType.ToString(),
ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())),
};
chainNode.SetProtocolExtra(chainExtraItem);
return (chainNode, result);
}
/// <summary>
/// Dispatches registration to either <see cref="RegisterGroupNodeAsync"/> or
/// <see cref="RegisterSingleNodeAsync"/> based on the node's config type.
/// </summary>
private static async Task<NodeValidatorResult> RegisterNodeAsync(CoreConfigContext context, ProfileItem node)
{
if (node.ConfigType.IsGroupType())
{
return await RegisterGroupNodeAsync(context, node);
}
else
{
return RegisterSingleNodeAsync(context, node);
}
}
/// <summary>
/// Validates a single (non-group) node and, on success, adds it to the proxy map
/// and records any domain addresses that should bypass the proxy.
/// </summary>
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
{
if (node.ConfigType.IsGroupType())
{
return NodeValidatorResult.Empty();
}
var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType);
if (!nodeValidatorResult.Success)
{
return nodeValidatorResult;
}
context.AllProxiesMap[node.IndexId] = node;
var address = node.Address;
if (Utils.IsDomain(address))
{
context.ProtectDomainList.Add(address);
}
if (!node.EchConfigList.IsNullOrEmpty())
{
var echQuerySni = node.Sni;
if (node.StreamSecurity == Global.StreamSecurity
&& node.EchConfigList?.Contains("://") == true)
{
var idx = node.EchConfigList.IndexOf('+');
echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni;
}
if (Utils.IsDomain(echQuerySni))
{
context.ProtectDomainList.Add(echQuerySni);
}
}
return nodeValidatorResult;
}
/// <summary>
/// Entry point for registering a group node. Initialises the visited/ancestor sets
/// and delegates to <see cref="TraverseGroupNodeAsync"/>.
/// </summary>
private static async Task<NodeValidatorResult> RegisterGroupNodeAsync(CoreConfigContext context,
ProfileItem node)
{
if (!node.ConfigType.IsGroupType())
{
return NodeValidatorResult.Empty();
}
HashSet<string> ancestors = [node.IndexId];
HashSet<string> globalVisited = [node.IndexId];
return await TraverseGroupNodeAsync(context, node, globalVisited, ancestors);
}
/// <summary>
/// Recursively walks the children of a group node, registering valid leaf nodes
/// and nested groups. Detects cycles via <paramref name="ancestorsGroup"/> and
/// deduplicates shared nodes via <paramref name="globalVisitedGroup"/>.
/// </summary>
private static async Task<NodeValidatorResult> TraverseGroupNodeAsync(
CoreConfigContext context,
ProfileItem node,
HashSet<string> globalVisitedGroup,
HashSet<string> ancestorsGroup)
{
var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node);
List<string> childIndexIdList = [];
var childNodeValidatorResult = NodeValidatorResult.Empty();
foreach (var childNode in groupChildList)
{
if (ancestorsGroup.Contains(childNode.IndexId))
{
childNodeValidatorResult.Errors.Add(
string.Format(ResUI.MsgGroupCycleDependency, node.Remarks, childNode.Remarks));
continue;
}
if (globalVisitedGroup.Contains(childNode.IndexId))
{
childIndexIdList.Add(childNode.IndexId);
continue;
}
if (!childNode.ConfigType.IsGroupType())
{
var childNodeResult = RegisterSingleNodeAsync(context, childNode);
childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w =>
string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w)));
childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e =>
string.Format(ResUI.MsgGroupChildNodeError, node.Remarks, childNode.Remarks, e)));
if (!childNodeResult.Success)
{
continue;
}
globalVisitedGroup.Add(childNode.IndexId);
childIndexIdList.Add(childNode.IndexId);
continue;
}
var newAncestorsGroup = new HashSet<string>(ancestorsGroup) { childNode.IndexId };
var childGroupResult =
await TraverseGroupNodeAsync(context, childNode, globalVisitedGroup, newAncestorsGroup);
childNodeValidatorResult.Warnings.AddRange(childGroupResult.Warnings.Select(w =>
string.Format(ResUI.MsgGroupChildGroupNodeWarning, node.Remarks, childNode.Remarks, w)));
childNodeValidatorResult.Errors.AddRange(childGroupResult.Errors.Select(e =>
string.Format(ResUI.MsgGroupChildGroupNodeError, node.Remarks, childNode.Remarks, e)));
if (!childGroupResult.Success)
{
continue;
}
globalVisitedGroup.Add(childNode.IndexId);
childIndexIdList.Add(childNode.IndexId);
}
if (childIndexIdList.Count == 0)
{
childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks));
return childNodeValidatorResult;
}
else
{
childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors);
childNodeValidatorResult.Errors.Clear();
}
node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), });
context.AllProxiesMap[node.IndexId] = node;
return childNodeValidatorResult;
}
}

View File

@@ -0,0 +1,177 @@
namespace ServiceLib.Handler.Builder;
public record NodeValidatorResult(List<string> Errors, List<string> Warnings)
{
public bool Success => Errors.Count == 0;
public static NodeValidatorResult Empty()
{
return new NodeValidatorResult([], []);
}
}
public class NodeValidator
{
// Static validator rules
private static readonly HashSet<string> SingboxUnsupportedTransports =
[nameof(ETransport.kcp), nameof(ETransport.xhttp)];
private static readonly HashSet<EConfigType> SingboxTransportSupportedProtocols =
[EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks];
private static readonly HashSet<string> SingboxShadowsocksAllowedTransports =
[nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)];
public static NodeValidatorResult Validate(ProfileItem item, ECoreType coreType)
{
var v = new ValidationContext();
ValidateNodeAndCoreSupport(item, coreType, v);
return v.ToResult();
}
private class ValidationContext
{
public List<string> Errors { get; } = [];
public List<string> Warnings { get; } = [];
public void Error(string message)
{
Errors.Add(message);
}
public void Warning(string message)
{
Warnings.Add(message);
}
public void Assert(bool condition, string errorMsg)
{
if (!condition)
{
Error(errorMsg);
}
}
public NodeValidatorResult ToResult()
{
return new NodeValidatorResult(Errors, Warnings);
}
}
private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v)
{
if (item.ConfigType is EConfigType.Custom)
{
return;
}
if (item.ConfigType.IsGroupType())
{
// Group logic is handled in ValidateGroupNode
return;
}
// Basic Property Validation
v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address"));
v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port"));
// Network & Core Logic
var net = item.GetNetwork();
if (coreType == ECoreType.sing_box)
{
var transportError = ValidateSingboxTransport(item.ConfigType, net);
if (transportError != null)
{
v.Error(transportError);
}
if (!Global.SingboxSupportConfigType.Contains(item.ConfigType))
{
v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.sing_box), item.ConfigType));
}
}
else if (coreType is ECoreType.Xray)
{
if (!Global.XraySupportConfigType.Contains(item.ConfigType))
{
v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType));
}
}
// Protocol Specifics
var protocolExtra = item.GetProtocolExtra();
switch (item.ConfigType)
{
case EConfigType.VMess:
v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password),
string.Format(ResUI.MsgInvalidProperty, "Password"));
break;
case EConfigType.VLESS:
v.Assert(
!item.Password.IsNullOrEmpty()
&& (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30),
string.Format(ResUI.MsgInvalidProperty, "Password")
);
v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty),
string.Format(ResUI.MsgInvalidProperty, "Flow"));
break;
case EConfigType.Shadowsocks:
v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password"));
v.Assert(
!string.IsNullOrEmpty(protocolExtra.SsMethod) &&
Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod),
string.Format(ResUI.MsgInvalidProperty, "SsMethod"));
break;
}
// TLS & Security
if (item.StreamSecurity == Global.StreamSecurity)
{
if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 &&
!item.CertSha.IsNullOrEmpty())
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate"));
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)
{
v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey"));
}
if (item.Network == nameof(ETransport.xhttp) && !item.Extra.IsNullOrEmpty())
{
if (JsonUtils.ParseJson(item.Extra) is null)
{
v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra"));
}
}
}
private static string? ValidateSingboxTransport(EConfigType configType, string net)
{
// sing-box does not support xhttp / kcp transports
if (SingboxUnsupportedTransports.Contains(net))
{
return string.Format(ResUI.MsgCoreNotSupportNetwork, nameof(ECoreType.sing_box), net);
}
// sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks
if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp))
{
return string.Format(ResUI.MsgCoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
// sing-box shadowsocks only supports tcp/ws/quic transports
if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net))
{
return string.Format(ResUI.MsgCoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
return null;
}
}

View File

@@ -154,6 +154,7 @@ public static class ConfigHandler
DownMbps = 100
};
config.ClashUIItem ??= new();
config.ClashUIItem.ConnectionsColumnItem ??= new();
config.SystemProxyItem ??= new();
config.WebDavItem ??= new();
config.CheckUpdateItem ??= new();
@@ -231,6 +232,7 @@ public static class ConfigHandler
item.Address = profileItem.Address;
item.Port = profileItem.Port;
item.Username = profileItem.Username;
item.Password = profileItem.Password;
item.Network = profileItem.Network;
@@ -721,8 +723,6 @@ public static class ConfigHandler
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(),
UpMbps = profileItem.GetProtocolExtra().UpMbps is null or < 0 ? config.HysteriaItem.UpMbps : profileItem.GetProtocolExtra().UpMbps,
DownMbps = profileItem.GetProtocolExtra().DownMbps is null or < 0 ? config.HysteriaItem.DownMbps : profileItem.GetProtocolExtra().DownMbps,
HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(),
});
@@ -1040,8 +1040,8 @@ public static class ConfigHandler
if (profileItem.StreamSecurity.IsNotEmpty())
{
if (profileItem.StreamSecurity != Global.StreamSecurity
&& profileItem.StreamSecurity != Global.StreamSecurityReality)
if (profileItem.StreamSecurity is not Global.StreamSecurity
and not Global.StreamSecurityReality)
{
profileItem.StreamSecurity = string.Empty;
}
@@ -1165,46 +1165,28 @@ public static class ConfigHandler
/// <summary>
/// Create a group server that combines multiple servers for load balancing
/// Generates a configuration file that references multiple servers
/// Generates a PolicyGroup profile with references to the sub-items
/// </summary>
/// <param name="config">Current configuration</param>
/// <param name="selecteds">Selected servers to combine</param>
/// <param name="coreType">Core type to use (Xray or sing_box)</param>
/// <param name="multipleLoad">Load balancing algorithm</param>
/// <param name="subItem">Sub-item for grouping</param>
/// <returns>Result object with success state and data</returns>
public static async Task<RetResult> AddGroupServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId)
public static async Task<RetResult> AddGroupAllServer(Config config, SubItem? subItem)
{
var result = new RetResult();
var indexId = Utils.GetGuid(false);
var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList());
var subId = subItem?.Id;
if (subId.IsNullOrEmpty())
{
result.Success = false;
return result;
}
var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId))?.Remarks} ";
if (coreType == ECoreType.Xray)
{
remark += multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback,
EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom,
EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad,
_ => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
};
}
else if (coreType == ECoreType.sing_box)
{
remark += multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback,
_ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
};
}
var indexId = Utils.GetGuid(false);
var remark = $"{subItem.Remarks} - {ResUI.TbConfigTypePolicyGroup}";
var profile = new ProfileItem
{
IndexId = indexId,
CoreType = coreType,
CoreType = ECoreType.Xray,
ConfigType = EConfigType.PolicyGroup,
Remarks = remark,
IsSub = false
@@ -1215,8 +1197,10 @@ public static class ConfigHandler
}
var extraItem = new ProtocolExtraItem
{
ChildItems = childProfileIndexId,
MultipleLoad = multipleLoad,
MultipleLoad = EMultipleLoad.LeastPing,
GroupType = profile.ConfigType.ToString(),
SubChildItems = subId,
Filter = Global.PolicyGroupDefaultAllFilter,
};
profile.SetProtocolExtra(extraItem);
var ret = await AddServerCommon(config, profile, true);
@@ -1225,6 +1209,92 @@ public static class ConfigHandler
return result;
}
private static string CombineWithDefaultAllFilter(string regionPattern)
{
return $"^(?!.*(?:{Global.PolicyGroupExcludeKeywords})).*(?:{regionPattern}).*$";
}
private static readonly Dictionary<string, string> PolicyGroupRegionFilters = new()
{
{ "JP", "日本|\\b[Jj][Pp]\\b|🇯🇵|[Jj]apan" },
{ "US", "美国|\\b[Uu][Ss]\\b|🇺🇸|[Uu]nited [Ss]tates|\\b[Uu][Ss][Aa]\\b" },
{ "HK", "香港|\\b[Hh][Kk]\\b|🇭🇰|[Hh]ong ?[Kk]ong" },
{ "TW", "台湾|台灣|\\b[Tt][Ww]\\b|🇹🇼|[Tt]aiwan" },
{ "KR", "韩国|\\b[Kk][Rr]\\b|🇰🇷|[Kk]orea" },
{ "SG", "新加坡|\\b[Ss][Gg]\\b|🇸🇬|[Ss]ingapore" },
{ "DE", "德国|\\b[Dd][Ee]\\b|🇩🇪|[Gg]ermany" },
{ "FR", "法国|\\b[Ff][Rr]\\b|🇫🇷|[Ff]rance" },
{ "GB", "英国|\\b[Gg][Bb]\\b|🇬🇧|[Uu]nited [Kk]ingdom|[Bb]ritain" },
{ "CA", "加拿大|🇨🇦|[Cc]anada" },
{ "AU", "澳大利亚|\\b[Aa][Uu]\\b|🇦🇺|[Aa]ustralia" },
{ "RU", "俄罗斯|\\b[Rr][Uu]\\b|🇷🇺|[Rr]ussia" },
{ "BR", "巴西|\\b[Bb][Rr]\\b|🇧🇷|[Bb]razil" },
{ "IN", "印度|🇮🇳|[Ii]ndia" },
{ "VN", "越南|\\b[Vv][Nn]\\b|🇻🇳|[Vv]ietnam" },
{ "ID", "印度尼西亚|\\b[Ii][Dd]\\b|🇮🇩|[Ii]ndonesia" },
{ "MX", "墨西哥|\\b[Mm][Xx]\\b|🇲🇽|[Mm]exico" }
};
public static async Task<RetResult> AddGroupRegionServer(Config config, SubItem? subItem)
{
var result = new RetResult();
var subId = subItem?.Id;
if (subId.IsNullOrEmpty())
{
result.Success = false;
return result;
}
var childProfiles = await AppManager.Instance.ProfileItems(subId);
List<string> indexIdList = [];
foreach (var regionFilter in PolicyGroupRegionFilters)
{
var indexId = Utils.GetGuid(false);
var remark = $"{subItem.Remarks} - {ResUI.TbConfigTypePolicyGroup} - {regionFilter.Key}";
var profile = new ProfileItem
{
IndexId = indexId,
CoreType = ECoreType.Xray,
ConfigType = EConfigType.PolicyGroup,
Remarks = remark,
IsSub = false
};
if (!subId.IsNullOrEmpty())
{
profile.Subid = subId;
}
var extraItem = new ProtocolExtraItem
{
MultipleLoad = EMultipleLoad.LeastPing,
GroupType = profile.ConfigType.ToString(),
SubChildItems = subId,
Filter = CombineWithDefaultAllFilter(regionFilter.Value),
};
profile.SetProtocolExtra(extraItem);
var matchedChildProfiles = childProfiles?.Where(p =>
p != null &&
p.IsValid() &&
!p.ConfigType.IsComplexType() &&
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
)
.ToList() ?? [];
if (matchedChildProfiles.Count == 0)
{
continue;
}
var ret = await AddServerCommon(config, profile, true);
if (ret == 0)
{
indexIdList.Add(indexId);
}
}
result.Success = indexIdList.Count > 0;
result.Data = indexIdList;
return result;
}
/// <summary>
/// Get a SOCKS server profile for pre-SOCKS functionality
/// Used when TUN mode is enabled or when a custom config has a pre-SOCKS port
@@ -1251,47 +1321,6 @@ public static class ConfigHandler
return itemSocks;
}
public static CoreConfigContext? GetPreSocksCoreConfigContext(CoreConfigContext nodeContext)
{
var config = nodeContext.AppConfig;
var node = nodeContext.Node;
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var preSocksItem = GetPreSocksItem(config, node, coreType);
if (preSocksItem != null)
{
return nodeContext with { Node = preSocksItem, };
}
if ((!nodeContext.IsTunEnabled)
|| coreType != ECoreType.Xray
|| node.ConfigType == EConfigType.Custom)
{
return null;
}
var tunProtectSsPort = Utils.GetFreePort();
var proxyRelaySsPort = Utils.GetFreePort();
var preItem = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.Shadowsocks,
Address = Global.Loopback,
Port = proxyRelaySsPort,
Password = Global.None,
};
preItem.SetProtocolExtra(preItem.GetProtocolExtra() with
{
SsMethod = Global.None,
});
var preContext = nodeContext with
{
Node = preItem,
TunProtectSsPort = tunProtectSsPort,
ProxyRelaySsPort = proxyRelaySsPort,
};
return preContext;
}
/// <summary>
/// Remove servers with invalid test results (timeout)
/// Useful for cleaning up subscription lists
@@ -1302,6 +1331,7 @@ public static class ConfigHandler
public static async Task<int> RemoveInvalidServerResult(Config config, string subid)
{
var lstModel = await AppManager.Instance.ProfileModels(subid, "");
lstModel.RemoveAll(t => t.ConfigType.IsComplexType());
if (lstModel is { Count: <= 0 })
{
return -1;

View File

@@ -21,7 +21,7 @@ public static class CoreConfigHandler
_ => await GenerateClientCustomConfig(node, fileName)
};
}
else if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
else if (context.RunCoreType == ECoreType.sing_box)
{
result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
}
@@ -93,13 +93,16 @@ public static class CoreConfigHandler
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, string fileName, List<ServerTestItem> selecteds, ECoreType coreType)
{
var result = new RetResult();
var context = await BuildCoreConfigContext(config, new());
var ids = selecteds.Where(serverTestItem => !serverTestItem.IndexId.IsNullOrEmpty())
.Select(serverTestItem => serverTestItem.IndexId);
var nodes = await AppManager.Instance.GetProfileItemsByIndexIds(ids);
foreach (var node in nodes)
var dummyNode = new ProfileItem
{
var actNode = await FillNodeContext(context, node, true);
CoreType = coreType
};
var builderResult = await CoreConfigContextBuilder.Build(config, dummyNode);
var context = builderResult.Context;
foreach (var testItem in selecteds)
{
var node = testItem.Profile;
var (actNode, _) = await CoreConfigContextBuilder.ResolveNodeAsync(context, node, true);
if (node.IndexId == actNode.IndexId)
{
continue;
@@ -125,12 +128,11 @@ public static class CoreConfigHandler
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, CoreConfigContext context, ServerTestItem testItem, string fileName)
{
var result = new RetResult();
var node = context.Node;
var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest);
var port = Utils.GetFreePort(initPort + testItem.QueueNum);
testItem.Port = port;
if (AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box)
if (context.RunCoreType == ECoreType.sing_box)
{
result = new CoreConfigSingboxService(context).GenerateClientSpeedtestConfig(port);
}
@@ -146,128 +148,4 @@ public static class CoreConfigHandler
await File.WriteAllTextAsync(fileName, result.Data.ToString());
return result;
}
public static async Task<CoreConfigContext> BuildCoreConfigContext(Config config, ProfileItem node)
{
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
var context = new CoreConfigContext()
{
Node = node,
AllProxiesMap = [],
AppConfig = config,
FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType),
IsTunEnabled = config.TunModeItem.EnableTun,
SimpleDnsItem = config.SimpleDNSItem,
ProtectDomainList = [],
TunProtectSsPort = 0,
ProxyRelaySsPort = 0,
RawDnsItem = await AppManager.Instance.GetDNSItem(coreType),
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
};
context = context with
{
Node = await FillNodeContext(context, node)
};
if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true))
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(context.RoutingItem?.RuleSet);
foreach (var ruleItem in rules.Where(ruleItem => !Global.OutboundTags.Contains(ruleItem.OutboundTag)))
{
var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag);
if (ruleOutboundNode != null)
{
var ruleOutboundNodeAct = await FillNodeContext(context, ruleOutboundNode, false);
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = ruleOutboundNodeAct;
}
}
}
return context;
}
private static async Task<ProfileItem> FillNodeContext(CoreConfigContext context, ProfileItem node, bool includeSubChain = true)
{
if (node.IndexId.IsNullOrEmpty())
{
return node;
}
var newItems = new List<ProfileItem> { node };
if (node.ConfigType.IsGroupType())
{
var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node);
foreach (var childItem in groupChildList.Where(childItem => !context.AllProxiesMap.ContainsKey(childItem.IndexId)))
{
await FillNodeContext(context, childItem, false);
}
node.SetProtocolExtra(node.GetProtocolExtra() with
{
ChildItems = Utils.List2String(groupChildList.Select(n => n.IndexId).ToList()),
});
newItems.AddRange(groupChildList);
}
context.AllProxiesMap[node.IndexId] = node;
foreach (var item in newItems)
{
var address = item.Address;
if (Utils.IsDomain(address))
{
context.ProtectDomainList.Add(address);
}
if (item.EchConfigList.IsNullOrEmpty())
{
continue;
}
var echQuerySni = item.Sni;
if (item.StreamSecurity == Global.StreamSecurity
&& item.EchConfigList?.Contains("://") == true)
{
var idx = item.EchConfigList.IndexOf('+');
echQuerySni = idx > 0 ? item.EchConfigList[..idx] : item.Sni;
}
if (!Utils.IsDomain(echQuerySni))
{
continue;
}
context.ProtectDomainList.Add(echQuerySni);
}
if (!includeSubChain || node.Subid.IsNullOrEmpty())
{
return node;
}
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
if (subItem == null)
{
return node;
}
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (prevNode is null && nextNode is null)
{
return node;
}
var prevNodeAct = prevNode is null ? null : await FillNodeContext(context, prevNode, false);
var nextNodeAct = nextNode is null ? null : await FillNodeContext(context, nextNode, false);
// Build new proxy chain node
var chainNode = new ProfileItem()
{
IndexId = $"inner-{Utils.GetGuid(false)}",
ConfigType = EConfigType.ProxyChain,
CoreType = node.CoreType ?? ECoreType.Xray,
};
List<string?> childItems = [prevNodeAct?.IndexId, node.IndexId, nextNodeAct?.IndexId];
var chainExtraItem = chainNode.GetProtocolExtra() with
{
GroupType = chainNode.ConfigType.ToString(),
ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())),
};
chainNode.SetProtocolExtra(chainExtraItem);
context.AllProxiesMap[chainNode.IndexId] = chainNode;
return chainNode;
}
}

View File

@@ -24,13 +24,13 @@ public class DownloaderHelper
var downloadOpt = new DownloadConfiguration()
{
Timeout = timeout * 1000,
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration =
{
Headers = headers,
UserAgent = userAgent,
Timeout = timeout * 1000,
ConnectTimeout = timeout * 1000,
Proxy = webProxy
}
};
@@ -62,11 +62,11 @@ public class DownloaderHelper
var downloadOpt = new DownloadConfiguration()
{
Timeout = timeout * 1000,
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration =
{
Timeout= timeout * 1000,
ConnectTimeout= timeout * 1000,
Proxy = webProxy
}
};
@@ -139,11 +139,11 @@ public class DownloaderHelper
var downloadOpt = new DownloadConfiguration()
{
Timeout = timeout * 1000,
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration =
{
Timeout= timeout * 1000,
ConnectTimeout= timeout * 1000,
Proxy = webProxy
}
};

View File

@@ -1,352 +0,0 @@
namespace ServiceLib.Manager;
/// <summary>
/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.).
/// </summary>
public class ActionPrecheckManager
{
private static readonly Lazy<ActionPrecheckManager> _instance = new();
public static ActionPrecheckManager Instance => _instance.Value;
// sing-box supported transports for different protocol types
private static readonly HashSet<string> SingboxUnsupportedTransports = [nameof(ETransport.kcp), nameof(ETransport.xhttp)];
private static readonly HashSet<EConfigType> SingboxTransportSupportedProtocols =
[EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks];
private static readonly HashSet<string> SingboxShadowsocksAllowedTransports =
[nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)];
public async Task<List<string>> Check(string? indexId)
{
if (indexId.IsNullOrEmpty())
{
return [ResUI.PleaseSelectServer];
}
var item = await AppManager.Instance.GetProfileItem(indexId);
if (item is null)
{
return [ResUI.PleaseSelectServer];
}
return await Check(item);
}
public async Task<List<string>> Check(ProfileItem? item)
{
if (item is null)
{
return [ResUI.PleaseSelectServer];
}
var errors = new List<string>();
errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item));
errors.AddRange(await ValidateRelatedNodesExistAndValid(item));
return errors;
}
private async Task<List<string>> ValidateCurrentNodeAndCoreSupport(ProfileItem item)
{
if (item.ConfigType == EConfigType.Custom)
{
return [];
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
return await ValidateNodeAndCoreSupport(item, coreType);
}
private async Task<List<string>> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType);
if (item.ConfigType is EConfigType.Custom)
{
errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString()));
return errors;
}
else if (item.ConfigType.IsGroupType())
{
var groupErrors = await ValidateGroupNode(item, coreType);
errors.AddRange(groupErrors);
return errors;
}
else if (!item.IsComplex())
{
var normalErrors = await ValidateNormalNode(item, coreType);
errors.AddRange(normalErrors);
return errors;
}
return errors;
}
private async Task<List<string>> ValidateNormalNode(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
if (item.Address.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "Address"));
return errors;
}
if (item.Port is <= 0 or > 65535)
{
errors.Add(string.Format(ResUI.InvalidProperty, "Port"));
return errors;
}
var net = item.GetNetwork();
if (coreType == ECoreType.sing_box)
{
var transportError = ValidateSingboxTransport(item.ConfigType, net);
if (transportError != null)
{
errors.Add(transportError);
}
if (!Global.SingboxSupportConfigType.Contains(item.ConfigType))
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol,
nameof(ECoreType.sing_box), item.ConfigType.ToString()));
}
}
else if (coreType is ECoreType.Xray)
{
// Xray core does not support these protocols
if (!Global.XraySupportConfigType.Contains(item.ConfigType))
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol,
nameof(ECoreType.Xray), item.ConfigType.ToString()));
}
}
var protocolExtra = item.GetProtocolExtra();
switch (item.ConfigType)
{
case EConfigType.VMess:
if (item.Password.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Password))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Password"));
}
break;
case EConfigType.VLESS:
if (item.Password.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Password) && item.Password.Length > 30))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Password"));
}
if (!Global.Flows.Contains(protocolExtra.Flow ?? string.Empty))
{
errors.Add(string.Format(ResUI.InvalidProperty, "Flow"));
}
break;
case EConfigType.Shadowsocks:
if (item.Password.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "Password"));
}
if (string.IsNullOrEmpty(protocolExtra.SsMethod) || !Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod))
{
errors.Add(string.Format(ResUI.InvalidProperty, "SsMethod"));
}
break;
}
if (item.StreamSecurity == Global.StreamSecurity)
{
// check certificate validity
if (!item.Cert.IsNullOrEmpty()
&& (CertPemManager.ParsePemChain(item.Cert).Count == 0)
&& !item.CertSha.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate"));
}
}
if (item.StreamSecurity == Global.StreamSecurityReality)
{
if (item.PublicKey.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey"));
}
}
if (item.Network == nameof(ETransport.xhttp)
&& !item.Extra.IsNullOrEmpty())
{
// check xhttp extra json validity
var xhttpExtra = JsonUtils.ParseJson(item.Extra);
if (xhttpExtra is null)
{
errors.Add(string.Format(ResUI.InvalidProperty, "XHTTP Extra"));
}
}
return errors;
}
private async Task<List<string>> ValidateGroupNode(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
var hasCycle = await GroupProfileManager.HasCycle(item.IndexId, item.GetProtocolExtra());
if (hasCycle)
{
errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks));
return errors;
}
var (childItems, _) = await GroupProfileManager.GetChildProfileItems(item);
foreach (var childItem in childItems)
{
var childErrors = new List<string>();
if (childItem is null)
{
childErrors.Add(string.Format(ResUI.NodeTagNotExist, ""));
continue;
}
if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain)
{
childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks));
continue;
}
childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType));
errors.AddRange(childErrors.Select(s => s.Insert(0, $"{childItem.Remarks}: ")));
}
return errors;
}
private static string? ValidateSingboxTransport(EConfigType configType, string net)
{
// sing-box does not support xhttp / kcp transports
if (SingboxUnsupportedTransports.Contains(net))
{
return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net);
}
// sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks
if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp))
{
return string.Format(ResUI.CoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
// sing-box shadowsocks only supports tcp/ws/quic transports
if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net))
{
return string.Format(ResUI.CoreNotSupportProtocolTransport,
nameof(ECoreType.sing_box), configType.ToString(), net);
}
return null;
}
private async Task<List<string>> ValidateRelatedNodesExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item));
errors.AddRange(await ValidateRoutingNodeExistAndValid(item));
return errors;
}
private async Task<List<string>> ValidateProxyChainedNodeExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
if (item is null)
{
return errors;
}
// prev node and next node
var subItem = await AppManager.Instance.GetSubItem(item.Subid);
if (subItem is null)
{
return errors;
}
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors);
await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors);
return errors;
}
private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List<string> errors)
{
if (node is not null)
{
var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType);
errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + $"{node.Remarks}: " + s));
}
else if (tag.IsNotEmpty())
{
errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag));
}
}
private async Task<List<string>> ValidateRoutingNodeExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
if (item is null)
{
return errors;
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
var routing = await ConfigHandler.GetDefaultRouting(AppManager.Instance.Config);
if (routing == null)
{
return errors;
}
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var ruleItem in rules ?? [])
{
if (!ruleItem.Enabled)
{
continue;
}
var outboundTag = ruleItem.OutboundTag;
if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag))
{
continue;
}
var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (tagItem is null)
{
errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag));
continue;
}
var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType);
errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + $"{tagItem.Remarks}: " + s));
}
return errors;
}
}

View File

@@ -242,6 +242,30 @@ public sealed class AppManager
.ToListAsync();
}
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
{
var items = await GetProfileItemsByIndexIds(indexIds);
return items.ToDictionary(it => it.IndexId);
}
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
{
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (idList.Count == 0)
{
return [];
}
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => idList.Contains(it.IndexId))
.ToListAsync();
var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(id => itemMap.GetValueOrDefault(id))
.Where(item => item != null)
.ToList();
}
public async Task<ProfileItem?> GetProfileItemViaRemarks(string? remarks)
{
if (remarks.IsNullOrEmpty())

View File

@@ -57,26 +57,19 @@ public class CoreManager
}
}
public async Task LoadCore(ProfileItem? node)
/// <param name="mainContext">Resolved main context (with pre-socks ports already merged if applicable).</param>
/// <param name="preContext">Optional pre-socks context passed to <see cref="CoreStartPreService"/>.</param>
public async Task LoadCore(CoreConfigContext? mainContext, CoreConfigContext? preContext)
{
if (node == null)
if (mainContext == null)
{
await UpdateFunc(false, ResUI.CheckServerSettings);
return;
}
var node = mainContext.Node;
var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName);
var context = await CoreConfigHandler.BuildCoreConfigContext(_config, node);
var preContext = ConfigHandler.GetPreSocksCoreConfigContext(context);
if (preContext is not null)
{
context = context with
{
TunProtectSsPort = preContext.TunProtectSsPort,
ProxyRelaySsPort = preContext.ProxyRelaySsPort,
};
}
var result = await CoreConfigHandler.GenerateClientConfig(context, fileName);
var result = await CoreConfigHandler.GenerateClientConfig(mainContext, fileName);
if (result.Success != true)
{
await UpdateFunc(true, result.Msg);
@@ -95,7 +88,7 @@ public class CoreManager
await WindowsUtils.RemoveTunDevice();
}
await CoreStart(context);
await CoreStart(mainContext);
await CoreStartPreService(preContext);
if (_processService != null)
{
@@ -105,7 +98,7 @@ public class CoreManager
public async Task<ProcessService?> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
{
var coreType = selecteds.Any(t => Global.SingboxOnlyConfigType.Contains(t.ConfigType)) ? ECoreType.sing_box : ECoreType.Xray;
var coreType = selecteds.FirstOrDefault()?.CoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray;
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
var configPath = Utils.GetBinConfigPath(fileName);
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType);
@@ -132,14 +125,14 @@ public class CoreManager
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
var configPath = Utils.GetBinConfigPath(fileName);
var context = await CoreConfigHandler.BuildCoreConfigContext(_config, node);
var (context, _) = await CoreConfigContextBuilder.Build(_config, node);
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, context, testItem, configPath);
if (result.Success != true)
{
return null;
}
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreType = context.RunCoreType;
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
return await RunProcess(coreInfo, fileName, true, false);
}

View File

@@ -52,10 +52,10 @@ public class GroupProfileManager
return false;
}
foreach (var child in childIds)
var childItems = await AppManager.Instance.GetProfileItemsByIndexIds(childIds);
foreach (var childItem in childItems)
{
var childItem = await AppManager.Instance.GetProfileItem(child);
if (await HasCycle(child, childItem?.GetProtocolExtra(), visited, stack))
if (await HasCycle(childItem.IndexId, childItem?.GetProtocolExtra(), visited, stack))
{
return true;
}
@@ -103,26 +103,7 @@ public class GroupProfileManager
return [];
}
var childProfiles = await AppManager.Instance.GetProfileItemsByIndexIds(childProfileIds);
if (childProfiles == null || childProfiles.Count == 0)
{
return [];
}
var profileMap = childProfiles
.Where(p => p != null && !p.IndexId.IsNullOrEmpty())
.GroupBy(p => p!.IndexId!)
.ToDictionary(g => g.Key, g => g.First());
var ordered = new List<ProfileItem>(childProfileIds.Count);
foreach (var id in childProfileIds)
{
if (id != null && profileMap.TryGetValue(id, out var item) && item != null)
{
ordered.Add(item);
}
}
var ordered = await AppManager.Instance.GetProfileItemsOrderedByIndexIds(childProfileIds);
return ordered;
}
@@ -143,26 +124,27 @@ public class GroupProfileManager
.ToList() ?? [];
}
public static async Task<List<ProfileItem>> GetAllChildProfileItems(ProfileItem profileItem)
public static async Task<Dictionary<string, ProfileItem>> GetAllChildProfileItems(ProfileItem profileItem)
{
var allChildItems = new List<ProfileItem>();
var itemMap = new Dictionary<string, ProfileItem>();
var visited = new HashSet<string>();
await CollectChildItems(profileItem, allChildItems, visited);
await CollectChildItems(profileItem, itemMap, visited);
return allChildItems;
return itemMap;
}
private static async Task CollectChildItems(ProfileItem profileItem, List<ProfileItem> allChildItems, HashSet<string> visited)
private static async Task CollectChildItems(ProfileItem profileItem, Dictionary<string, ProfileItem> itemMap,
HashSet<string> visited)
{
var (childItems, _) = await GetChildProfileItems(profileItem);
foreach (var child in childItems.Where(child => visited.Add(child.IndexId)))
{
allChildItems.Add(child);
itemMap[child.IndexId] = child;
if (child.ConfigType.IsGroupType())
{
await CollectChildItems(child, allChildItems, visited);
await CollectChildItems(child, itemMap, visited);
}
}
}

View File

@@ -38,4 +38,25 @@ public class NoticeManager
Enqueue(msg);
SendMessage(msg);
}
/// <summary>
/// Sends each error and warning in <paramref name="validatorResult"/> to the message panel
/// and enqueues a summary snack notification (capped at 10 messages).
/// Returns <c>true</c> when there were any messages so the caller can decide on early-return
/// based on <see cref="NodeValidatorResult.Success"/>.
/// </summary>
public bool NotifyValidatorResult(NodeValidatorResult validatorResult)
{
var msgs = new List<string>([.. validatorResult.Errors, .. validatorResult.Warnings]);
if (msgs.Count == 0)
{
return false;
}
foreach (var msg in msgs)
{
SendMessage(msg);
}
Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return true;
}
}

View File

@@ -208,6 +208,7 @@ public class ClashUIItem
public int ProxiesAutoDelayTestInterval { get; set; } = 10;
public bool ConnectionsAutoRefresh { get; set; }
public int ConnectionsRefreshInterval { get; set; } = 2;
public List<ColumnItem> ConnectionsColumnItem { get; set; }
}
[Serializable]

View File

@@ -3,6 +3,7 @@ namespace ServiceLib.Models;
public record CoreConfigContext
{
public required ProfileItem Node { get; init; }
public required ECoreType RunCoreType { get; init; }
public RoutingItem? RoutingItem { get; init; }
public DNSItem? RawDnsItem { get; init; }
public SimpleDNSItem SimpleDnsItem { get; init; } = new();

View File

@@ -9,4 +9,6 @@ public class ServerTestItem
public EConfigType ConfigType { get; set; }
public bool AllowTest { get; set; }
public int QueueNum { get; set; }
public ProfileItem Profile { get; set; }
public ECoreType CoreType { get; set; }
}

View File

@@ -28,6 +28,7 @@ public class Dns4Sbox
public bool? disable_cache { get; set; }
public bool? disable_expire { get; set; }
public bool? independent_cache { get; set; }
public int? cache_capacity { get; set; }
public bool? reverse_mapping { get; set; }
public string? client_subnet { get; set; }
}

View File

@@ -220,12 +220,6 @@ public class DnsServer4Ray
public List<string>? domains { get; set; }
public bool? skipFallback { get; set; }
public List<string>? expectedIPs { get; set; }
public List<string>? unexpectedIPs { get; set; }
public string? clientIp { get; set; }
public string? queryStrategy { get; set; }
public int? timeoutMs { get; set; }
public bool? disableCache { get; set; }
public bool? finalQuery { get; set; }
public string? tag { get; set; }
}
@@ -366,6 +360,7 @@ public class TlsSettings4Ray
public bool? disableSystemRoot { get; set; }
public string? echConfigList { get; set; }
public string? echForceQuery { get; set; }
public Sockopt4Ray? echSockopt { get; set; }
}
public class CertificateSettings4Ray

View File

@@ -132,33 +132,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support network type &apos;{1}&apos;. 的本地化字符串。
/// </summary>
public static string CoreNotSupportNetwork {
get {
return ResourceManager.GetString("CoreNotSupportNetwork", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos;. 的本地化字符串。
/// </summary>
public static string CoreNotSupportProtocol {
get {
return ResourceManager.GetString("CoreNotSupportProtocol", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos; when using transport &apos;{2}&apos;. 的本地化字符串。
/// </summary>
public static string CoreNotSupportProtocolTransport {
get {
return ResourceManager.GetString("CoreNotSupportProtocolTransport", resourceCulture);
}
}
/// <summary>
/// 查找类似 Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. 的本地化字符串。
/// </summary>
@@ -312,24 +285,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Group &apos;{0}&apos; is empty. Please add at least one node. 的本地化字符串。
/// </summary>
public static string GroupEmpty {
get {
return ResourceManager.GetString("GroupEmpty", resourceCulture);
}
}
/// <summary>
/// 查找类似 {0} Group cannot reference itself or have a circular reference 的本地化字符串。
/// </summary>
public static string GroupSelfReference {
get {
return ResourceManager.GetString("GroupSelfReference", resourceCulture);
}
}
/// <summary>
/// 查找类似 This is not the correct configuration, please check 的本地化字符串。
/// </summary>
@@ -357,15 +312,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 The {0} property is invalid, please check. 的本地化字符串。
/// </summary>
public static string InvalidProperty {
get {
return ResourceManager.GetString("InvalidProperty", resourceCulture);
}
}
/// <summary>
/// 查找类似 Invalid address (URL) 的本地化字符串。
/// </summary>
@@ -888,6 +834,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 All configurations 的本地化字符串。
/// </summary>
public static string menuAllServers {
get {
return ResourceManager.GetString("menuAllServers", resourceCulture);
}
}
/// <summary>
/// 查找类似 Backup and Restore 的本地化字符串。
/// </summary>
@@ -1060,74 +1015,20 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。
/// 查找类似 Generate Policy Group 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServer {
public static string menuGenGroupServer {
get {
return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture);
return ResourceManager.GetString("menuGenGroupServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback by sing-box 的本地化字符串。
/// 查找类似 Group by Region 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerSingBoxFallback {
public static string menuGenRegionGroup {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture);
}
}
/// <summary>
/// 查找类似 LeastPing by sing-box 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerSingBoxLeastPing {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayFallback {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture);
}
}
/// <summary>
/// 查找类似 LeastLoad by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayLeastLoad {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture);
}
}
/// <summary>
/// 查找类似 LeastPing by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayLeastPing {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture);
}
}
/// <summary>
/// 查找类似 Random by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayRandom {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture);
}
}
/// <summary>
/// 查找类似 RoundRobin by Xray 的本地化字符串。
/// </summary>
public static string menuGenGroupMultipleServerXrayRoundRobin {
get {
return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture);
return ResourceManager.GetString("menuGenRegionGroup", resourceCulture);
}
}
@@ -1914,6 +1815,33 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support network type &apos;{1}&apos; 的本地化字符串。
/// </summary>
public static string MsgCoreNotSupportNetwork {
get {
return ResourceManager.GetString("MsgCoreNotSupportNetwork", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos; 的本地化字符串。
/// </summary>
public static string MsgCoreNotSupportProtocol {
get {
return ResourceManager.GetString("MsgCoreNotSupportProtocol", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support protocol &apos;{1}&apos; when using transport &apos;{2}&apos; 的本地化字符串。
/// </summary>
public static string MsgCoreNotSupportProtocolTransport {
get {
return ResourceManager.GetString("MsgCoreNotSupportProtocolTransport", resourceCulture);
}
}
/// <summary>
/// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。
/// </summary>
@@ -1959,6 +1887,60 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Group {0} child group node {1} error: {2}. Skipping this node. 的本地化字符串。
/// </summary>
public static string MsgGroupChildGroupNodeError {
get {
return ResourceManager.GetString("MsgGroupChildGroupNodeError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} child group node {1} warning: {2} 的本地化字符串。
/// </summary>
public static string MsgGroupChildGroupNodeWarning {
get {
return ResourceManager.GetString("MsgGroupChildGroupNodeWarning", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} child node {1} error: {2}. Skipping this node. 的本地化字符串。
/// </summary>
public static string MsgGroupChildNodeError {
get {
return ResourceManager.GetString("MsgGroupChildNodeError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} child node {1} warning: {2} 的本地化字符串。
/// </summary>
public static string MsgGroupChildNodeWarning {
get {
return ResourceManager.GetString("MsgGroupChildNodeWarning", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} has a cycle dependency on child node {1}. Skipping this node. 的本地化字符串。
/// </summary>
public static string MsgGroupCycleDependency {
get {
return ResourceManager.GetString("MsgGroupCycleDependency", resourceCulture);
}
}
/// <summary>
/// 查找类似 Group {0} has no valid child node. 的本地化字符串。
/// </summary>
public static string MsgGroupNoValidChildNode {
get {
return ResourceManager.GetString("MsgGroupNoValidChildNode", resourceCulture);
}
}
/// <summary>
/// 查找类似 Information 的本地化字符串。
/// </summary>
@@ -1968,6 +1950,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 The {0} property is invalid, please check 的本地化字符串。
/// </summary>
public static string MsgInvalidProperty {
get {
return ResourceManager.GetString("MsgInvalidProperty", resourceCulture);
}
}
/// <summary>
/// 查找类似 Please enter the URL 的本地化字符串。
/// </summary>
@@ -1977,6 +1968,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Not support protocol &apos;{0}&apos; 的本地化字符串。
/// </summary>
public static string MsgNotSupportProtocol {
get {
return ResourceManager.GetString("MsgNotSupportProtocol", resourceCulture);
}
}
/// <summary>
/// 查找类似 No valid subscriptions set 的本地化字符串。
/// </summary>
@@ -1995,6 +1995,42 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Routing rule {0} has an empty outbound tag. Fallback to proxy node only. 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleEmptyOutboundTag {
get {
return ResourceManager.GetString("MsgRoutingRuleEmptyOutboundTag", resourceCulture);
}
}
/// <summary>
/// 查找类似 Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleOutboundNodeError {
get {
return ResourceManager.GetString("MsgRoutingRuleOutboundNodeError", resourceCulture);
}
}
/// <summary>
/// 查找类似 Routing rule {0} outbound node {1} not found. Fallback to proxy node only. 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleOutboundNodeNotFound {
get {
return ResourceManager.GetString("MsgRoutingRuleOutboundNodeNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 Routing rule {0} outbound node {1} warning: {2} 的本地化字符串。
/// </summary>
public static string MsgRoutingRuleOutboundNodeWarning {
get {
return ResourceManager.GetString("MsgRoutingRuleOutboundNodeWarning", resourceCulture);
}
}
/// <summary>
/// 查找类似 Filter, press Enter to execute 的本地化字符串。
/// </summary>
@@ -2049,6 +2085,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Subscription next proxy {0} not found. Skipping. 的本地化字符串。
/// </summary>
public static string MsgSubscriptionNextProfileNotFound {
get {
return ResourceManager.GetString("MsgSubscriptionNextProfileNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 Subscription previous proxy {0} not found. Skipping. 的本地化字符串。
/// </summary>
public static string MsgSubscriptionPrevProfileNotFound {
get {
return ResourceManager.GetString("MsgSubscriptionPrevProfileNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 Unpacking... 的本地化字符串。
/// </summary>
@@ -2103,15 +2157,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Node alias &apos;{0}&apos; does not exist. 的本地化字符串。
/// </summary>
public static string NodeTagNotExist {
get {
return ResourceManager.GetString("NodeTagNotExist", resourceCulture);
}
}
/// <summary>
/// 查找类似 Non-VMess or SS protocol 的本地化字符串。
/// </summary>
@@ -2139,15 +2184,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Not support protocol &apos;{0}&apos;. 的本地化字符串。
/// </summary>
public static string NotSupportProtocol {
get {
return ResourceManager.GetString("NotSupportProtocol", resourceCulture);
}
}
/// <summary>
/// 查找类似 Scan completed, no valid QR code found 的本地化字符串。
/// </summary>
@@ -2229,24 +2265,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Policy group: 的本地化字符串。
/// </summary>
public static string PolicyGroupPrefix {
get {
return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture);
}
}
/// <summary>
/// 查找类似 Proxy chained: 的本地化字符串。
/// </summary>
public static string ProxyChainedPrefix {
get {
return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture);
}
}
/// <summary>
/// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。
/// </summary>
@@ -2310,15 +2328,6 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Routing rule outbound: 的本地化字符串。
/// </summary>
public static string RoutingRuleOutboundPrefix {
get {
return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture);
}
}
/// <summary>
/// 查找类似 Run as Admin 的本地化字符串。
/// </summary>

View File

@@ -1371,24 +1371,6 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>مخفی و پورت می شود، با کاما (،) جدا می شود</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>چند سرور تصادفی توسط Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>چند سرور RoundRobin توسط Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>چند سرور LeastPing توسط Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>چند سرور LeastLoad توسط Xray</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>LeastPing چند سرور توسط sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>صادر کردن سرور</value>
</data>
@@ -1533,44 +1515,20 @@
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
<data name="MsgInvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
@@ -1668,4 +1626,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>Generate Policy Group</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>All configurations</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root>

View File

@@ -1368,24 +1368,6 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>Écrase le port ; pour plusieurs groupes, séparer par virgules (,)</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Générer un groupe de stratégie depuis plusieurs profils</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Xray aléatoire (multi-sélection)</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Xray équilibrage (tourniquet) multi-sélection</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Xray latence minimale (multi-sélection)</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Xray le plus stable (multi-sélection)</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>sing-box latence minimale (multi-sélection)</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>Exporter</value>
</data>
@@ -1530,44 +1512,20 @@
<data name="TbFallback" xml:space="preserve">
<value>Basculement (failover)</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>sing-box basculement (multi-sélection)</value>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le type de réseau « {1} »</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Xray basculement (multi-sélection)</value>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} »</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le type de réseau « {1} ».</value>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le protocole « {1} »</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} ».</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Le cœur « {0} » ne prend pas en charge le protocole « {1} ».</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Chaîne de proxy : </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Règle de routage sortante : </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Groupe de stratégie : </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Lalias « {0} » nexiste pas.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Le groupe « {0} » est vide. Veuillez ajouter au moins une configuration.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<data name="MsgInvalidProperty" xml:space="preserve">
<value>La propriété {0} est invalide, veuillez vérifier</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>Le groupe {0} ne peut pas se référencer lui-même ni créer de référence circulaire</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Protocole « {0} » non pris en charge.</value>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Protocole « {0} » non pris en charge</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>Si le système na pas de zone de notif., nactivez pas cette option</value>
@@ -1633,36 +1591,81 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
<value>Certificat complet (chaîne), format PEM</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
<value>Empreinte du certificat (SHA-256)</value>
</data>
<data name="TbServeStale" xml:space="preserve">
<value>Serve Stale</value>
<value>Cache optimiste</value>
</data>
<data name="TbParallelQuery" xml:space="preserve">
<value>Parallel Query</value>
<value>Requête parallèle</value>
</data>
<data name="TbDomesticDNSTips" xml:space="preserve">
<value>By default, invoked only during routing for resolution</value>
<value>Par défaut, utilisé uniquement lors du routage pour la résolution.</value>
</data>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>By default, invoked only during routing for resolution; ensure the remote server can reach this DNS</value>
<value>Par défaut, invoqué uniquement au routage pour la résolution. Vérifiez que le serveur distant peut joindre ce DNS.</value>
</data>
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
<value>If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used.</value>
<value>Si non défini ou « AsIs », le DNS système est utilisé ; sinon, le module DNS interne est utilisé.</value>
</data>
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
<value>If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used.</value>
<value>Si non défini ou « AsIs », la résolution DNS est assurée par le serveur distant ; sinon, le module DNS interne est utilisé.</value>
</data>
<data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value>
<value>Intervalle de saut de port</value>
</data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
<value>Aperçu des sous-config</value>
</data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
</root>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>Generate Policy Group</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>All configurations</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root>

View File

@@ -1371,24 +1371,6 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>A portot lefedi, vesszővel (,) elválasztva</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Több konfiguráció véletlenszerűen Xray szerint</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Több konfiguráció RoundRobin Xray szerint</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Több konfiguráció legkisebb pinggel Xray szerint</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Több konfiguráció legkisebb terheléssel Xray szerint</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Több konfiguráció legkisebb pinggel sing-box szerint</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>Konfiguráció exportálása</value>
</data>
@@ -1533,44 +1515,20 @@
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
<data name="MsgInvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
@@ -1668,4 +1626,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>Generate Policy Group</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>All configurations</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root>

View File

@@ -1371,24 +1371,6 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>Will cover the port, separate with commas (,)</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Random by Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>RoundRobin by Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>LeastPing by Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>LeastLoad by Xray</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>LeastPing by sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>Export</value>
</data>
@@ -1533,44 +1515,20 @@
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Fallback by sing-box</value>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Fallback by Xray</value>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
<data name="MsgInvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
@@ -1668,4 +1626,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>Generate Policy Group</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>All configurations</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root>

View File

@@ -1371,24 +1371,6 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Случайный (Xray)</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Круговой (Xray)</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Минимальное RTT (минимальное время туда-обратно) (Xray)</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Минимальная нагрузка (Xray)</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Минимальное RTT (минимальное время туда-обратно) (sing-box)</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>Экспортировать конфигурацию</value>
</data>
@@ -1533,44 +1515,20 @@
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
<data name="MsgInvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
@@ -1668,4 +1626,49 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>Routing rule {0} outbound node {1} warning: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>Generate Policy Group</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>All configurations</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root>

View File

@@ -1368,24 +1368,6 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>会覆盖端口,多组时用逗号 (,) 隔开</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>多选生成策略组</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>多选随机 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>多选负载均衡 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>多选最低延迟 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>多选最稳定 Xray</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>多选最低延迟 sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>导出</value>
</data>
@@ -1530,44 +1512,20 @@
<data name="TbFallback" xml:space="preserve">
<value>故障转移</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>多选故障转移 sing-box</value>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支持网络类型 '{1}'</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>多选故障转移 Xray</value>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支持网络类型 '{1}'</value>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支持协议 '{1}'</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。</value>
<data name="MsgInvalidProperty" xml:space="preserve">
<value>{0} 属性无效,请检查</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支持协议 '{1}'</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>代理链: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>路由规则出站: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>策略组: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>别名 '{0}' 不存在。</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>组“{0}”为空。请至少添加一个配置。</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>{0}属性无效,请检查</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} 分组不能引用自身或循环引用</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>不支持协议 '{0}'。</value>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>不支持协议 '{0}'</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>如果系统没有托盘功能,请不要开启</value>
@@ -1665,4 +1623,49 @@
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>路由规则 {0} 出站节点 {1} 警告:{2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>路由规则 {0} 出站节点 {1} 错误:{2}。已回退为仅使用代理节点。</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>节点组 {0} 与子节点 {1} 存在循环依赖,已跳过该节点。</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>节点组 {0} 子节点 {1} 警告:{2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>节点组 {0} 子节点 {1} 错误:{2}。已跳过该节点。</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>节点组 {0} 子节点组 {1} 警告:{2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>节点组 {0} 子节点组 {1} 错误:{2}。已跳过该节点。</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>节点组 {0} 下没有有效的子节点。</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>路由规则 {0} 的出站标签为空,已回退为仅使用代理节点。</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>路由规则 {0} 的出站节点 {1} 未找到,已回退为仅使用代理节点。</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>订阅前置节点 {0} 未找到,已跳过。</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>订阅后置节点 {0} 未找到,已跳过。</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>一键生成策略组</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>全部配置项</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>按地区分组</value>
</data>
</root>

View File

@@ -127,7 +127,7 @@
<value>設定格式不正確</value>
</data>
<data name="CustomServerTips" xml:space="preserve">
<value>注意,自訂設定完全依賴您自己的設定,不能使用所有設定功能。如需使用系統代理請手動修改偵聽埠。</value>
<value>注意,自訂設定完全依賴您自行輸入的內容,部分功能可能無法使用。如需用系統代理請手動調整監聽埠。</value>
</data>
<data name="Downloading" xml:space="preserve">
<value>下載開始...</value>
@@ -139,7 +139,7 @@
<value>生成預設設定檔失敗</value>
</data>
<data name="FailedGetDefaultConfiguration" xml:space="preserve">
<value>取預設設定失敗</value>
<value>取預設設定失敗</value>
</data>
<data name="FailedImportedCustomServer" xml:space="preserve">
<value>匯入自訂設定失敗</value>
@@ -148,7 +148,7 @@
<value>讀取設定失敗</value>
</data>
<data name="FillCorrectServerPort" xml:space="preserve">
<value>請填寫正確格式的埠</value>
<value>請填寫有效的埠</value>
</data>
<data name="FillLocalListeningPort" xml:space="preserve">
<value>請填寫本機偵聽埠</value>
@@ -247,7 +247,7 @@
<value>非 VMess 或 SS 協定</value>
</data>
<data name="NotFoundCore" xml:space="preserve">
<value>在資料夾 ({0}) 下未找到 Core 檔案 (檔案名: {1})請下載後放入資料夾下載網址: {2}</value>
<value>在資料夾 ({0}) 中找不到 Core 檔案(檔名:{1})。請下載後放入資料夾下載網址{2}</value>
</data>
<data name="NoValidQRcodeFound" xml:space="preserve">
<value>掃描完成,未發現有效二維碼</value>
@@ -304,7 +304,7 @@
<value>是否確定移除規則?</value>
</data>
<data name="RoutingRuleDetailRequiredTips" xml:space="preserve">
<value>{0}必填其中一項.</value>
<value>{0}至少需填寫其中一項</value>
</data>
<data name="LvRemarks" xml:space="preserve">
<value>別名</value>
@@ -385,7 +385,7 @@
<value>所有</value>
</data>
<data name="FillServerAddressCustom" xml:space="preserve">
<value>請瀏覽匯入設定</value>
<value>請選擇要匯入設定</value>
</data>
<data name="Speedtesting" xml:space="preserve">
<value>測試中...</value>
@@ -472,7 +472,7 @@
<value>語言 (需重啟)</value>
</data>
<data name="menuAddServerViaClipboard" xml:space="preserve">
<value>從剪貼簿入分享連結</value>
<value>從剪貼簿入分享連結</value>
</data>
<data name="menuAddServerViaScan" xml:space="preserve">
<value>掃描螢幕上的二維碼</value>
@@ -616,10 +616,10 @@
<value>SNI</value>
</data>
<data name="TbStreamSecurity" xml:space="preserve">
<value>傳輸層安全 (TLS)</value>
<value>傳輸層安全 (TLS)</value>
</data>
<data name="TipNetwork" xml:space="preserve">
<value>*預設 TCP選錯會無法連</value>
<value>*預設 TCP選錯會無法連</value>
</data>
<data name="TbCoreType" xml:space="preserve">
<value>Core 類型</value>
@@ -652,7 +652,7 @@
<value>SOCKS 埠</value>
</data>
<data name="TipPreSocksPort" xml:space="preserve">
<value>*自訂設定的 Socks 埠值,可不設定;當設定此值後,將使用 Xray/sing-box (Tun) 額外啟動一個前置 Socks 服務,提供分流和速度顯示等功能</value>
<value>*自訂設定的 Socks 埠值,可留空;當設定此值後,將使用 Xray/sing-box (Tun) 額外啟動一個前置 Socks 服務,提供分流和速度顯示等功能</value>
</data>
<data name="TbBrowse" xml:space="preserve">
<value>瀏覽</value>
@@ -1309,7 +1309,7 @@
<value>安裝字體到系統中,選擇或填入字體名稱,重新啟動後生效</value>
</data>
<data name="menuExitTips" xml:space="preserve">
<value>是否確定退出?</value>
<value>確定退出</value>
</data>
<data name="LvMemo" xml:space="preserve">
<value>備註備忘</value>
@@ -1368,24 +1368,6 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>會覆蓋埠,多組時用逗號 (,) 隔開</value>
</data>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>多選生成策略組</value>
</data>
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>多選隨機 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>多選負載平衡 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>多選最低延遲 Xray</value>
</data>
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>多選最穩定 Xray</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>多選最低延遲 sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
<value>匯出</value>
</data>
@@ -1513,13 +1495,13 @@
<value>策略組類型</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>添加策略組</value>
<value>新增策略組</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>添加鏈式代理</value>
<value>新增鏈式代理</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>添加子配置</value>
<value>新增子配置</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>刪除子配置</value>
@@ -1530,44 +1512,20 @@
<data name="TbFallback" xml:space="preserve">
<value>容錯移轉</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>多選容錯移轉 sing-box</value>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支援網路類型 '{1}'</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>多選容錯移轉 Xray</value>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支援網路類型 '{1}'.</value>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支援協定 '{1}'</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'.</value>
<data name="MsgInvalidProperty" xml:space="preserve">
<value>{0} 屬性無效,請檢查</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支援協定 '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>代理鏈: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>路由規則出站: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>策略組: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>別名 '{0}' 不存在。</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>組“{0}”為空.請至少添加一個配置。</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>{0}屬性無效,請檢查</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} 分組不能引用自身或循環引用</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>不支援協定 '{0}'.</value>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>不支援協定 '{0}'</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>如果系統沒有託盤功能,請不要開啟</value>
@@ -1633,36 +1591,81 @@
<value>EchForceQuery</value>
</data>
<data name="TbFullCertTips" xml:space="preserve">
<value>Full certificate (chain), PEM format</value>
<value>完整憑證PEM 格式</value>
</data>
<data name="TbCertSha256Tips" xml:space="preserve">
<value>Certificate fingerprint (SHA-256)</value>
<value>憑證指紋(SHA-256</value>
</data>
<data name="TbServeStale" xml:space="preserve">
<value>Serve Stale</value>
<value>提供過期快取(Serve Stale</value>
</data>
<data name="TbParallelQuery" xml:space="preserve">
<value>Parallel Query</value>
<value>并行查詢</value>
</data>
<data name="TbDomesticDNSTips" xml:space="preserve">
<value>By default, invoked only during routing for resolution</value>
<value>預設僅在路由期間進行解析時調用</value>
</data>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>By default, invoked only during routing for resolution; ensure the remote server can reach this DNS</value>
<value>預設僅在路由期間進行解析時調用;請確保遠端伺服器能連線至此 DNS</value>
</data>
<data name="TbDirectResolveStrategyTips" xml:space="preserve">
<value>If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used.</value>
<value>若未設定或為 "AsIs",使用系統 DNS 解析;否則將使用內建 DNS 模組。</value>
</data>
<data name="TbRemoteResolveStrategyTips" xml:space="preserve">
<value>If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used.</value>
<value>若未設定或為 "AsIs",由遠端伺服器的 DNS 解析;否則將使用內建 DNS 模組。</value>
</data>
<data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value>
<value>連接埠跳轉間隔</value>
</data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
<value>子配置項預覽</value>
</data>
<data name="TbFinalmask" xml:space="preserve">
<value>Finalmask</value>
</data>
<data name="MsgRoutingRuleOutboundNodeWarning" xml:space="preserve">
<value>路由規則 {0} 的出站節點 {1} 發出警告:{2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>路由規則 {0} 的出站節點 {1} 發生錯誤:{2}。已回退為僅使用代理節點。</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>節點組 {0} 與子節點 {1} 存在循環依賴。已跳過此節點。</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>節點組 {0} 的子節點 {1} 發出警告:{2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>節點組 {0} 的子節點 {1} 發生錯誤:{2}。已跳過此節點。</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>節點組 {0} 的子節點組 {1} 發出警告:{2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>節點組 {0} 的子節點組 {1} 發生錯誤:{2}。已跳過此節點。</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>節點組 {0} 沒有可用的有效子節點。</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>路由規則 {0} 的出站標籤為空。已回退為僅使用代理節點。</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>找不到路由規則 {0} 的出站節點 {1}。已回退為僅使用代理節點。</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>找不到訂閱的前一個代理 {0}。已跳過。</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>找不到訂閱的下一個代理 {0}。已跳過。</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>生成策略組</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>所有配置項</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>按區域分組</value>
</data>
</root>

View File

@@ -149,11 +149,11 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
foreach (var it in selecteds)
{
if (!Global.SingboxSupportConfigType.Contains(it.ConfigType))
if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType()))
{
continue;
}
if (it.Port <= 0)
if (!it.ConfigType.IsComplexType() && it.Port <= 0)
{
continue;
}

View File

@@ -26,11 +26,15 @@ public partial class CoreConfigSingboxService
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
useDirectDns = rules?.LastOrDefault() is { } lastRule &&
lastRule.OutboundTag == Global.DirectTag &&
(lastRule.Port == "0-65535" ||
lastRule.Network == "tcp,udp" ||
lastRule.Ip?.Contains("0.0.0.0/0") == true);
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
{
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
var isAnyIp = lastRule.Ip == null || lastRule.Ip.Count == 0 || lastRule.Ip.Contains("0.0.0.0/0");
var isAnyPort = string.IsNullOrEmpty(lastRule.Port) || lastRule.Port == "0-65535";
var isAnyNetwork = string.IsNullOrEmpty(lastRule.Network) || lastRule.Network == "tcp,udp";
useDirectDns = noDomain && noProcess && isAnyIp && isAnyPort && isAnyNetwork;
}
}
_coreConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag;
var simpleDnsItem = context.SimpleDnsItem;
@@ -89,7 +93,23 @@ public partial class CoreConfigSingboxService
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts))
{
hostsDns.predefined[kvp.Key] = kvp.Value.Where(Utils.IsIpAddress).ToList();
// only allow full match
// like example.com and full:example.com,
// but not domain:example.com, keyword:example.com or regex:example.com etc.
var testRule = new Rule4Sbox();
if (!ParseV2Domain(kvp.Key, testRule))
{
continue;
}
if (testRule.domain_keyword?.Count > 0 && !kvp.Key.Contains(':'))
{
testRule.domain = testRule.domain_keyword;
testRule.domain_keyword = null;
}
if (testRule.domain?.Count == 1)
{
hostsDns.predefined[testRule.domain.First()] = kvp.Value.Where(Utils.IsIpAddress).ToList();
}
}
foreach (var host in hostsDns.predefined)
@@ -144,15 +164,20 @@ public partial class CoreConfigSingboxService
_coreConfig.dns ??= new Dns4Sbox();
_coreConfig.dns.rules ??= [];
_coreConfig.dns.rules.AddRange(new[]
_coreConfig.dns.rules.Add(new() { ip_accept_any = true, server = Global.SingboxHostsDNSTag });
if (context.ProtectDomainList.Count > 0)
{
new Rule4Sbox { ip_accept_any = true, server = Global.SingboxHostsDNSTag },
new Rule4Sbox
_coreConfig.dns.rules.Add(new()
{
server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
domain = context.ProtectDomainList.ToList(),
},
});
}
_coreConfig.dns.rules.AddRange(new[]
{
new Rule4Sbox
{
server = Global.SingboxRemoteDNSTag,
@@ -170,44 +195,65 @@ public partial class CoreConfigSingboxService
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDnsItem.Hosts))
{
var predefined = kvp.Value.First();
if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined))
if (predefined.IsNullOrEmpty())
{
continue;
}
if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode))
var rule = new Rule4Sbox()
{
// xray syntactic sugar for predefined
// etc. #0 -> NOERROR
_coreConfig.dns.rules.Add(new()
{
query_type = [1, 28],
domain = [kvp.Key],
action = "predefined",
rcode = rcode switch
{
0 => "NOERROR",
1 => "FORMERR",
2 => "SERVFAIL",
3 => "NXDOMAIN",
4 => "NOTIMP",
5 => "REFUSED",
_ => "NOERROR",
},
});
continue;
}
// CNAME record
Rule4Sbox rule = new()
{
query_type = [1, 28],
query_type = [1, 5, 28], // A, CNAME and AAAA
action = "predefined",
rcode = "NOERROR",
answer = [$"*. IN CNAME {predefined}."],
};
if (ParseV2Domain(kvp.Key, rule))
if (!ParseV2Domain(kvp.Key, rule))
{
_coreConfig.dns.rules.Add(rule);
continue;
}
// see: https://xtls.github.io/en/config/dns.html#dnsobject
// The matching format (domain:, full:, etc.) is the same as the domain
// in the commonly used Routing System. The difference is that without a prefix,
// it defaults to using the full: prefix (similar to the common hosts file syntax).
if (rule.domain_keyword?.Count > 0 && !kvp.Key.Contains(':'))
{
rule.domain = rule.domain_keyword;
rule.domain_keyword = null;
}
// example.com #0 -> example.com with NOERROR
if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode))
{
rule.rcode = rcode switch
{
0 => "NOERROR",
1 => "FORMERR",
2 => "SERVFAIL",
3 => "NXDOMAIN",
4 => "NOTIMP",
5 => "REFUSED",
_ => "NOERROR",
};
}
else if (Utils.IsDomain(predefined))
{
// example.com CNAME target.com -> example.com with CNAME target.com
rule.answer = new List<string> { $"*. IN CNAME {predefined}." };
}
else if (Utils.IsIpAddress(predefined) && (rule.domain?.Count ?? 0) == 0)
{
// not full match, but an IP address, treat it as predefined answer
if (Utils.IsIpv6(predefined))
{
rule.answer = new List<string> { $"*. IN AAAA {predefined}" };
}
else
{
rule.answer = new List<string> { $"*. IN A {predefined}" };
}
}
else
{
continue;
}
_coreConfig.dns.rules.Add(rule);
}
if (simpleDnsItem.BlockBindingQuery == true)
@@ -424,7 +470,11 @@ public partial class CoreConfigSingboxService
localDnsServer.tag = tag;
dns4Sbox.servers.Add(localDnsServer);
dns4Sbox.rules.Insert(0, BuildProtectDomainRule());
var protectDomainRule = BuildProtectDomainRule();
if (protectDomainRule != null)
{
dns4Sbox.rules.Insert(0, protectDomainRule);
}
_coreConfig.dns = dns4Sbox;
}
@@ -446,8 +496,12 @@ public partial class CoreConfigSingboxService
_coreConfig.dns?.servers?.Add(localDnsServer);
}
private Rule4Sbox BuildProtectDomainRule()
private Rule4Sbox? BuildProtectDomainRule()
{
if (context.ProtectDomainList.Count == 0)
{
return null;
}
return new()
{
server = Global.SingboxLocalDNSTag,

View File

@@ -20,10 +20,13 @@ public partial class CoreConfigSingboxService
{
proxyOutboundList.AddRange(BuildGroupProxyOutbounds(baseTagName));
}
var proxyTags = proxyOutboundList.Where(n => n.tag.StartsWith(Global.ProxyTag)).Select(n => n.tag).ToList();
if (proxyTags.Count > 1)
if (withSelector)
{
proxyOutboundList.InsertRange(0, BuildSelectorOutbounds(proxyTags, baseTagName));
var proxyTags = proxyOutboundList.Where(n => n.tag.StartsWith(Global.ProxyTag)).Select(n => n.tag).ToList();
if (proxyTags.Count > 1)
{
proxyOutboundList.InsertRange(0, BuildSelectorOutbounds(proxyTags, baseTagName));
}
}
return proxyOutboundList;
}

View File

@@ -84,11 +84,58 @@ public partial class CoreConfigSingboxService
}
if (hostsDomains.Count > 0)
{
_coreConfig.route.rules.Add(new()
var hostsResolveRule = new Rule4Sbox
{
action = "resolve",
domain = hostsDomains,
});
};
var hostsCounter = 0;
foreach (var host in hostsDomains)
{
var domainRule = new Rule4Sbox();
if (!ParseV2Domain(host, domainRule))
{
continue;
}
if (domainRule.domain_keyword?.Count > 0 && !host.Contains(':'))
{
domainRule.domain = domainRule.domain_keyword;
domainRule.domain_keyword = null;
}
if (domainRule.domain?.Count > 0)
{
hostsResolveRule.domain ??= [];
hostsResolveRule.domain.AddRange(domainRule.domain);
hostsCounter++;
}
else if (domainRule.domain_keyword?.Count > 0)
{
hostsResolveRule.domain_keyword ??= [];
hostsResolveRule.domain_keyword.AddRange(domainRule.domain_keyword);
hostsCounter++;
}
else if (domainRule.domain_suffix?.Count > 0)
{
hostsResolveRule.domain_suffix ??= [];
hostsResolveRule.domain_suffix.AddRange(domainRule.domain_suffix);
hostsCounter++;
}
else if (domainRule.domain_regex?.Count > 0)
{
hostsResolveRule.domain_regex ??= [];
hostsResolveRule.domain_regex.AddRange(domainRule.domain_regex);
hostsCounter++;
}
else if (domainRule.geosite?.Count > 0)
{
hostsResolveRule.geosite ??= [];
hostsResolveRule.geosite.AddRange(domainRule.geosite);
hostsCounter++;
}
}
if (hostsCounter > 0)
{
_coreConfig.route.rules.Add(hostsResolveRule);
}
}
_coreConfig.route.rules.Add(new()
@@ -355,6 +402,11 @@ public partial class CoreConfigSingboxService
rule.domain_keyword ??= [];
rule.domain_keyword?.Add(domain.Substring(8));
}
else if (domain.StartsWith("dotless:"))
{
rule.domain_keyword ??= [];
rule.domain_keyword?.Add(domain.Substring(8));
}
else
{
rule.domain_keyword ??= [];

View File

@@ -60,6 +60,12 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
GenStatistic();
var finalRule = BuildFinalRule();
if (!string.IsNullOrEmpty(finalRule?.balancerTag))
{
_coreConfig.routing.rules.Add(finalRule);
}
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = ApplyFullConfigTemplate();
@@ -106,11 +112,11 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
foreach (var it in selecteds)
{
if (!Global.XraySupportConfigType.Contains(it.ConfigType))
if (!(Global.XraySupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType()))
{
continue;
}
if (it.Port <= 0)
if (!it.ConfigType.IsComplexType() && it.Port <= 0)
{
continue;
}
@@ -174,13 +180,13 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
//rule
RulesItem4Ray rule = new()
{
inboundTag = new List<string> { inbound.tag },
inboundTag = [inbound.tag],
outboundTag = tag,
type = "field"
};
if (isBalancer)
{
rule.balancerTag = tag;
rule.balancerTag = tag + Global.BalancerTagSuffix;
rule.outboundTag = null;
}
_coreConfig.routing.rules.Add(rule);
@@ -234,6 +240,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
GenLog();
GenOutbounds();
_coreConfig.routing.domainStrategy = Global.AsIs;
_coreConfig.routing.rules.Clear();
_coreConfig.inbounds.Clear();
_coreConfig.inbounds.Add(new()
@@ -244,6 +251,8 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
protocol = EInboundProtocol.mixed.ToString(),
});
_coreConfig.routing.rules.Add(BuildFinalRule());
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig);
@@ -292,6 +301,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
GenLog();
_coreConfig.outbounds.Clear();
GenOutbounds();
GenStatistic();
var protectNode = new ProfileItem()
{
@@ -306,29 +316,36 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
SsMethod = Global.None,
});
foreach (var outbound in _coreConfig.outbounds.Where(outbound => outbound.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
foreach (var outbound in _coreConfig.outbounds
.Where(o => o.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
{
outbound.streamSettings ??= new StreamSettings4Ray();
outbound.streamSettings.sockopt ??= new Sockopt4Ray();
outbound.streamSettings.sockopt.dialerProxy = "tun-project-ss";
outbound.streamSettings ??= new();
outbound.streamSettings.sockopt ??= new();
outbound.streamSettings.sockopt.dialerProxy = "tun-protect-ss";
}
// ech protected
foreach (var outbound in _coreConfig.outbounds
.Where(outbound => outbound.streamSettings?.tlsSettings?.echConfigList?.IsNullOrEmpty() == false))
{
outbound.streamSettings!.tlsSettings!.echSockopt ??= new();
outbound.streamSettings.tlsSettings.echSockopt.dialerProxy = "tun-protect-ss";
}
_coreConfig.outbounds.Add(new CoreConfigV2rayService(context with
{
Node = protectNode,
}).BuildProxyOutbound("tun-project-ss"));
}).BuildProxyOutbound("tun-protect-ss"));
_coreConfig.routing.rules ??= [];
var hasBalancer = _coreConfig.routing.balancers is { Count: > 0 };
_coreConfig.routing.rules =
[
new()
{
inboundTag = new List<string> { "proxy-relay-ss" },
outboundTag = hasBalancer ? null : Global.ProxyTag,
balancerTag = hasBalancer ? Global.ProxyTag + Global.BalancerTagSuffix: null,
type = "field"
}
];
_coreConfig.inbounds.Clear();
_coreConfig.routing.rules.Add(new()
{
inboundTag = ["proxy-relay-ss"],
outboundTag = hasBalancer ? null : Global.ProxyTag,
balancerTag = hasBalancer ? Global.ProxyTag + Global.BalancerTagSuffix : null,
type = "field"
});
//_coreConfig.inbounds.Clear();
var configNode = JsonUtils.ParseJson(JsonUtils.Serialize(_coreConfig))!;
configNode["inbounds"]!.AsArray().Add(new

View File

@@ -70,18 +70,36 @@ public partial class CoreConfigV2rayService
dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null;
dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null;
if (_coreConfig.routing.domainStrategy == Global.IPIfNonMatch)
// DNS routing
var directDnsTags = dnsItem.servers
.Select(server =>
{
var tagNode = (server as JsonObject)?["tag"];
return tagNode is JsonValue value && value.TryGetValue<string>(out var tag) ? tag : null;
})
.Where(tag => tag is not null && tag.StartsWith(Global.DirectDnsTag, StringComparison.Ordinal))
.Select(tag => tag!)
.ToList();
if (directDnsTags.Count > 0)
{
// DNS routing
dnsItem.tag = Global.DnsTag;
_coreConfig.routing.rules.Add(new RulesItem4Ray
_coreConfig.routing.rules.Add(new()
{
type = "field",
inboundTag = new List<string> { Global.DnsTag },
outboundTag = Global.ProxyTag,
inboundTag = directDnsTags,
outboundTag = Global.DirectTag,
});
}
var finalRule = BuildFinalRule();
dnsItem.tag = Global.DnsTag;
_coreConfig.routing.rules.Add(new()
{
type = "field",
inboundTag = [Global.DnsTag],
outboundTag = finalRule.outboundTag,
balancerTag = finalRule.balancerTag,
});
_coreConfig.dns = dnsItem;
}
catch (Exception ex)
@@ -93,45 +111,6 @@ public partial class CoreConfigV2rayService
private void FillDnsServers(Dns4Ray dnsItem)
{
var simpleDNSItem = context.SimpleDnsItem;
static List<string> ParseDnsAddresses(string? dnsInput, string defaultAddress)
{
var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList() ?? new List<string> { defaultAddress };
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
}
static object? CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
{
var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress);
var domainFinal = dnsAddress;
int? portFinal = null;
if (scheme.IsNullOrEmpty() || scheme.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = domain;
portFinal = port > 0 ? port : null;
}
else if (scheme.StartsWith("tcp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = scheme + "://" + domain;
portFinal = port > 0 ? port : null;
}
var dnsServer = new DnsServer4Ray
{
address = domainFinal,
port = portFinal,
skipFallback = true,
domains = domains.Count > 0 ? domains : null,
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
};
return JsonUtils.SerializeToNode(dnsServer, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.First());
var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.First());
@@ -252,35 +231,102 @@ public partial class CoreConfigV2rayService
dnsItem.servers ??= [];
void AddDnsServers(List<string> dnsAddresses, List<string> domains, List<string>? expectedIPs = null)
{
if (domains.Count > 0)
{
foreach (var dnsAddress in dnsAddresses)
{
dnsItem.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs));
}
}
}
var directDnsTagIndex = 1;
AddDnsServers(remoteDNSAddress, proxyDomainList);
AddDnsServers(directDNSAddress, directDomainList);
AddDnsServers(directDNSAddress, directDomainList, true);
AddDnsServers(remoteDNSAddress, proxyGeositeList);
AddDnsServers(directDNSAddress, directGeositeList);
AddDnsServers(directDNSAddress, expectedDomainList, expectedIPs);
AddDnsServers(directDNSAddress, directGeositeList, true);
AddDnsServers(directDNSAddress, expectedDomainList, true, expectedIPs);
if (dnsServerDomains.Count > 0)
{
AddDnsServers(bootstrapDNSAddress, dnsServerDomains);
}
var useDirectDns = rules?.LastOrDefault() is { } lastRule
&& lastRule.OutboundTag == Global.DirectTag
&& (lastRule.Port == "0-65535"
|| lastRule.Network == "tcp,udp"
|| lastRule.Ip?.Contains("0.0.0.0/0") == true);
var useDirectDns = false;
var defaultDnsServers = useDirectDns ? directDNSAddress : remoteDNSAddress;
dnsItem.servers.AddRange(defaultDnsServers);
if (rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag)
{
var noDomain = lastRule.Domain == null || lastRule.Domain.Count == 0;
var noProcess = lastRule.Process == null || lastRule.Process.Count == 0;
var isAnyIp = lastRule.Ip == null || lastRule.Ip.Count == 0 || lastRule.Ip.Contains("0.0.0.0/0");
var isAnyPort = string.IsNullOrEmpty(lastRule.Port) || lastRule.Port == "0-65535";
var isAnyNetwork = string.IsNullOrEmpty(lastRule.Network) || lastRule.Network == "tcp,udp";
useDirectDns = noDomain && noProcess && isAnyIp && isAnyPort && isAnyNetwork;
}
if (!useDirectDns)
{
dnsItem.servers.AddRange(remoteDNSAddress);
}
else
{
foreach (var dns in directDNSAddress)
{
var dnsServer = CreateDnsServer(dns, []);
dnsServer.tag = $"{Global.DirectDnsTag}-{directDnsTagIndex++}";
dnsServer.skipFallback = false;
dnsItem.servers.Add(JsonUtils.SerializeToNode(dnsServer,
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
}
}
return;
static List<string> ParseDnsAddresses(string? dnsInput, string defaultAddress)
{
var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';')
.Select(addr => addr.Trim())
.Where(addr => !string.IsNullOrEmpty(addr))
.Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr)
.Distinct()
.ToList() ?? [defaultAddress];
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
}
static DnsServer4Ray CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
{
var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress);
var domainFinal = dnsAddress;
int? portFinal = null;
if (scheme.IsNullOrEmpty() || scheme.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = domain;
portFinal = port > 0 ? port : null;
}
else if (scheme.StartsWith("tcp", StringComparison.OrdinalIgnoreCase))
{
domainFinal = scheme + "://" + domain;
portFinal = port > 0 ? port : null;
}
var dnsServer = new DnsServer4Ray
{
address = domainFinal,
port = portFinal,
skipFallback = true,
domains = domains.Count > 0 ? domains : null,
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
};
return dnsServer;
}
void AddDnsServers(List<string> dnsAddresses, List<string> domains, bool isDirectDns = false, List<string>? expectedIPs = null)
{
if (domains.Count <= 0)
{
return;
}
foreach (var dnsAddress in dnsAddresses)
{
var dnsServer = CreateDnsServer(dnsAddress, domains, expectedIPs);
if (isDirectDns)
{
dnsServer.tag = $"{Global.DirectDnsTag}-{directDnsTagIndex++}";
}
var dnsServerNode = JsonUtils.SerializeToNode(dnsServer,
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
dnsItem.servers.Add(dnsServerNode);
}
}
}
private void FillDnsHosts(Dns4Ray dnsItem)
@@ -391,7 +437,7 @@ public partial class CoreConfigV2rayService
FillDnsDomainsCustom(obj);
_coreConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(obj));
_coreConfig.dns = obj;
}
catch (Exception ex)
{

View File

@@ -181,4 +181,28 @@ public partial class CoreConfigV2rayService
return tag;
}
private RulesItem4Ray BuildFinalRule()
{
var finalRule = new RulesItem4Ray()
{
type = "field",
network = "tcp,udp",
outboundTag = Global.ProxyTag,
};
var balancer =
_coreConfig?.routing?.balancers?.FirstOrDefault(b => b.tag == Global.ProxyTag + Global.BalancerTagSuffix, null);
var domainStrategy = _coreConfig.routing?.domainStrategy ?? Global.AsIs;
if (balancer is not null)
{
finalRule.outboundTag = null;
finalRule.balancerTag = balancer.tag;
}
if (domainStrategy == Global.IPIfNonMatch)
{
finalRule.network = null;
finalRule.ip = ["0.0.0.0/0", "::/0"];
}
return finalRule;
}
}

View File

@@ -61,26 +61,36 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<List<ServerTestItem>> GetClearItem(ESpeedActionType actionType, List<ProfileItem> selecteds)
{
var lstSelected = new List<ServerTestItem>();
foreach (var it in selecteds)
var lstSelected = new List<ServerTestItem>(selecteds.Count);
var ids = selecteds.Where(it => !it.IndexId.IsNullOrEmpty()
&& it.ConfigType != EConfigType.Custom
&& (it.ConfigType.IsComplexType() || it.Port > 0))
.Select(it => it.IndexId)
.ToList();
var profileMap = await AppManager.Instance.GetProfileItemsByIndexIdsAsMap(ids);
for (var i = 0; i < selecteds.Count; i++)
{
if (it.ConfigType.IsComplexType())
var it = selecteds[i];
if (it.ConfigType == EConfigType.Custom)
{
continue;
}
if (it.Port <= 0)
if (!it.ConfigType.IsComplexType() && it.Port <= 0)
{
continue;
}
var profile = profileMap.GetValueOrDefault(it.IndexId, it);
lstSelected.Add(new ServerTestItem()
{
IndexId = it.IndexId,
Address = it.Address,
Port = it.Port,
ConfigType = it.ConfigType,
QueueNum = selecteds.IndexOf(it)
QueueNum = i,
Profile = profile,
CoreType = AppManager.Instance.GetCoreType(profile, it.ConfigType),
});
}
@@ -353,8 +363,8 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
{
List<List<ServerTestItem>> lstTest = new();
var lst1 = lstSelected.Where(t => Global.XraySupportConfigType.Contains(t.ConfigType)).ToList();
var lst2 = lstSelected.Where(t => Global.SingboxOnlyConfigType.Contains(t.ConfigType)).ToList();
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++)
{

View File

@@ -363,44 +363,36 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
var geoipFiles = new List<string>();
var geoSiteFiles = new List<string>();
//Collect used files list
// Collect from routing rules
var routingItems = await AppManager.Instance.RoutingItems();
foreach (var routing in routingItems)
{
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item in rules ?? [])
{
foreach (var ip in item.Ip ?? [])
{
var prefix = "geoip:";
if (ip.StartsWith(prefix))
{
geoipFiles.Add(ip.Substring(prefix.Length));
}
}
foreach (var domain in item.Domain ?? [])
{
var prefix = "geosite:";
if (domain.StartsWith(prefix))
{
geoSiteFiles.Add(domain.Substring(prefix.Length));
}
}
AddPrefixedItems(item.Ip, "geoip:", geoipFiles);
AddPrefixedItems(item.Domain, "geosite:", geoSiteFiles);
}
}
//append dns items TODO
geoSiteFiles.Add("google");
geoSiteFiles.Add("cn");
geoSiteFiles.Add("geolocation-cn");
geoSiteFiles.Add("category-ads-all");
// Collect from DNS configuration
var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (dnsItem != null)
{
ExtractDnsRuleSets(dnsItem.NormalDNS, geoipFiles, geoSiteFiles);
ExtractDnsRuleSets(dnsItem.TunDNS, geoipFiles, geoSiteFiles);
}
// Append default items
geoSiteFiles.AddRange(["google", "cn", "geolocation-cn", "category-ads-all"]);
// Download files
var path = Utils.GetBinPath("srss");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
foreach (var item in geoipFiles.Distinct())
{
await UpdateSrsFile("geoip", item);
@@ -412,6 +404,63 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
}
}
private void AddPrefixedItems(List<string>? items, string prefix, List<string> output)
{
if (items == null)
{
return;
}
foreach (var item in items)
{
if (item.StartsWith(prefix))
{
output.Add(item.Substring(prefix.Length));
}
}
}
private void ExtractDnsRuleSets(string? dnsJson, List<string> geoipFiles, List<string> geoSiteFiles)
{
if (string.IsNullOrEmpty(dnsJson))
{
return;
}
try
{
var dns = JsonUtils.Deserialize<Dns4Sbox>(dnsJson);
if (dns?.rules != null)
{
foreach (var rule in dns.rules)
{
ExtractSrsRuleSets(rule, geoipFiles, geoSiteFiles);
}
}
}
catch { }
}
private void ExtractSrsRuleSets(Rule4Sbox? rule, List<string> geoipFiles, List<string> geoSiteFiles)
{
if (rule == null)
{
return;
}
AddPrefixedItems(rule.rule_set, "geosite-", geoSiteFiles);
AddPrefixedItems(rule.rule_set, "geoip-", geoipFiles);
// Handle nested rules recursively
if (rule.rules != null)
{
foreach (var nestedRule in rule.rules)
{
ExtractSrsRuleSets(nestedRule, geoipFiles, geoSiteFiles);
}
}
}
private async Task UpdateSrsFile(string type, string srsName)
{
var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl)

View File

@@ -99,15 +99,8 @@ public class AddGroupServerViewModel : MyReactiveObject
Filter = protocolExtra?.Filter;
var childIndexIds = Utils.String2List(protocolExtra?.ChildItems) ?? [];
foreach (var item in childIndexIds)
{
var child = await AppManager.Instance.GetProfileItem(item);
if (child == null)
{
continue;
}
ChildItemsObs.Add(child);
}
var childItemList = await AppManager.Instance.GetProfileItemsOrderedByIndexIds(childIndexIds);
ChildItemsObs.AddRange(childItemList);
}
public async Task ChildRemoveAsync()
@@ -234,13 +227,6 @@ public class AddGroupServerViewModel : MyReactiveObject
SelectedSource.SetProtocolExtra(protocolExtra);
var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, protocolExtra);
if (hasCycle)
{
NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks));
return;
}
if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0)
{
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);

View File

@@ -27,10 +27,10 @@ public class AddServerViewModel : MyReactiveObject
public string Ports { get; set; }
[Reactive]
public int UpMbps { get; set; }
public int? UpMbps { get; set; }
[Reactive]
public int DownMbps { get; set; }
public int? DownMbps { get; set; }
[Reactive]
public string HopInterval { get; set; }
@@ -113,8 +113,8 @@ public class AddServerViewModel : MyReactiveObject
AlterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
Flow = protocolExtra?.Flow ?? string.Empty;
SalamanderPass = protocolExtra?.SalamanderPass ?? string.Empty;
UpMbps = protocolExtra?.UpMbps ?? _config.HysteriaItem.UpMbps;
DownMbps = protocolExtra?.DownMbps ?? _config.HysteriaItem.DownMbps;
UpMbps = protocolExtra?.UpMbps;
DownMbps = protocolExtra?.DownMbps;
HopInterval = protocolExtra?.HopInterval.IsNullOrEmpty() ?? true ? Global.Hysteria2DefaultHopInt.ToString() : protocolExtra.HopInterval;
VmessSecurity = protocolExtra?.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity;
VlessEncryption = protocolExtra?.VlessEncryption.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None;
@@ -175,8 +175,8 @@ public class AddServerViewModel : MyReactiveObject
AlterId = AlterId > 0 ? AlterId.ToString() : null,
Flow = Flow.NullIfEmpty(),
SalamanderPass = SalamanderPass.NullIfEmpty(),
UpMbps = UpMbps >= 0 ? UpMbps : null,
DownMbps = DownMbps >= 0 ? DownMbps : null,
UpMbps = UpMbps,
DownMbps = DownMbps,
HopInterval = HopInterval.NullIfEmpty(),
VmessSecurity = VmessSecurity.NullIfEmpty(),
VlessEncryption = VlessEncryption.NullIfEmpty(),

View File

@@ -540,20 +540,21 @@ public class MainWindowViewModel : MyReactiveObject
{
SetReloadEnabled(false);
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
var profileItem = await ConfigHandler.GetDefaultServer(_config);
if (profileItem == null)
{
NoticeManager.Instance.Enqueue(ResUI.CheckServerSettings);
return;
}
var allResult = await CoreConfigContextBuilder.BuildAll(_config, profileItem);
if (NoticeManager.Instance.NotifyValidatorResult(allResult.CombinedValidatorResult) && !allResult.Success)
{
foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return;
}
await Task.Run(async () =>
{
await LoadCore();
await LoadCore(allResult.ResolvedMainContext, allResult.PreSocksResult?.Context);
await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000);
});
@@ -594,10 +595,9 @@ public class MainWindowViewModel : MyReactiveObject
RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled);
}
private async Task LoadCore()
private async Task LoadCore(CoreConfigContext? mainContext, CoreConfigContext? preContext)
{
var node = await ConfigHandler.GetDefaultServer(_config);
await CoreManager.Instance.LoadCore(node);
await CoreManager.Instance.LoadCore(mainContext, preContext);
}
#endregion core job

View File

@@ -22,8 +22,8 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public string defUserAgent { get; set; }
[Reactive] public string mux4SboxProtocol { get; set; }
[Reactive] public bool enableCacheFile4Sbox { get; set; }
[Reactive] public int hyUpMbps { get; set; }
[Reactive] public int hyDownMbps { get; set; }
[Reactive] public int? hyUpMbps { get; set; }
[Reactive] public int? hyDownMbps { get; set; }
[Reactive] public bool enableFragment { get; set; }
#endregion Core
@@ -336,8 +336,8 @@ public class OptionSettingViewModel : MyReactiveObject
_config.CoreBasicItem.DefUserAgent = defUserAgent;
_config.Mux4SboxItem.Protocol = mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = hyUpMbps;
_config.HysteriaItem.DownMbps = hyDownMbps;
_config.HysteriaItem.UpMbps = hyUpMbps ?? 0;
_config.HysteriaItem.DownMbps = hyDownMbps ?? 0;
_config.CoreBasicItem.EnableFragment = enableFragment;
_config.GuiItem.AutoRun = AutoRun;

View File

@@ -255,19 +255,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
{
return null;
}
var lst = new List<ProfileItem>();
foreach (var sp in SelectedProfiles)
{
if (string.IsNullOrEmpty(sp?.IndexId))
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(sp.IndexId);
if (item != null)
{
lst.Add(item);
}
}
var lst = await AppManager.Instance.GetProfileItemsOrderedByIndexIds(SelectedProfiles.Select(sp => sp?.IndexId));
if (lst.Count == 0)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);

View File

@@ -8,6 +8,7 @@ public class ProfilesViewModel : MyReactiveObject
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
private SpeedtestService? _speedtestService;
private string? _pendingSelectIndexId;
#endregion private prop
@@ -43,13 +44,8 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> CopyServerCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; }
public ReactiveCommand<Unit, Unit> ShareServerCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRandomCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRoundRobinCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastLoadCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayFallbackCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxFallbackCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupAllServerCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupRegionServerCmd { get; }
//servers move
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
@@ -134,33 +130,13 @@ public class ProfilesViewModel : MyReactiveObject
{
await ShareServerAsync();
}, canEditRemove);
GenGroupMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () =>
GenGroupAllServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Random);
await GenGroupAllServer();
}, canEditRemove);
GenGroupMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () =>
GenGroupRegionServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin);
}, canEditRemove);
GenGroupMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing);
}, canEditRemove);
GenGroupMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad);
}, canEditRemove);
GenGroupMultipleServerXrayFallbackCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Fallback);
}, canEditRemove);
GenGroupMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing);
}, canEditRemove);
GenGroupMultipleServerSingBoxFallbackCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.Fallback);
await GenGroupRegionServer();
}, canEditRemove);
//servers move
@@ -392,15 +368,14 @@ public class ProfilesViewModel : MyReactiveObject
ProfileItems.AddRange(lstModel);
if (lstModel.Count > 0)
{
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
if (selected != null)
ProfileItemModel? selected = null;
if (!_pendingSelectIndexId.IsNullOrEmpty())
{
SelectedProfile = selected;
}
else
{
SelectedProfile = lstModel.First();
selected = lstModel.FirstOrDefault(t => t.IndexId == _pendingSelectIndexId);
_pendingSelectIndexId = null;
}
selected ??= lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
SelectedProfile = selected ?? lstModel.First();
}
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
@@ -481,14 +456,7 @@ public class ProfilesViewModel : MyReactiveObject
var orderProfiles = SelectedProfiles?.OrderBy(t => t.Sort);
if (latest)
{
foreach (var profile in orderProfiles)
{
var item = await AppManager.Instance.GetProfileItem(profile.IndexId);
if (item is not null)
{
lstSelected.Add(item);
}
}
lstSelected.AddRange(await AppManager.Instance.GetProfileItemsOrderedByIndexIds(orderProfiles.Select(sp => sp?.IndexId)));
}
else
{
@@ -641,29 +609,29 @@ public class ProfilesViewModel : MyReactiveObject
await _updateView?.Invoke(EViewAction.ShareServer, url);
}
private async Task GenGroupMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad)
private async Task GenGroupAllServer()
{
var lstSelected = await GetProfileItems(true);
if (lstSelected == null)
{
return;
}
var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id);
var ret = await ConfigHandler.AddGroupAllServer(_config, SelectedSub);
if (ret.Success != true)
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
if (ret?.Data?.ToString() == _config.IndexId)
_pendingSelectIndexId = ret.Data?.ToString();
await RefreshServers();
}
private async Task GenGroupRegionServer()
{
var ret = await ConfigHandler.AddGroupRegionServer(_config, SelectedSub);
if (ret.Success != true)
{
await RefreshServers();
Reload();
}
else
{
await SetDefaultServer(ret?.Data?.ToString());
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
var indexIdList = ret.Data as List<string>;
_pendingSelectIndexId = indexIdList?.FirstOrDefault();
await RefreshServers();
}
public async Task SortServer(string colName)
@@ -788,20 +756,14 @@ public class ProfilesViewModel : MyReactiveObject
return;
}
var msgs = await ActionPrecheckManager.Instance.Check(item);
if (msgs.Count > 0)
var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item);
if (NoticeManager.Instance.NotifyValidatorResult(validatorResult) && !validatorResult.Success)
{
foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return;
}
if (blClipboard)
{
var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item);
var result = await CoreConfigHandler.GenerateClientConfig(context, null);
if (result.Success != true)
{
@@ -825,7 +787,11 @@ public class ProfilesViewModel : MyReactiveObject
{
return;
}
var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item);
var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item);
if (NoticeManager.Instance.NotifyValidatorResult(validatorResult) && !validatorResult.Success)
{
return;
}
var result = await CoreConfigHandler.GenerateClientConfig(context, fileName);
if (result.Success != true)
{

View File

@@ -95,7 +95,7 @@
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}">
<Grid
Margin="{StaticResource Margin8}"
ColumnDefinitions="300,Auto,Auto"
ColumnDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto">
<TextBlock
@@ -108,7 +108,7 @@
x:Name="cmbSubChildItems"
Grid.Row="0"
Grid.Column="1"
Width="200"
Width="600"
Margin="{StaticResource Margin4}"
DisplayMemberBinding="{Binding Remarks}"
ItemsSource="{Binding SubItems}" />
@@ -117,7 +117,8 @@
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPolicyGroupSubChildTip}" />
Text="{x:Static resx:ResUI.TbPolicyGroupSubChildTip}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="1"
@@ -125,12 +126,14 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvFilter}" />
<TextBox
x:Name="txtFilter"
<ComboBox
x:Name="cmbFilter"
Grid.Row="1"
Grid.Column="1"
Width="600"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center" />
VerticalAlignment="Center"
IsEditable="True" />
</Grid>
</TabItem>
@@ -184,11 +187,11 @@
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Width="200"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Width="200"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn
@@ -226,11 +229,11 @@
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Width="200"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Width="200"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn

View File

@@ -29,6 +29,7 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
ResUI.TbRoundRobin,
ResUI.TbLeastLoad,
};
cmbFilter.ItemsSource = Global.PolicyGroupDefaultFilterList;
switch (profileItem.ConfigType)
{
@@ -53,7 +54,7 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
@@ -147,14 +148,7 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
private async void MenuAddChild_Click(object? sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
if (ViewModel?.SelectedSource?.ConfigType == EConfigType.PolicyGroup)
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
}
else
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
}
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
selectWindow.AllowMultiSelect(true);
var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true)

View File

@@ -74,23 +74,28 @@
<DataGridTextColumn
Width="300"
Binding="{Binding Host}"
Header="{x:Static resx:ResUI.TbSortingHost}" />
Header="{x:Static resx:ResUI.TbSortingHost}"
Tag="Host" />
<DataGridTextColumn
Width="500"
Binding="{Binding Chain}"
Header="{x:Static resx:ResUI.TbSortingChain}" />
Header="{x:Static resx:ResUI.TbSortingChain}"
Tag="Chain" />
<DataGridTextColumn
Width="80"
Binding="{Binding Network}"
Header="{x:Static resx:ResUI.TbSortingNetwork}" />
Header="{x:Static resx:ResUI.TbSortingNetwork}"
Tag="Network" />
<DataGridTextColumn
Width="160"
Binding="{Binding Type}"
Header="{x:Static resx:ResUI.TbSortingType}" />
Header="{x:Static resx:ResUI.TbSortingType}"
Tag="Type" />
<DataGridTextColumn
Width="100"
Binding="{Binding Elapsed}"
Header="{x:Static resx:ResUI.TbSortingTime}" />
Header="{x:Static resx:ResUI.TbSortingTime}"
Tag="Elapsed" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>

View File

@@ -2,9 +2,15 @@ namespace v2rayN.Desktop.Views;
public partial class ClashConnectionsView : ReactiveUserControl<ClashConnectionsViewModel>
{
private static Config _config;
private static readonly string _tag = "ClashConnectionsView";
public ClashConnectionsView()
{
InitializeComponent();
_config = AppManager.Instance.Config;
ViewModel = new ClashConnectionsViewModel(UpdateViewHandler);
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
@@ -19,7 +25,15 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
this.Bind(ViewModel, vm => vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
AppEvents.AppExitRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => StorageUI())
.DisposeWith(disposables);
});
RestoreUI();
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@@ -51,4 +65,74 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
{
ViewModel?.ClashConnectionClose(false);
}
#region UI
private void RestoreUI()
{
try
{
var lvColumnItem = _config.ClashUIItem?.ConnectionsColumnItem?.OrderBy(t => t.Index).ToList();
if (lvColumnItem == null)
{
return;
}
var displayIndex = 0;
foreach (var item in lvColumnItem)
{
foreach (var item2 in lstConnections.Columns)
{
if (item2.Tag == null)
{
continue;
}
if (item2.Tag.Equals(item.Name))
{
if (item.Width < 0)
{
item2.IsVisible = false;
}
else
{
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
item2.DisplayIndex = displayIndex++;
}
}
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private void StorageUI()
{
try
{
List<ColumnItem> lvColumnItem = new();
foreach (var item2 in lstConnections.Columns)
{
if (item2.Tag == null)
{
continue;
}
lvColumnItem.Add(new()
{
Name = (string)item2.Tag,
Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1),
Index = item2.DisplayIndex
});
}
_config.ClashUIItem.ConnectionsColumnItem = lvColumnItem;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
#endregion UI
}

View File

@@ -90,17 +90,17 @@
Header="{x:Static resx:ResUI.LvServiceType}"
Tag="ConfigType" />
<DataGridTextColumn
Width="120"
Width="200"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}"
Tag="Remarks" />
<DataGridTextColumn
Width="120"
Width="200"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}"
Tag="Address" />
<DataGridTextColumn
Width="60"
Width="100"
Binding="{Binding Port}"
Header="{x:Static resx:ResUI.LvPort}"
Tag="Port" />

View File

@@ -193,14 +193,9 @@
<MenuItem x:Name="menuExport2ShareUrlBase64" Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
</MenuItem>
<Separator />
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupMultipleServer}">
<MenuItem x:Name="menuGenGroupMultipleServerXrayRandom" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRandom}" />
<MenuItem x:Name="menuGenGroupMultipleServerXrayRoundRobin" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRoundRobin}" />
<MenuItem x:Name="menuGenGroupMultipleServerXrayLeastPing" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastPing}" />
<MenuItem x:Name="menuGenGroupMultipleServerXrayLeastLoad" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastLoad}" />
<Separator />
<MenuItem x:Name="menuGenGroupMultipleServerSingBoxLeastPing" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxLeastPing}" />
<MenuItem x:Name="menuGenGroupMultipleServerSingBoxFallback" Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxFallback}" />
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
<MenuItem x:Name="menuGenGroupAllServer" Header="{x:Static resx:ResUI.menuAllServers}" />
<MenuItem x:Name="menuGenGroupRegionServer" Header="{x:Static resx:ResUI.menuGenRegionGroup}" />
</MenuItem>
</ContextMenu>
</DataGrid.ContextMenu>

View File

@@ -7,6 +7,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
{
private static Config _config;
private Window? _window;
private static readonly string _tag = "ProfilesView";
public ProfilesView()
{
@@ -60,12 +61,8 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupRegionServerCmd, v => v.menuGenGroupRegionServer).DisposeWith(disposables);
//servers move
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables);
@@ -385,7 +382,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
}
catch (Exception ex)
{
Logging.SaveLog("ProfilesView", ex);
Logging.SaveLog(_tag, ex);
}
}
@@ -403,53 +400,67 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
private void RestoreUI()
{
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
var displayIndex = 0;
foreach (var item in lvColumnItem)
try
{
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
var displayIndex = 0;
foreach (var item in lvColumnItem)
{
foreach (var item2 in lstProfiles.Columns)
{
if (item2.Tag == null)
{
continue;
}
if (item2.Tag.Equals(item.Name))
{
if (item.Width < 0)
{
item2.IsVisible = false;
}
else
{
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
item2.DisplayIndex = displayIndex++;
}
if (item.Name.ToLower().StartsWith("to"))
{
item2.IsVisible = _config.GuiItem.EnableStatistics;
}
}
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private void StorageUI()
{
try
{
List<ColumnItem> lvColumnItem = new();
foreach (var item2 in lstProfiles.Columns)
{
if (item2.Tag == null)
{
continue;
}
if (item2.Tag.Equals(item.Name))
lvColumnItem.Add(new()
{
if (item.Width < 0)
{
item2.IsVisible = false;
}
else
{
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
item2.DisplayIndex = displayIndex++;
}
if (item.Name.ToLower().StartsWith("to"))
{
item2.IsVisible = _config.GuiItem.EnableStatistics;
}
}
Name = (string)item2.Tag,
Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1),
Index = item2.DisplayIndex
});
}
_config.UiItem.MainColumnItem = lvColumnItem;
}
}
private void StorageUI()
{
List<ColumnItem> lvColumnItem = new();
foreach (var item2 in lstProfiles.Columns)
catch (Exception ex)
{
if (item2.Tag == null)
{
continue;
}
lvColumnItem.Add(new()
{
Name = (string)item2.Tag,
Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1),
Index = item2.DisplayIndex
});
Logging.SaveLog(_tag, ex);
}
_config.UiItem.MainColumnItem = lvColumnItem;
}
#endregion UI

View File

@@ -59,7 +59,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true)
{
@@ -74,7 +74,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
private async void BtnSelectNextProfile_Click(object? sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true)
{

View File

@@ -147,7 +147,7 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
@@ -163,7 +163,7 @@
x:Name="cmbSubChildItems"
Grid.Row="0"
Grid.Column="1"
Width="200"
Width="600"
Margin="{StaticResource Margin4}"
DisplayMemberPath="Remarks"
Style="{StaticResource DefComboBox}" />
@@ -173,7 +173,8 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbPolicyGroupSubChildTip}" />
Text="{x:Static resx:ResUI.TbPolicyGroupSubChildTip}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="1"
@@ -182,14 +183,15 @@
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.LvFilter}" />
<TextBox
x:Name="txtFilter"
<ComboBox
x:Name="cmbFilter"
Grid.Row="1"
Grid.Column="1"
Width="600"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
AcceptsReturn="True"
Style="{StaticResource DefTextBox}" />
IsEditable="True"
Style="{StaticResource DefComboBox}" />
</Grid>
</TabItem>
@@ -251,11 +253,11 @@
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Width="200"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Width="200"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn
@@ -292,11 +294,11 @@
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Width="200"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Width="200"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn

View File

@@ -24,6 +24,7 @@ public partial class AddGroupServerWindow
ResUI.TbRoundRobin,
ResUI.TbLeastLoad,
};
cmbFilter.ItemsSource = Global.PolicyGroupDefaultFilterList;
switch (profileItem.ConfigType)
{
@@ -48,7 +49,7 @@ public partial class AddGroupServerWindow
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.Text).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
@@ -127,14 +128,7 @@ public partial class AddGroupServerWindow
private async void MenuAddChild_Click(object sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
if (ViewModel?.SelectedSource?.ConfigType == EConfigType.PolicyGroup)
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom }, exclude: true);
}
else
{
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
}
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
selectWindow.AllowMultiSelect(true);
if (selectWindow.ShowDialog() == true)
{

View File

@@ -2,6 +2,7 @@
x:Class="v2rayN.Views.ClashConnectionsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:v2rayN.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -78,25 +79,30 @@
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn
<base:MyDGTextColumn
Width="300"
Binding="{Binding Host}"
ExName="Host"
Header="{x:Static resx:ResUI.TbSortingHost}" />
<DataGridTextColumn
<base:MyDGTextColumn
Width="500"
Binding="{Binding Chain}"
ExName="Chain"
Header="{x:Static resx:ResUI.TbSortingChain}" />
<DataGridTextColumn
<base:MyDGTextColumn
Width="80"
Binding="{Binding Network}"
ExName="Network"
Header="{x:Static resx:ResUI.TbSortingNetwork}" />
<DataGridTextColumn
<base:MyDGTextColumn
Width="160"
Binding="{Binding Type}"
ExName="Type"
Header="{x:Static resx:ResUI.TbSortingType}" />
<DataGridTextColumn
<base:MyDGTextColumn
Width="100"
Binding="{Binding Elapsed}"
ExName="Elapsed"
Header="{x:Static resx:ResUI.TbSortingTime}" />
</DataGrid.Columns>
</DataGrid>

View File

@@ -1,4 +1,5 @@
using System.Windows.Controls;
using v2rayN.Base;
namespace v2rayN.Views;
@@ -7,9 +8,14 @@ namespace v2rayN.Views;
/// </summary>
public partial class ClashConnectionsView
{
private static Config _config;
private static readonly string _tag = "ClashConnectionsView";
public ClashConnectionsView()
{
InitializeComponent();
_config = AppManager.Instance.Config;
ViewModel = new ClashConnectionsViewModel(UpdateViewHandler);
btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click;
@@ -24,7 +30,15 @@ public partial class ClashConnectionsView
this.Bind(ViewModel, vm => vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
AppEvents.AppExitRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => StorageUI())
.DisposeWith(disposables);
});
RestoreUI();
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@@ -48,7 +62,7 @@ public partial class ClashConnectionsView
}
catch (Exception ex)
{
Logging.SaveLog("ClashConnectionsView", ex);
Logging.SaveLog(_tag, ex);
}
}
@@ -56,4 +70,71 @@ public partial class ClashConnectionsView
{
ViewModel?.ClashConnectionClose(false);
}
#region UI
private void RestoreUI()
{
try
{
var lvColumnItem = _config.ClashUIItem?.ConnectionsColumnItem?.OrderBy(t => t.Index).ToList();
if (lvColumnItem == null)
{
return;
}
var displayIndex = 0;
foreach (var item in lvColumnItem)
{
foreach (var col in lstConnections.Columns.Cast<MyDGTextColumn>())
{
if (col.ExName == item.Name)
{
if (item.Width > 0)
{
col.Width = item.Width;
}
col.DisplayIndex = displayIndex++;
break;
}
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private void StorageUI()
{
try
{
List<ColumnItem> lvColumnItem = new();
foreach (var col in lstConnections.Columns.Cast<MyDGTextColumn>())
{
var name = col.ExName;
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
lvColumnItem.Add(new()
{
Name = name,
Width = (int)col.ActualWidth,
Index = col.DisplayIndex
});
}
_config.ClashUIItem.ConnectionsColumnItem = lvColumnItem;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
#endregion UI
}

View File

@@ -34,8 +34,8 @@ public partial class DNSSettingWindow
this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Strategy4Freedom, v => v.cmbDirectDNSStrategy.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Strategy4Proxy, v => v.cmbRemoteDNSStrategy.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Strategy4Freedom, v => v.cmbDirectDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Strategy4Proxy, v => v.cmbRemoteDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ParallelQuery, v => v.togParallelQuery.IsChecked).DisposeWith(disposables);

View File

@@ -119,17 +119,17 @@
ExName="ConfigType"
Header="{x:Static resx:ResUI.LvServiceType}" />
<base:MyDGTextColumn
Width="150"
Width="200"
Binding="{Binding Remarks}"
ExName="Remarks"
Header="{x:Static resx:ResUI.LvRemarks}" />
<base:MyDGTextColumn
Width="120"
Width="200"
Binding="{Binding Address}"
ExName="Address"
Header="{x:Static resx:ResUI.LvAddress}" />
<base:MyDGTextColumn
Width="60"
Width="100"
Binding="{Binding Port}"
ExName="Port"
Header="{x:Static resx:ResUI.LvPort}" />

View File

@@ -97,8 +97,7 @@ public partial class ProfilesSelectWindow
private void LstProfiles_ColumnHeader_Click(object sender, RoutedEventArgs e)
{
var colHeader = sender as DataGridColumnHeader;
if (colHeader == null || colHeader.TabIndex < 0 || colHeader.Column == null)
if (sender is not DataGridColumnHeader colHeader || colHeader.TabIndex < 0 || colHeader.Column == null)
{
return;
}

View File

@@ -240,32 +240,15 @@
Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
</MenuItem>
<Separator />
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupMultipleServer}">
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
<MenuItem
x:Name="menuGenGroupMultipleServerXrayRandom"
x:Name="menuGenGroupAllServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRandom}" />
Header="{x:Static resx:ResUI.menuAllServers}" />
<MenuItem
x:Name="menuGenGroupMultipleServerXrayRoundRobin"
x:Name="menuGenGroupRegionServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayRoundRobin}" />
<MenuItem
x:Name="menuGenGroupMultipleServerXrayLeastPing"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastPing}" />
<MenuItem
x:Name="menuGenGroupMultipleServerXrayLeastLoad"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenGroupMultipleServerXrayLeastLoad}" />
<Separator />
<MenuItem
x:Name="menuGenGroupMultipleServerSingBoxLeastPing"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxLeastPing}" />
<MenuItem
x:Name="menuGenGroupMultipleServerSingBoxFallback"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenGroupMultipleServerSingBoxFallback}" />
Header="{x:Static resx:ResUI.menuGenRegionGroup}" />
</MenuItem>
</ContextMenu>
</DataGrid.ContextMenu>

View File

@@ -10,6 +10,7 @@ namespace v2rayN.Views;
public partial class ProfilesView
{
private static Config _config;
private static readonly string _tag = "ProfilesView";
public ProfilesView()
{
@@ -54,12 +55,8 @@ public partial class ProfilesView
this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupRegionServerCmd, v => v.menuGenGroupRegionServer).DisposeWith(disposables);
//servers move
this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables);
@@ -236,8 +233,7 @@ public partial class ProfilesView
private void LstProfiles_ColumnHeader_Click(object sender, RoutedEventArgs e)
{
var colHeader = sender as DataGridColumnHeader;
if (colHeader == null || colHeader.TabIndex < 0 || colHeader.Column == null)
if (sender is not DataGridColumnHeader colHeader || colHeader.TabIndex < 0 || colHeader.Column == null)
{
return;
}
@@ -343,7 +339,7 @@ public partial class ProfilesView
}
catch (Exception ex)
{
Logging.SaveLog("ProfilesView", ex);
Logging.SaveLog(_tag, ex);
}
}
@@ -361,46 +357,59 @@ public partial class ProfilesView
private void RestoreUI()
{
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
var displayIndex = 0;
foreach (var item in lvColumnItem)
try
{
foreach (MyDGTextColumn item2 in lstProfiles.Columns)
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
var displayIndex = 0;
foreach (var item in lvColumnItem)
{
if (item2.ExName == item.Name)
foreach (var item2 in lstProfiles.Columns.Cast<MyDGTextColumn>())
{
if (item.Width < 0)
if (item2.ExName == item.Name)
{
item2.Visibility = Visibility.Hidden;
}
else
{
item2.Width = item.Width;
item2.DisplayIndex = displayIndex++;
}
if (item.Name.ToLower().StartsWith("to"))
{
item2.Visibility = _config.GuiItem.EnableStatistics ? Visibility.Visible : Visibility.Hidden;
if (item.Width < 0)
{
item2.Visibility = Visibility.Hidden;
}
else
{
item2.Width = item.Width;
item2.DisplayIndex = displayIndex++;
}
if (item.Name.ToLower().StartsWith("to"))
{
item2.Visibility = _config.GuiItem.EnableStatistics ? Visibility.Visible : Visibility.Hidden;
}
}
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private void StorageUI()
{
List<ColumnItem> lvColumnItem = new();
foreach (var t in lstProfiles.Columns)
try
{
var item2 = (MyDGTextColumn)t;
lvColumnItem.Add(new()
List<ColumnItem> lvColumnItem = new();
foreach (var item2 in lstProfiles.Columns.Cast<MyDGTextColumn>())
{
Name = item2.ExName,
Width = (int)(item2.Visibility == Visibility.Visible ? item2.ActualWidth : -1),
Index = item2.DisplayIndex
});
lvColumnItem.Add(new()
{
Name = item2.ExName,
Width = (int)(item2.Visibility == Visibility.Visible ? item2.ActualWidth : -1),
Index = item2.DisplayIndex
});
}
_config.UiItem.MainColumnItem = lvColumnItem;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
_config.UiItem.MainColumnItem = lvColumnItem;
}
#endregion UI
@@ -409,7 +418,7 @@ public partial class ProfilesView
private Point startPoint = new();
private int startIndex = -1;
private string formatData = "ProfileItemModel";
private readonly string formatData = "ProfileItemModel";
/// <summary>
/// Helper to search up the VisualTree

View File

@@ -53,7 +53,7 @@ public partial class SubEditWindow
private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
if (selectWindow.ShowDialog() == true)
{
var profile = await selectWindow.ProfileItem;
@@ -67,7 +67,7 @@ public partial class SubEditWindow
private async void BtnSelectNextProfile_Click(object sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter(new[] { EConfigType.Custom, EConfigType.PolicyGroup, EConfigType.ProxyChain }, exclude: true);
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
if (selectWindow.ShowDialog() == true)
{
var profile = await selectWindow.ProfileItem;