Compare commits

...

33 Commits

Author SHA1 Message Date
2dust
a74ca3f9d0 up 7.22.4 2026-05-29 11:08:39 +08:00
2dust
ed61f5c420 Rename ViewModel properties to PascalCase 2026-05-29 11:05:43 +08:00
2dust
bf98c4007f Use configurable page size and delay in speedtests
https://github.com/2dust/v2rayN/pull/9392
2026-05-29 10:37:00 +08:00
dependabot[bot]
78dcf51c3c Bump actions/setup-dotnet from 5.2.0 to 5.3.0 (#9407)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.3.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-05-29 09:56:43 +08:00
2dust
1e59344074 Add balancer fallbackTag
https://github.com/2dust/v2rayN/issues/9401
2026-05-29 09:55:59 +08:00
JieXu
1d2442d58a Fix the problem of font select (#9405)
* Update OptionSettingWindow.axaml.cs

* Update OptionSettingWindow.axaml.cs
2026-05-28 15:31:06 +08:00
2dust
3448782925 Remove unused C# style setting from .editorconfig 2026-05-28 14:31:27 +08:00
2dust
e6f4a57913 Limit update checks to selected core types
https://github.com/2dust/v2rayN/issues/9381
2026-05-28 14:31:01 +08:00
Roffild
4ae5c021fd Add in Global.cs speed and ping test URLs (#9374)
* Add in Global.cs speed and ping test URLs

* 100mb

* Update Global.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-05-27 16:54:26 +08:00
xihan123
0a357fd1a7 Change the SelectedValue bound to Text #9388 (#9389) 2026-05-26 20:20:15 +08:00
2dust
5ba5a805ce Make MTU combobox editable
https://github.com/2dust/v2rayN/issues/9370
2026-05-24 14:54:06 +08:00
YFdyh000
807f0aba06 Make Admin AutoRun faster (#9366)
* Set Admin AutoRun task priority to Normal rather than Below Normal

* Remove 30s delay for Admin AutoRun
2026-05-23 13:58:00 +08:00
2dust
f4a2086dfb Refine reload logic after RefreshServers
https://github.com/2dust/v2rayN/issues/9344
2026-05-22 10:07:36 +08:00
2dust
ccb0ffb3b6 up 7.22.3 2026-05-21 17:25:50 +08:00
2dust
1fa1246c0b Bug fix
https://github.com/2dust/v2rayN/issues/9351
2026-05-21 14:15:07 +08:00
2dust
14cc37d07a Add 'New Update' notification flow
Introduce a small update-notification feature: add AppEvents.HasUpdateNotified event and have TaskManager publish it when update messages exist. Add localized resource key (menuNewUpdate) and expose it in the ResUI designer; update resource files for several languages. In the UI, add a New Update button in MainWindow.xaml, wire its click to the existing check-update handler, bind its visibility to a new BlNewUpdate property on MainWindowViewModel, and subscribe the viewmodel to the new event. Also reset the notification flag after showing the Check Update dialog.
2026-05-21 11:25:49 +08:00
2dust
c7519f8ea7 Case-insensitive name checks and IpInfo visibility 2026-05-21 10:33:17 +08:00
2dust
0c796a157b Add IP info & flag emoji to test,add ip info column for main window
Fetch and display tested server IP and country (with emoji) in speed tests. Adds CountryExtension for country->emoji mapping and a new IpInfoResult type; ConnectionHandler now retrieves/parses IP API results and GetRealPingTime signature adjusted. Models and entities (ProfileItemModel, ProfileExItem, SpeedTestResult) gain IpInfo fields; ProfileExManager can store test IP info. UI/UX updated: new IpInfo column in ProfilesView (desktop and Avalonia), ResUI resource strings for "IP Info", and EServerColName ordering supports IpInfo. SpeedtestService now captures IP info and forwards it to the view model via the update function.
2026-05-20 19:16:58 +08:00
2dust
a9824fe6ec Improve ConnectionHandler DownloadService 2026-05-20 15:42:47 +08:00
2dust
f18758d4bf Support socks4/5 URIs and improve userinfo parsing 2026-05-20 14:28:01 +08:00
JieXu
c1a009a409 bump dependencies (#9341)
* Update build-linux.yml

* Update Directory.Packages.props

* Update Directory.Packages.props
2026-05-20 09:01:22 +08:00
2dust
0f3fc8e053 up 7.22.2 2026-05-18 17:36:05 +08:00
2dust
f7206f3405 Improve CheckUpdateModel 2026-05-18 17:32:07 +08:00
2dust
460a674ebc Add 'Check Only' update action
Introduce a new "Check Only" feature: add CheckOnlyCmd to CheckUpdateViewModel with CheckOnlyTask that queries updates (via UpdateService.CheckHasUpdateOnly) for selected cores and reports results without performing updates. Wire up a new btnCheckOnly in both Desktop and Avalonia views and bind the command. Add localized menuCheckOnly entries to multiple .resx files and update ResUI.Designer. Also shorten the pre-release label to "Check for pre-release" in resource files.
2026-05-17 19:15:57 +08:00
DHR60
e2428f2500 Add tun inbound rule (#9327) 2026-05-17 18:54:12 +08:00
DHR60
bc3cbb4277 Fix (#9325)
* Fix

* Rename tun tag
2026-05-17 18:52:42 +08:00
2dust
ac9d0a0193 Add periodic update checks and core support 2026-05-17 17:09:34 +08:00
2dust
2291214b6f up 7.22.1 2026-05-16 19:00:46 +08:00
Miheichev Aleksandr Sergeevich
5b47d8ba05 i18n(ru): translate untranslated PreSharedKey label and Export menu (#9309)
Two strings in ResUI.ru.resx were left with their English (or identifier) values and surfaced as untranslated text on a Russian UI culture. This commit translates both, keeping the existing translation style and the existing technical vocabulary established earlier in this file.

Changes:

1. TbPreSharedKey: 'PreSharedKey' -> 'Общий ключ (PSK)'

   The label is the WireGuard pre-shared-key field. The sibling WireGuard fields are already translated in the same file (TbPublicKey -> 'Открытый ключ', TbPrivateKey -> 'Приватный ключ'), so leaving this one as the raw identifier was inconsistent. 'Общий ключ' matches the wording used in Russian-localized network UIs for this concept (NetworkManager, MikroTik, OpenVPN GUIs); the '(PSK)' suffix preserves the technical abbreviation so users familiar with the WireGuard documentation immediately recognize the field.

2. menuExport2InnerUri: 'Export v2rayN Internal Share Link to Clipboard' -> 'Экспорт внутренней ссылки v2rayN в буфер обмена'

   This context-menu item was added recently and the Russian resource kept the English string verbatim. The translation follows the convention of the sibling export-to-clipboard items (menuExport2ShareUrlBase64 uses 'Экспорт ... в буфер обмена'), and 'внутренней ссылки v2rayN' preserves the 'internal' qualifier because this share-link format is specific to v2rayN's own importer and not interchangeable with the standard VMess/VLESS/Trojan/etc. URI schemes.

Verified:

- Diff scope: only ResUI.ru.resx, only the two <value> elements; the .resx XML schema headers and all other keys are untouched.

- Full audit of ResUI.ru.resx vs ResUI.resx: every other key is present and translated; these were the only two strings still surfacing in English on a Russian UI culture.

- 'dotnet build v2rayN/v2rayN.sln -c Release' passes with 0 errors and 0 warnings.
2026-05-15 15:09:17 +08:00
JieXu
b193c39ad7 Remove patch for riscv (#9310)
* Delete .github/workflows/update-riscv-depand.yml

* Update package-rhel-riscv.sh

* Update package-debian-riscv.sh

* Create package-debian-loong.sh

* Update build-linux.yml

* Update build-linux.yml

* Update package-debian-loong.sh

* Update build-linux.yml

* Update Directory.Packages.props
2026-05-15 15:09:04 +08:00
2dust
e68842bb78 Add SkiaSharp Linux native assets package
Add SkiaSharp.NativeAssets.Linux (v3.119.1) to Directory.Packages.props and add a PackageReference in v2rayN.Desktop.csproj so the Linux native SkiaSharp binaries are included for the desktop build/runtime.
2026-05-14 20:34:42 +08:00
JieXu
6c06c8a00a Update (#9303)
* Update DOTNET_RISCV_VERSION to 10.0.108

* Update DOTNET_RISCV_VERSION to 10.0.108
2026-05-14 19:27:31 +08:00
Miheichev Aleksandr Sergeevich
58d2641559 chore: remove NoWarn and fix .NET 10 build warnings across platforms (#9301)
* deps: bump ZXing.Net.Bindings.SkiaSharp from 0.16.14 to 0.16.22

Patch update to the latest stable release on the 0.16.x line. No breaking changes, no public API changes - purely internal fixes.

Verified by a full Release build of v2rayN.sln on .NET 10; no new warnings or errors are introduced.

* chore: remove NoWarn and fix .NET 10 build warnings

Removes the repository-level NoWarn suppression from Directory.Build.props and addresses the warnings that surface on top of the .NET 10 migration in #9179, keeping Debug, Release, and cross-platform publishes warning-free without suppressing warnings globally.

Changes:

- Removes <NoWarn>CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200</NoWarn> from Directory.Build.props.

- Annotates Windows-only APIs with [SupportedOSPlatform] and [SupportedOSPlatformGuard] so CA1416 accepts that the Windows surface is gated behind Utils.IsWindows() / Utils.IsNonWindows().

- Splits Utils.SetUnixFileMode into a cross-platform wrapper and a private [UnsupportedOSPlatform("windows")] implementation so File.SetUnixFileMode never reaches the analyzer on Windows builds.

- Adds a parameterless constructor to MessageBoxDialog so Avalonia's runtime XAML loader (AVLN3001) can instantiate the dialog.

- Moves the WPF high-DPI configuration from app.manifest to <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode> in v2rayN.csproj, fixing WFO0003.

- Adds global using System.Runtime.Versioning; to ServiceLib and v2rayN.Desktop so the platform attributes are usable project-wide.

* test: make cycle dependency tests locale-independent

Accept the localized Russian cycle dependency diagnostic in CoreConfigContextBuilderTests so the assertions pass when tests run under a Russian UI culture.

* fix: tighten Unix platform handling

Adds Linux and macOS platform guards so the analyzer can narrow calls through Utils.IsLinux() and Utils.IsMacOS().

Marks the Linux/macOS autostart and system proxy helpers with explicit platform attributes.

Updates Utils.GetSystemHosts() to read /etc/hosts on Linux and macOS while keeping the existing Windows hosts and hosts.ics merge behavior.
2026-05-14 19:25:07 +08:00
83 changed files with 2006 additions and 765 deletions

View File

@@ -100,7 +100,6 @@ csharp_style_prefer_tuple_swap = true:warning
csharp_style_prefer_utf8_string_literals = true:warning
csharp_style_throw_expression = true:warning
csharp_style_unused_value_assignment_preference = discard_variable:warning
csharp_style_unused_value_expression_statement_preference = discard_variable:warning
csharp_using_directive_placement = outside_namespace:warning
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:warning
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning

View File

@@ -325,3 +325,178 @@ jobs:
--data-binary @"$f" \
"${upload_url}?name=${f##*/}"
done
deb-loong64:
name: build and release deb loong64
if: |
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-24.04
env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
QCOW2_IMAGE: debian13-loong64.qcow2
EFI_CODE: edk2-loongarch64-code.fd
EFI_VARS: edk2-loongarch64-vars.fd
QEMU_VERSION: 10.2.2
steps:
- name: Prepare host tools
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y rsync qemu-utils expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0
- name: Checkout repo
uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Download QEMU prebuild
run: |
set -euo pipefail
wget -O qemu-linux-x64.tar.gz \
"https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64.tar.gz"
tar -xzf qemu-linux-x64.tar.gz
mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware
shell: bash
run: |
set -euo pipefail
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
wget -O "$EFI_CODE" "$EFI_CODE_URL"
wget -O "$EFI_VARS" "$EFI_VARS_URL"
qemu-img info "$QCOW2_IMAGE"
- name: Build loong64 DEB in VM through serial console
shell: bash
timeout-minutes: 180
env:
RELEASE_TAG: ${{ env.RELEASE_TAG }}
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
expect <<'EOF'
log_user 1
set timeout -1
set release_tag $env(RELEASE_TAG)
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
set workspace $env(GITHUB_WORKSPACE)
set repo $env(GITHUB_REPOSITORY)
set sha $env(GITHUB_SHA)
set qcow2 $env(QCOW2_IMAGE)
set efi_code $env(EFI_CODE)
set efi_vars $env(EFI_VARS)
proc wait_prompt {} {
expect {
-re "__CI_PROMPT__# " {}
timeout { exit 1 }
eof { exit 1 }
}
}
proc run_cmd {cmd} {
send -- "$cmd\r"
wait_prompt
}
spawn $qemu_bin \
-L $qemu_rom_dir \
-machine virt \
-accel tcg,thread=multi,tb-size=2048 \
-cpu la464 \
-m 9216 \
-smp 4 \
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
-device virtio-blk-pci,drive=hd0,bootindex=1 \
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
-netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \
-virtfs local,path=$workspace,mount_tag=workspace,security_model=none,id=workspace \
-display none \
-serial stdio \
-monitor none
expect -re "login:|debian login:"
send -- "root\r"
expect -re "Password:|密码:|密码:"
send -- "password\r"
expect {
-re "# " {}
timeout { exit 1 }
eof { exit 1 }
}
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
wait_prompt
run_cmd "mkdir -p /workspace"
run_cmd "mount -t 9p -o trans=virtio,version=9p2000.L workspace /workspace || mount -t 9p -o trans=virtio workspace /workspace"
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
run_cmd "dhclient \$IFACE || true"
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
run_cmd "apt-get update || apt-get update || apt-get update"
run_cmd "DEBIAN_FRONTEND=noninteractive 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 gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1"
run_cmd "rm -rf /root/v2rayN-src"
run_cmd "git clone --recursive https://github.com/$repo.git /root/v2rayN-src"
run_cmd "cd /root/v2rayN-src && git fetch --depth=1 origin $sha && git checkout FETCH_HEAD && git submodule update --init --recursive"
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r"
expect {
-re "__BUILD_DONE__0" {
send -- "poweroff\r"
}
default {
exit 1
}
}
EOF
- name: Collect DEBs
shell: bash
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" \
-exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
echo "==== Dist tree ===="
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
- name: Upload DEBs to release
uses: svenstaro/upload-release-action@v2
with:
file: dist/deb-loong64/**/*.deb
tag: ${{ env.RELEASE_TAG }}
file_glob: true
prerelease: true

View File

@@ -52,7 +52,7 @@ jobs:
fetch-depth: '0'
- name: Setup .NET
uses: actions/setup-dotnet@v5.2.0
uses: actions/setup-dotnet@v5.3.0
with:
dotnet-version: '10.0.1xx'

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: '0'
- name: Setup .NET
uses: actions/setup-dotnet@v5.2.0
uses: actions/setup-dotnet@v5.3.0
with:
dotnet-version: '8.0.x'

View File

@@ -1,256 +0,0 @@
name: update riscv dependent versions
on:
workflow_dispatch:
push:
branches:
- master
permissions:
contents: write
concurrency:
group: update-riscv-dependent
cancel-in-progress: false
jobs:
update:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.1xx
- run: sudo apt-get update && sudo apt-get install -y jq
- name: resolve
id: resolve
shell: bash
run: |
set -euo pipefail
TARGET_FILES=(
package-rhel-riscv.sh
package-debian-riscv.sh
)
echo "==> restore projects"
find . -name '*.csproj' | while read -r p; do
if grep -q '<TargetFramework>.*-windows' "$p"; then
echo "[skip] $p"
else
echo "[restore] $p"
dotnet restore "$p" -p:EnableWindowsTargeting=true || true
fi
done
echo "==> collect assets"
mapfile -t ASSETS < <(find . -path '*/obj/project.assets.json')
printf ' %s\n' "${ASSETS[@]}"
ALL_LIBS=()
for f in "${ASSETS[@]}"; do
mapfile -t libs < <(jq -r '.libraries | keys[]' "$f")
ALL_LIBS+=("${libs[@]}")
done
mapfile -t LIBS < <(printf '%s\n' "${ALL_LIBS[@]}" | sort -u)
extract() {
local name="$1"
for i in "${LIBS[@]}"; do
[[ "$i" == "$name/"* ]] && echo "${i#*/}"
done | sort -u
}
norm_version() { echo "${1#v}"; }
is_preview() { [[ "$(norm_version "$1")" == *-* ]]; }
base_version() { local v; v="$(norm_version "$1")"; echo "${v%%-*}"; }
key() {
local v core pre a b c p1 p2 p3
v="$(norm_version "$1")"
core="${v%%-*}"
[[ "$v" == *-* ]] && pre="${v#*-}" || pre=""
IFS='.' read -r a b c <<< "$core"
a=${a//[^0-9]/}; a=${a:-0}
b=${b//[^0-9]/}; b=${b:-0}
c=${c//[^0-9]/}; c=${c:-0}
if [[ -z "$pre" ]]; then
printf "%05d.%05d.%05d.1\n" "$a" "$b" "$c"
else
pre="${pre#preview.}"
IFS='.' read -ra p <<< "$pre"
p1=${p[0]:-0}; p1=${p1//[^0-9]/}; p1=${p1:-0}
p2=${p[1]:-0}; p2=${p2//[^0-9]/}; p2=${p2:-0}
p3=${p[2]:-0}; p3=${p3//[^0-9]/}; p3=${p3:-0}
printf "%05d.%05d.%05d.0.%05d.%05d.%05d\n" \
"$a" "$b" "$c" "$p1" "$p2" "$p3"
fi
}
latest() {
local best="" best_key="" cur cur_key
for cur in "$@"; do
[[ -n "$cur" ]] || continue
cur_key="$(key "$cur")"
echo " candidate: $cur -> $cur_key" >&2
if [[ -z "$best_key" || "$cur_key" > "$best_key" ]]; then
best="$cur"
best_key="$cur_key"
fi
done
echo "$best"
}
log_mixed_versions() {
local name="$1"; shift
local versions=("$@") bases=() v b
mapfile -t bases < <(
for v in "${versions[@]}"; do
[[ -n "$v" ]] && base_version "$v"
done | sort -u
)
for b in "${bases[@]}"; do
local has_stable=0 has_preview=0 matched=()
for v in "${versions[@]}"; do
[[ -n "$v" ]] || continue
[[ "$(base_version "$v")" == "$b" ]] || continue
matched+=("$v")
if is_preview "$v"; then
has_preview=1
else
has_stable=1
fi
done
if [[ "$has_stable" -eq 1 && "$has_preview" -eq 1 ]]; then
echo "[warn] $name: stable and preview both exist for base $b, prefer stable for this base" >&2
printf ' %s\n' "${matched[@]}" >&2
fi
done
}
filter_mixed_versions() {
local versions=("$@") stable_bases=() v b
mapfile -t stable_bases < <(
for v in "${versions[@]}"; do
if [[ -n "$v" ]] && ! is_preview "$v"; then
base_version "$v"
fi
done | sort -u
)
for v in "${versions[@]}"; do
[[ -n "$v" ]] || continue
b="$(base_version "$v")"
if is_preview "$v" && printf '%s\n' "${stable_bases[@]}" | grep -qxF "$b"; then
continue
fi
echo "$v"
done
}
read_var() {
sed -nE "s/^$2=\"\\\$\\{$2:-([^\"]+)\\}\".*/\\1/p" "$1" | head -n1
}
choose_final_version() {
local old="$1" new="$2"
[[ -n "$new" ]] || { echo "$old"; return; }
[[ -n "$old" ]] || { echo "$new"; return; }
if [[ "$(key "$old")" > "$(key "$new")" ]]; then
echo "$old"
else
echo "$new"
fi
}
update_file() {
local file="$1"
local old_skia old_harf final_skia final_harf changed=0
old_skia="$(read_var "$file" SKIA_VER)"
old_harf="$(read_var "$file" HARFBUZZ_VER)"
final_skia="$(choose_final_version "$old_skia" "$NEW_SKIA")"
final_harf="$(choose_final_version "$old_harf" "$NEW_HARF")"
echo "==> check $file"
echo " SKIA_VER: ${old_skia} -> ${NEW_SKIA} (apply: ${final_skia})"
echo " HARFBUZZ_VER: ${old_harf} -> ${NEW_HARF} (apply: ${final_harf})"
if [[ "$old_skia" != "$final_skia" ]]; then
sed -i -E "s|^SKIA_VER=.*|SKIA_VER=\"\\\${SKIA_VER:-$final_skia}\"|" "$file"
changed=1
fi
if [[ "$old_harf" != "$final_harf" ]]; then
sed -i -E "s|^HARFBUZZ_VER=.*|HARFBUZZ_VER=\"\\\${HARFBUZZ_VER:-$final_harf}\"|" "$file"
changed=1
fi
grep -qF "SKIA_VER=\"\${SKIA_VER:-$final_skia}\"" "$file"
grep -qF "HARFBUZZ_VER=\"\${HARFBUZZ_VER:-$final_harf}\"" "$file"
bash -n "$file"
[[ "$changed" -eq 1 ]]
}
mapfile -t SKIA < <(extract SkiaSharp)
mapfile -t HARF < <(extract HarfBuzzSharp)
echo "==> SkiaSharp"
printf ' %s\n' "${SKIA[@]}"
echo "==> HarfBuzzSharp"
printf ' %s\n' "${HARF[@]}"
log_mixed_versions "SkiaSharp" "${SKIA[@]}"
log_mixed_versions "HarfBuzzSharp" "${HARF[@]}"
mapfile -t SKIA < <(filter_mixed_versions "${SKIA[@]}")
mapfile -t HARF < <(filter_mixed_versions "${HARF[@]}")
NEW_SKIA="$(latest "${SKIA[@]}")"
NEW_HARF="$(latest "${HARF[@]}")"
echo "==> selected"
echo " SKIA_VER=$NEW_SKIA"
echo " HARFBUZZ_VER=$NEW_HARF"
any_changed=0
changed_files=()
for file in "${TARGET_FILES[@]}"; do
if update_file "$file"; then
any_changed=1
changed_files+=("$file")
fi
done
if [[ "$any_changed" -eq 0 ]]; then
echo "changed=0" >> "$GITHUB_OUTPUT"
exit 0
fi
{
echo "changed=1"
echo "changed_files<<EOF"
printf '%s\n' "${changed_files[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: commit
if: steps.resolve.outputs.changed == '1'
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git add package-rhel-riscv.sh package-debian-riscv.sh
if git diff --cached --quiet; then
exit 0
fi
git commit -m "chore: update riscv native dependency versions"
git push

742
package-debian-loong.sh Normal file
View File

@@ -0,0 +1,742 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION_ARG=""
WITH_CORE="both"
FORCE_NETCORE=0
BUILD_FROM=""
XRAY_VER="${XRAY_VER:-}"
SING_VER="${SING_VER:-}"
MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.108"
DOTNET_LOONGARCH_TAG="v10.0.108-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
OS_ID=""
OS_NAME=""
OS_VERSION_ID=""
HOST_ARCH=""
SCRIPT_DIR=""
PROJECT=""
VERSION=""
declare -a BUILT_DEBS=()
die() {
echo "$*" >&2
exit 1
}
parse_args() {
local first_arg="${1:-}"
if [[ -n "$first_arg" && "$first_arg" != --* ]]; then
VERSION_ARG="$first_arg"
shift || true
fi
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 ;;
--buildfrom) BUILD_FROM="${2:-}"; shift 2 ;;
*)
[[ -n "${VERSION_ARG:-}" ]] || VERSION_ARG="$1"
shift
;;
esac
done
if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then
die "You cannot specify both an explicit version and --buildfrom at the same time.
Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3."
fi
}
detect_environment() {
local current_kernel=""
local lowest=""
. /etc/os-release
OS_ID="${ID:-}"
OS_NAME="${NAME:-$OS_ID}"
OS_VERSION_ID="${VERSION_ID:-}"
HOST_ARCH="$(uname -m)"
case "$OS_ID" in
debian)
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
;;
*)
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
This script only supports: Debian."
;;
esac
case "$HOST_ARCH" in
loongarch64) ;;
*) die "Only supports loongarch64" ;;
esac
current_kernel="$(uname -r)"
lowest="$(printf '%s\n%s\n' "$MIN_KERNEL" "$current_kernel" | sort -V | head -n1)"
[[ "$lowest" == "$MIN_KERNEL" ]] || die "Kernel $current_kernel is below $MIN_KERNEL"
echo "[OK] Kernel $current_kernel verified."
}
install_dependencies() {
local install_ok=0
local tmp_dotnet=""
mkdir -p "$OUTPUT_DIR"
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 gcc make pkg-config \
libicu-dev libssl-dev libfontconfig1 libfreetype6 zlib1g
mkdir -p "$HOME/.dotnet"
tmp_dotnet="$(mktemp -d)"
curl -fL "$DOTNET_SDK_URL" -o "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
tar -C "$HOME/.dotnet" -xzf "$tmp_dotnet/$DOTNET_LOONGARCH_FILE"
rm -rf "$tmp_dotnet"
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
mkdir -p "$HOME/.nuget/NuGet"
cat > "$HOME/.nuget/NuGet/NuGet.Config" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="loongnix" value="https://lnuget.loongnix.cn/v3/index.json" allowInsecureConnections="true" />
</packageSources>
</configuration>
EOF
dotnet --info >/dev/null 2>&1 && install_ok=1
fi
if [[ "$install_ok" -ne 1 ]]; then
echo "Could not auto-install dependencies for '$OS_ID'. Make sure these are available:"
echo "dotnet-loongarch SDK, curl, unzip, tar, rsync, git, gcc, make, dpkg-deb, fakeroot, libicu-dev, libssl-dev"
exit 1
fi
}
prepare_workspace() {
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
PROJECT="$PROJECT_HINT"
[[ -f "$PROJECT" ]] || PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)"
[[ -f "$PROJECT" ]] || die "v2rayN.Desktop.csproj not found"
}
choose_channel() {
local ch="latest"
local sel=""
if [[ -n "${BUILD_FROM:-}" ]]; then
case "$BUILD_FROM" in
1) echo "latest"; return 0 ;;
2) echo "prerelease"; return 0 ;;
3) echo "keep"; return 0 ;;
*) die "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." ;;
esac
fi
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//'
}
sync_submodules() {
if [[ -f .gitmodules ]]; then
git submodule sync --recursive || true
git submodule update --init --recursive || true
fi
}
git_try_checkout() {
local want="$1"
local ref=""
if git rev-parse --git-dir >/dev/null 2>&1; then
git fetch --tags --force --prune --depth=1 || true
git rev-parse "refs/tags/${want}" >/dev/null 2>&1 && ref="$want"
if [[ -n "$ref" ]]; then
echo "[OK] Found ref '${ref}', checking out..."
git checkout -f "$ref"
sync_submodules
return 0
fi
fi
return 1
}
apply_channel_or_keep() {
local ch="$1"
local 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..."
case "$ch" in
latest) tag="$(get_latest_tag_latest || true)" ;;
prerelease) tag="$(get_latest_tag_prerelease || true)" ;;
*) die "Failed to resolve latest tag for channel '${ch}'." ;;
esac
[[ -n "$tag" ]] || die "Failed to resolve latest tag for channel '${ch}'."
echo "[*] Latest tag for '${ch}': ${tag}"
git_try_checkout "$tag" || die "Failed to checkout '${tag}'."
VERSION="${tag#v}"
}
resolve_version() {
if git rev-parse --git-dir >/dev/null 2>&1; then
if [[ -n "${VERSION_ARG:-}" ]]; then
local clean_ver="${VERSION_ARG#v}"
if git_try_checkout "$clean_ver"; then
VERSION="$clean_ver"
else
echo "[WARN] Tag '${VERSION_ARG}' not found."
apply_channel_or_keep "$(choose_channel)"
fi
else
apply_channel_or_keep "$(choose_channel)"
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}"
}
xray_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-loongarch64) echo "https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-loong64.zip" ;;
*) return 1 ;;
esac
}
singbox_url_for_rid() {
local rid="$1"
local ver="$2"
case "$rid" in
linux-loongarch64) echo "https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-loong64.tar.gz" ;;
*) return 1 ;;
esac
}
bundle_url_for_rid() {
local rid="$1"
case "$rid" in
linux-loongarch64) echo "https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-loong64.zip" ;;
*) return 1 ;;
esac
}
download_xray() {
local outdir="$1"
local rid="$2"
local ver="${XRAY_VER:-}"
local url=""
local tmp=""
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; }
url="$(xray_url_for_rid "$rid" "$ver")" || { echo "[xray] Unsupported RID: $rid"; return 1; }
echo "[+] Download xray: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/xray.zip" || { rm -rf "$tmp"; return 1; }
unzip -q "$tmp/xray.zip" -d "$tmp" || { rm -rf "$tmp"; return 1; }
install -m 755 "$tmp/xray" "$outdir/xray" || { rm -rf "$tmp"; return 1; }
rm -rf "$tmp"
}
download_singbox() {
local outdir="$1"
local rid="$2"
local ver="${SING_VER:-}"
local url=""
local tmp=""
local bin=""
local cronet=""
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; }
url="$(singbox_url_for_rid "$rid" "$ver")" || { echo "[sing-box] Unsupported RID: $rid"; return 1; }
echo "[+] Download sing-box: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
tar -C "$tmp" -xzf "$tmp/singbox.tar.gz" || { rm -rf "$tmp"; return 1; }
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"; return 1; }
cronet="$(find "$tmp" -type f -name 'libcronet*.so*' | head -n1 || true)"
[[ -n "$cronet" ]] && install -m 644 "$cronet" "$outdir/libcronet.so" || true
rm -rf "$tmp"
}
unify_geo_layout() {
local outroot="$1"
local n
local names=(
geosite.dat
geoip.dat
geoip-only-cn-private.dat
Country.mmdb
geoip.metadb
)
mkdir -p "$outroot/bin"
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"
local f=""
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"
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/refs/heads/rule-set-geoip/$f"
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/refs/heads/rule-set-geosite/$f"
done
unify_geo_layout "$outroot"
}
populate_assets_zip_mode() {
local outroot="$1"
local rid="$2"
local url=""
local tmp=""
local nested_dir=""
url="$(bundle_url_for_rid "$rid")" || { echo "[!] Bundle unsupported RID: $rid"; return 1; }
echo "[+] Try v2rayN bundle archive: $url"
tmp="$(mktemp -d)"
curl -fL "$url" -o "$tmp/v2rayn.zip" || { echo "[!] Bundle download failed"; rm -rf "$tmp"; return 1; }
unzip -q "$tmp/v2rayn.zip" -d "$tmp" || { echo "[!] Bundle unzip failed"; rm -rf "$tmp"; 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
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_geo_layout "$outroot"
rm -rf "$tmp"
echo "[+] Bundle extracted to $outroot"
}
populate_assets_netcore_mode() {
local outroot="$1"
local rid="$2"
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then
download_xray "$outroot/bin/xray" "$rid" || echo "[!] xray download failed (skipped)"
fi
if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then
download_singbox "$outroot/bin/sing_box" "$rid" || echo "[!] sing-box download failed (skipped)"
fi
download_geo_assets "$outroot" || echo "[!] Geo rules download failed (skipped)"
}
stage_runtime_assets() {
local outroot="$1"
local rid="$2"
mkdir -p "$outroot/bin/xray" "$outroot/bin/sing_box"
if [[ "$FORCE_NETCORE" -eq 0 ]]; then
if populate_assets_zip_mode "$outroot" "$rid"; then
echo "[*] Using v2rayN bundle archive."
else
echo "[*] Bundle failed, fallback to separate core + rules."
populate_assets_netcore_mode "$outroot" "$rid"
fi
else
echo "[*] --netcore specified: use separate core + rules."
populate_assets_netcore_mode "$outroot" "$rid"
fi
}
describe_target() {
local short="$1"
case "$short" in
loongarch64) printf '%s\n%s\n' "linux-loongarch64" "loong64" ;;
*) echo "Unknown arch '$short' (use loongarch64)" >&2; return 1 ;;
esac
}
publish_binary() {
local rid="$1"
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true
}
write_launcher_file() {
local stage="$1"
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
}
write_desktop_file() {
local stage="$1"
install -m 644 /dev/stdin "$stage/usr/share/applications/v2rayn.desktop" <<'EOF'
[Desktop Entry]
Type=Application
Name=v2rayN
Comment=v2rayN for Debian GNU Linux
Exec=v2rayn
Icon=v2rayn
Terminal=false
Categories=Network;
EOF
}
write_maintainer_scripts() {
local debian_dir="$1"
install -m 755 /dev/stdin "$debian_dir/postinst" <<'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
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
}
package_binary() {
local short="$1"
local rid="$2"
local deb_arch="$3"
local pubdir=""
local workdir=""
local stage=""
local debian_dir=""
local project_dir=""
local icon_candidate=""
local shlibs_depends=""
local extra_depends=""
local final_depends=""
local multiarch=""
local sys_libdir=""
local sys_usrlibdir=""
local deb_out=""
pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish"
[[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; }
workdir="$(mktemp -d)"
trap '[[ -n "${workdir:-}" ]] && rm -rf "$workdir"' RETURN
stage="$workdir/${PKGROOT}_${VERSION}_${deb_arch}"
debian_dir="$stage/DEBIAN"
mkdir -p "$stage/opt/v2rayN" "$stage/usr/bin" "$stage/usr/share/applications" "$stage/usr/share/icons/hicolor/256x256/apps" "$debian_dir"
cp -a "$pubdir/." "$stage/opt/v2rayN/"
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
stage_runtime_assets "$stage/opt/v2rayN" "$rid"
write_launcher_file "$stage"
write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir"
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
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
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
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
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_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")
}
select_targets() {
printf '%s\n' loongarch64
}
build_one_target() {
local short="$1"
local meta=()
local rid=""
local deb_arch=""
mapfile -t meta < <(describe_target "$short") || return 1
rid="${meta[0]}"
deb_arch="${meta[1]}"
echo "[*] Building for target: $short (RID=$rid, DEB arch=$deb_arch)"
publish_binary "$rid"
package_binary "$short" "$rid" "$deb_arch"
}
print_summary() {
local pkg=""
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 "==============================================="
}
main() {
local targets=()
local arch=""
parse_args "$@"
detect_environment
install_dependencies
prepare_workspace
resolve_version
mapfile -t targets < <(select_targets)
for arch in "${targets[@]}"; do
build_one_target "$arch"
done
print_summary
}
main "$@"

View File

@@ -12,13 +12,10 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0"
DOTNET_RISCV_VERSION="10.0.107"
DOTNET_RISCV_VERSION="10.0.108"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
SKIA_VER="${SKIA_VER:-3.119.2}"
HARFBUZZ_VER="${HARFBUZZ_VER:-8.3.1.3}"
OS_ID=""
OS_NAME=""
@@ -27,7 +24,6 @@ HOST_ARCH=""
SCRIPT_DIR=""
PROJECT=""
VERSION=""
BUILT_ALL=0
declare -a BUILT_DEBS=()
@@ -261,70 +257,6 @@ resolve_version() {
echo "[*] GUI version resolved as: ${VERSION}"
}
apply_riscv_patch() {
local f=""
find . -type f \( -name "*.csproj" -o -name "*.props" -o -name "*.targets" \) \
-exec sed -Ei 's#<TargetFramework>[^<]+</TargetFramework>#<TargetFramework>'"$DOTNET_TFM"'</TargetFramework>#g' {} +
while IFS= read -r -d '' f; do
sed -i \
-e "s#<PackageVersion Include=\"SkiaSharp\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"SkiaSharp\" Version=\"$SKIA_VER\" />#g" \
-e "s#<PackageVersion Include=\"SkiaSharp.NativeAssets.Linux\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"SkiaSharp.NativeAssets.Linux\" Version=\"$SKIA_VER\" />#g" \
-e "s#<PackageVersion Include=\"HarfBuzzSharp\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"HarfBuzzSharp\" Version=\"$HARFBUZZ_VER\" />#g" \
-e "s#<PackageVersion Include=\"HarfBuzzSharp.NativeAssets.Linux\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"HarfBuzzSharp.NativeAssets.Linux\" Version=\"$HARFBUZZ_VER\" />#g" \
"$f"
grep -q 'PackageVersion Include="SkiaSharp"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"SkiaSharp\" Version=\"$SKIA_VER\" />" "$f"
grep -q 'PackageVersion Include="SkiaSharp.NativeAssets.Linux"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"SkiaSharp.NativeAssets.Linux\" Version=\"$SKIA_VER\" />" "$f"
grep -q 'PackageVersion Include="HarfBuzzSharp"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"HarfBuzzSharp\" Version=\"$HARFBUZZ_VER\" />" "$f"
grep -q 'PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"HarfBuzzSharp.NativeAssets.Linux\" Version=\"$HARFBUZZ_VER\" />" "$f"
done < <(find . -type f -name 'Directory.Packages.props' -print0)
f="$(find "$DOTNET_ROOT/sdk/$(dotnet --version)" -type f -name 'Microsoft.NETCoreSdk.BundledVersions.props' | head -n1 || true)"
if [[ -f "$f" ]] && ! grep -q 'linux-riscv64' "$f"; then
sed -i \
-e 's/linux-arm64/&;linux-riscv64/g' \
-e 's/linux-musl-arm64/&;linux-musl-riscv64/g' \
"$f"
fi
}
copy_skiasharp_native_riscv64() {
local outdir="$1"
local skia_so=""
local harfbuzz_so=""
mkdir -p "$outdir"
skia_so="$(find "$HOME/.nuget/packages" -path "*/skiasharp.nativeassets.linux/${SKIA_VER}/runtimes/linux-riscv64/native/libSkiaSharp.so" | head -n1 || true)"
[[ -n "$skia_so" ]] || skia_so="$(find "$HOME/.nuget/packages" -path "*/runtimes/linux-riscv64/native/libSkiaSharp.so" | head -n1 || true)"
harfbuzz_so="$(find "$HOME/.nuget/packages" -path "*/harfbuzzsharp.nativeassets.linux/${HARFBUZZ_VER}/runtimes/linux-riscv64/native/libHarfBuzzSharp.so" | head -n1 || true)"
[[ -n "$harfbuzz_so" ]] || harfbuzz_so="$(find "$HOME/.nuget/packages" -path "*/runtimes/linux-riscv64/native/libHarfBuzzSharp.so" | head -n1 || true)"
if [[ -n "$skia_so" && -f "$skia_so" ]]; then
echo "[+] Copy libSkiaSharp.so from NuGet cache"
install -m 755 "$skia_so" "$outdir/libSkiaSharp.so"
else
echo "[WARN] libSkiaSharp.so for linux-riscv64 not found in NuGet cache"
fi
if [[ -n "$harfbuzz_so" && -f "$harfbuzz_so" ]]; then
echo "[+] Copy libHarfBuzzSharp.so from NuGet cache"
install -m 755 "$harfbuzz_so" "$outdir/libHarfBuzzSharp.so"
else
echo "[WARN] libHarfBuzzSharp.so for linux-riscv64 not found in NuGet cache"
fi
}
xray_url_for_rid() {
local rid="$1"
local ver="$2"
@@ -554,10 +486,10 @@ describe_target() {
publish_binary() {
local rid="$1"
dotnet clean "$PROJECT" -c Release -p:TargetFramework="$DOTNET_TFM"
rm -rf "$(dirname "$PROJECT")/bin/Release/${DOTNET_TFM}" || true
dotnet restore "$PROJECT" -r "$rid" -p:TargetFramework="$DOTNET_TFM"
dotnet publish "$PROJECT" -c Release -r "$rid" -p:TargetFramework="$DOTNET_TFM" -p:PublishSingleFile=false -p:SelfContained=true
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true
}
write_launcher_file() {
@@ -567,7 +499,6 @@ write_launcher_file() {
#!/usr/bin/env bash
set -euo pipefail
DIR="/opt/v2rayN"
export LD_LIBRARY_PATH="$DIR:${LD_LIBRARY_PATH:-}"
cd "$DIR"
if [[ -x "$DIR/v2rayN" ]]; then
@@ -643,7 +574,7 @@ package_binary() {
local sys_usrlibdir=""
local deb_out=""
pubdir="$(dirname "$PROJECT")/bin/Release/${DOTNET_TFM}/${rid}/publish"
pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish"
[[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; }
workdir="$(mktemp -d)"
@@ -655,8 +586,6 @@ package_binary() {
mkdir -p "$stage/opt/v2rayN" "$stage/usr/bin" "$stage/usr/share/applications" "$stage/usr/share/icons/hicolor/256x256/apps" "$debian_dir"
cp -a "$pubdir/." "$stage/opt/v2rayN/"
copy_skiasharp_native_riscv64 "$stage/opt/v2rayN" || echo "[!] SkiaSharp native copy failed (skipped)"
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
@@ -732,9 +661,7 @@ EOF
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
[[ -f "$stage/opt/v2rayN/libSkiaSharp.so" ]] && chmod 0755 "$stage/opt/v2rayN/libSkiaSharp.so" || true
[[ -f "$stage/opt/v2rayN/libHarfBuzzSharp.so" ]] && chmod 0755 "$stage/opt/v2rayN/libHarfBuzzSharp.so" || true
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
@@ -787,7 +714,6 @@ main() {
install_dependencies
prepare_workspace
resolve_version
apply_riscv_patch
mapfile -t targets < <(select_targets)

View File

@@ -12,13 +12,10 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_TFM="net10.0"
DOTNET_RISCV_VERSION="10.0.107"
DOTNET_RISCV_VERSION="10.0.108"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
SKIA_VER="${SKIA_VER:-3.119.2}"
HARFBUZZ_VER="${HARFBUZZ_VER:-8.3.1.3}"
OS_ID=""
OS_NAME=""
@@ -27,7 +24,6 @@ HOST_ARCH=""
SCRIPT_DIR=""
PROJECT=""
VERSION=""
BUILT_ALL=0
declare -a BUILT_RPMS=()
@@ -258,70 +254,6 @@ resolve_version() {
echo "[*] GUI version resolved as: ${VERSION}"
}
apply_riscv_patch() {
local f=""
find . -type f \( -name "*.csproj" -o -name "*.props" -o -name "*.targets" \) \
-exec sed -Ei 's#<TargetFramework>[^<]+</TargetFramework>#<TargetFramework>'"$DOTNET_TFM"'</TargetFramework>#g' {} +
while IFS= read -r -d '' f; do
sed -i \
-e "s#<PackageVersion Include=\"SkiaSharp\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"SkiaSharp\" Version=\"$SKIA_VER\" />#g" \
-e "s#<PackageVersion Include=\"SkiaSharp.NativeAssets.Linux\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"SkiaSharp.NativeAssets.Linux\" Version=\"$SKIA_VER\" />#g" \
-e "s#<PackageVersion Include=\"HarfBuzzSharp\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"HarfBuzzSharp\" Version=\"$HARFBUZZ_VER\" />#g" \
-e "s#<PackageVersion Include=\"HarfBuzzSharp.NativeAssets.Linux\" Version=\"[^\"]*\" */>#<PackageVersion Include=\"HarfBuzzSharp.NativeAssets.Linux\" Version=\"$HARFBUZZ_VER\" />#g" \
"$f"
grep -q 'PackageVersion Include="SkiaSharp"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"SkiaSharp\" Version=\"$SKIA_VER\" />" "$f"
grep -q 'PackageVersion Include="SkiaSharp.NativeAssets.Linux"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"SkiaSharp.NativeAssets.Linux\" Version=\"$SKIA_VER\" />" "$f"
grep -q 'PackageVersion Include="HarfBuzzSharp"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"HarfBuzzSharp\" Version=\"$HARFBUZZ_VER\" />" "$f"
grep -q 'PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux"' "$f" || \
sed -i "/<\/ItemGroup>/i\ <PackageVersion Include=\"HarfBuzzSharp.NativeAssets.Linux\" Version=\"$HARFBUZZ_VER\" />" "$f"
done < <(find . -type f -name 'Directory.Packages.props' -print0)
f="$(find "$DOTNET_ROOT/sdk/$(dotnet --version)" -type f -name 'Microsoft.NETCoreSdk.BundledVersions.props' | head -n1 || true)"
if [[ -f "$f" ]] && ! grep -q 'linux-riscv64' "$f"; then
sed -i \
-e 's/linux-arm64/&;linux-riscv64/g' \
-e 's/linux-musl-arm64/&;linux-musl-riscv64/g' \
"$f"
fi
}
copy_skiasharp_native_riscv64() {
local outdir="$1"
local skia_so=""
local harfbuzz_so=""
mkdir -p "$outdir"
skia_so="$(find "$HOME/.nuget/packages" -path "*/skiasharp.nativeassets.linux/${SKIA_VER}/runtimes/linux-riscv64/native/libSkiaSharp.so" | head -n1 || true)"
[[ -n "$skia_so" ]] || skia_so="$(find "$HOME/.nuget/packages" -path "*/runtimes/linux-riscv64/native/libSkiaSharp.so" | head -n1 || true)"
harfbuzz_so="$(find "$HOME/.nuget/packages" -path "*/harfbuzzsharp.nativeassets.linux/${HARFBUZZ_VER}/runtimes/linux-riscv64/native/libHarfBuzzSharp.so" | head -n1 || true)"
[[ -n "$harfbuzz_so" ]] || harfbuzz_so="$(find "$HOME/.nuget/packages" -path "*/runtimes/linux-riscv64/native/libHarfBuzzSharp.so" | head -n1 || true)"
if [[ -n "$skia_so" && -f "$skia_so" ]]; then
echo "[+] Copy libSkiaSharp.so from NuGet cache"
install -m 755 "$skia_so" "$outdir/libSkiaSharp.so"
else
echo "[WARN] libSkiaSharp.so for linux-riscv64 not found in NuGet cache"
fi
if [[ -n "$harfbuzz_so" && -f "$harfbuzz_so" ]]; then
echo "[+] Copy libHarfBuzzSharp.so from NuGet cache"
install -m 755 "$harfbuzz_so" "$outdir/libHarfBuzzSharp.so"
else
echo "[WARN] libHarfBuzzSharp.so for linux-riscv64 not found in NuGet cache"
fi
}
xray_url_for_rid() {
local rid="$1"
local ver="$2"
@@ -551,10 +483,10 @@ describe_target() {
publish_binary() {
local rid="$1"
dotnet clean "$PROJECT" -c Release -p:TargetFramework="$DOTNET_TFM"
rm -rf "$(dirname "$PROJECT")/bin/Release/${DOTNET_TFM}" || true
dotnet restore "$PROJECT" -r "$rid" -p:TargetFramework="$DOTNET_TFM"
dotnet publish "$PROJECT" -c Release -r "$rid" -p:TargetFramework="$DOTNET_TFM" -p:PublishSingleFile=false -p:SelfContained=true
dotnet clean "$PROJECT" -c Release
rm -rf "$(dirname "$PROJECT")/bin/Release/net10.0" || true
dotnet restore "$PROJECT"
dotnet publish "$PROJECT" -c Release -r "$rid" -p:PublishSingleFile=false -p:SelfContained=true
}
write_spec_file() {
@@ -604,15 +536,12 @@ cp -a * %{buildroot}/opt/v2rayN/
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 || :
[ -f %{buildroot}/opt/v2rayN/libSkiaSharp.so ] && chmod 0755 %{buildroot}/opt/v2rayN/libSkiaSharp.so || :
[ -f %{buildroot}/opt/v2rayN/libHarfBuzzSharp.so ] && chmod 0755 %{buildroot}/opt/v2rayN/libHarfBuzzSharp.so || :
install -dm0755 %{buildroot}%{_bindir}
install -m0755 /dev/stdin %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash
set -euo pipefail
DIR="/opt/v2rayN"
export LD_LIBRARY_PATH="$DIR:${LD_LIBRARY_PATH:-}"
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi
@@ -673,7 +602,7 @@ package_binary() {
local icon_candidate=""
local f=""
pubdir="$(dirname "$PROJECT")/bin/Release/${DOTNET_TFM}/${rid}/publish"
pubdir="$(dirname "$PROJECT")/bin/Release/net10.0/${rid}/publish"
[[ -d "$pubdir" ]] || { echo "Publish directory not found: $pubdir"; return 1; }
workdir="$(mktemp -d)"
@@ -682,8 +611,6 @@ package_binary() {
mkdir -p "$workdir/$PKGROOT"
cp -a "$pubdir/." "$workdir/$PKGROOT/"
copy_skiasharp_native_riscv64 "$workdir/$PKGROOT" || echo "[!] SkiaSharp native copy failed (skipped)"
project_dir="$(cd "$(dirname "$PROJECT")" && pwd)"
icon_candidate="$project_dir/v2rayN.png"
[[ -f "$icon_candidate" ]] || { echo "Required icon not found: $icon_candidate"; return 1; }
@@ -755,7 +682,6 @@ main() {
install_dependencies
prepare_workspace
resolve_version
apply_riscv_patch
mapfile -t targets < <(select_targets)
@@ -766,4 +692,4 @@ main() {
print_summary
}
main "$@"
main "$@"

View File

@@ -1,14 +1,13 @@
<Project>
<PropertyGroup>
<Version>7.22.0</Version>
<Version>7.22.4</Version>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<NoWarn>CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200</NoWarn>
<Nullable>annotations</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Authors>2dust</Authors>

View File

@@ -1,38 +1,39 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
<PackageVersion Include="CliWrap" Version="3.10.1" />
<PackageVersion Include="Downloader" Version="5.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.27" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.4" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
</ItemGroup>
</Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.16" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.16" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.12" />
<PackageVersion Include="CliWrap" Version="3.10.1" />
<PackageVersion Include="Downloader" Version="5.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.27" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.27" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" />
<PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.1.7" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4-preview.1.1" />
</ItemGroup>
</Project>

View File

@@ -99,7 +99,8 @@ public class CoreConfigContextBuilderTests
{
return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase)
|| message.Contains("循环依赖", StringComparison.Ordinal)
|| message.Contains("循環依賴", StringComparison.Ordinal);
|| message.Contains("循環依賴", StringComparison.Ordinal)
|| message.Contains("циклическую зависимость", StringComparison.OrdinalIgnoreCase);
}
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)

View File

@@ -0,0 +1,92 @@
namespace ServiceLib.Common;
/// <summary>
/// Extension methods for country code utilities
/// </summary>
public static class CountryExtension
{
/// <summary>
/// Country code to emoji flag mapping for common countries
/// </summary>
private static readonly Dictionary<string, string> CountryEmojiMap = new(StringComparer.OrdinalIgnoreCase)
{
// Asia
{ "CN", "🇨🇳" }, // China
{ "HK", "🇭🇰" }, // Hong Kong
{ "TW", "🇹🇼" }, // Taiwan
{ "JP", "🇯🇵" }, // Japan
{ "SG", "🇸🇬" }, // Singapore
{ "KR", "🇰🇷" }, // South Korea
{ "TH", "🇹🇭" }, // Thailand
{ "VN", "🇻🇳" }, // Vietnam
{ "ID", "🇮🇩" }, // Indonesia
{ "PH", "🇵🇭" }, // Philippines
{ "MY", "🇲🇾" }, // Malaysia
{ "IN", "🇮🇳" }, // India
{ "PK", "🇵🇰" }, // Pakistan
{ "BD", "🇧🇩" }, // Bangladesh
{ "LK", "🇱🇰" }, // Sri Lanka
{ "KH", "🇰🇭" }, // Cambodia
{ "LA", "🇱🇦" }, // Laos
{ "MM", "🇲🇲" }, // Myanmar
// Americas
{ "US", "🇺🇸" }, // United States
{ "CA", "🇨🇦" }, // Canada
{ "MX", "🇲🇽" }, // Mexico
{ "BR", "🇧🇷" }, // Brazil
{ "AR", "🇦🇷" }, // Argentina
{ "CL", "🇨🇱" }, // Chile
{ "CO", "🇨🇴" }, // Colombia
// Europe
{ "GB", "🇬🇧" }, // United Kingdom
{ "DE", "🇩🇪" }, // Germany
{ "FR", "🇫🇷" }, // France
{ "IT", "🇮🇹" }, // Italy
{ "ES", "🇪🇸" }, // Spain
{ "RU", "🇷🇺" }, // Russia
{ "NL", "🇳🇱" }, // Netherlands
{ "CH", "🇨🇭" }, // Switzerland
{ "SE", "🇸🇪" }, // Sweden
{ "NO", "🇳🇴" }, // Norway
{ "DK", "🇩🇰" }, // Denmark
{ "FI", "🇫🇮" }, // Finland
{ "PL", "🇵🇱" }, // Poland
{ "CZ", "🇨🇿" }, // Czech Republic
{ "AT", "🇦🇹" }, // Austria
{ "GR", "🇬🇷" }, // Greece
{ "PT", "🇵🇹" }, // Portugal
{ "TR", "🇹🇷" }, // Turkey
{ "UA", "🇺🇦" }, // Ukraine
{ "RO", "🇷🇴" }, // Romania
// Middle East & Central Asia
{ "AE", "🇦🇪" }, // United Arab Emirates
{ "SA", "🇸🇦" }, // Saudi Arabia
{ "IL", "🇮🇱" }, // Israel
{ "KZ", "🇰🇿" }, // Kazakhstan
// Oceania
{ "AU", "🇦🇺" }, // Australia
{ "NZ", "🇳🇿" }, // New Zealand
// Africa
{ "ZA", "🇿🇦" }, // South Africa
{ "EG", "🇪🇬" }, // Egypt
};
/// <summary>
/// Converts country code to flag emoji using predefined mapping
/// Example: "US" -> "🇺🇸", "CN" -> "🇨🇳"
/// </summary>
public static string? CountryToEmoji(this string? countryCode)
{
if (countryCode.IsNullOrEmpty())
{
return null;
}
return CountryEmojiMap.TryGetValue(countryCode, out var emoji) ? emoji : null;
}
}

View File

@@ -864,15 +864,25 @@ public class Utils
public static Dictionary<string, string> GetSystemHosts()
{
var hosts = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts");
var hostsIcs = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts.ics");
foreach (var (key, value) in hostsIcs)
if (IsWindows())
{
hosts[key] = value;
var hosts = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts");
var hostsIcs = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts.ics");
foreach (var (key, value) in hostsIcs)
{
hosts[key] = value;
}
return hosts;
}
return hosts;
if (IsLinux() || IsMacOS())
{
return GetSystemHosts("/etc/hosts");
}
return new Dictionary<string, string>();
}
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg)
@@ -1114,12 +1124,16 @@ public class Utils
#region Platform
[SupportedOSPlatformGuard("windows")]
public static bool IsWindows() => OperatingSystem.IsWindows();
[SupportedOSPlatformGuard("linux")]
public static bool IsLinux() => OperatingSystem.IsLinux();
[SupportedOSPlatformGuard("macos")]
public static bool IsMacOS() => OperatingSystem.IsMacOS();
[UnsupportedOSPlatformGuard("windows")]
public static bool IsNonWindows() => !OperatingSystem.IsWindows();
public static string GetExeName(string name)
@@ -1214,6 +1228,16 @@ public class Utils
}
public static bool SetUnixFileMode(string? fileName)
{
if (IsWindows())
{
return false;
}
return SetUnixFileModeInternal(fileName);
}
[UnsupportedOSPlatform("windows")]
private static bool SetUnixFileModeInternal(string? fileName)
{
try
{

View File

@@ -2,6 +2,7 @@ using Microsoft.Win32;
namespace ServiceLib.Common;
[SupportedOSPlatform("windows")]
internal static class WindowsUtils
{
private static readonly string _tag = "WindowsUtils";

View File

@@ -12,6 +12,7 @@ public enum EServerColName
SubRemarks,
DelayVal,
SpeedVal,
IpInfo,
TodayDown,
TodayUp,

View File

@@ -7,6 +7,7 @@ public static class AppEvents
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<bool> HasUpdateNotified = new();
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();

View File

@@ -55,7 +55,7 @@ public class Global
public const string DnsOutboundTag = "dns";
public const string DnsTag = "dns-module";
public const string DirectDnsTag = "direct-dns";
public const string BalancerTagSuffix = "-round";
public const string BalancerTagSuffix = "-balancer";
public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality";
public const string Loopback = "127.0.0.1";
@@ -149,6 +149,9 @@ public class Global
public static readonly List<string> SpeedTestUrls =
[
@"https://cachefly.cachefly.net/50mb.test",
@"https://cachefly.cachefly.net/100mb.test",
@"https://cachefly.cachefly.net/1mb.test",
@"https://cachefly.cachefly.net/10mb.test",
@"https://speed.cloudflare.com/__down?bytes=10000000",
@"https://speed.cloudflare.com/__down?bytes=50000000",
@"https://speed.cloudflare.com/__down?bytes=99999999",
@@ -157,6 +160,8 @@ public class Global
public static readonly List<string> SpeedPingTestUrls =
[
@"https://www.google.com/generate_204",
@"https://www.youtube.com/generate_204",
@"https://www.googlevideo.com/generate_204",
@"https://www.gstatic.com/generate_204",
@"https://www.apple.com/library/test/success.html",
@"http://www.msftconnecttest.com/connecttest.txt"
@@ -207,6 +212,10 @@ public class Global
public const string NaiveQuicProtocolShare = "naive+quic://";
public const string SOCKS5Protocol = "socks5://";
public const string SOCKS4Protocol = "socks4://";
public static readonly Dictionary<EConfigType, string> ProtocolShares = new()
{
{ EConfigType.VMess, "vmess://" },
@@ -505,6 +514,7 @@ public class Global
public static readonly List<string> InboundTags =
[
"tun",
"socks",
"socks2",
"socks3"

View File

@@ -8,6 +8,7 @@ global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using System.Reflection;
global using System.Runtime.InteropServices;
global using System.Runtime.Versioning;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.Encodings.Web;

View File

@@ -41,6 +41,7 @@ public static class AutoStartupHandler
#region Windows
[SupportedOSPlatform("windows")]
private static async Task ClearTaskWindows()
{
var autoRunName = GetAutoRunNameWindows();
@@ -53,6 +54,7 @@ public static class AutoStartupHandler
await Task.CompletedTask;
}
[SupportedOSPlatform("windows")]
private static async Task SetTaskWindows()
{
try
@@ -82,6 +84,7 @@ public static class AutoStartupHandler
/// <param name="fileName"></param>
/// <param name="description"></param>
/// <exception cref="ArgumentNullException"></exception>
[SupportedOSPlatform("windows")]
public static void AutoStartTaskService(string taskName, string fileName, string description)
{
if (taskName.IsNullOrEmpty())
@@ -108,7 +111,8 @@ public static class AutoStartupHandler
task.Settings.RunOnlyIfIdle = false;
task.Settings.IdleSettings.StopOnIdleEnd = false;
task.Settings.ExecutionTimeLimit = TimeSpan.Zero;
task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser, Delay = TimeSpan.FromSeconds(30) });
task.Settings.Priority = ProcessPriorityClass.Normal;
task.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger { UserId = logonUser });
task.Principal.RunLevel = Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest;
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));
@@ -124,6 +128,7 @@ public static class AutoStartupHandler
#region Linux
[SupportedOSPlatform("linux")]
private static async Task ClearTaskLinux()
{
try
@@ -137,6 +142,7 @@ public static class AutoStartupHandler
await Task.CompletedTask;
}
[SupportedOSPlatform("linux")]
private static async Task SetTaskLinux()
{
try
@@ -157,6 +163,7 @@ public static class AutoStartupHandler
}
}
[SupportedOSPlatform("linux")]
private static string GetHomePathLinux()
{
var homePath = Path.Combine(Utils.GetHomePath(), ".config", "autostart", $"{Global.AppName}.desktop");
@@ -168,6 +175,7 @@ public static class AutoStartupHandler
#region macOS
[SupportedOSPlatform("macos")]
private static async Task ClearTaskOSX()
{
try
@@ -187,6 +195,7 @@ public static class AutoStartupHandler
}
}
[SupportedOSPlatform("macos")]
private static async Task SetTaskOSX()
{
try
@@ -204,6 +213,7 @@ public static class AutoStartupHandler
}
}
[SupportedOSPlatform("macos")]
private static string GetLaunchAgentPathMacOS()
{
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
@@ -212,6 +222,7 @@ public static class AutoStartupHandler
return launchAgentPath;
}
[SupportedOSPlatform("macos")]
private static string GenerateLaunchAgentPlist()
{
var exePath = Utils.GetExePath();

View File

@@ -923,6 +923,7 @@ public static class ConfigHandler
Delay = t33?.Delay ?? 0,
Speed = t33?.Speed ?? 0,
Sort = t33?.Sort ?? 0,
IpInfo = t33?.IpInfo ?? string.Empty,
TodayDown = (t22?.TodayDown ?? 0).ToString("D16"),
TodayUp = (t22?.TodayUp ?? 0).ToString("D16"),
TotalDown = (t22?.TotalDown ?? 0).ToString("D16"),
@@ -943,6 +944,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderBy(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderBy(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderBy(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderBy(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderBy(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderBy(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderBy(t => t.TodayUp).ToList(),
@@ -963,6 +965,7 @@ public static class ConfigHandler
EServerColName.StreamSecurity => lstProfile.OrderByDescending(t => t.StreamSecurity).ToList(),
EServerColName.DelayVal => lstProfile.OrderByDescending(t => t.Delay).ToList(),
EServerColName.SpeedVal => lstProfile.OrderByDescending(t => t.Speed).ToList(),
EServerColName.IpInfo => lstProfile.OrderByDescending(t => t.IpInfo).ToList(),
EServerColName.SubRemarks => lstProfile.OrderByDescending(t => t.Subid).ToList(),
EServerColName.TodayDown => lstProfile.OrderByDescending(t => t.TodayDown).ToList(),
EServerColName.TodayUp => lstProfile.OrderByDescending(t => t.TodayUp).ToList(),

View File

@@ -4,53 +4,41 @@ public static class ConnectionHandler
{
private static readonly string _tag = "ConnectionHandler";
/// <summary>
/// Runs ping and IP checks and returns a formatted result string.
/// </summary>
public static async Task<string> RunAvailabilityCheck()
{
var time = await GetRealPingTimeInfo();
var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None;
var ip = time > 0 ? await GetIPInfo() : Global.None;
return string.Format(ResUI.TestMeOutput, time, ip);
}
/// <summary>
/// Gets IP information using the default local proxy.
/// </summary>
private static async Task<string?> GetIPInfo()
{
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty())
{
return null;
}
var webProxy = await GetWebProxy();
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, true, "");
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code;
return $"({country ?? "unknown"}) {ip}";
var ipInfo = await GetIPInfo(webProxy);
return ipInfo?.ToString() ?? Global.None;
}
/// <summary>
/// Measures real ping time using configured test URL.
/// </summary>
private static async Task<int> GetRealPingTimeInfo()
{
var responseTime = -1;
try
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}");
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var webProxy = await GetWebProxy();
for (var i = 0; i < 2; i++)
{
responseTime = await GetRealPingTime(url, webProxy, 10);
responseTime = await GetRealPingTime(webProxy, 10);
if (responseTime > 0)
{
break;
@@ -66,8 +54,21 @@ public static class ConnectionHandler
return responseTime;
}
public static async Task<int> GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout)
/// <summary>
/// Creates local SOCKS proxy instance.
/// </summary>
private static async Task<WebProxy?> GetWebProxy()
{
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
return new WebProxy($"socks5://{Global.Loopback}:{port}");
}
/// <summary>
/// Measures response time by sending HTTP requests through proxy.
/// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout)
{
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1;
try
{
@@ -95,4 +96,41 @@ public static class ConnectionHandler
}
return responseTime;
}
/// <summary>
/// Gets IP and country information through specified proxy.
/// </summary>
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
{
try
{
var url = AppManager.Instance.Config.SpeedTestItem.IPAPIUrl;
if (url.IsNullOrEmpty())
{
return null;
}
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, webProxy, "");
if (result == null)
{
return null;
}
var ipInfo = JsonUtils.Deserialize<IPAPIInfo>(result);
if (ipInfo == null)
{
return null;
}
var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query;
var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code ?? "unknown";
return new IpInfoResult(country, ip);
}
catch
{
return null;
}
}
}

View File

@@ -53,7 +53,9 @@ public class FmtHandler
{
return ShadowsocksFmt.Resolve(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS]))
else if (str.StartsWith(Global.ProtocolShares[EConfigType.SOCKS])
|| str.StartsWith(Global.SOCKS5Protocol)
|| str.StartsWith(Global.SOCKS4Protocol))
{
return SocksFmt.Resolve(str, out msg);
}
@@ -65,7 +67,8 @@ public class FmtHandler
{
return VLESSFmt.Resolve(str, out msg);
}
else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2]) || str.StartsWith(Global.Hysteria2ProtocolShare))
else if (str.StartsWith(Global.ProtocolShares[EConfigType.Hysteria2])
|| str.StartsWith(Global.Hysteria2ProtocolShare))
{
return Hysteria2Fmt.Resolve(str, out msg);
}

View File

@@ -99,12 +99,17 @@ public class SocksFmt : BaseFmt
};
// parse base64 UserInfo
var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo);
var userInfo = Utils.Base64Decode(rawUserInfo);
var userInfoParts = userInfo.Split([':'], 2);
if (userInfoParts.Length == 2)
if (rawUserInfo.IsNotEmpty())
{
item.Username = userInfoParts.First();
item.Password = userInfoParts[1];
var userInfoParts = rawUserInfo.Contains(':')
? rawUserInfo.Split(":", 2)
: Utils.Base64Decode(rawUserInfo).Split(":", 2);
if (userInfoParts.Length == 2)
{
item.Username = userInfoParts.First();
item.Password = userInfoParts.Last();
}
}
return item;

View File

@@ -1,5 +1,6 @@
namespace ServiceLib.Handler.SysProxy;
[SupportedOSPlatform("linux")]
public static class ProxySettingLinux
{
private static readonly string _proxySetFileName = $"{Global.ProxySetLinuxShellFileName.Replace(Global.NamespaceSample, "")}.sh";

View File

@@ -1,5 +1,6 @@
namespace ServiceLib.Handler.SysProxy;
[SupportedOSPlatform("macos")]
public static class ProxySettingOSX
{
private static readonly string _proxySetFileName = $"{Global.ProxySetOSXShellFileName.Replace(Global.NamespaceSample, "")}.sh";

View File

@@ -2,6 +2,7 @@ using static ServiceLib.Handler.SysProxy.ProxySettingWindows.InternetConnectionO
namespace ServiceLib.Handler.SysProxy;
[SupportedOSPlatform("windows")]
public static class ProxySettingWindows
{
private const string _regPath = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";

View File

@@ -88,6 +88,7 @@ public static class SysProxyHandler
}
}
[SupportedOSPlatform("windows")]
private static async Task SetWindowsProxyPac(int port)
{
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);

View File

@@ -48,6 +48,13 @@ public sealed class AppManager
}
}
public Dictionary<ECoreType, string> LastCheckUpdateResults { get; set; } = new();
public void SetLastCheckUpdateResult(ECoreType coreType, string result)
{
LastCheckUpdateResults[coreType] = result;
}
#endregion Property
#region App

View File

@@ -50,6 +50,50 @@ public sealed class CoreInfoManager
return fileName;
}
public List<ECoreType> GetCheckUpdateCoreTypes()
{
var lst = new List<ECoreType>();
if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
{
if (IsCheckUpdateSupported(ECoreType.v2rayN))
{
lst.Add(ECoreType.v2rayN);
}
if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10))
{
lst.Add(ECoreType.Xray);
lst.Add(ECoreType.mihomo);
lst.Add(ECoreType.sing_box);
}
}
return lst;
}
public bool IsCheckUpdateSupported(ECoreType type)
{
return type switch
{
ECoreType.v2rayN => !Utils.IsPackagedInstall(),
ECoreType.Xray => true,
ECoreType.mihomo => true,
ECoreType.sing_box => true,
_ => false,
};
}
public bool GetCheckPreRelease(ECoreType type, bool preRelease)
{
return type switch
{
ECoreType.v2rayN => preRelease,
ECoreType.Xray => preRelease,
_ => false,
};
}
private void InitCoreInfo()
{
var urlN = GetCoreUrl(ECoreType.v2rayN);

View File

@@ -8,6 +8,7 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value;
private Config _config;
[SupportedOSPlatform("windows")]
private WindowsJobService? _processJob;
private ProcessService? _processService;
private ProcessService? _processPreService;

View File

@@ -150,6 +150,14 @@ public class ProfileExManager
IndexIdEnqueue(indexId);
}
public void SetTestIpInfo(string indexId, string ipInfo)
{
var profileEx = GetProfileExItem(indexId);
profileEx.IpInfo = ipInfo;
IndexIdEnqueue(indexId);
}
public void SetSort(string indexId, int sort)
{
var profileEx = GetProfileExItem(indexId);

View File

@@ -70,6 +70,18 @@ public class TaskManager
}
}
//Execute once 24 hour
if (numOfExecuted % 1440 == 1)
{
try
{
await UpdateTaskRunCheckUpdate();
}
catch (Exception ex)
{
Logging.SaveLog("ScheduledTasks - UpdateTaskRunCheckUpdate", ex);
}
}
numOfExecuted++;
}
}
@@ -117,4 +129,23 @@ public class TaskManager
}).UpdateGeoFileAll();
}
}
private async Task UpdateTaskRunCheckUpdate()
{
Logging.SaveLog("Execute check update");
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.CheckPreReleaseUpdate);
foreach (var msg in msgs)
{
await _updateFunc?.Invoke(false, msg);
}
NoticeManager.Instance.Enqueue(string.Join("\n", msgs));
if (msgs.Count > 0)
{
AppEvents.HasUpdateNotified.Publish(true);
}
}
}

