Compare commits

...

6 Commits

Author SHA1 Message Date
2dust
5fbcc46013 up 7.19.5 2026-03-20 17:59:19 +08:00
2dust
90f7b8b751 Update Directory.Packages.props 2026-03-20 16:45:55 +08:00
DHR60
dd94199bbb Json editor with syntax highlighting (#8932)
* Add json editor

* Replace JsonEditor with TextBox

* Replace JsonEditor with TextBox

* Remove TextMateSharp.Grammars

* Fix two way bind

* Update ResUI.ru.resx

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-03-20 16:32:02 +08:00
Miheichev Aleksandr Sergeevich
0cec5986cd i18n(ru): translate 68 untranslated strings in Russian localization (#8931)
Complete the Russian (ru) localization by translating all remaining
English strings in ResUI.ru.resx. Categories include:
- Policy Group and Proxy Chain UI elements
- Routing and DNS settings/tips
- Certificate Pinning UI
- Core error and warning messages
- Server list and menu items
- System proxy and miscellaneous settings

5 universal technical abbreviations (LAN, UUID, User-Agent, MTU, TLS)
are intentionally kept as-is per standard Russian IT terminology.
2026-03-20 16:19:37 +08:00
JieXu
a2929c6086 Update package-debian.sh (#8941)
* Update package-debian.sh

* Update build-linux.yml

* Update package-debian.sh

* Update package-debian.sh

* Update package-debian.sh

* Update package-rhel.sh

* Update package-debian.sh

* Update package-debian.sh

* Update package-rhel.sh

* Update package-debian.sh

* Update package-rhel.sh

* Update package-debian.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-debian.sh

* Update package-rhel.sh
2026-03-20 14:25:42 +08:00
2dust
eb0ef90ed2 Bug fix
https://github.com/2dust/v2rayN/issues/8906
2026-03-15 20:11:45 +08:00
20 changed files with 1021 additions and 223 deletions

View File

@@ -9,9 +9,6 @@ on:
push:
branches:
- master
tags:
- 'v*'
- 'V*'
permissions:
contents: write
@@ -56,23 +53,6 @@ jobs:
path: |
${{ github.workspace }}/v2rayN/Release/linux*
# release debian package
- name: Package debian
if: github.event.inputs.release_tag != ''
run: |
chmod 755 package-debian.sh
./package-debian.sh "$OutputArch" "$OutputPath64" "${{ github.event.inputs.release_tag }}"
./package-debian.sh "$OutputArchArm" "$OutputPathArm64" "${{ github.event.inputs.release_tag }}"
- name: Upload deb to release
uses: svenstaro/upload-release-action@v2
if: github.event.inputs.release_tag != ''
with:
file: ${{ github.workspace }}/v2rayN*.deb
tag: ${{ github.event.inputs.release_tag }}
file_glob: true
prerelease: true
# release zip archive
- name: Package release zip archive
if: github.event.inputs.release_tag != ''
@@ -90,6 +70,65 @@ jobs:
file_glob: true
prerelease: true
deb:
needs: build
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
container:
image: debian:13
env:
RELEASE_TAG: ${{ github.event.inputs.release_tag != '' && github.event.inputs.release_tag || github.ref_name }}
steps:
- name: Prepare tools (Debian)
shell: bash
run: |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file \
ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev \
libc6 libgcc-s1 libstdc++6 zlib1g libicu-dev libssl-dev
- name: Checkout repo (for scripts)
uses: actions/checkout@v6.0.2
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Ensure script permissions
run: chmod 755 package-debian.sh
- name: Package DEB (Debian-family)
run: ./package-debian.sh "${RELEASE_TAG}" --arch all
- name: Collect DEBs into workspace
run: |
mkdir -p "$GITHUB_WORKSPACE/dist/deb"
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb/" || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_amd64.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-64.deb" \; || true
find "$GITHUB_WORKSPACE/dist/deb" -name "v2rayn_*_arm64.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb/v2rayN-linux-arm64.deb" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb" || true
- name: Upload DEB artifacts
uses: actions/upload-artifact@v7.0.0
with:
name: v2rayN-deb
path: dist/deb/**/*.deb
- name: Upload DEBs to release
uses: svenstaro/upload-release-action@v2
with:
file: dist/deb/**/*.deb
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true
rpm:
needs: build
if: |

View File

@@ -1,70 +1,607 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
Arch="$1"
OutputPath="$2"
Version="$3"
# Require Debian base branch
. /etc/os-release
FileName="v2rayN-${Arch}.zip"
wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/$FileName"
7z x $FileName
cp -rf v2rayN-${Arch}/* $OutputPath
case "${ID:-}" in
debian)
echo "Detected supported system: ${NAME:-$ID} ${VERSION_ID:-}"
;;
*)
echo "Unsupported system: ${NAME:-unknown} (${ID:-unknown})."
echo "This script only supports: Debian."
exit 1
;;
esac
PackagePath="v2rayN-Package-${Arch}"
mkdir -p "${PackagePath}/DEBIAN"
mkdir -p "${PackagePath}/opt"
cp -rf $OutputPath "${PackagePath}/opt/v2rayN"
echo "When this file exists, app will not store configs under this folder" > "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt"
# Kernel version
MIN_KERNEL="6.11"
CURRENT_KERNEL="$(uname -r)"
if [ $Arch = "linux-64" ]; then
Arch2="amd64"
else
Arch2="arm64"
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
echo $Arch2
# basic
cat >"${PackagePath}/DEBIAN/control" <<-EOF
Package: v2rayN
Version: $Version
Architecture: $Arch2
Maintainer: https://github.com/2dust/v2rayN
Depends: libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
echo "[OK] Kernel $CURRENT_KERNEL verified."
# 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
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
# If the first argument starts with --, do not treat it as a version number
if [[ "${VERSION_ARG:-}" == --* ]]; then
VERSION_ARG=""
fi
# Take the first non --* argument as version, discard it
if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi
# Parse remaining optional arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--with-core) WITH_CORE="${2:-both}"; shift 2;;
--xray-ver) XRAY_VER="${2:-}"; shift 2;;
--singbox-ver) SING_VER="${2:-}"; shift 2;;
--netcore) FORCE_NETCORE=1; shift;;
--arch) ARCH_OVERRIDE="${2:-}"; shift 2;;
--buildfrom) BUILD_FROM="${2:-}"; shift 2;;
*)
if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi
shift;;
esac
done
# Conflict: version number AND --buildfrom cannot be used together
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
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
# 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
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get -y install \
curl unzip tar jq rsync ca-certificates git dpkg-dev fakeroot file \
desktop-file-utils xdg-utils wget
if [[ "$host_arch" == "aarch64" ]]; then
sudo dpkg --add-architecture amd64 || true
sudo apt-get update
sudo apt-get -y install \
libc6:amd64 libgcc-s1:amd64 libstdc++6:amd64 zlib1g:amd64 libfontconfig1:amd64
elif [[ "$host_arch" == "x86_64" ]]; then
sudo dpkg --add-architecture arm64 || true
sudo apt-get update
sudo apt-get -y install \
libc6:arm64 libgcc-s1:arm64 libstdc++6:arm64 zlib1g:arm64 libfontconfig1:arm64
fi
# Install .NET SDK 8 via official script
wget -q https://dot.net/v1/dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --channel 8.0 --install-dir "$HOME/.dotnet"
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
dotnet --info >/dev/null 2>&1 && install_ok=1
fi
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, git, dpkg-deb, desktop-file-utils, xdg-utils"
exit 1
fi
# Root directory
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Git submodules (best effort)
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
# Locate project
PROJECT="v2rayN.Desktop/v2rayN.Desktop.csproj"
if [[ ! -f "$PROJECT" ]]; then
PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
fi
[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; }
choose_channel() {
# If --buildfrom provided, map it directly and skip interaction.
if [[ -n "${BUILD_FROM:-}" ]]; then
case "$BUILD_FROM" in
1) echo "latest"; return 0;;
2) echo "prerelease"; return 0;;
3) echo "keep"; return 0;;
*) echo "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." >&2; exit 1;;
esac
fi
# 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" ;;
esac
fi
fi
echo "$ch"
}
get_latest_tag_latest() {
curl -fsSL "https://api.github.com/repos/2dust/v2rayN/releases/latest" \
| jq -re '.tag_name' \
| sed 's/^v//'
}
get_latest_tag_prerelease() {
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() {
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/${want}" >/dev/null 2>&1; then
ref="${want}"
fi
if [[ -n "$ref" ]]; then
echo "[OK] Found ref '${ref}', checking out..."
git checkout -f "${ref}"
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
return 0
fi
fi
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
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)"
apply_channel_or_keep "$ch"
fi
else
ch="$(choose_channel)"
apply_channel_or_keep "$ch"
fi
else
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}"
download_xray() {
local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir"
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
fi
[[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; }
if [[ "$rid" == "linux-arm64" ]]; then
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip"
else
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip"
fi
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/$zipname"
unzip -q "$tmp/$zipname" -d "$tmp"
install -m 755 "$tmp/xray" "$outdir/xray"
rm -rf "$tmp"
}
download_singbox() {
local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir"
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
fi
[[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; }
if [[ "$rid" == "linux-arm64" ]]; then
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz"
else
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz"
fi
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/$tarname"
tar -C "$tmp" -xzf "$tmp/$tarname"
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; rm -rf "$tmp"; return 1; }
install -m 755 "$bin" "$outdir/sing-box"
rm -rf "$tmp"
}
unify_geo_layout() {
local outroot="$1"
mkdir -p "$outroot/bin"
local names=(
"geosite.dat"
"geoip.dat"
"geoip-only-cn-private.dat"
"Country.mmdb"
"geoip.metadb"
)
for n in "${names[@]}"; do
if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
done
}
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
local srss_dir="$bin_dir/srss"
mkdir -p "$bin_dir" "$srss_dir"
echo "[+] Download Xray Geo to ${bin_dir}"
curl -fsSL -o "$bin_dir/geosite.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat"
curl -fsSL -o "$bin_dir/geoip.dat" \
"https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat"
curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat"
curl -fsSL -o "$bin_dir/Country.mmdb" \
"https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
echo "[+] Download sing-box rule DB & rule-sets"
curl -fsSL -o "$bin_dir/geoip.metadb" \
"https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" || true
for f in \
geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs \
geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geoip/$f" || true
done
for f in \
geosite-cn.srs geosite-gfw.srs geosite-google.srs geosite-greatfire.srs \
geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do
curl -fsSL -o "$srss_dir/$f" \
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done
unify_geo_layout "$outroot"
}
download_v2rayn_bundle() {
local outroot="$1" rid="$2"
local url=""
if [[ "$rid" == "linux-arm64" ]]; then
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64.zip"
else
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip"
fi
echo "[+] Try v2rayN bundle archive: $url"
local tmp zipname
tmp="$(mktemp -d)"; zipname="$tmp/v2rayn.zip"
curl -fL "$url" -o "$zipname" || { echo "[!] Bundle download failed"; return 1; }
unzip -q "$zipname" -d "$tmp" || { echo "[!] Bundle unzip failed"; return 1; }
if [[ -d "$tmp/bin" ]]; then
mkdir -p "$outroot/bin"
rsync -a "$tmp/bin/" "$outroot/bin/"
else
rsync -a "$tmp/" "$outroot/"
fi
rm -f "$outroot/v2rayn.zip" 2>/dev/null || true
find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true
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
mkdir -p "$outroot/bin"
rsync -a "$nested_dir/bin/" "$outroot/bin/"
rm -rf "$nested_dir"
fi
# Unify to bin/
unify_geo_layout "$outroot"
echo "[+] Bundle extracted to $outroot"
}
BUILT_DEBS=()
BUILT_ALL=0
OUTPUT_DIR="$HOME/debbuild"
mkdir -p "$OUTPUT_DIR"
build_for_arch() {
local short="$1"
local rid deb_arch outdir_name
case "$short" in
x64) rid="linux-x64"; deb_arch="amd64"; outdir_name="amd64" ;;
arm64) rid="linux-arm64"; deb_arch="arm64"; outdir_name="arm64" ;;
*) echo "Unknown arch '$short' (use x64|arm64)"; return 1 ;;
esac
echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)"
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" \
-c Release -r "$rid" \
-p:PublishSingleFile=false \
-p:SelfContained=true
local RID_DIR="$rid"
local PUBDIR
PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish"
[[ -d "$PUBDIR" ]] || { echo "Publish directory not found: $PUBDIR"; return 1; }
local WORKDIR PKGROOT STAGE DEBIAN_DIR
WORKDIR="$(mktemp -d)"
PKGROOT="v2rayN-publish"
STAGE="$WORKDIR/${PKGROOT}_${VERSION}_${deb_arch}"
DEBIAN_DIR="$STAGE/DEBIAN"
mkdir -p "$STAGE/opt/v2rayN"
mkdir -p "$STAGE/usr/bin"
mkdir -p "$STAGE/usr/share/applications"
mkdir -p "$STAGE/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$DEBIAN_DIR"
# Stage publish content from source build
cp -a "$PUBDIR/." "$STAGE/opt/v2rayN/"
local ICON_CANDIDATE
PROJECT_DIR="$(cd "$(dirname "$PROJECT")" && pwd)"
ICON_CANDIDATE="$PROJECT_DIR/v2rayN.png"
[[ -f "$ICON_CANDIDATE" ]] && cp "$ICON_CANDIDATE" "$STAGE/usr/share/icons/hicolor/256x256/apps/v2rayn.png" || true
mkdir -p "$STAGE/opt/v2rayN/bin/xray" "$STAGE/opt/v2rayN/bin/sing_box"
fetch_separate_cores_and_rules() {
local outroot="$1"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" "$RID_DIR" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" "$RID_DIR" || 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 "$STAGE/opt/v2rayN" "$RID_DIR"; then
echo "[*] Using v2rayN bundle bin assets."
else
echo "[*] Bundle failed, fallback to separate core + rules."
fetch_separate_cores_and_rules "$STAGE/opt/v2rayN"
fi
else
echo "[*] --netcore specified: use separate core + rules."
fetch_separate_cores_and_rules "$STAGE/opt/v2rayN"
fi
# Wrapper
install -m 755 /dev/stdin "$STAGE/usr/bin/v2rayn" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR="/opt/v2rayN"
cd "$DIR"
if [[ -x "$DIR/v2rayN" ]]; then
exec "$DIR/v2rayN" "$@"
fi
for dll in v2rayN.Desktop.dll v2rayN.dll; do
if [[ -f "$DIR/$dll" ]]; then
exec /usr/bin/dotnet "$DIR/$dll" "$@"
fi
done
echo "v2rayN launcher: no executable found in $DIR" >&2
ls -l "$DIR" >&2 || true
exit 1
EOF
mkdir -p "${PackagePath}/usr/share/applications"
cat >"${PackagePath}/usr/share/applications/v2rayN.desktop" <<-EOF
SHLIBS_DEPENDS=""
EXTRA_DEPENDS="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)"
mkdir -p "$WORKDIR/debian"
cat > "$WORKDIR/debian/control" <<EOF
Source: v2rayn
Section: net
Priority: optional
Maintainer: 2dust <noreply@github.com>
Standards-Version: 4.7.0
Package: v2rayn
Architecture: ${deb_arch}
Description: v2rayN
EOF
local SYS_LIBDIR=""
local SYS_USRLIBDIR=""
multiarch="$(dpkg-architecture -a"$deb_arch" -qDEB_HOST_MULTIARCH)"
SYS_LIBDIR="/lib/$multiarch"
SYS_USRLIBDIR="/usr/lib/$multiarch"
: > "$DEBIAN_DIR/substvars"
mapfile -t ELF_FILES < <(
find "$STAGE/opt/v2rayN" -type f \( -name "*.so*" -o -perm -111 \) ! -name 'libcoreclrtraceptprovider.so'
)
if [[ "${#ELF_FILES[@]}" -gt 0 ]]; then
(
cd "$WORKDIR"
dpkg-shlibdeps \
-l"$STAGE/opt/v2rayN" \
-l"$SYS_LIBDIR" \
-l"$SYS_USRLIBDIR" \
-T"$DEBIAN_DIR/substvars" \
"${ELF_FILES[@]}"
) >/dev/null 2>&1 || true
fi
SHLIBS_DEPENDS="$(sed -n 's/^shlibs:Depends=//p' "$DEBIAN_DIR/substvars" | head -n1 || true)"
if [[ -n "$SHLIBS_DEPENDS" ]]; then
SHLIBS_DEPENDS="$(echo "$SHLIBS_DEPENDS" \
| sed -E 's/ *\([^)]*\)//g' \
| sed -E 's/ *, */, /g' \
| sed -E 's/^, *//; s/, *$//')"
FINAL_DEPENDS="${SHLIBS_DEPENDS}, ${EXTRA_DEPENDS}"
else
FINAL_DEPENDS="${EXTRA_DEPENDS}"
fi
# Desktop file
install -m 644 /dev/stdin "$STAGE/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
Exec=/opt/v2rayN/v2rayN
Icon=/opt/v2rayN/v2rayN.png
Terminal=false
Type=Application
Categories=Network;Application;
Name=v2rayN
Comment=v2rayN for Debian GNU Linux
Exec=v2rayn
Icon=v2rayn
Terminal=false
Categories=Network;
EOF
cat >"${PackagePath}/DEBIAN/postinst" <<-'EOF'
# Control file
cat > "$DEBIAN_DIR/control" <<EOF
Package: v2rayn
Version: ${VERSION}
Architecture: ${deb_arch}
Maintainer: 2dust <noreply@github.com>
Homepage: https://github.com/2dust/v2rayN
Section: net
Priority: optional
Depends: ${FINAL_DEPENDS}
Description: v2rayN (Avalonia) GUI client for Linux
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 /
Shadowsocks / tuic / WireGuard.
EOF
# postinst
install -m 755 /dev/stdin "$DEBIAN_DIR/postinst" <<'EOF'
#!/bin/sh
set -e
update-desktop-database || true
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
exit 0
EOF
sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool"
# postrm
install -m 755 /dev/stdin "$DEBIAN_DIR/postrm" <<'EOF'
#!/bin/sh
set -e
update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
exit 0
EOF
# Patch
# set owner to root:root
sudo chown -R root:root "${PackagePath}"
# set all directories to 755 (readable & traversable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type d -exec chmod 755 {} +
# set all regular files to 644 (readable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type f -exec chmod 644 {} +
# ensure main binaries are 755 (executable by all users)
sudo chmod 755 "${PackagePath}/opt/v2rayN/v2rayN" 2>/dev/null || true
sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true
# Normalize permissions
find "$STAGE/opt/v2rayN" -type d -exec chmod 0755 {} +
find "$STAGE/opt/v2rayN" -type f -exec chmod 0644 {} +
[[ -f "$STAGE/opt/v2rayN/v2rayN" ]] && chmod 0755 "$STAGE/opt/v2rayN/v2rayN" || true
local deb_out
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
# build deb package
sudo dpkg-deb -Zxz --build $PackagePath
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"
dpkg-deb --root-owner-group --build "$STAGE" "$deb_out"
echo "Build done for $short. DEB at:"
echo " $deb_out"
BUILT_DEBS+=("$deb_out")
rm -rf "$WORKDIR"
}
case "${ARCH_OVERRIDE:-}" in
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
for arch in "${targets[@]}"; do
build_for_arch "$arch"
done
echo ""
echo "================ Build Summary ================="
if [[ "${#BUILT_DEBS[@]}" -gt 0 ]]; then
echo "Output directory: $OUTPUT_DIR"
for pkg in "${BUILT_DEBS[@]}"; do
echo "$pkg"
done
else
echo "No DEBs detected in summary (check build logs above)."
fi
echo "==============================================="

View File

@@ -210,46 +210,48 @@ echo "[*] GUI version resolved as: ${VERSION}"
# Helpers for core
download_xray() {
# Download Xray core
local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
local outdir="$1" rid="$2" ver="${XRAY_VER:-}" url tmp zipname="xray.zip"
mkdir -p "$outdir"
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
fi
[[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; }
if [[ "$RID_DIR" == "linux-arm64" ]]; then
if [[ "$rid" == "linux-arm64" ]]; then
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip"
else
url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip"
fi
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/$zipname"
unzip -q "$tmp/$zipname" -d "$tmp"
install -Dm755 "$tmp/xray" "$outdir/xray"
install -m 755 "$tmp/xray" "$outdir/xray"
rm -rf "$tmp"
}
download_singbox() {
# Download sing-box
local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
local outdir="$1" rid="$2" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin
mkdir -p "$outdir"
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
fi
[[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; }
if [[ "$RID_DIR" == "linux-arm64" ]]; then
if [[ "$rid" == "linux-arm64" ]]; then
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz"
else
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz"
fi
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/$tarname"
tar -C "$tmp" -xzf "$tmp/$tarname"
bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)"
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; return 1; }
install -Dm755 "$bin" "$outdir/sing-box"
[[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; rm -rf "$tmp"; return 1; }
install -m 755 "$bin" "$outdir/sing-box"
rm -rf "$tmp"
}
# Move geo files to outroot/bin
@@ -310,9 +312,9 @@ download_geo_assets() {
# Prefer the prebuilt v2rayN core bundle; then unify geo layout
download_v2rayn_bundle() {
local outroot="$1"
local outroot="$1" rid="$2"
local url=""
if [[ "$RID_DIR" == "linux-arm64" ]]; then
if [[ "$rid" == "linux-arm64" ]]; then
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64.zip"
else
url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip"
@@ -378,10 +380,7 @@ build_for_arch() {
local RID_DIR="$rid"
local PUBDIR
PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish"
[[ -d "$PUBDIR" ]]
# Make RID_DIR visible to download helpers (they read this var)
export RID_DIR
[[ -d "$PUBDIR" ]] || { echo "Publish directory not found: $PUBDIR"; return 1; }
# Per-arch working area
local PKGROOT="v2rayN-publish"
@@ -400,10 +399,12 @@ build_for_arch() {
mkdir -p "$WORKDIR/$PKGROOT"
cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/"
# Optional icon
# Required icon
local ICON_CANDIDATE
ICON_CANDIDATE="$(dirname "$PROJECT")/../v2rayN.Desktop/v2rayN.png"
[[ -f "$ICON_CANDIDATE" ]] && cp "$ICON_CANDIDATE" "$WORKDIR/$PKGROOT/v2rayn.png" || true
PROJECT_DIR="$(cd "$(dirname "$PROJECT")" && pwd)"
ICON_CANDIDATE="$PROJECT_DIR/v2rayN.png"
[[ -f "$ICON_CANDIDATE" ]] || { echo "Required icon not found: $ICON_CANDIDATE"; return 1; }
cp "$ICON_CANDIDATE" "$WORKDIR/$PKGROOT/v2rayn.png"
# Prepare bin structure
mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box"
@@ -413,16 +414,16 @@ build_for_arch() {
local outroot="$1"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" || echo "[!] xray download failed (skipped)"
download_xray "$outroot/bin/xray" "$RID_DIR" || 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)"
download_singbox "$outroot/bin/sing_box" "$RID_DIR" || 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
if download_v2rayn_bundle "$WORKDIR/$PKGROOT" "$RID_DIR"; then
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
@@ -484,9 +485,14 @@ https://github.com/2dust/v2rayN
install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/
# Normalize permissions
find %{buildroot}/opt/v2rayN -type d -exec chmod 0755 {} +
find %{buildroot}/opt/v2rayN -type f -exec chmod 0644 {} +
[ -f %{buildroot}/opt/v2rayN/v2rayN ] && chmod 0755 %{buildroot}/opt/v2rayN/v2rayN || :
# Launcher (prefer native ELF first, then DLL fallback)
install -dm0755 %{buildroot}%{_bindir}
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash
set -euo pipefail
DIR="/opt/v2rayN"
@@ -503,11 +509,10 @@ echo "v2rayN launcher: no executable found in $DIR" >&2
ls -l "$DIR" >&2 || true
exit 1
EOF
chmod 0755 %{buildroot}%{_bindir}/v2rayn
# Desktop file
install -dm0755 %{buildroot}%{_datadir}/applications
cat > %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
install -m0644 /dev/stdin %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=v2rayN
@@ -519,10 +524,8 @@ Categories=Network;
EOF
# Icon
if [ -f "%{_builddir}/__PKGROOT__/v2rayn.png" ]; then
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
fi
install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png
%post
/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.19.4</Version>
<Version>7.19.5</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -13,7 +13,7 @@
<PackageVersion Include="CliWrap" Version="3.10.0" />
<PackageVersion Include="Downloader" Version="5.1.0" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.1" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.3.1.1" />
<PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="ReactiveUI" Version="23.1.8" />

View File

@@ -924,6 +924,42 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Copy 的本地化字符串。
/// </summary>
public static string menuEditCopy {
get {
return ResourceManager.GetString("menuEditCopy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Format 的本地化字符串。
/// </summary>
public static string menuEditFormat {
get {
return ResourceManager.GetString("menuEditFormat", resourceCulture);
}
}
/// <summary>
/// 查找类似 Paste 的本地化字符串。
/// </summary>
public static string menuEditPaste {
get {
return ResourceManager.GetString("menuEditPaste", resourceCulture);
}
}
/// <summary>
/// 查找类似 Select all 的本地化字符串。
/// </summary>
public static string menuEditSelectAll {
get {
return ResourceManager.GetString("menuEditSelectAll", resourceCulture);
}
}
/// <summary>
/// 查找类似 Edit 的本地化字符串。
/// </summary>

View File

@@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>کپی</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>انتخاب همه</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1665,4 +1665,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>Copier</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Tout sélect</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>Másolás</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Összes kijelölése</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1668,4 +1668,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>Copy</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Select all</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1027,7 +1027,7 @@
<value>Протокол Mux для sing-box</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Process (Linux/Windows)</value>
<value>Процесс (Linux/Windows)</value>
</data>
<data name="TbRoutingRuleIP" xml:space="preserve">
<value>IP-адрес или сеть CIDR</value>
@@ -1393,10 +1393,10 @@
<value>Внутренний DNS</value>
</data>
<data name="TbDirectResolveStrategy" xml:space="preserve">
<value>Direct Target Resolution Strategy</value>
<value>Стратегия разрешения прямых соединений</value>
</data>
<data name="TbRemoteResolveStrategy" xml:space="preserve">
<value>Proxy Target Resolution Strategy</value>
<value>Стратегия разрешения прокси-соединений</value>
</data>
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Добавить стандартные записи hosts (DNS)</value>
@@ -1429,7 +1429,7 @@
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Block ECH and HTTP/3 availability checks when enabled</value>
<value>При включении блокирует проверки доступности ECH и HTTP/3</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Пожалуйста, заполните корректный шаблон конфигурации</value>
@@ -1462,127 +1462,127 @@
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
</data>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
<value>Начинается разбор и обработка содержимого подписки</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
<value>Выбрать профиль</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
<value>По умолчанию применяется глобально, со встроенной фильтрацией FakeIP (только sing-box).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
<value>Добавьте хотя бы одну конфигурацию</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
<value>Группа политик</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
<value>Цепочка прокси</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
<value>Наименьшая задержка</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
<value>Случайный</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
<value>Циклический (Round Robin)</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
<value>Наиболее стабильный</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
<value>Тип группы политик</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
<value>Добавить группу политик </value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
<value>Добавить цепочку прокси</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
<value>Добавить дочернюю конфигурацию </value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
<value>Удалить дочернюю конфигурацию </value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Configuration item 1, Auto add from subscription group</value>
<value>Конфигурация 1: автодобавление из группы подписки</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
<value>Резервный (Fallback)</value>
</data>
<data name="MsgCoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'</value>
<value>Ядро «{0}» не поддерживает тип сети «{1}»</value>
</data>
<data name="MsgCoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'</value>
<value>Ядро «{0}» не поддерживает протокол «{1}» при транспорте «{2}»</value>
</data>
<data name="MsgCoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'</value>
<value>Ядро «{0}» не поддерживает протокол «{1}»</value>
</data>
<data name="MsgInvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check</value>
<value>Свойство {0} недопустимо, проверьте его</value>
</data>
<data name="MsgNotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'</value>
<value>Протокол «{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>
<value>Если в системе нет функции трея, не включайте эту опцию</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
<value>Можно задать отдельные правила для маршрутизации и DNS или выбрать «ALL» для применения к обоим</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>Rule Type</value>
<value>Тип правила</value>
</data>
<data name="TbBootstrapDNS" xml:space="preserve">
<value>Bootstrap DNS</value>
</data>
<data name="TbBootstrapDNSTips" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
<value>Разрешает домены DNS-серверов, требуется IP-адрес</value>
</data>
<data name="menuFastRealPing" xml:space="preserve">
<value>Test real delay</value>
<value>Тест реальной задержки</value>
</data>
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
<value>Auto add filtered configuration from subscription groups</value>
<value>Автодобавление отфильтрованных конфигураций из групп подписки</value>
</data>
<data name="TbCertPinning" xml:space="preserve">
<value>Certificate Pinning</value>
<value>Привязка сертификата</value>
</data>
<data name="TbCertPinningTips" xml:space="preserve">
<value>Pinned certificate (fill in either one)
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
<value>Привязанный сертификат (заполните любое из полей)
При указании сертификат будет привязан, а «Разрешить небезопасные» отключится.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
Получение сертификата может завершиться неудачей при использовании самоподписанного сертификата или при наличии ненадёжного / вредоносного ЦС в системе.</value>
</data>
<data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value>
<value>Получить сертификат</value>
</data>
<data name="TbFetchCertChain" xml:space="preserve">
<value>Fetch Certificate Chain</value>
<value>Получить цепочку сертификатов</value>
</data>
<data name="ServerNameMustBeValidDomain" xml:space="preserve">
<value>Please set a valid domain</value>
<value>Укажите корректный домен</value>
</data>
<data name="CertNotSet" xml:space="preserve">
<value>Certificate not set</value>
<value>Сертификат не задан</value>
</data>
<data name="CertSet" xml:space="preserve">
<value>Certificate set</value>
<value>Сертификат задан</value>
</data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
<value>Путь к пользовательскому PAC-файлу</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
<value>Путь к скрипту системного прокси</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
<value>Отображать в Dock на macOS (требуется перезапуск)</value>
</data>
<data name="menuServerList2" xml:space="preserve">
<value>Configuration Item 2, Select and add from self-built</value>
<value>Конфигурация 2: выбор и добавление из собственных</value>
</data>
<data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value>
@@ -1591,81 +1591,93 @@ 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>Полный сертификат (цепочка) в формате 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 удалённого сервера; иначе — встроенный DNS-модуль.</value>
</data>
<data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value>
<value>Интервал смены портов (Port Hopping)</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>Routing rule {0} outbound node {1} warning: {2}</value>
<value>Правило маршрутизации {0}, исходящий узел {1}, предупреждение: {2}</value>
</data>
<data name="MsgRoutingRuleOutboundNodeError" xml:space="preserve">
<value>Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only.</value>
<value>Правило маршрутизации {0}, исходящий узел {1}, ошибка: {2}. Используется только прокси-узел.</value>
</data>
<data name="MsgGroupCycleDependency" xml:space="preserve">
<value>Group {0} has a cycle dependency on child node {1}. Skipping this node.</value>
<value>Группа {0} имеет циклическую зависимость на дочерний узел {1}. Узел пропущен.</value>
</data>
<data name="MsgGroupChildNodeWarning" xml:space="preserve">
<value>Group {0} child node {1} warning: {2}</value>
<value>Группа {0}: предупреждение дочернего узла {1}: {2}</value>
</data>
<data name="MsgGroupChildNodeError" xml:space="preserve">
<value>Group {0} child node {1} error: {2}. Skipping this node.</value>
<value>Группа {0}: ошибка дочернего узла {1}: {2}. Узел пропущен.</value>
</data>
<data name="MsgGroupChildGroupNodeWarning" xml:space="preserve">
<value>Group {0} child group node {1} warning: {2}</value>
<value>Группа {0}: предупреждение дочернего узла группы {1}: {2}</value>
</data>
<data name="MsgGroupChildGroupNodeError" xml:space="preserve">
<value>Group {0} child group node {1} error: {2}. Skipping this node.</value>
<value>Группа {0}: ошибка дочернего узла группы {1}: {2}. Узел пропущен.</value>
</data>
<data name="MsgGroupNoValidChildNode" xml:space="preserve">
<value>Group {0} has no valid child node.</value>
<value>У группы {0} нет допустимых дочерних узлов.</value>
</data>
<data name="MsgRoutingRuleEmptyOutboundTag" xml:space="preserve">
<value>Routing rule {0} has an empty outbound tag. Fallback to proxy node only.</value>
<value>У правила маршрутизации {0} пустой исходящий тег. Используется только прокси-узел.</value>
</data>
<data name="MsgRoutingRuleOutboundNodeNotFound" xml:space="preserve">
<value>Routing rule {0} outbound node {1} not found. Fallback to proxy node only.</value>
<value>Правило маршрутизации {0}, исходящий узел {1} не найден. Используется только прокси-узел.</value>
</data>
<data name="MsgSubscriptionPrevProfileNotFound" xml:space="preserve">
<value>Subscription previous proxy {0} not found. Skipping.</value>
<value>Предыдущий прокси подписки {0} не найден. Пропущено.</value>
</data>
<data name="MsgSubscriptionNextProfileNotFound" xml:space="preserve">
<value>Subscription next proxy {0} not found. Skipping.</value>
<value>Следующий прокси подписки {0} не найден. Пропущено.</value>
</data>
<data name="menuGenGroupServer" xml:space="preserve">
<value>Generate Policy Group</value>
<value>Сгенерировать группу политик</value>
</data>
<data name="menuAllServers" xml:space="preserve">
<value>All configurations</value>
<value>Все конфигурации</value>
</data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
<value>Группировка по регионам</value>
</data>
</root>
<data name="menuEditCopy" xml:space="preserve">
<value>Скопировать</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>Выбрать все</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -1665,4 +1665,16 @@
<data name="menuGenRegionGroup" xml:space="preserve">
<value>按地区分组</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>复制</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>全选</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>粘贴</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>格式化</value>
</data>
</root>

View File

@@ -1665,4 +1665,16 @@
<data name="menuGenRegionGroup" xml:space="preserve">
<value>按區域分組</value>
</data>
<data name="menuEditCopy" xml:space="preserve">
<value>複製</value>
</data>
<data name="menuEditSelectAll" xml:space="preserve">
<value>全選</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
</data>
</root>

View File

@@ -86,15 +86,25 @@ public class MsgViewModel : MyReactiveObject
}
catch (Exception ex)
{
_queueMsg.Enqueue(ex.Message);
EnqueueWithLimit(ex.Message);
_lastMsgFilterNotAvailable = true;
}
}
_queueMsg.Enqueue(msg);
EnqueueWithLimit(msg);
if (!msg.EndsWith(Environment.NewLine))
{
_queueMsg.Enqueue(Environment.NewLine);
EnqueueWithLimit(Environment.NewLine);
}
}
private void EnqueueWithLimit(string item)
{
_queueMsg.Enqueue(item);
while (_queueMsg.Count > NumMaxMsg)
{
_queueMsg.TryDequeue(out _);
}
}

View File

@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:views="clr-namespace:v2rayN.Desktop.Views"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.menuServers}"
Width="900"
@@ -658,16 +659,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TransportExtraTip}" />
<TextBox
<views:JsonEditor
x:Name="txtExtra"
Width="400"
MinHeight="100"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Classes="TextArea"
MinLines="6"
TextWrapping="Wrap" />
VerticalAlignment="Center" />
</StackPanel>
</Flyout>
</Button.Flyout>
@@ -749,13 +747,13 @@
<Button.Flyout>
<Flyout>
<StackPanel>
<TextBox
<views:JsonEditor
x:Name="txtFinalmask"
Width="400"
MinHeight="100"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
Classes="TextArea"
TextWrapping="NoWrap" />
HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
</StackPanel>
</Flyout>
</Button.Flyout>

View File

@@ -3,6 +3,7 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:v2rayN.Desktop.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -399,12 +400,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="HTTP/SOCKS">
<TextBox
Name="txtnormalDNSCompatible"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<local:JsonEditor Name="txtnormalDNSCompatible" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</DockPanel>
</TabItem>
@@ -473,12 +469,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="HTTP/SOCKS">
<TextBox
Name="txtnormalDNS2Compatible"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<local:JsonEditor Name="txtnormalDNS2Compatible" VerticalAlignment="Stretch" />
</HeaderedContentControl>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
@@ -488,12 +479,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="{x:Static resx:ResUI.TbSettingsTunMode}">
<TextBox
Name="txttunDNS2Compatible"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<local:JsonEditor Name="txttunDNS2Compatible" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</Grid>

View File

@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:views="clr-namespace:v2rayN.Desktop.Views"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
Title="{x:Static resx:ResUI.menuFullConfigTemplate}"
Width="900"
@@ -94,12 +95,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="xray config template json">
<TextBox
x:Name="rayFullConfigTemplate"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<views:JsonEditor x:Name="rayFullConfigTemplate" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</DockPanel>
</TabItem>
@@ -166,12 +162,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="sing-box config template json">
<TextBox
x:Name="sbFullConfigTemplate"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<views:JsonEditor x:Name="sbFullConfigTemplate" VerticalAlignment="Stretch" />
</HeaderedContentControl>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
@@ -181,12 +172,7 @@
BorderBrush="Gray"
BorderThickness="1"
Header="sing-box tun config template json">
<TextBox
x:Name="sbFullTunConfigTemplate"
VerticalAlignment="Stretch"
Classes="TextArea"
MinLines="10"
TextWrapping="Wrap" />
<views:JsonEditor x:Name="sbFullTunConfigTemplate" VerticalAlignment="Stretch" />
</HeaderedContentControl>
</Grid>
</DockPanel>

View File

@@ -0,0 +1,23 @@
<UserControl
x:Class="v2rayN.Desktop.Views.JsonEditor"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ae="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib">
<ae:TextEditor
Name="Editor"
FontFamily="Cascadia Code,Consolas,Monospace"
FontSize="14"
ShowLineNumbers="True">
<ae:TextEditor.ContextMenu>
<ContextMenu>
<MenuItem Click="FormatJson_Click" Header="{x:Static resx:ResUI.menuEditFormat}" />
<Separator />
<MenuItem Click="Copy_Click" Header="{x:Static resx:ResUI.menuEditCopy}" />
<MenuItem Click="Paste_Click" Header="{x:Static resx:ResUI.menuEditPaste}" />
<MenuItem Click="SelectAll_Click" Header="{x:Static resx:ResUI.menuEditSelectAll}" />
</ContextMenu>
</ae:TextEditor.ContextMenu>
</ae:TextEditor>
</UserControl>

View File

@@ -0,0 +1,96 @@
using System.Text.Json;
using System.Xml;
using AvaloniaEdit.Highlighting;
using AvaloniaEdit.Highlighting.Xshd;
namespace v2rayN.Desktop.Views;
public partial class JsonEditor : UserControl
{
private static readonly JsonSerializerOptions SIndentedOptions = new() { WriteIndented = true };
private static readonly Lazy<IHighlightingDefinition> SHighlightingDark =
new(() => BuildHighlighting(dark: true), isThreadSafe: true);
private static readonly Lazy<IHighlightingDefinition> SHighlightingLight =
new(() => BuildHighlighting(dark: false), isThreadSafe: true);
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<JsonEditor, string>(nameof(Text), defaultValue: string.Empty);
public string Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public JsonEditor()
{
InitializeComponent();
var isDark = Application.Current?.ActualThemeVariant != ThemeVariant.Light;
Editor.SyntaxHighlighting = isDark ? SHighlightingDark.Value : SHighlightingLight.Value;
Editor.TextArea.TextView.Options.EnableHyperlinks = false;
Editor.TextChanged += (_, _) =>
{
if (Text != Editor.Text)
{
SetCurrentValue(TextProperty, Editor.Text);
}
};
this.GetObservable(TextProperty).Subscribe(text =>
{
if (Editor.Text != text)
{
Editor.Text = text ?? string.Empty;
}
});
}
private static IHighlightingDefinition BuildHighlighting(bool dark)
{
var keyColor = dark ? "#9CDCFE" : "#0451A5";
var strColor = dark ? "#CE9178" : "#A31515";
var numColor = dark ? "#B5CEA8" : "#098658";
var kwColor = dark ? "#569CD6" : "#0000FF";
var xshd = $"""
<?xml version="1.0"?>
<SyntaxDefinition name="JSON" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Key" foreground="{keyColor}" />
<Color name="String" foreground="{strColor}" />
<Color name="Number" foreground="{numColor}" />
<Color name="Keyword" foreground="{kwColor}" fontWeight="bold" />
<RuleSet>
<Rule color="Key">"([^"\\]|\\.)*"(?=\s*:)</Rule>
<Rule color="String">"([^"\\]|\\.)*"</Rule>
<Rule color="Number">-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?</Rule>
<Keywords color="Keyword">
<Word>true</Word>
<Word>false</Word>
<Word>null</Word>
</Keywords>
</RuleSet>
</SyntaxDefinition>
""";
using var reader = XmlReader.Create(new StringReader(xshd));
return HighlightingLoader.Load(reader, HighlightingManager.Instance);
}
private void FormatJson_Click(object? sender, RoutedEventArgs e)
{
try
{
var obj = JsonUtils.ParseJson(Editor.Text);
Editor.Text = JsonUtils.Serialize(obj, SIndentedOptions);
}
catch
{
// ignored
}
}
private void Copy_Click(object? sender, RoutedEventArgs e) => Editor.Copy();
private void Paste_Click(object? sender, RoutedEventArgs e) => Editor.Paste();
private void SelectAll_Click(object? sender, RoutedEventArgs e) => Editor.SelectAll();
}

View File

@@ -79,8 +79,8 @@
<MenuItem
x:Name="menuMsgViewSelectAll"
Click="menuMsgViewSelectAll_Click"
InputGesture="Ctrl+A"
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" />
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}"
InputGesture="Ctrl+A" />
<MenuItem
x:Name="menuMsgViewCopy"
Click="menuMsgViewCopy_Click"