View File

@@ -159,6 +159,8 @@ public class SpeedTestItem
public int MixedConcurrencyCount { get; set; }
public string IPAPIUrl { get; set; }
public string UdpTestTarget { get; set; }
public int? SpeedTestPageSize { get; set; }
public int? SpeedTestDelayInterval { get; set; }
}
[Serializable]

View File

@@ -276,6 +276,7 @@ public class BalancersItem4Ray
public List<string>? selector { get; set; }
public BalancersStrategy4Ray? strategy { get; set; }
public string? tag { get; set; }
public string? fallbackTag { get; set; }
}
public class BalancersStrategy4Ray

View File

@@ -3,8 +3,10 @@ namespace ServiceLib.Models.Dto;
public class CheckUpdateModel : ReactiveObject
{
public bool? IsSelected { get; set; }
public string? CoreType { get; set; }
public ECoreType? CoreType { get; set; }
[Reactive] public string? Remarks { get; set; }
public string? FileName { get; set; }
public bool? IsFinished { get; set; }
public bool IsGeoFile { get; set; }
public string CoreTypeForStorage => IsGeoFile ? "GeoFiles" : (CoreType?.ToString() ?? "");
}

View File

@@ -17,3 +17,12 @@ public class LocationInfo
{
public string? country_code { get; set; }
}
public readonly record struct IpInfoResult(string Country, string? Ip)
{
public override string ToString()
{
var emoji = Country.CountryToEmoji();
return $"{emoji}({Country}) {Ip}";
}
}

View File

@@ -26,6 +26,9 @@ public class ProfileItemModel : ReactiveObject
[Reactive]
public string SpeedVal { get; set; }
[Reactive]
public string IpInfo { get; set; }
[Reactive]
public string TodayUp { get; set; }

View File

@@ -8,4 +8,6 @@ public class SpeedTestResult
public string? Delay { get; set; }
public string? Speed { get; set; }
public string? IpInfo { get; set; }
}

View File

@@ -10,4 +10,5 @@ public class ProfileExItem
public decimal Speed { get; set; }
public int Sort { get; set; }
public string? Message { get; set; }
public string? IpInfo { get; set; }
}

View File

@@ -564,6 +564,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 IP Info 的本地化字符串。
/// </summary>
public static string LvTestIpInfo {
get {
return ResourceManager.GetString("LvTestIpInfo", resourceCulture);
}
}
/// <summary>
/// 查找类似 Speed (MB/s) 的本地化字符串。
/// </summary>
@@ -870,6 +879,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Only Check 的本地化字符串。
/// </summary>
public static string menuCheckOnly {
get {
return ResourceManager.GetString("menuCheckOnly", resourceCulture);
}
}
/// <summary>
/// 查找类似 Check Update 的本地化字符串。
/// </summary>
@@ -1302,6 +1320,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 New Update 的本地化字符串。
/// </summary>
public static string menuNewUpdate {
get {
return ResourceManager.GetString("menuNewUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Open the storage location 的本地化字符串。
/// </summary>
@@ -1887,6 +1914,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 {0} has a new version available: {1} 的本地化字符串。
/// </summary>
public static string MsgCheckUpdateHasNewVersion {
get {
return ResourceManager.GetString("MsgCheckUpdateHasNewVersion", resourceCulture);
}
}
/// <summary>
/// 查找类似 Core &apos;{0}&apos; does not support network type &apos;{1}&apos; 的本地化字符串。
/// </summary>
@@ -2040,6 +2076,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Not Support 的本地化字符串。
/// </summary>
public static string MsgNotSupport {
get {
return ResourceManager.GetString("MsgNotSupport", resourceCulture);
}
}
/// <summary>
/// 查找类似 Not support protocol &apos;{0}&apos; 的本地化字符串。
/// </summary>
@@ -3484,7 +3529,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// 查找类似 tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。
/// </summary>
public static string TbRoutingInboundTagTips {
get {
@@ -3925,7 +3970,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Check for pre-release updates 的本地化字符串。
/// 查找类似 Check for pre-release 的本地化字符串。
/// </summary>
public static string TbSettingsEnableCheckPreReleaseUpdate {
get {

View File

@@ -1731,4 +1731,19 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Export v2rayN Internal Share Link to Clipboard</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

View File

@@ -1728,4 +1728,19 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Export v2rayN Internal Share Link to Clipboard</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

View File

@@ -1731,4 +1731,19 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Export v2rayN Internal Share Link to Clipboard</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

View File

@@ -691,7 +691,7 @@
<value>Automatically adjust column width after subscription update</value>
</data>
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>Check for pre-release updates</value>
<value>Check for pre-release</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>Exception</value>
@@ -1336,7 +1336,7 @@
<value>Enable second mixed port</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>socks: local port, socks2: second local port, socks3: LAN port</value>
<value>tun: TUN inbound, socks: local port, socks2: second local port, socks3: LAN port</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>Theme</value>
@@ -1731,4 +1731,19 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Export v2rayN Internal Share Link to Clipboard</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

View File

@@ -1726,9 +1726,24 @@
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value>
</data>
<data name="TbPreSharedKey" xml:space="preserve">
<value>PreSharedKey</value>
<value>Общий ключ (PSK)</value>
</data>
<data name="menuExport2InnerUri" xml:space="preserve">
<value>Export v2rayN Internal Share Link to Clipboard</value>
<value>Экспорт внутренней ссылки v2rayN в буфер обмена</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} has a new version available: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Only Check</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>New Update</value>
</data>
</root>

View File

@@ -691,7 +691,7 @@
<value>自动调整配置列宽在更新订阅后</value>
</data>
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>检查 Pre-Release 更新 (请谨慎启用)</value>
<value>检查 Pre-Release</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>例外</value>
@@ -1333,7 +1333,7 @@
<value>开启第二个本地监听端口</value>
</data>
<data name="TbRoutingInboundTagTips" xml:space="preserve">
<value>Socks本地端口Socks2第二个本地端口Socks3局域网端口</value>
<value>TunTUN 入站,Socks本地端口Socks2第二个本地端口Socks3局域网端口</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>主题</value>
@@ -1728,4 +1728,19 @@
<data name="menuExport2InnerUri" xml:space="preserve">
<value>导出 v2rayN 内部分享链接至剪贴板 (多选)</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} 有新版本可用:{1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>仅检查</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>不支持</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 信息</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
</root>

View File

@@ -691,7 +691,7 @@
<value>在更新訂閱後自動調整列寬</value>
</data>
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>檢查 Pre-Release 更新 (請謹慎啟用)</value>
<value>檢查 Pre-Release</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>例外</value>
@@ -1728,4 +1728,19 @@
<data name="menuExport2InnerUri" xml:space="preserve">
<value>匯出 v2rayN 內部分享連結至剪貼簿(多選)</value>
</data>
<data name="MsgCheckUpdateHasNewVersion" xml:space="preserve">
<value>{0} 有新版本可用:{1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>僅檢查</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>不支援</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP 資訊</value>
</data>
<data name="menuNewUpdate" xml:space="preserve">
<value>有更新</value>
</data>
</root>

View File

@@ -1,6 +1,6 @@
{
"type": "tun",
"tag": "tun-in",
"tag": "tun",
"interface_name": "singbox_tun",
"address": [
"172.18.0.1/30",

View File

@@ -87,8 +87,14 @@ public partial class CoreConfigSingboxService
});
_coreConfig.route.rules.Add(new()
{
protocol = ["dns"],
action = "hijack-dns"
type = "logical",
mode = "or",
action = "hijack-dns",
rules =
[
new() { port = [53] },
new() { protocol = ["dns"] },
],
});
}
else
@@ -96,7 +102,7 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.Add(new()
{
port = [53],
action = "hijack-dns"
action = "hijack-dns",
});
}

View File

@@ -104,6 +104,7 @@ public partial class CoreConfigV2rayService
},
},
tag = balancerTag,
fallbackTag = multipleLoad == EMultipleLoad.Fallback ? Global.DirectTag : null,
};
_coreConfig.routing.balancers ??= new();
_coreConfig.routing.balancers.Add(balancer);

View File

@@ -27,6 +27,7 @@ public partial class CoreConfigV2rayService
});
_coreConfig.routing.rules.Add(new()
{
inboundTag = ["tun"],
port = "53",
outboundTag = Global.DnsOutboundTag,
});

View File

@@ -3,7 +3,7 @@ using System.Net.Http.Headers;
namespace ServiceLib.Services;
/// <summary>
///Download
/// Download
/// </summary>
public class DownloadService
{
@@ -13,7 +13,10 @@ public class DownloadService
private static readonly string _tag = "DownloadService";
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
/// <summary>
/// Downloads data with the specified proxy and reports progress messages.
/// </summary>
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
{
try
{
@@ -36,6 +39,9 @@ public class DownloadService
return 0;
}
/// <summary>
/// Downloads a file and reports progress through events.
/// </summary>
public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout)
{
try
@@ -64,6 +70,9 @@ public class DownloadService
}
}
/// <summary>
/// Gets redirect target URL without following redirects automatically.
/// </summary>
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
{
var webRequestHandler = new SocketsHttpHandler
@@ -86,11 +95,23 @@ public class DownloadService
}
}
/// <summary>
/// Tries to download string content using proxy switch setting.
/// </summary>
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
{
var webProxy = await GetWebProxy(blProxy);
return await TryDownloadString(url, webProxy, userAgent);
}
/// <summary>
/// Tries to download string content with a specified proxy.
/// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
{
try
{
var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15);
var result1 = await DownloadStringAsync(url, webProxy, userAgent, 15);
if (result1.IsNotEmpty())
{
return result1;
@@ -108,7 +129,7 @@ public class DownloadService
try
{
var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15);
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, 15);
if (result2.IsNotEmpty())
{
return result2;
@@ -128,14 +149,12 @@ public class DownloadService
}
/// <summary>
/// DownloadString
/// Downloads string content via HttpClient.
/// </summary>
/// <param name="url"></param>
private async Task<string?> DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
{
try
{
var webProxy = await GetWebProxy(blProxy);
var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
@@ -172,15 +191,12 @@ public class DownloadService
}
/// <summary>
/// DownloadString
/// Downloads string content via DownloaderHelper.
/// </summary>
/// <param name="url"></param>
private async Task<string?> DownloadStringViaDownloader(string url, bool blProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
{
try
{
var webProxy = await GetWebProxy(blProxy);
if (userAgent.IsNullOrEmpty())
{
userAgent = Utils.GetVersion(false);
@@ -200,6 +216,9 @@ public class DownloadService
return null;
}
/// <summary>
/// Creates local SOCKS proxy when proxy switch is enabled.
/// </summary>
private async Task<WebProxy?> GetWebProxy(bool blProxy)
{
if (!blProxy)
@@ -215,6 +234,9 @@ public class DownloadService
return new WebProxy($"socks5://{Global.Loopback}:{port}");
}
/// <summary>
/// Checks whether the specified TCP endpoint is reachable.
/// </summary>
private async Task<bool> SocketCheck(string ip, int port)
{
try

View File

@@ -8,6 +8,8 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = new();
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
{
@@ -135,32 +137,39 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task RunTcpingAsync(List<ServerTestItem> selecteds)
{
List<Task> tasks = [];
foreach (var it in selecteds)
{
tasks.Add(Task.Run(async () =>
{
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
foreach (var lst in lstBatch)
{
List<Task> tasks = [];
foreach (var it in lst)
{
tasks.Add(Task.Run(async () =>
{
Logging.SaveLog(_tag, ex);
}
}));
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}));
}
await Task.WhenAll(tasks);
await Task.Delay(_delayInterval);
}
await Task.WhenAll(tasks);
}
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
{
if (pageSize <= 0)
{
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize;
pageSize = Math.Min(lstSelected.Count, _speedTestPageSize);
}
var lstTest = GetTestBatchItem(lstSelected, pageSize);
@@ -172,7 +181,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
lstFailed.AddRange(lst);
}
await Task.Delay(100);
await Task.Delay(_delayInterval);
}
//Retest the failed part
@@ -249,7 +258,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
if (pageSize <= 0)
{
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize;
pageSize = Math.Min(lstSelected.Count, _speedTestPageSize);
}
var lstTest = GetTestBatchItem(lstSelected, pageSize);
@@ -261,7 +270,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
lstFailed.AddRange(lst);
}
await Task.Delay(100);
await Task.Delay(_delayInterval);
}
//Retest the failed part
@@ -392,10 +401,23 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<int> DoRealPing(ServerTestItem it)
{
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, 10);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
if (responseTime > 0)
{
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipStr = ipInfo?.ToString() ?? Global.None;
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
await UpdateIpInfoFunc(it.IndexId, ipStr);
}
else
{
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
}
return responseTime;
}
@@ -491,4 +513,9 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestMessage(indexId, speed);
}
}
private async Task UpdateIpInfoFunc(string indexId, string ip)
{
await _updateFunc?.Invoke(new() { IndexId = indexId, IpInfo = ip });
}
}

View File

@@ -100,6 +100,43 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
}
}
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease)
{
if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type))
{
return new UpdateResult(false, ResUI.MsgNotSupport);
}
var downloadHandle = new DownloadService();
var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease);
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease);
}
public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease)
{
var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
{
if (!(_config.CheckUpdateItem.SelectedCoreTypes?.Contains(type.ToString()) ?? true))
{
continue;
}
var result = await CheckHasUpdateOnly(type, preRelease);
if (result.Success && result.Version != null)
{
var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version);
msgs.Add(msg);
AppManager.Instance.SetLastCheckUpdateResult(type, msg);
}
else
{
AppManager.Instance.SetLastCheckUpdateResult(type, result.Msg);
}
}
return msgs;
}
public async Task UpdateGeoFileAll()
{
await UpdateGeoFiles();

View File

@@ -3,6 +3,7 @@ namespace ServiceLib.Services;
/// <summary>
/// http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class WindowsJobService : IDisposable
{
private nint handle = nint.Zero;

View File

@@ -3,12 +3,13 @@ namespace ServiceLib.ViewModels;
public class CheckUpdateViewModel : MyReactiveObject
{
private const string _geo = "GeoFiles";
private readonly string _v2rayN = ECoreType.v2rayN.ToString();
private readonly ECoreType _v2rayN = ECoreType.v2rayN;
private List<CheckUpdateModel> _lstUpdated = [];
private static readonly string _tag = "CheckUpdateViewModel";
public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>();
public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> CheckOnlyCmd { get; }
[Reactive] public bool EnableCheckPreReleaseUpdate { get; set; }
public CheckUpdateViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
@@ -23,12 +24,19 @@ public class CheckUpdateViewModel : MyReactiveObject
_ = UpdateView(_v2rayN, ex.Message);
});
CheckOnlyCmd = ReactiveCommand.CreateFromTask(CheckOnly);
CheckOnlyCmd.ThrownExceptions.Subscribe(ex =>
{
Logging.SaveLog(_tag, ex);
_ = UpdateView(_v2rayN, ex.Message);
});
EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate;
this.WhenAnyValue(
x => x.EnableCheckPreReleaseUpdate,
y => y == true)
.Subscribe(c => _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate);
.Subscribe(c => _ = OnCheckPreReleaseUpdateChanged());
RefreshCheckUpdateItems();
}
@@ -37,21 +45,15 @@ public class CheckUpdateViewModel : MyReactiveObject
{
CheckUpdateModels.Clear();
if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
{
CheckUpdateModels.Add(GetCheckUpdateModel(_v2rayN));
//Not Windows and under Win10
if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10))
{
CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.Xray.ToString()));
CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString()));
CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString()));
}
CheckUpdateModels.Add(GetCheckUpdateModel(type));
}
CheckUpdateModels.Add(GetCheckUpdateModel(_geo));
CheckUpdateModels.Add(GetGeoFileCheckUpdateModel());
}
private CheckUpdateModel GetCheckUpdateModel(string coreType)
private CheckUpdateModel GetCheckUpdateModel(ECoreType coreType)
{
if (coreType == _v2rayN && Utils.IsPackagedInstall())
{
@@ -59,34 +61,64 @@ public class CheckUpdateViewModel : MyReactiveObject
{
IsSelected = false,
CoreType = coreType,
Remarks = ResUI.menuCheckUpdate + " (Not Support)",
IsGeoFile = false,
Remarks = ResUI.menuCheckUpdate + $" ({ResUI.MsgNotSupport})",
};
}
AppManager.Instance.LastCheckUpdateResults.TryGetValue(coreType, out var lastResult);
return new()
{
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true,
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType.ToString()) ?? true,
CoreType = coreType,
IsGeoFile = false,
Remarks = lastResult ?? ResUI.menuCheckUpdate,
};
}
private CheckUpdateModel GetGeoFileCheckUpdateModel()
{
return new()
{
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(_geo) ?? true,
CoreType = null,
IsGeoFile = true,
Remarks = ResUI.menuCheckUpdate,
};
}
private async Task OnCheckPreReleaseUpdateChanged()
{
if (_config.CheckUpdateItem.CheckPreReleaseUpdate == EnableCheckPreReleaseUpdate)
{
return;
}
_config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate;
await SaveSelectedCoreTypes();
}
private async Task SaveSelectedCoreTypes()
{
_config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList();
_config.CheckUpdateItem.SelectedCoreTypes =
CheckUpdateModels.Where(t => t.IsSelected == true)
.Select(t => t.CoreTypeForStorage)
.ToList();
await ConfigHandler.SaveConfig(_config);
}
private async Task CheckOnly()
{
await Task.Run(CheckOnlyTask);
}
private async Task CheckUpdate()
{
await Task.Run(CheckUpdateTask);
}
private async Task CheckUpdateTask()
private async Task CheckOnlyTask()
{
_lstUpdated.Clear();
_lstUpdated = CheckUpdateModels.Where(x => x.IsSelected == true)
.Select(x => new CheckUpdateModel() { CoreType = x.CoreType }).ToList();
await SaveSelectedCoreTypes();
for (var k = CheckUpdateModels.Count - 1; k >= 0; k--)
@@ -98,7 +130,56 @@ public class CheckUpdateViewModel : MyReactiveObject
}
await UpdateView(item.CoreType, "...");
if (item.CoreType == _geo)
if (item.IsGeoFile || item.CoreType == null)
{
await UpdateView(item.CoreType, ResUI.menuCheckOnly + $" ({ResUI.MsgNotSupport})");
continue;
}
if (item.CoreType == null)
{
await UpdateView(item.CoreType, ResUI.MsgNotSupport);
continue;
}
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, EnableCheckPreReleaseUpdate);
if (result.Success && result.Version != null)
{
await UpdateView(item.CoreType, string.Format(ResUI.MsgCheckUpdateHasNewVersion, item.CoreType, result.Version));
}
else
{
await UpdateView(item.CoreType, result.Msg);
}
}
}
private async Task CheckUpdateTask()
{
_lstUpdated.Clear();
_lstUpdated = CheckUpdateModels
.Where(x => x.IsSelected == true)
.Select(x => new CheckUpdateModel()
{
CoreType = x.CoreType,
IsGeoFile = x.IsGeoFile
})
.ToList();
await SaveSelectedCoreTypes();
for (var k = CheckUpdateModels.Count - 1; k >= 0; k--)
{
var item = CheckUpdateModels[k];
if (item.IsSelected != true)
{
continue;
}
await UpdateView(item.CoreType, "...");
if (item.IsGeoFile)
{
await CheckUpdateGeo();
}
@@ -106,16 +187,16 @@ public class CheckUpdateViewModel : MyReactiveObject
{
if (Utils.IsPackagedInstall())
{
await UpdateView(_v2rayN, "Not Support");
await UpdateView(_v2rayN, ResUI.MsgNotSupport);
continue;
}
await CheckUpdateN(EnableCheckPreReleaseUpdate);
}
else if (item.CoreType == ECoreType.Xray.ToString())
else if (item.CoreType == ECoreType.Xray)
{
await CheckUpdateCore(item, EnableCheckPreReleaseUpdate);
}
else
else if (item.CoreType.HasValue)
{
await CheckUpdateCore(item, false);
}
@@ -124,7 +205,7 @@ public class CheckUpdateViewModel : MyReactiveObject
await UpdateFinished();
}
private void UpdatedPlusPlus(string coreType, string fileName)
private void UpdatedPlusPlus(ECoreType? coreType, string fileName)
{
var item = _lstUpdated.FirstOrDefault(x => x.CoreType == coreType);
if (item == null)
@@ -142,14 +223,14 @@ public class CheckUpdateViewModel : MyReactiveObject
{
async Task _updateUI(bool success, string msg)
{
await UpdateView(_geo, msg);
await UpdateView(null, msg);
if (success)
{
UpdatedPlusPlus(_geo, "");
UpdatedPlusPlus(null, "");
}
}
await new UpdateService(_config, _updateUI).UpdateGeoFileAll()
.ContinueWith(t => UpdatedPlusPlus(_geo, ""));
.ContinueWith(t => UpdatedPlusPlus(null, ""));
}
private async Task CheckUpdateN(bool preRelease)
@@ -175,13 +256,15 @@ public class CheckUpdateViewModel : MyReactiveObject
if (success)
{
await UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore);
UpdatedPlusPlus(model.CoreType, msg);
}
}
var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType);
await new UpdateService(_config, _updateUI).CheckUpdateCore(type, preRelease)
.ContinueWith(t => UpdatedPlusPlus(model.CoreType, ""));
if (model.CoreType.HasValue)
{
await new UpdateService(_config, _updateUI).CheckUpdateCore(model.CoreType.Value, preRelease)
.ContinueWith(t => UpdatedPlusPlus(model.CoreType, ""));
}
}
private async Task UpdateFinished()
@@ -257,7 +340,7 @@ public class CheckUpdateViewModel : MyReactiveObject
{
foreach (var item in _lstUpdated)
{
if (item.FileName.IsNullOrEmpty())
if (item.FileName.IsNullOrEmpty() || item.IsGeoFile)
{
continue;
}
@@ -267,7 +350,9 @@ public class CheckUpdateViewModel : MyReactiveObject
{
continue;
}
var toPath = Utils.GetBinPath("", item.CoreType);
var coreTypeStr = item.CoreType?.ToString() ?? "";
var toPath = Utils.GetBinPath("", coreTypeStr);
if (fileName.Contains(".tar.gz"))
{
@@ -284,7 +369,7 @@ public class CheckUpdateViewModel : MyReactiveObject
}
else if (fileName.Contains(".gz"))
{
FileUtils.DecompressFile(fileName, toPath, item.CoreType);
FileUtils.DecompressFile(fileName, toPath, coreTypeStr);
}
else
{
@@ -296,7 +381,7 @@ public class CheckUpdateViewModel : MyReactiveObject
var filesList = new DirectoryInfo(toPath).GetFiles().Select(u => u.FullName).ToList();
foreach (var file in filesList)
{
await Utils.SetLinuxChmod(Path.Combine(toPath, item.CoreType.ToLower()));
await Utils.SetLinuxChmod(Path.Combine(toPath, coreTypeStr.ToLower()));
}
}
@@ -309,11 +394,12 @@ public class CheckUpdateViewModel : MyReactiveObject
}
}
private async Task UpdateView(string coreType, string msg)
private async Task UpdateView(ECoreType? coreType, string msg)
{
var item = new CheckUpdateModel()
{
CoreType = coreType,
IsGeoFile = coreType == null,
Remarks = msg,
};
@@ -327,7 +413,7 @@ public class CheckUpdateViewModel : MyReactiveObject
public async Task UpdateViewResult(CheckUpdateModel model)
{
var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType);
var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType && t.IsGeoFile == model.IsGeoFile);
if (found == null)
{
return;

View File

@@ -65,6 +65,8 @@ public class MainWindowViewModel : MyReactiveObject
[Reactive] public bool BlIsWindows { get; set; }
[Reactive] public bool BlNewUpdate { get; set; }
#endregion Menu
#region Init
@@ -251,6 +253,11 @@ public class MainWindowViewModel : MyReactiveObject
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
AppEvents.HasUpdateNotified
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async bl => BlNewUpdate = bl);
#endregion AppEvents
_ = Init();
@@ -297,10 +304,22 @@ public class MainWindowViewModel : MyReactiveObject
{
var indexIdOld = _config.IndexId;
await RefreshServers();
if (indexIdOld != _config.IndexId)
// If indexId changed or subIndexId is empty, directly reload.
if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty())
{
await Reload();
}
else
{
// The activity config belongs to the current group.
var profile = await AppManager.Instance.GetProfileItem(_config.IndexId);
if (profile != null && profile.Subid == _config.SubIndexId)
{
await Reload();
}
}
if (_config.UiItem.EnableAutoAdjustMainLvColWidth)
{
AppEvents.AdjustMainLvColWidthRequested.Publish();

View File

@@ -4,29 +4,29 @@ public class OptionSettingViewModel : MyReactiveObject
{
#region Core
[Reactive] public int localPort { get; set; }
[Reactive] public int LocalPort { get; set; }
[Reactive] public bool SecondLocalPortEnabled { get; set; }
[Reactive] public bool udpEnabled { get; set; }
[Reactive] public bool sniffingEnabled { get; set; }
public IList<string> destOverride { get; set; }
[Reactive] public bool routeOnly { get; set; }
[Reactive] public bool allowLANConn { get; set; }
[Reactive] public bool newPort4LAN { get; set; }
[Reactive] public string user { get; set; }
[Reactive] public string pass { get; set; }
[Reactive] public bool muxEnabled { get; set; }
[Reactive] public bool logEnabled { get; set; }
[Reactive] public string loglevel { get; set; }
[Reactive] public bool defAllowInsecure { get; set; }
[Reactive] public string defFingerprint { get; set; }
[Reactive] public string defUserAgent { get; set; }
[Reactive] public string sendThrough { get; set; }
[Reactive] public string bindInterface { 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 bool enableFragment { get; set; }
[Reactive] public bool UdpEnabled { get; set; }
[Reactive] public bool SniffingEnabled { get; set; }
public IList<string> DestOverride { get; set; }
[Reactive] public bool RouteOnly { get; set; }
[Reactive] public bool AllowLANConn { get; set; }
[Reactive] public bool NewPort4LAN { get; set; }
[Reactive] public string User { get; set; }
[Reactive] public string Pass { get; set; }
[Reactive] public bool MuxEnabled { get; set; }
[Reactive] public bool LogEnabled { get; set; }
[Reactive] public string Loglevel { get; set; }
[Reactive] public bool DefAllowInsecure { get; set; }
[Reactive] public string DefFingerprint { get; set; }
[Reactive] public string DefUserAgent { get; set; }
[Reactive] public string SendThrough { get; set; }
[Reactive] public string BindInterface { 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 bool EnableFragment { get; set; }
#endregion Core
@@ -83,9 +83,9 @@ public class OptionSettingViewModel : MyReactiveObject
#region System proxy
[Reactive] public bool notProxyLocalAddress { get; set; }
[Reactive] public string systemProxyAdvancedProtocol { get; set; }
[Reactive] public string systemProxyExceptions { get; set; }
[Reactive] public bool NotProxyLocalAddress { get; set; }
[Reactive] public string SystemProxyAdvancedProtocol { get; set; }
[Reactive] public string SystemProxyExceptions { get; set; }
[Reactive] public string CustomSystemProxyPacPath { get; set; }
[Reactive] public string CustomSystemProxyScriptPath { get; set; }
@@ -142,28 +142,28 @@ public class OptionSettingViewModel : MyReactiveObject
#region Core
var inbound = _config.Inbound.First();
localPort = inbound.LocalPort;
LocalPort = inbound.LocalPort;
SecondLocalPortEnabled = inbound.SecondLocalPortEnabled;
udpEnabled = inbound.UdpEnabled;
sniffingEnabled = inbound.SniffingEnabled;
routeOnly = inbound.RouteOnly;
allowLANConn = inbound.AllowLANConn;
newPort4LAN = inbound.NewPort4LAN;
user = inbound.User;
pass = inbound.Pass;
muxEnabled = _config.CoreBasicItem.MuxEnabled;
logEnabled = _config.CoreBasicItem.LogEnabled;
loglevel = _config.CoreBasicItem.Loglevel;
defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure;
defFingerprint = _config.CoreBasicItem.DefFingerprint;
defUserAgent = _config.CoreBasicItem.DefUserAgent;
sendThrough = _config.CoreBasicItem.SendThrough ?? string.Empty;
bindInterface = _config.CoreBasicItem.BindInterface ?? string.Empty;
mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
hyUpMbps = _config.HysteriaItem.UpMbps;
hyDownMbps = _config.HysteriaItem.DownMbps;
enableFragment = _config.CoreBasicItem.EnableFragment;
UdpEnabled = inbound.UdpEnabled;
SniffingEnabled = inbound.SniffingEnabled;
RouteOnly = inbound.RouteOnly;
AllowLANConn = inbound.AllowLANConn;
NewPort4LAN = inbound.NewPort4LAN;
User = inbound.User;
Pass = inbound.Pass;
MuxEnabled = _config.CoreBasicItem.MuxEnabled;
LogEnabled = _config.CoreBasicItem.LogEnabled;
Loglevel = _config.CoreBasicItem.Loglevel;
DefAllowInsecure = _config.CoreBasicItem.DefAllowInsecure;
DefFingerprint = _config.CoreBasicItem.DefFingerprint;
DefUserAgent = _config.CoreBasicItem.DefUserAgent;
SendThrough = _config.CoreBasicItem.SendThrough ?? string.Empty;
BindInterface = _config.CoreBasicItem.BindInterface ?? string.Empty;
Mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
EnableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
HyUpMbps = _config.HysteriaItem.UpMbps;
HyDownMbps = _config.HysteriaItem.DownMbps;
EnableFragment = _config.CoreBasicItem.EnableFragment;
#endregion Core
@@ -211,9 +211,9 @@ public class OptionSettingViewModel : MyReactiveObject
#region System proxy
notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress;
systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol;
systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions;
NotProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress;
SystemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol;
SystemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions;
CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath;
CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath;
@@ -297,13 +297,13 @@ public class OptionSettingViewModel : MyReactiveObject
private async Task SaveSettingAsync()
{
if (localPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(localPort.ToString())
|| localPort <= 0 || localPort >= Global.MaxPort)
if (LocalPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(LocalPort.ToString())
|| LocalPort <= 0 || LocalPort >= Global.MaxPort)
{
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return;
}
var sendThroughValue = sendThrough.TrimEx();
var sendThroughValue = SendThrough.TrimEx();
if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue))
{
NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4);
@@ -328,33 +328,33 @@ public class OptionSettingViewModel : MyReactiveObject
//}
//Core
_config.Inbound.First().LocalPort = localPort;
_config.Inbound.First().LocalPort = LocalPort;
_config.Inbound.First().SecondLocalPortEnabled = SecondLocalPortEnabled;
_config.Inbound.First().UdpEnabled = udpEnabled;
_config.Inbound.First().SniffingEnabled = sniffingEnabled;
_config.Inbound.First().DestOverride = destOverride?.ToList();
_config.Inbound.First().RouteOnly = routeOnly;
_config.Inbound.First().AllowLANConn = allowLANConn;
_config.Inbound.First().NewPort4LAN = newPort4LAN;
_config.Inbound.First().User = user;
_config.Inbound.First().Pass = pass;
_config.Inbound.First().UdpEnabled = UdpEnabled;
_config.Inbound.First().SniffingEnabled = SniffingEnabled;
_config.Inbound.First().DestOverride = DestOverride?.ToList();
_config.Inbound.First().RouteOnly = RouteOnly;
_config.Inbound.First().AllowLANConn = AllowLANConn;
_config.Inbound.First().NewPort4LAN = NewPort4LAN;
_config.Inbound.First().User = User;
_config.Inbound.First().Pass = Pass;
if (_config.Inbound.Count > 1)
{
_config.Inbound.RemoveAt(1);
}
_config.CoreBasicItem.LogEnabled = logEnabled;
_config.CoreBasicItem.Loglevel = loglevel;
_config.CoreBasicItem.MuxEnabled = muxEnabled;
_config.CoreBasicItem.DefAllowInsecure = defAllowInsecure;
_config.CoreBasicItem.DefFingerprint = defFingerprint;
_config.CoreBasicItem.DefUserAgent = defUserAgent;
_config.CoreBasicItem.SendThrough = sendThrough.TrimEx();
_config.CoreBasicItem.BindInterface = bindInterface.TrimEx();
_config.Mux4SboxItem.Protocol = mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = hyUpMbps ?? 0;
_config.HysteriaItem.DownMbps = hyDownMbps ?? 0;
_config.CoreBasicItem.EnableFragment = enableFragment;
_config.CoreBasicItem.LogEnabled = LogEnabled;
_config.CoreBasicItem.Loglevel = Loglevel;
_config.CoreBasicItem.MuxEnabled = MuxEnabled;
_config.CoreBasicItem.DefAllowInsecure = DefAllowInsecure;
_config.CoreBasicItem.DefFingerprint = DefFingerprint;
_config.CoreBasicItem.DefUserAgent = DefUserAgent;
_config.CoreBasicItem.SendThrough = SendThrough.TrimEx();
_config.CoreBasicItem.BindInterface = BindInterface.TrimEx();
_config.Mux4SboxItem.Protocol = Mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = EnableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = HyUpMbps ?? 0;
_config.HysteriaItem.DownMbps = HyDownMbps ?? 0;
_config.CoreBasicItem.EnableFragment = EnableFragment;
_config.GuiItem.AutoRun = AutoRun;
_config.GuiItem.EnableStatistics = EnableStatistics;
@@ -383,9 +383,9 @@ public class OptionSettingViewModel : MyReactiveObject
_config.SpeedTestItem.IPAPIUrl = IPAPIUrl;
//systemProxy
_config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions;
_config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress;
_config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol;
_config.SystemProxyItem.SystemProxyExceptions = SystemProxyExceptions;
_config.SystemProxyItem.NotProxyLocalAddress = NotProxyLocalAddress;
_config.SystemProxyItem.SystemProxyAdvancedProtocol = SystemProxyAdvancedProtocol;
_config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath;
_config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath;

View File

@@ -303,6 +303,10 @@ public class ProfilesViewModel : MyReactiveObject
{
item.SpeedVal = result.Speed ?? string.Empty;
}
if (result.IpInfo.IsNotEmpty())
{
item.IpInfo = result.IpInfo ?? string.Empty;
}
await Task.CompletedTask;
}
@@ -437,6 +441,7 @@ public class ProfilesViewModel : MyReactiveObject
Speed = t33?.Speed ?? 0,
DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty,
SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty,
IpInfo = t33?.IpInfo ?? string.Empty,
TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown),
TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp),
TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown),

View File

@@ -23,6 +23,7 @@ public partial class QRCodeAvaloniaUtils
}
}
[SupportedOSPlatform("windows")]
private static byte[]? CaptureScreenWindows()
{
var hdcScreen = IntPtr.Zero;

View File

@@ -5,6 +5,7 @@ global using System.IO;
global using System.Linq;
global using System.Reactive.Disposables.Fluent;
global using System.Reactive.Linq;
global using System.Runtime.Versioning;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;

View File

@@ -8,6 +8,7 @@ public sealed class HotkeyManager
private static readonly Lazy<HotkeyManager> _instance = new(() => new());
public static HotkeyManager Instance = _instance.Value;
private readonly Dictionary<int, EGlobalHotkey> _hotkeyTriggerDic = new();
[SupportedOSPlatform("windows")]
private GlobalHotKeys.HotKeyManager? _hotKeyManager;
private Config? _config;
@@ -16,6 +17,7 @@ public sealed class HotkeyManager
public bool IsPause { get; set; } = false;
[SupportedOSPlatform("windows")]
public void Init(Config config, Action<EGlobalHotkey> updateFunc)
{
_config = config;
@@ -26,9 +28,14 @@ public sealed class HotkeyManager
public void Dispose()
{
if (!Utils.IsWindows())
{
return;
}
_hotKeyManager?.Dispose();
}
[SupportedOSPlatform("windows")]
private void Register()
{
if (_config.GlobalHotkeys.Any(t => t.KeyCode > 0) == false)

View File

@@ -33,6 +33,12 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<Button
x:Name="btnCheckOnly"
Width="100"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.menuCheckOnly}" />
<Button
x:Name="btnCheckUpdate"
Width="100"
@@ -70,7 +76,7 @@
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding CoreType}" />
Text="{Binding CoreTypeForStorage}" />
<TextBlock
Grid.Column="2"
HorizontalAlignment="Left"

View File

@@ -13,6 +13,7 @@ public partial class CheckUpdateView : ReactiveUserControl<CheckUpdateViewModel>
this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCheckPreReleaseUpdate, v => v.togEnableCheckPreReleaseUpdate.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckOnlyCmd, v => v.btnCheckOnly).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
});
}

View File

@@ -102,6 +102,13 @@
<MenuItem x:Name="menuClose" Header="{x:Static resx:ResUI.menuExit}" />
</Menu>
<Button
x:Name="btnNewUpdate"
Margin="{StaticResource MarginLr8}"
HorizontalAlignment="Left"
Content="{x:Static resx:ResUI.menuNewUpdate}"
IsVisible="False" />
</DockPanel>
<view:StatusBarView DockPanel.Dock="Bottom" />

View File

@@ -25,6 +25,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
menuSettingsSetUWP.Click += MenuSettingsSetUWP_Click;
menuPromotion.Click += MenuPromotion_Click;
menuCheckUpdate.Click += MenuCheckUpdate_Click;
btnNewUpdate.Click += MenuCheckUpdate_Click;
menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
menuClose.Click += MenuClose_Click;
@@ -102,6 +103,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.ReloadCmd, v => v.menuReload).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.BlReloadEnabled, v => v.menuReload.IsEnabled).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.BlNewUpdate, v => v.btnNewUpdate.IsVisible).DisposeWith(disposables);
switch (_config.UiItem.MainGirdOrientation)
{
@@ -367,6 +369,8 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
{
_checkUpdateView ??= new CheckUpdateView();
DialogHost.Show(_checkUpdateView);
AppEvents.HasUpdateNotified.Publish(false);
}
private void MenuBackupAndRestore_Click(object? sender, RoutedEventArgs e)

View File

@@ -4,6 +4,11 @@ namespace v2rayN.Desktop.Views;
public partial class MessageBoxDialog : Window
{
public MessageBoxDialog()
: this(string.Empty, string.Empty)
{
}
public MessageBoxDialog(string caption, string message)
{
InitializeComponent();

View File

@@ -876,7 +876,8 @@
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
HorizontalAlignment="Left"
IsEditable="True" />
<TextBlock
Grid.Row="6"

View File

@@ -60,30 +60,30 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.localPort, v => v.txtlocalPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.LocalPort, v => v.txtlocalPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SecondLocalPortEnabled, v => v.togSecondLocalPortEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.udpEnabled, v => v.togudpEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sniffingEnabled, v => v.togsniffingEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.routeOnly, v => v.togrouteOnly.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.allowLANConn, v => v.togAllowLANConn.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.newPort4LAN, v => v.togNewPort4LAN.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.newPort4LAN, v => v.txtuser.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.newPort4LAN, v => v.txtpass.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.user, v => v.txtuser.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.pass, v => v.txtpass.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.muxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.logEnabled, v => v.toglogEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.loglevel, v => v.cmbloglevel.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.bindInterface, v => v.txtbindInterface.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.UdpEnabled, v => v.togudpEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SniffingEnabled, v => v.togsniffingEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RouteOnly, v => v.togrouteOnly.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowLANConn, v => v.togAllowLANConn.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NewPort4LAN, v => v.togNewPort4LAN.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NewPort4LAN, v => v.txtuser.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NewPort4LAN, v => v.txtpass.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.User, v => v.txtuser.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Pass, v => v.txtpass.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.LogEnabled, v => v.toglogEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Loglevel, v => v.cmbloglevel.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DefAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DefFingerprint, v => v.cmbdefFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DefUserAgent, v => v.cmbdefUserAgent.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BindInterface, v => v.txtbindInterface.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Mux4SboxProtocol, v => v.cmbmux4SboxProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.HyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRun, v => v.togAutoRun.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableStatistics, v => v.togEnableStatistics.IsChecked).DisposeWith(disposables);
@@ -104,21 +104,21 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.SubConvertUrl, v => v.cmbSubConvertUrl.Text).DisposeWith(disposables);
this.Bind<OptionSettingViewModel, OptionSettingWindow, int, int>(ViewModel,
vm => vm.MainGirdOrientation, view => view.cmbMainGirdOrientation.SelectedIndex).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeoFileSourceUrl, v => v.cmbGetFilesSourceUrl.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SrsFileSourceUrl, v => v.cmbSrsFilesSourceUrl.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RoutingRulesSourceUrl, v => v.cmbRoutingRulesSourceUrl.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeoFileSourceUrl, v => v.cmbGetFilesSourceUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SrsFileSourceUrl, v => v.cmbSrsFilesSourceUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RoutingRulesSourceUrl, v => v.cmbRoutingRulesSourceUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IPAPIUrl, v => v.cmbIPAPIUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NotProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SystemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SystemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyPacPath, v => v.txtCustomSystemProxyPacPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyScriptPath, v => v.txtCustomSystemProxyScriptPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStack, v => v.cmbStack.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunMtu, v => v.cmbMtu.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunEnableIPv6Address, v => v.togEnableIPv6Address.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunIcmpRouting, v => v.cmbIcmpRoutingPolicy.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunEnableLegacyProtect, v => v.togEnableLegacyProtect.IsChecked).DisposeWith(disposables);
@@ -161,29 +161,18 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
private async Task<List<string>> GetFonts()
{
await Task.CompletedTask;
var lstFonts = new List<string>();
try
{
if (Utils.IsWindows())
{
return lstFonts;
}
else if (Utils.IsNonWindows())
{
var result = await Utils.GetLinuxFontFamily("zh");
if (result.IsNullOrEmpty())
{
return lstFonts;
}
var lst = result.Split(Environment.NewLine)
.Where(t => t.IsNotEmpty())
.ToList()
.Select(t => t.Split(",").FirstOrDefault() ?? "")
.OrderBy(t => t)
.ToList();
return lst;
}
var lst = Avalonia.Media.FontManager.Current.SystemFonts
.Select(t => t.Name)
.Where(t => t.IsNotEmpty())
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(t => t)
.ToList();
return lst;
}
catch (Exception ex)
{
@@ -196,7 +185,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
{
if (ViewModel != null)
{
ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList();
ViewModel.DestOverride = clbdestOverride.SelectedItems.Cast<string>().ToList();
}
}

View File

@@ -277,6 +277,12 @@
Header="{x:Static resx:ResUI.LvTestSpeed}"
Tag="SpeedVal" />
<DataGridTextColumn
Width="100"
Binding="{Binding IpInfo}"
Header="{x:Static resx:ResUI.LvTestIpInfo}"
Tag="IpInfo" />
<DataGridTextColumn
Width="100"
Binding="{Binding TodayUp}"

View File

@@ -425,10 +425,14 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
item2.DisplayIndex = displayIndex++;
}
if (item.Name.ToLower().StartsWith("to"))
if (item.Name.StartsWith("to", StringComparison.CurrentCultureIgnoreCase))
{
item2.IsVisible = _config.GuiItem.EnableStatistics;
}
if (item.Name.Equals("IpInfo", StringComparison.CurrentCultureIgnoreCase))
{
item2.IsVisible = _config.SpeedTestItem.IPAPIUrl.IsNotEmpty();
}
}
}
}

View File

@@ -16,7 +16,7 @@
<DockPanel>
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="Auto,Auto,Auto"
ColumnDefinitions="Auto,Auto,*"
DockPanel.Dock="Top"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
@@ -66,7 +66,8 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRuleTypeTips}" />
Text="{x:Static resx:ResUI.TbRuleTypeTips}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="2"
@@ -93,7 +94,10 @@
Margin="0,0,8,0"
Click="BtnSelectProfile_Click"
Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
<TextBlock
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock
@@ -115,7 +119,8 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRuleMatchingTips}" />
Text="{x:Static resx:ResUI.TbRuleMatchingTips}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="4"
@@ -135,7 +140,8 @@
Grid.Row="4"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center">
VerticalAlignment="Center"
TextWrapping="Wrap">
<HyperlinkButton Classes="WithIcon" Click="linkRuleobjectDoc_Click">
<TextBlock Text="{x:Static resx:ResUI.TbRuleobjectDoc}" />
</HyperlinkButton>
@@ -160,7 +166,8 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRoutingInboundTagTips}" />
Text="{x:Static resx:ResUI.TbRoutingInboundTagTips}"
TextWrapping="Wrap" />
<TextBlock
Grid.Row="6"
@@ -181,7 +188,8 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRoutingTips}" />
Text="{x:Static resx:ResUI.TbRoutingTips}"
TextWrapping="Wrap" />
</Grid>
<StackPanel

View File

@@ -29,6 +29,7 @@
<PackageReference Include="ReactiveUI.Fody">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
</ItemGroup>
<ItemGroup>

View File

@@ -32,6 +32,13 @@
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<Button
x:Name="btnCheckOnly"
Width="100"
Margin="{StaticResource Margin8}"
Content="{x:Static resx:ResUI.menuCheckOnly}"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCheckUpdate"
Width="100"
@@ -77,7 +84,7 @@
<TextBlock
Grid.Column="1"
Style="{StaticResource ListItemTitle}"
Text="{Binding CoreType}" />
Text="{Binding CoreTypeForStorage}" />
<TextBlock
Grid.Column="2"
Style="{StaticResource ListItemSubTitle}"

View File

@@ -13,6 +13,7 @@ public partial class CheckUpdateView
this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCheckPreReleaseUpdate, v => v.togEnableCheckPreReleaseUpdate.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckOnlyCmd, v => v.btnCheckOnly).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
});
}

View File

@@ -313,6 +313,13 @@
Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<ContentControl x:Name="pbTheme" />
</materialDesign:PopupBox>
<Button
x:Name="btnNewUpdate"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.menuNewUpdate}"
Style="{StaticResource DefButton}"
Visibility="Hidden" />
</ToolBar>
</ToolBarTray>

View File

@@ -25,6 +25,7 @@ public partial class MainWindow
menuPromotion.Click += MenuPromotion_Click;
menuClose.Click += MenuClose_Click;
menuCheckUpdate.Click += MenuCheckUpdate_Click;
btnNewUpdate.Click += MenuCheckUpdate_Click;
menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
ViewModel = new MainWindowViewModel(UpdateViewHandler);
@@ -102,6 +103,8 @@ public partial class MainWindow
this.BindCommand(ViewModel, vm => vm.ReloadCmd, v => v.menuReload).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.BlReloadEnabled, v => v.menuReload.IsEnabled).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.BlNewUpdate, v => v.btnNewUpdate.Visibility).DisposeWith(disposables);
switch (_config.UiItem.MainGirdOrientation)
{
case EGirdOrientation.Horizontal:
@@ -363,6 +366,8 @@ public partial class MainWindow
{
_checkUpdateView ??= new CheckUpdateView();
DialogHost.Show(_checkUpdateView, "RootDialog");
AppEvents.HasUpdateNotified.Publish(false);
}
private void MenuBackupAndRestore_Click(object sender, RoutedEventArgs e)

View File

@@ -1141,6 +1141,7 @@
Width="200"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left"
IsEditable="True"
Style="{StaticResource DefComboBox}" />
<TextBlock

View File

@@ -57,30 +57,30 @@ public partial class OptionSettingWindow
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.localPort, v => v.txtlocalPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.LocalPort, v => v.txtlocalPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SecondLocalPortEnabled, v => v.togSecondLocalPortEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.udpEnabled, v => v.togudpEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sniffingEnabled, v => v.togsniffingEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.routeOnly, v => v.togrouteOnly.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.allowLANConn, v => v.togAllowLANConn.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.newPort4LAN, v => v.togNewPort4LAN.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.newPort4LAN, v => v.txtuser.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.newPort4LAN, v => v.txtpass.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.user, v => v.txtuser.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.pass, v => v.txtpass.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.muxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.logEnabled, v => v.toglogEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.loglevel, v => v.cmbloglevel.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.bindInterface, v => v.txtbindInterface.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.UdpEnabled, v => v.togudpEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SniffingEnabled, v => v.togsniffingEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RouteOnly, v => v.togrouteOnly.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowLANConn, v => v.togAllowLANConn.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NewPort4LAN, v => v.togNewPort4LAN.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NewPort4LAN, v => v.txtuser.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NewPort4LAN, v => v.txtpass.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.User, v => v.txtuser.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Pass, v => v.txtpass.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.LogEnabled, v => v.toglogEnabled.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Loglevel, v => v.cmbloglevel.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DefAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DefFingerprint, v => v.cmbdefFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DefUserAgent, v => v.cmbdefUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BindInterface, v => v.txtbindInterface.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Mux4SboxProtocol, v => v.cmbmux4SboxProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.HyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.Kcpmtu, v => v.txtKcpmtu.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.Kcptti, v => v.txtKcptti.Text).DisposeWith(disposables);
@@ -114,9 +114,9 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.RoutingRulesSourceUrl, v => v.cmbRoutingRulesSourceUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IPAPIUrl, v => v.cmbIPAPIUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NotProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SystemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SystemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyPacPath, v => v.txtCustomSystemProxyPacPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables);
@@ -217,7 +217,7 @@ public partial class OptionSettingWindow
{
if (ViewModel != null)
{
ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList();
ViewModel.DestOverride = clbdestOverride.SelectedItems.Cast<string>().ToList();
}
}

View File

@@ -345,6 +345,14 @@
Binding="{Binding TodayUp}"
ExName="TodayUp"
Header="{x:Static resx:ResUI.LvTodayUploadDataAmount}" />
<base:MyDGTextColumn
x:Name="colIpInfo"
Width="100"
Binding="{Binding IpInfo}"
ExName="IpInfo"
Header="{x:Static resx:ResUI.LvTestIpInfo}" />
<base:MyDGTextColumn
x:Name="colTodayDown"
Width="100"

View File

@@ -378,10 +378,14 @@ public partial class ProfilesView
item2.Width = item.Width;
item2.DisplayIndex = displayIndex++;
}
if (item.Name.ToLower().StartsWith("to"))
if (item.Name.StartsWith("to", StringComparison.CurrentCultureIgnoreCase))
{
item2.Visibility = _config.GuiItem.EnableStatistics ? Visibility.Visible : Visibility.Hidden;
}
if (item.Name.Equals("IpInfo", StringComparison.CurrentCultureIgnoreCase))
{
item2.Visibility = _config.SpeedTestItem.IPAPIUrl.IsNotEmpty() ? Visibility.Visible : Visibility.Hidden;
}
}
}
}

View File

@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
<dependency>
<dependentAssembly>

View File

@@ -7,6 +7,7 @@
<UseWPF>true</UseWPF>
<ApplicationIcon>Resources\v2rayN.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>